login
This commit is contained in:
@@ -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, "<")
|
||||||
|
.replace(/>/g, ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escAttr(s) {
|
||||||
|
if (typeof s !== "string" && typeof s !== "number") return "";
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
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
@@ -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 — link expires
|
|
||||||
in 15 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 -->
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user