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
+26
View File
@@ -45,6 +45,21 @@ def cookie_kwargs() -> dict:
return kwargs
def single_user_owner() -> Owner:
"""The one Owner every request shares under `COOKIE_SECURE=single_user`.
No cookie is read, so the owner has to be findable from the DB alone: the
oldest row wins. That way a deployment that used to run cookie-scoped keeps
the printers it already had instead of waking up empty, and repeat requests
never mint a second row. It is marked permanent so nothing treats it as a
throwaway first-visit session.
"""
owner = Owner.select().order_by(Owner.id).first()
if owner is None:
owner = Owner.create(key=generate_key(), is_permanent=True)
return owner
def _request_hosts(request: Request) -> set[str]:
"""Every host spelling that legitimately identifies this deployment."""
hosts = {request.url.netloc}
@@ -98,6 +113,16 @@ class OwnerSessionMiddleware(BaseHTTPMiddleware):
if path in _UNSCOPED_PATHS or path.startswith(_UNSCOPED_PREFIXES):
return await call_next(request)
if cfg.SINGLE_USER:
# No cookie is read and none is set: identity comes from the DB, so
# onboarding ("keep your printers?") and the ephemeral-cookie
# warning are both meaningless here.
request.state.owner = single_user_owner()
request.state.is_new_owner = False
request.state.ephemeral_session = False
request.state.single_user = True
return await call_next(request)
key = request.cookies.get(COOKIE_NAME)
owner = Owner.get_or_none(Owner.key == key) if key else None
is_new = owner is None
@@ -106,6 +131,7 @@ class OwnerSessionMiddleware(BaseHTTPMiddleware):
request.state.owner = owner
request.state.is_new_owner = is_new
request.state.single_user = False
# Templates warn about it; see cookie_kwargs().
request.state.ephemeral_session = not cfg.COOKIE_SECURE