This commit is contained in:
oliver
2026-06-10 17:34:36 -03:00
parent 8be61e7704
commit ee4aca58e1
3 changed files with 188 additions and 84 deletions
+143 -2
View File
@@ -132,8 +132,8 @@ function switchAuthTab(tab) {
document.getElementById("signin-error").style.display = "none"; document.getElementById("signin-error").style.display = "none";
document.getElementById("signup-error").style.display = "none"; document.getElementById("signup-error").style.display = "none";
document.getElementById("signup-success").style.display = "none"; document.getElementById("signup-success").style.display = "none";
const _rpw = document.getElementById("reset-pw-wrap"); const note = document.getElementById("signin-reset-note");
if (_rpw) _rpw.style.display = "none"; if (note) note.style.display = "none";
} }
/* ═══════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════
@@ -1294,3 +1294,144 @@ function exportInvoicesCSV() {
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
/* ═══════════════════════════════════════════════════════════════
HELPERS
═══════════════════════════════════════════════════════════════ */
function escHtml(s) {
if (typeof s !== "string" && typeof s !== "number") return "";
return String(s)
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function escAttr(s) {
if (typeof s !== "string" && typeof s !== "number") return "";
return String(s)
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function isValidEmail(e) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e);
}
function formatBackupDate(s) {
if (!s) return "—";
const d = new Date(s);
if (isNaN(d.getTime())) return s;
return d.toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
function formatDate(s) {
if (!s) return "—";
const d = new Date(s);
if (isNaN(d.getTime())) return s;
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const year = d.getFullYear();
const hour = String(d.getHours()).padStart(2, "0");
const minute = String(d.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hour}:${minute}`;
}
function show(id) {
const el = document.getElementById(id);
if (el) el.style.display = "";
}
function hide(id) {
const el = document.getElementById(id);
if (el) el.style.display = "none";
}
function btnLoad(btn, label) {
if (!btn) return;
btn._origHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML =
label +
' <span class="spinner-sm" style="display:inline-block;vertical-align:middle;margin-left:4px"></span>';
}
function btnReset(btn) {
if (!btn) return;
btn.disabled = false;
btn.innerHTML = btn._origHTML || btn.innerHTML;
}
function showAuthError(id, msg) {
const el = document.getElementById(id);
if (!el) return;
el.innerHTML = `<span>${escHtml(msg)}</span>`;
el.style.display = "flex";
}
function hideAuthError(id) {
const el = document.getElementById(id);
if (!el) return;
el.style.display = "none";
}
function showResetOption() {
const note = document.getElementById("signin-reset-note");
if (note) note.style.display = "";
}
async function sendPasswordReset() {
const email = document.getElementById("signin-email").value.trim();
if (!isValidEmail(email)) {
toast("Please enter a valid email address.", "warning");
return;
}
const btn = document.getElementById("signin-reset-btn");
if (btn) btn.disabled = true;
try {
await fetch(PW_RESET_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
});
// Always show success — never reveal whether address exists
const note = document.getElementById("signin-reset-note");
if (note) note.style.display = "";
} catch (_) {
// silently ignore
} finally {
if (btn) btn.disabled = false;
}
}
function setCookie(name, value, days) {
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`;
}
function getCookie(name) {
const k = `${name}=`;
const parts = document.cookie.split("; ");
for (const p of parts) {
if (p.startsWith(k)) return decodeURIComponent(p.slice(k.length));
}
return null;
}
function deleteCookie(name) {
document.cookie = `${name}=; max-age=0; path=/`;
}
async function apiFetch(url, opts) {
const headers = { ...(opts?.headers || {}) };
if (currentSession) headers["X-Session-Id"] = currentSession;
return fetch(url, { ...opts, headers });
}
+38 -79
View File
@@ -132,93 +132,52 @@
</div> </div>
<div id="signin-error" class="auth-error"></div> <div id="signin-error" class="auth-error"></div>
<!-- Password reset — shown only after a failed sign-in -->
<div <div
id="reset-pw-wrap" id="signin-footer"
class="reset-pw-wrap" class="signin-footer"
style="display: none" style="
display: flex;
gap: 10px;
align-items: stretch;
"
> >
<div id="reset-pw-idle" class="reset-pw-idle">
<span class="reset-pw-hint"
>Forgot your password?</span
>
<button
id="reset-pw-btn"
class="reset-pw-btn"
onclick="sendPasswordReset()"
>
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
x="2"
y="4"
width="20"
height="16"
rx="2"
/>
<polyline points="22,4 12,13 2,4" />
</svg>
Send reset email
</button>
</div>
<div
id="reset-pw-sent"
class="reset-pw-sent"
style="display: none"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
x="2"
y="4"
width="20"
height="16"
rx="2"
/>
<polyline points="22,4 12,13 2,4" />
<polyline
points="2,20 9,13"
style="opacity: 0.5"
/>
<polyline
points="22,20 15,13"
style="opacity: 0.5"
/>
</svg>
<div class="reset-pw-sent-text">
<span class="reset-pw-sent-title"
>Reset link sent</span
>
<span class="reset-pw-sent-sub"
>Check your inbox &mdash; link expires
in 15&nbsp;minutes.</span
>
</div>
</div>
</div>
<button <button
type="submit" type="submit"
id="signin-btn" id="signin-btn"
class="btn btn-primary btn-block" class="btn btn-primary"
style="flex: 1"
> >
Sign In Sign In
</button> </button>
<button
type="button"
id="signin-reset-btn"
class="btn btn-ghost btn-sm"
onclick="sendPasswordReset()"
style="
flex: 1;
white-space: normal;
font-size: 12px;
padding: 8px 10px;
"
>
Send password reset
</button>
</div>
<div
id="signin-reset-note"
style="
display: none;
font-size: 11px;
color: var(--text-muted);
text-align: center;
line-height: 1.5;
margin-top: 2px;
"
>
First time customer? A password reset email has
already been sent to your address.
</div>
</form> </form>
</div> </div>
<!-- /auth-pane signin --> <!-- /auth-pane signin -->
+4
View File
@@ -410,6 +410,10 @@ body::after {
padding: 20px 24px 28px; padding: 20px 24px 28px;
} }
.signin-footer {
margin-top: 4px;
}
.auth-error { .auth-error {
display: none; display: none;
align-items: flex-start; align-items: flex-start;