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>
KC-App client (Flutter)
Single Flutter codebase for the KC-App platform. Web is the only target
enabled so far (flutter config --enable-web); Android/iOS/desktop can be
added later with flutter create --platforms=... in this directory — the
lib/ code is platform-agnostic.
Run
flutter pub get
flutter run -d chrome --dart-define=API_BASE=http://localhost:3000/api
API_BASE defaults to http://localhost:3000/api (the local NestJS
backend, which also serves the interim plain-HTML client at /).
What's implemented
- Login (
lib/screens/login_screen.dart) — three tabs:- Konfi / Gast: KC invite code + first/last name →
POST /auth/guest. - Team-Login: email + password for local Gemeinde Teamer →
POST /auth/team-login. (Leitungsteam / Verantwortliche use the Authentik Authorization-Code flow, not yet wired into this client.) - Einladung: redeem a Teamer invite token →
POST /auth/teamer/register.
- Konfi / Gast: KC invite code + first/last name →
- The token is stored via
shared_preferences(localStorage on web) and restored on start;GET /auth/meresolves the role for a role-aware home. - Home (
lib/screens/home_screen.dart) — identity card + navigation. - Workshop-Wahl (
lib/screens/wahl_screen.dart, guests) — two tabs: Wünsche (GET /wahl/guest/overview, tap workshops in order, max 3,POST /wahl/:id/teilnehmer) and Ergebnis (GET /wahl/guest/results— PENDING / ASSIGNED with workshop + wish rank / UNASSIGNED). - Dateien (
lib/screens/files_screen.dart) —GET /files/:kcId, filtered server-side by the caller's visibility tier. - Chat (
lib/screens/chat_screen.dart+lib/chat_socket.dart) — channel list, REST history, then a live/chatWebSocket connection (chat:join/chat:send/chat:message) with a compose bar.
Architecture
lib/api.dart—Api(thin REST wrapper + models) andAppState(ChangeNotifier: session, login/logout, token persistence).lib/main.dart—AppScope(anInheritedNotifier<AppState>) exposesAppScope.of(context);_AuthGateswitches Login/Home. No third-party state-management package.