98 lines
3.6 KiB
JavaScript
98 lines
3.6 KiB
JavaScript
/* 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.
|
|
*
|
|
* Phase 19 (phase 34 task 03, owner confirmation 2026-08-26): the
|
|
* whoami check runs on the shared header module's cached promise
|
|
* (assets/header.js) — one request per page, and the module's
|
|
* initSharedHeader() settles the login page's FULL shared header —
|
|
* the SAME bar as every other page (nav incl. the admin-only links,
|
|
* Tuning toggle, Sync, New chat, the auth pair).
|
|
*
|
|
* No CDN, no state in this file: the signed cookie is the whole session.
|
|
* All DOM ids match frontend/login.html.
|
|
*/
|
|
|
|
import { fetchIsAdmin, initSharedHeader } from "./header.js";
|
|
|
|
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();
|
|
}
|
|
|
|
/* Phase 19: the shared header module IS the whoami call site (cached
|
|
* promise, anonymous-safe) — same result as the private fetch it
|
|
* replaces: a network failure stays on the form (submit will explain).
|
|
*/
|
|
function alreadySignedIn() {
|
|
return fetchIsAdmin();
|
|
}
|
|
|
|
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.");
|
|
}
|
|
});
|
|
|
|
/* The bar settles in BOTH branches (phase 34 task 05 — the login page
|
|
* carries the FULL shared header, so a signed-in admin who lands here
|
|
* gets the settled admin bar for the frame before the redirect, not
|
|
* the ship-hidden state): the whoami promise is already settled by
|
|
* this point, so initSharedHeader adds no request and no delay, and
|
|
* the phase-16 redirect itself is unchanged. For anonymous visitors it
|
|
* settles the reduced bar: Sign in visible, the admin-only controls
|
|
* stay hidden, the steering panel removed (the navbar toggle no longer
|
|
* ships at all — removed at owner request, 2026-08-28). */
|
|
(async () => {
|
|
const admin = await alreadySignedIn();
|
|
await initSharedHeader();
|
|
if (admin) {
|
|
window.location.replace(safeNext());
|
|
return;
|
|
}
|
|
passwordInput.focus();
|
|
})();
|