feat(auth): single-admin password login (signed cookie) — gate tuning + Sources catalog, keep chat and document viewer public
This commit is contained in:
+58
-5
@@ -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)
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user