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.
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
- Config (fail-loud):
BOR_ADMIN_PASSWORD(plaintext,.env) andBOR_SESSION_SECRET(random hex) — if either is unset the app refuses to start with a message naming the missing variable(s). POST /api/login {password}→ 204 + signed session cookie (StarletteSessionMiddleware,same_site=lax, 12h default max age); wrong password → 401{"detail": "invalid password"}(constant-time compare, one generic message — no user enumeration).POST /api/logout→ 204 (session cleared, idempotent);GET /api/whoami→{"authenticated": bool, "role": "admin"|"anonymous"}.- Admin-only:
GET /api/docsandGET/POST/DELETE/api/steering→ 403 for anonymous. Public (unchanged):/api/chat,/api/documents/content,/api/suggestions,/api/health, all static pages. - Login page
/login.html(no CDN, Phase-08 tokens, WCAG 2.1 AA): app-frame header, labeled password field, submit ≥44px,role=alerterror region; on success redirects to?next(same-origin only, default/sources.html); an already-signed-in visit redirects immediately. - 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. - Sources page: anonymous sees
#sources-gate("sign in to view the full catalog" + sign-in link) with stat cards and#docs-tablehidden and no/api/docscall; admin sees the current full Sources page. - Document viewer: unchanged — anonymous can open any indexed document by direct URL.
- Non-goals: no rate limiting / account lockout, no HTTPS enforcement (homelab HTTP), no multi-user, no per-user history, no schema change.
- Unit + integration green,
app/coverage >90%, story E2E green in isolation, regression suites adapted + green in isolation, one--no-gpg-signcommit.
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):
test_anonymous_chat_without_tuning— question → streamed answer renders; no.tune-btnunder the brain bubble;#steering-toggleabsent;#sign-in-linkvisible,#sign-out-btnnot.test_anonymous_sources_gated_viewer_open—/sources.html:#sources-gatevisible,#docs-tablehidden; then/document.html?source=…&path=…(seeded fixture doc) renders its content while anonymous.test_login_wrong_password_shows_error— wrong password →#login-error(role=alert) visible, no redirect, still anonymous.test_admin_login_unlocks_sources_and_tuning— correct password → redirect to/sources.htmlwith stats + table visible; chat page shows#steering-toggle(count badge) and.tune-btnunder the answer;#sign-out-btnvisible,#sign-in-linknot.test_logout_returns_to_anonymous— Sign out → tuning hidden,#sign-in-linkback,/sources.htmlgated again.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).