Add optional Authentik (OIDC) sign-in with server-side preference sync

Signed-in users get their preferences (hidden feeds, weather location,
weather toggle) stored server-side keyed by their OIDC subject and synced
across devices. Anonymous visitors keep using browser localStorage, so the
site stays public. First sign-in migrates existing localStorage prefs up.

- config: [web.auth] section (issuer, client_id/secret, redirect, session_secret)
- storage: user_preferences table + Get/PutUserPrefs
- web/auth: OIDC code flow, HMAC-signed session cookie, CSRF state + nonce
- web/prefs_api: GET/PUT /api/preferences (auth-gated, 64KB cap)
- frontend: prefs.js sync layer seeds localStorage from server, pushes on write
- header: sign-in / account control

OIDC discovery is non-fatal at boot: if Authentik is down, Pete serves
anonymously rather than refusing to start.
This commit is contained in:
prosolis
2026-06-21 15:44:53 -07:00
parent 1a43616248
commit cbbedd9894
18 changed files with 722 additions and 32 deletions

View File

@@ -33,6 +33,22 @@ listen_addr = ":8080"
site_title = "Pete"
base_url = "https://news.parodia.dev"
# Optional OIDC sign-in (Authentik). When enabled, signed-in users get their
# preferences (hidden feeds, weather location, toggles) stored server-side keyed
# by their OIDC subject and synced across devices. Anonymous visitors keep using
# browser localStorage — the site stays public. If the provider is unreachable
# at startup, Pete logs a warning and serves anonymously rather than refusing to
# boot. Create an OAuth2/OIDC provider + application in Authentik, set the
# redirect URI to <base_url>/auth/callback, then fill these in.
[web.auth]
enabled = false
issuer = "https://authentik.parodia.dev/application/o/pete/"
client_id = "${PETE_OIDC_CLIENT_ID}"
client_secret = "${PETE_OIDC_CLIENT_SECRET}"
redirect_url = "https://news.parodia.dev/auth/callback"
# HMAC key that signs the session cookie. Generate with: openssl rand -hex 32
session_secret = "${PETE_SESSION_SECRET}"
# Every enabled source MUST set direct_route to a key from [matrix.channels] above.
# There is no automatic classification — Pete posts each story to its configured channel.
# Optional: language = "en" drops feed items whose per-item <language> tag is