// Login overlay + Account panel (logout / change password). // // Cookie is set server-side as HttpOnly so JS never sees it -- that defeats // XSS-based exfiltration. We only briefly hold the password during input. "use strict"; // We deliberately don't import apiJson here -- change-password returns its // own error messages and we want to surface them verbatim to the user. let overlayShown = false; async function showOverlay(stateOverride) { if (overlayShown) return; overlayShown = true; let state = stateOverride; if (!state) { try { const res = await fetch("/api/auth/state"); if (res.ok) state = await res.json(); } catch { /* network blip -- fall through to login */ } } state = state || { needsSetup: false }; if (state.needsSetup) { const overlay = document.getElementById("setupOverlay"); if (overlay) { overlay.hidden = false; document.getElementById("setupPassword")?.focus(); } } else { const overlay = document.getElementById("loginOverlay"); if (overlay) { overlay.hidden = false; document.getElementById("loginPassword")?.focus(); } } } export function setupAuth() { document.addEventListener("authrequired", () => showOverlay()); setupLoginForm(); setupAccountPanel(); setupSetupForm(); // Eager state probe so the right overlay appears before any API call fails. fetch("/api/auth/state") .then(r => r.ok ? r.json() : null) .then(state => { if (state && !state.authed) showOverlay(state); }) .catch(() => { /* let authrequired handle it */ }); } function setupLoginForm() { const overlay = document.getElementById("loginOverlay"); const input = document.getElementById("loginPassword"); const button = document.getElementById("loginSubmit"); const errorEl = document.getElementById("loginError"); if (!overlay || !input || !button || !errorEl) return; async function tryLogin() { const pw = input.value; if (!pw) return; errorEl.textContent = ""; button.disabled = true; try { const res = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password: pw }), }); if (res.status === 401) { errorEl.textContent = "Wrong password."; input.select(); return; } if (res.status === 429) { errorEl.textContent = "Too many attempts. Wait a minute."; return; } if (!res.ok) { errorEl.textContent = `Error ${res.status}`; return; } // Server set the cookie. Reload so SSE / pollers pick it up. location.reload(); } catch (e) { errorEl.textContent = e.message; } finally { button.disabled = false; input.value = ""; } } button.addEventListener("click", tryLogin); input.addEventListener("keydown", e => { if (e.key === "Enter") tryLogin(); }); } function setupSetupForm() { const overlay = document.getElementById("setupOverlay"); const pw1 = document.getElementById("setupPassword"); const pw2 = document.getElementById("setupConfirm"); const button = document.getElementById("setupSubmit"); const errorEl = document.getElementById("setupError"); if (!overlay || !pw1 || !pw2 || !button || !errorEl) return; async function trySetup() { errorEl.textContent = ""; if (pw1.value.length < 8) { errorEl.textContent = "Must be at least 8 characters."; pw1.select(); return; } if (pw1.value !== pw2.value) { errorEl.textContent = "Passwords don't match."; pw2.select(); return; } button.disabled = true; try { const res = await fetch("/api/auth/setup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password: pw1.value }), }); if (res.status === 429) { errorEl.textContent = "Too many attempts. Wait a minute."; return; } const body = await res.json().catch(() => ({})); if (!res.ok) { errorEl.textContent = body.error || `Error ${res.status}`; return; } // Server has set the cookie and saved the password -- reload to enter // the panel as a freshly-authed session. location.reload(); } catch (e) { errorEl.textContent = e.message; } finally { button.disabled = false; pw1.value = pw2.value = ""; } } button.addEventListener("click", trySetup); pw2.addEventListener("keydown", e => { if (e.key === "Enter") trySetup(); }); } function setupAccountPanel() { const logoutBtn = document.getElementById("acctLogout"); const changeBtn = document.getElementById("acctChangePw"); const form = document.getElementById("acctChangeForm"); const cur = document.getElementById("acctCurrent"); const nxt = document.getElementById("acctNew"); const cnf = document.getElementById("acctConfirm"); const submit = document.getElementById("acctSubmit"); const cancel = document.getElementById("acctCancel"); const msg = document.getElementById("acctMsg"); if (!logoutBtn || !changeBtn) return; logoutBtn.addEventListener("click", async () => { if (!confirm("Log out of the panel?")) return; try { await fetch("/api/auth/logout", { method: "POST" }); } finally { location.reload(); } }); changeBtn.addEventListener("click", () => { form.hidden = !form.hidden; msg.textContent = ""; if (!form.hidden) cur.focus(); }); cancel.addEventListener("click", () => { form.hidden = true; cur.value = nxt.value = cnf.value = ""; msg.textContent = ""; }); submit.addEventListener("click", async () => { msg.className = "acct-msg"; msg.textContent = ""; if (nxt.value.length < 8) { msg.textContent = "New password must be at least 8 characters."; return; } if (nxt.value !== cnf.value) { msg.textContent = "New password and confirmation don't match."; return; } submit.disabled = true; try { const res = await fetch("/api/auth/change-password", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ current: cur.value, next: nxt.value }), }); const body = await res.json().catch(() => ({})); if (!res.ok) { msg.textContent = body.error || `Error ${res.status}`; return; } msg.className = "acct-msg ok"; msg.textContent = "Password changed."; cur.value = nxt.value = cnf.value = ""; setTimeout(() => { form.hidden = true; msg.textContent = ""; }, 1500); } catch (e) { msg.textContent = e.message; } finally { submit.disabled = false; } }); }