refactor(agents): migrate .agent/ planning tree to .agents/
Standardize on the .agents/ directory (shared with project skills): phases/, user_stories/, reports/, screenshots/, validate.sh, and phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves history; runtime artifacts move alongside). Updates every reference in AGENTS.md, README.md, .gitignore, app docstrings, and test story headers. Historical KB content in data/ and the runtime pipeline.log transcript are left untouched.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user