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