feat(auth): single-admin password login (signed cookie) — gate tuning + Sources catalog, keep chat and document viewer public

This commit is contained in:
2026-08-23 19:58:39 -04:00
parent fc0d9a2d5c
commit cbc263a4b2
46 changed files with 1555 additions and 691 deletions
+58 -5
View File
@@ -126,8 +126,11 @@ function announceSteering(message) {
/* "Tune" button in the meta row of a completed brain bubble. Reuses the
sources' .msg-meta row when it exists (role=list → the button joins as
a listitem so ARIA stays valid); otherwise creates a plain meta row. */
a listitem so ARIA stays valid); otherwise creates a plain meta row.
Phase 16: anonymous visitors never get the button — this single guard
covers both fresh turns and the phase-14 restore path. */
function appendTuneButton(wrap) {
if (!isAdmin) return; // phase 16: tuning is admin-only
const body = wrap.querySelector(".msg-body");
if (!body) return;
let meta = body.querySelector(".msg-meta");
@@ -656,6 +659,50 @@ function rememberBrainTurn(rawText, meta) {
* empty state (suggestions included). Ignored while a turn is in flight —
* a live stream must not be hijacked. Confirmation reuses the existing
* #send-status live region (aria-live=polite). */
/* ---------- single-admin auth (phase 16, A10 revised) ----------
*
* /api/whoami decides the header: anonymous → the Sign in link and NO
* tuning surface at all — the Tuning toggle + panel are removed from the
* DOM (the story says "absent", not just hidden), /api/steering is never
* fetched, and appendTuneButton injects nothing (new or restored
* messages). Admin → Sign out (POST /api/logout + reload) + the full
* phase-15 UI. Whoami is awaited BEFORE the phase-14 restore, so restored
* brain bubbles never flash a Tune button that should not be there.
*/
const signInLink = document.querySelector("#sign-in-link");
const signOutBtn = document.querySelector("#sign-out-btn");
let isAdmin = false;
function applyAuthState() {
if (signInLink) signInLink.hidden = isAdmin;
if (signOutBtn) signOutBtn.hidden = !isAdmin;
if (!isAdmin && steeringToggle) {
steeringToggle.remove();
steeringPanel?.remove();
}
}
async function loadAuthState() {
try {
const r = await fetch("/api/whoami");
if (r.ok) isAdmin = (await r.json()).authenticated === true;
} catch {
isAdmin = false; // API unreachable: anonymous-safe defaults
}
applyAuthState();
return isAdmin;
}
if (signOutBtn) {
signOutBtn.addEventListener("click", async () => {
signOutBtn.disabled = true;
try {
await fetch("/api/logout", { method: "POST" });
} catch { /* the reload resets the UI either way */ }
window.location.reload();
});
}
const newChatBtn = document.querySelector("#new-chat-btn");
function startNewChat() {
if (uiState === UI_STATE.thinking || uiState === UI_STATE.streaming) return;
@@ -799,7 +846,13 @@ input.addEventListener("keydown", (e) => {
});
composer.addEventListener("submit", handleSend);
restoreConversation(); // phase 14: the conversation comes back as left
loadSuggestions();
loadHealth();
loadSteering(); // phase 15: tuning notes (panel + count badge)
/* Boot: auth state FIRST — it decides whether the restored conversation
gets Tune buttons and whether the steering UI exists at all (phase 16).
Phase 14: the conversation then comes back exactly as left. */
(async () => {
await loadAuthState();
restoreConversation();
loadSuggestions();
loadHealth();
if (isAdmin) loadSteering(); // phase 15: panel + count badge (admin only)
})();