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
+66 -1
View File
@@ -83,10 +83,72 @@ uv run uvicorn app.main:app --reload
is shown as escaped monospace text. Unknown documents get a designed
not-found state with a link back to the index.
- **Sources** (`/sources.html`) — the indexed document list; the *Path*
column links each document to the viewer in a new tab.
column links each document to the viewer in a new tab. **Admin-only** —
anonymous visitors see a sign-in gate instead (the catalog is what the
login locks; the document viewer itself stays open to everyone).
## Admin & sign-in
Brain of Reese has exactly **one account: the admin (you)**. Signing in
unlocks the **full Sources catalog** and the **answer-tuning** controls;
everyone else stays anonymous and keeps **chat** and the **document
viewer** (any document an answer cites can be opened by its direct URL —
the catalog is gated, not the viewer).
### Setup (one-time)
```bash
python -c 'import secrets;print(secrets.token_hex(32))' # → paste into .env
```
```env
BOR_ADMIN_PASSWORD=your-password # plaintext — homelab scope, by design
BOR_SESSION_SECRET=<the hex from above> # signs the session cookie
```
**Fail-loud:** while either variable is empty the app refuses to start,
naming the missing one(s):
```
RuntimeError: Brain of Reese cannot start: admin auth is not configured.
Set the missing variable(s): BOR_ADMIN_PASSWORD, BOR_SESSION_SECRET …
```
### How it works
- `POST /api/login {"password": …}` → `204` + signed `bor_session` cookie
(Starlette `SessionMiddleware` — an itsdangerous-signed cookie, no
server-side store, no new service, no DB table); any mismatch → `401`
`{"detail": "invalid password"}` (constant-time compare, one generic
message — no user enumeration, there is only one user).
- `POST /api/logout` → `204` (session cleared and cookie expired;
idempotent for anonymous callers).
- `GET /api/whoami` → `{"authenticated": bool, "role": "admin"|"anonymous"}`
— the single source of truth for every UI gating decision.
- Cookie flags: `same_site="lax"`, `https_only` off — **no HTTPS
enforcement on purpose** (homelab HTTP; the cookie is single-admin
convenience, not a cloud boundary). Max age `BOR_SESSION_MAX_AGE`
(default `43200` = 12 h, refreshed while active).
- Sign in from the chat header (**Sign in**) or `/login.html` directly;
the header then offers **Sign out** (logout + reload).
### Who can do what
| Capability | Anonymous | Admin (signed in) |
|---|---|---|
| Chat (`/`) + suggestion chips | yes | yes |
| Document viewer (`/document.html?source=…&path=…`) | yes — any indexed doc by direct URL | yes |
| Sources catalog (`/sources.html`, `GET /api/docs`) | sign-in gate | full catalog |
| Tuning (Tune button, Tuning panel, `/api/steering`) | UI hidden | full |
The public API endpoints stay stateless — the signed cookie is the only
session state in the system.
## Tuning your answers
*Admin-only* — sign in first (see **Admin & sign-in** above); anonymous
visitors never see the Tune button or the Tuning panel.
If an answer isn't quite right — too chatty, wrong assumption, missing
context — **tune** Brain right there:
@@ -300,6 +362,9 @@ served locally (no CDN), `BOR_ENVIRONMENT=production`.
| `BOR_MAX_CONTEXT_CHARS` | `24000` | cap on total document text sent to the LLM |
| `BOR_STEERING_MAX_CHARS` | `8000` | char budget for the `<tuning>` (steering notes) prompt section |
| `BOR_SUGGESTIONS` | built-in list | JSON list of onboarding chips |
| `BOR_ADMIN_PASSWORD` | *(required)* | the single admin's password (plaintext, `.env`); app refuses to start when empty |
| `BOR_SESSION_SECRET` | *(required)* | signing key for the `bor_session` cookie; `python -c 'import secrets;print(secrets.token_hex(32))'` |
| `BOR_SESSION_MAX_AGE` | `43200` | session-cookie lifetime in seconds (12 h, sliding) |
| `DEBUGPY` | `0` | `1` ⇒ attach-on-demand debugpy on `DEBUGPY_PORT` (default 5678) |
| `BOR_LOG_LEVEL` | `INFO` | app log level |