Files
brain-of-reese/.agent/user_stories/admin-auth.md
T
ducoterra 4971e2859d
Build and Push Containers / build-and-push-app (push) Successful in 12s
Build and Push Containers / build-and-push-db (push) Successful in 10s
chore(agent): track .agent/ planning tree in git
Remove the blanket .agent/ gitignore so the phase roadmap, user
stories, reports, and PLAN.md are versioned with the code. Only
runtime artifacts (.agent/phase-sessions/, .agent/pipeline.log)
remain ignored. Update AGENTS.md git protocol rule to match.
2026-09-01 10:18:22 -04:00

4.8 KiB

Story: Admin Sign-In (Single-Admin Auth)

Phase: 16_admin_auth.md · E2E: tests/e2e/test_admin_auth.py

Narrative

As the one admin (the owner), I want to sign in with a password so that I — and only I — can tune how Brain answers (Phase 15 steering notes) and browse the entire Sources catalog, while anyone else stays anonymous and can still chat and open the documents the chat cites.

  • Given I am not signed in
  • When I open the app
  • Then chat, suggestion chips, and the document viewer all work, but the tuning UI is gone (no Tune button, no Tuning panel) and the Sources page shows a "sign in to view" state; a Sign in link is in the header.
  • When I sign in with the admin password
  • Then the tuning UI and the full Sources page are unlocked, and the header offers Sign out.
  • Anonymous users may open any indexed document by direct URL (soft rule — the catalog is what is gated, not the viewer).

Acceptance criteria

  1. Config (fail-loud): BOR_ADMIN_PASSWORD (plaintext, .env) and BOR_SESSION_SECRET (random hex) — if either is unset the app refuses to start with a message naming the missing variable(s).
  2. POST /api/login {password} → 204 + signed session cookie (Starlette SessionMiddleware, same_site=lax, 12h default max age); wrong password → 401 {"detail": "invalid password"} (constant-time compare, one generic message — no user enumeration).
  3. POST /api/logout → 204 (session cleared, idempotent); GET /api/whoami → {"authenticated": bool, "role": "admin"|"anonymous"}.
  4. Admin-only: GET /api/docs and GET/POST/DELETE /api/steering → 403 for anonymous. Public (unchanged): /api/chat, /api/documents/content, /api/suggestions, /api/health, all static pages.
  5. Login page /login.html (no CDN, Phase-08 tokens, WCAG 2.1 AA): app-frame header, labeled password field, submit ≥44px, role=alert error region; on success redirects to ?next (same-origin only, default /sources.html); an already-signed-in visit redirects immediately.
  6. Chat page: header shows Sign in when anonymous (→ /login.html?next=/sources.html) or Sign out when admin (logout → reload); when anonymous the Tune button (including on localStorage restored messages, Phase 14) and the Tuning panel/toggle are completely hidden; the chat flow itself is unchanged.
  7. Sources page: anonymous sees #sources-gate ("sign in to view the full catalog" + sign-in link) with stat cards and #docs-table hidden and no /api/docs call; admin sees the current full Sources page.
  8. Document viewer: unchanged — anonymous can open any indexed document by direct URL.
  9. Non-goals: no rate limiting / account lockout, no HTTPS enforcement (homelab HTTP), no multi-user, no per-user history, no schema change.
  10. Unit + integration green, app/ coverage >90%, story E2E green in isolation, regression suites adapted + green in isolation, one --no-gpg-sign commit.

Playwright Mapping Rule

Test Scenario → tests/e2e/test_admin_auth.py (the E2E app server is started with BOR_ADMIN_PASSWORD/BOR_SESSION_SECRET set in tests/e2e/conftest.py; the shared tests/e2e/auth_helpers.py::login performs the real form login):

  1. test_anonymous_chat_without_tuning — question → streamed answer renders; no .tune-btn under the brain bubble; #steering-toggle absent; #sign-in-link visible, #sign-out-btn not.
  2. test_anonymous_sources_gated_viewer_open — /sources.html: #sources-gate visible, #docs-table hidden; then /document.html?source=…&path=… (seeded fixture doc) renders its content while anonymous.
  3. test_login_wrong_password_shows_error — wrong password → #login-error (role=alert) visible, no redirect, still anonymous.
  4. test_admin_login_unlocks_sources_and_tuning — correct password → redirect to /sources.html with stats + table visible; chat page shows #steering-toggle (count badge) and .tune-btn under the answer; #sign-out-btn visible, #sign-in-link not.
  5. test_logout_returns_to_anonymous — Sign out → tuning hidden, #sign-in-link back, /sources.html gated again.
  6. test_login_page_a11y — password field labeled, error announced via role=alert, targets ≥44px, focus-visible; signed-in visit to /login.html?next=/ redirects immediately.

Regression adaptations (this phase makes their preconditions admin-only): test_steering.py (log in before tuning), test_import_documents.py + test_document_back_navigation.py (log in for Sources-table assertions), test_header_consistency.py (header gains the auth link — extend presence assertions; heights unchanged).