feat(auth): single-admin password login (signed cookie) — gate tuning + Sources catalog, keep chat and document viewer public
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
/* Brain of Reese — admin sign-in (phase 16, A10 revised).
|
||||
*
|
||||
* One admin, one password. On submit → POST /api/login: 204 sets the
|
||||
* signed session cookie and we redirect to `?next` (same-origin relative
|
||||
* URLs only — "/…" but never "//host" or an absolute URL; default
|
||||
* /sources.html). A 401 keeps the form and announces through the
|
||||
* role=alert error region. On load, /api/whoami already says admin →
|
||||
* straight to `next`, no form.
|
||||
*
|
||||
* No CDN, no state in this file: the signed cookie is the whole session.
|
||||
* All DOM ids match frontend/login.html.
|
||||
*/
|
||||
|
||||
const form = document.querySelector("#login-form");
|
||||
const passwordInput = document.querySelector("#login-password");
|
||||
const submitBtn = document.querySelector("#login-submit");
|
||||
const errorEl = document.querySelector("#login-error");
|
||||
|
||||
const DEFAULT_NEXT = "/sources.html";
|
||||
|
||||
/* Same-origin relative URLs only: honor `?next=/…`, reject anything that
|
||||
would leave the origin (protocol-relative "//…" or absolute). */
|
||||
function safeNext() {
|
||||
const next = new URLSearchParams(window.location.search).get("next") || DEFAULT_NEXT;
|
||||
return next.startsWith("/") && !next.startsWith("//") ? next : DEFAULT_NEXT;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
errorEl.textContent = message;
|
||||
errorEl.hidden = false;
|
||||
submitBtn.disabled = false;
|
||||
passwordInput.focus();
|
||||
passwordInput.select();
|
||||
}
|
||||
|
||||
async function alreadySignedIn() {
|
||||
try {
|
||||
const r = await fetch("/api/whoami");
|
||||
if (!r.ok) return false;
|
||||
return (await r.json()).authenticated === true;
|
||||
} catch {
|
||||
return false; // API unreachable: stay on the form — submit will explain
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
errorEl.hidden = true;
|
||||
errorEl.textContent = "";
|
||||
submitBtn.disabled = true;
|
||||
try {
|
||||
const r = await fetch("/api/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ password: passwordInput.value }),
|
||||
});
|
||||
if (r.status === 204) {
|
||||
// Session cookie set — off to the requested page.
|
||||
window.location.replace(safeNext());
|
||||
return;
|
||||
}
|
||||
// One generic failure (401); anything else is a server-side surprise.
|
||||
const detail =
|
||||
r.status === 401
|
||||
? "Invalid password — try again."
|
||||
: `Sign-in failed (HTTP ${r.status}) — try again.`;
|
||||
showError(detail);
|
||||
} catch {
|
||||
showError("Could not reach the server — try again.");
|
||||
}
|
||||
});
|
||||
|
||||
/* Already the admin? Skip the form and go straight to the target. */
|
||||
(async () => {
|
||||
if (await alreadySignedIn()) {
|
||||
window.location.replace(safeNext());
|
||||
return;
|
||||
}
|
||||
passwordInput.focus();
|
||||
})();
|
||||
Reference in New Issue
Block a user