feat: session mode parsing, memory-only cookies, single-user mode

- COOKIE_SECURE env now accepts: 'true' (secure), 'false' (memory-only), 'single_user' (no cookie)
- config.parse_cookie_mode() returns (COOKIE_SECURE, SINGLE_USER) tuple for routing
- single_user_owner() returns oldest Owner for test/local deployments
- session cookie respects mode: Max-Age only in secure mode, dropped for memory-only
- base.html renders ephemeral-session and single-user banners per mode
- Tests: comprehensive coverage for all three modes with monkeypatch configs
- CLAUDE.md + README docs updated

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 09:21:18 +02:00
co-authored by Claude Haiku 4.5
parent a8bcf7cdeb
commit f70ba93e7a
9 changed files with 231 additions and 12 deletions
+16 -2
View File
@@ -115,7 +115,7 @@ on narrow screens or in the add-printer sidebar (`.form-aside`).
|-----|---------|---------|
| `DATA_DIR` | `/data` | Storage root (DB + drivers + icons) |
| `PORT` | `8000` | Server port |
| `COOKIE_SECURE` | `true` | Owner-session cookie `Secure` flag. Set `false` for plain-HTTP serving or the browser drops the cookie and a new Owner is created on every request. `false` also makes the cookie **memory-only** (no `Max-Age`) — see below. |
| `COOKIE_SECURE` | `true` | Three-way session mode, parsed by `config.parse_cookie_mode()` into `(COOKIE_SECURE, SINGLE_USER)`: `true` = Secure + 10-year cookie; `false` = plain-HTTP serving (browser drops a Secure cookie new Owner per request), cookie becomes **memory-only** (no `Max-Age`); `single_user` (or `single-user`/`single`) = no cookie at all, one shared Owner — see below. |
`COOKIE_SECURE=false` degrades the session instead of weakening the credential:
`services/session.cookie_kwargs()` drops `max_age`, so the browser holds the
@@ -125,4 +125,18 @@ cookie-setting call sites (the middleware and `POST /session/restore`) must go
through `cookie_kwargs()`. `request.state.ephemeral_session` mirrors the flag,
and `base.html` renders the `#ephemeral-session-warning` banner plus an extra
paragraph in the onboarding modal off it. Changing this touches
`tests/test_session.py::test_insecure_mode_*` / `test_secure_mode_*`.
`tests/test_session.py::test_insecure_mode_*` / `test_secure_mode_*`.
`COOKIE_SECURE=single_user` (`cfg.SINGLE_USER`) removes sessions for test boxes
and single-person local prod: the middleware never reads or sets a cookie and
returns `services/session.single_user_owner()` — the **oldest** `Owner` row,
created on demand — so a deployment switched over from cookie mode keeps the
printers it already had and no second row is ever minted. `request.state.owner`
is still what every route filters on, so per-owner query code is unchanged.
`request.state.single_user` gates the sidebar "This session" menu and the
`#single-user-notice` banner in `base.html`; `is_new_owner`/`ephemeral_session`
are forced `False` (no onboarding modal, no memory-only warning). All three
`/session/*` routes 404 via `api/session._require_cookie_sessions()` — a key
can't re-point a cookie that isn't read, and downloading one would leak the
shared owner's bearer credential for a later switch back to cookie mode. Tests:
`test_session.py::test_single_user_*` + `test_parse_cookie_mode_*`.