web/index.html loads the Firebase compat SDK and defines
window.kcGetPushToken() — inits Firebase from window.KC_FIREBASE, asks for
notification permission, registers the service worker and returns an FCM
token (or null if not configured / denied). web/firebase-messaging-sw.js
shows background notifications.
browser_web.dart exposes getPushToken() over that JS function (stub returns
null off-web). After every successful login AppState fires
_registerForPush() -> POST /api/push/register, best-effort.
Config placeholders carry the known values (projectId konfi-castle-app,
messagingSenderId 307226979593); apiKey / appId / vapidKey still say
REPLACE_ME, so push stays inert until they're filled in — the app runs
either way. flutter analyze/test/build web green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend:
- PATCH /api/wahl/:wahlId (isOpen) to open/close a Wahl.
- GET /api/wahl/:wahlId/teilnehmer (LT): participants with their priorities
and any existing Force-Zuteilung.
- Verified against local Postgres: PATCH toggles isOpen, teilnehmer list
returns, CSV export works. (A stale dev server on :3000 masked this at
first — real routes are fine.)
Client (client/app/):
- Wahl detail: open/close switch, participant list with a "Zuteilen"
(Force-Zuteilung) action, CSV export via a browser download
(browser.downloadText).
- files_admin_screen.dart: LT file upload — browser.pickFile() +
visibility picker -> multipart POST /api/files/:kcId; list existing
files. Reachable from KcDetailScreen.
- browser_web.dart gains pickFile()/downloadText() (native <input file> +
Blob), with throwing stubs for the VM.
- Dropped the file_picker package again (heavy transitive deps, and the
native web input is enough); disk on this box is nearly full.
flutter analyze/test/build web green; backend npm test 56.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend:
- AuthentikStrategy / TokenVerificationService: normalise the issuer's
trailing slash and accept both `iss` spellings (Authentik's discovery
issuer and token `iss` carry a trailing slash; the JWKS URL must not
double it). Wire the real konfi-castle issuer into .env.example.
- team token path now goes through toAuthenticatedUser too, so a local
account flagged isLeitungsteam gets the synthetic global LT membership
regardless of token kind.
- LT-admin controllers (kc, gemeinde, onboarding, sync, teamer) accept
['authentik','team'] so such an account can use them. RolesGuard still
enforces the actual LT/role check.
- app.module serves the Flutter web build from client/app/build/web (SPA
fallback covers the OIDC redirect path /v1/auth/callback), falling back
to the interim client/web/ if it isn't built.
Client (client/app/):
- oidc.dart: Authorization-Code + PKCE against Authentik (discovery, S256
challenge, state, token exchange, refresh). Browser bits (sessionStorage,
redirect, URL) behind a conditional import so `flutter test` still
compiles on the VM.
- AppState handles the ?code= callback on bootstrap, stores access +
refresh, refreshes an expired token on restart.
- Login screen: "Mit Konfi-Castle-ID anmelden" button (Leitungsteam /
Verantwortliche) alongside the local Teamer password form.
- admin_screen.dart: LT-only "Verwaltung" — list/create KCs, per KC the
Gemeinden (list/create) and pending Verantwortlichen requests
(approve/reject). Verified end to end against local Postgres with an
isLeitungsteam account (create KC/Gemeinde, list + approve a request).
flutter analyze/test/build web green; backend npm test 56.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>