docs: push notifications module + FCM client wiring
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -61,17 +61,21 @@ in (`backend/prisma/migrations/`); the backend has been run end to end
|
||||
against a local PostgreSQL 16. `npm test` covers the assignment algorithm
|
||||
and the new auth/onboarding services (56 tests).
|
||||
|
||||
Phase 7 (Flutter client) in progress: `client/app/` is a single Flutter
|
||||
codebase with the **web** target enabled — guest / local-Teamer / invite
|
||||
login, the Authentik Authorization-Code + PKCE flow (`lib/oidc.dart`) for
|
||||
Phase 7 (Flutter client): `client/app/` is a single Flutter codebase with
|
||||
the **web** target enabled — guest / local-Teamer / invite login, the
|
||||
Authentik Authorization-Code + PKCE flow (`lib/oidc.dart`) for
|
||||
Leitungsteam/Verantwortliche, role-aware home, guest Workshop-Wahl (wishes +
|
||||
result), file list, live WebSocket chat, and a Leitungsteam admin screen
|
||||
(KCs, Gemeinden, onboarding approvals). `flutter build web` / `flutter test`
|
||||
pass; the backend serves the build at `/` (SPA fallback covers the OIDC
|
||||
redirect `/v1/auth/callback`). Still to do: a live browser test of the OIDC
|
||||
round-trip, the Teamer-management and Verantwortlichen-self-registration
|
||||
screens, LT Wahl administration, mobile/desktop targets, and push.
|
||||
result), file list, live WebSocket chat, FCM web-push registration, and the
|
||||
Leitungsteam admin screens: KCs, Gemeinden, onboarding approvals, full
|
||||
Workshop-Wahl administration (create/open/close, workshops, Force-Zuteilung,
|
||||
run assignment, CSV export), Teamer accounts + invites, LT file upload, plus
|
||||
the Verantwortlichen self-registration flow. `flutter build web` /
|
||||
`flutter test` pass; the backend serves the build at `/` (SPA fallback
|
||||
covers the OIDC redirect `/v1/auth/callback`).
|
||||
|
||||
Running end to end needs the Authentik redirect registered + a test account,
|
||||
plus Nextcloud/S3 credentials (see `backend/.env.example`).
|
||||
Still to do: a live browser test of the OIDC round-trip; mobile/desktop
|
||||
targets. Going live needs external config — the Authentik redirect + a test
|
||||
account, Nextcloud/S3 credentials, SMTP, and the Firebase push secrets
|
||||
(`apiKey`/`appId`/VAPID key + a service-account JSON). See
|
||||
`backend/.env.example` and `client/app/web/index.html`.
|
||||
|
||||
|
||||
+14
-7
@@ -83,6 +83,13 @@ client's host - no separate web server is needed.
|
||||
`MailService.sendTeamerInvite()` composes the personal-invite email with a
|
||||
link built from `APP_BASE_URL`. Delivery is best-effort — failures are
|
||||
logged and swallowed, never blocking the invite.
|
||||
- `push/` — global `PushProvider` abstraction; default `log`, `PUSH_PROVIDER=fcm`
|
||||
uses FCM HTTP v1 (service-account JWT → OAuth token, no extra dep;
|
||||
`FCM_PROJECT_ID`, `GOOGLE_APPLICATION_CREDENTIALS`). `DeviceToken` rows
|
||||
(bound to a `User` or `GuestAccount`) via `POST /push/register` +
|
||||
`/unregister`. `PushService.notifyChannel()` resolves a channel's readable
|
||||
audience → their tokens (minus the sender) → send, pruning invalid ones;
|
||||
`ChatService.sendMessage()` fires it best-effort.
|
||||
- `wahl/` — Wahl/Workshop administration (Leitungsteam-only), guest
|
||||
Teilnehmer submission, Force-Zuteilung overrides, and `ZuteilungService`:
|
||||
a faithful port of the WP plugin's `kc_run_zuteilung` (force-assignments →
|
||||
@@ -119,11 +126,11 @@ client's host - no separate web server is needed.
|
||||
- `common/` — `Role` enum, `@Roles()` decorator, `RolesGuard` (KC-scoped,
|
||||
Leitungsteam roles are global across all KCs).
|
||||
|
||||
All planned backend phases are implemented. `npm test` runs Jest unit tests
|
||||
(`ZuteilungService`, `TeamAuthService`, `TeamerService`, `OnboardingService`,
|
||||
All planned backend features are implemented (`prisma/migrations/` holds the
|
||||
schema history). `npm test` runs Jest unit tests (`ZuteilungService`,
|
||||
`TeamAuthService`, `TeamerService`, `OnboardingService`,
|
||||
`resolveOrProvisionAuthentikUser` / `toAuthenticatedUser`; Prisma mocked).
|
||||
Remaining work: the Flutter clients (see repo root README), push
|
||||
notifications, and the first real Prisma migration (only `schema.prisma`
|
||||
exists so far). Ops notes: the Authentik provider must emit a `groups` claim
|
||||
for the LT check, and `MAIL_PROVIDER=smtp` + `SMTP_*` must be set for invite
|
||||
emails to actually leave the box.
|
||||
Ops notes to go live: the Authentik provider must emit a `groups` claim for
|
||||
the LT check; `MAIL_PROVIDER=smtp` + `SMTP_*` for invite emails;
|
||||
`PUSH_PROVIDER=fcm` + a Firebase service-account JSON for push; and real
|
||||
Nextcloud/S3 credentials for file storage.
|
||||
|
||||
@@ -59,6 +59,11 @@ Teamer login only).
|
||||
(`verantwortliche_register_screen.dart`) — shown on the home screen to a
|
||||
logged-in Authentik user without a membership: enter a KC invite code,
|
||||
pick a Gemeinde, submit; a Leitungsteam member then approves.
|
||||
- **Push (web)** — `web/index.html` loads the Firebase compat SDK and
|
||||
`web/firebase-messaging-sw.js` handles background messages. After login
|
||||
`AppState` calls `window.kcGetPushToken()` and registers the token
|
||||
(`POST /push/register`). Inert until `apiKey` / `appId` / `vapidKey` are
|
||||
filled into both files (see the `REPLACE_ME` placeholders).
|
||||
- **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` —
|
||||
|
||||
@@ -44,6 +44,7 @@ Vollständiges Schema: [backend/prisma/schema.prisma](backend/prisma/schema.pris
|
||||
| Auth (Gemeinde Teamer) | Lokale Accounts: `User` mit `passwordHash`+`kcId`, `authentikSub` bleibt leer; eigenes JWT (`TEAM_JWT_SECRET`, Payload `typ:'team'`), Passwort-Login oder Invite-Redemption. Verantwortliche legen Teamer an (Direkt/Gruppen-Link/E-Mail-Invite) | Nutzervorgabe: Teamer laufen nicht über die Konfi-Castle-ID (Authentik), sondern werden pro KC lokal verwaltet (wie Guests, nur dauerhaft + mit Rolle) |
|
||||
| Datei-Storage | Provider-Abstraktion (`StorageProvider`), Default **Nextcloud/WebDAV**, umschaltbar auf S3 via `STORAGE_PROVIDER=s3` | Nutzer bestätigte: Nextcloud-Zugangsdaten kommen aus `.env` |
|
||||
| E-Mail | Provider-Abstraktion (`MailProvider`), Default **log-only** (kein Versand), umschaltbar auf SMTP via `MAIL_PROVIDER=smtp` (`nodemailer`) | Spiegelt das Storage-Muster; E-Mail ist best-effort und darf den Invite-Flow nie blockieren |
|
||||
| Push | Provider-Abstraktion (`PushProvider`), Default **log-only**, umschaltbar auf **FCM HTTP v1** via `PUSH_PROVIDER=fcm` (Service-Account-JWT → OAuth-Token, ohne extra Dependency). Client: Firebase-Compat-SDK im `index.html` + `firebase-messaging-sw.js`, Token via `POST /api/push/register` | Gleiches Muster wie Storage/E-Mail; Push ist best-effort und blockiert das Senden nie |
|
||||
| Chat-Transport | Raw `ws`-Gateway (`@nestjs/platform-ws`) statt Socket.IO | Passt zum schlanken REST-Stack; Rollen/Sichtbarkeits-Logik zentral in `ChatService`, geteilt zwischen REST und WS |
|
||||
| Sync-Richtung | **Lokaler Server initiiert immer** Push *und* Pull gegen die Cloud-URL | Cloud kann i. d. R. nicht in ein lokales Eventnetzwerk zurückwählen (NAT); lokaler Server kann aber ausgehend zur Cloud verbinden, wenn Internet verfügbar ist |
|
||||
| Sync-Konflikte | Keine Konfliktauflösung nötig | Nutzer bestätigte explizit: lokaler Server ist während eines laufenden Events alleinige Quelle der Wahrheit |
|
||||
@@ -51,7 +52,7 @@ Vollständiges Schema: [backend/prisma/schema.prisma](backend/prisma/schema.pris
|
||||
|
||||
### Bekannte Einschränkungen / offene Punkte
|
||||
- **Datei-Bytes werden nicht repliziert** – nur Metadaten (inkl. `storageKey`) laufen durchs Sync-Log. Lokaler und Cloud-Server müssen denselben Nextcloud/S3-Backend-Zugriff haben, damit ein `storageKey` auf beiden Seiten auflösbar ist.
|
||||
- **Push-Benachrichtigungen (FCM/APNs)** sind im Plan vorgesehen, aber noch nicht implementiert (Teil der noch ausstehenden Flutter-Client-Arbeit).
|
||||
- **Push-Benachrichtigungen**: `push/`-Modul (FCM HTTP v1) + `DeviceToken`-Modell + `POST /api/push/register`/`unregister`; `ChatService.sendMessage` fächert die Nachricht best-effort an die Kanal-Zielgruppe. Default-Provider **log-only**. Für echten Versand fehlen: die Firebase-Web-Secrets (`apiKey`, `appId`, VAPID-Key) in `client/app/web/index.html` + `firebase-messaging-sw.js`, sowie backend-seitig ein Service-Account-JSON + `PUSH_PROVIDER=fcm`. APNs (natives iOS) ist nicht separat gebaut — läuft über FCM, sobald ein iOS-Target existiert.
|
||||
- **Gemeinde-Verwaltung (CRUD)**: über `GemeindeController` (LT-only) verfügbar — Anlegen/Auflisten/Lesen/Umbenennen/Löschen von Gemeinden pro KC. Gemeinde Verantwortliche/Teamer erfahren ihre eigene Gemeinde weiterhin aus der `Membership`, nicht über diesen Endpunkt.
|
||||
- **Authentik-Provisionierung**: Jeder gültige Authentik-Login legt den lokalen `User` automatisch an (`AuthentikStrategy` → `resolveOrProvisionAuthentikUser`, JIT, race-sicher) **und** setzt `isLeitungsteam` aus dem `groups`-Claim. Voraussetzung: der Authentik-Provider muss den `groups`-Claim ins Access-Token schreiben (Scope „groups" hinzufügen) und der LT-Gruppenname muss zu `AUTHENTIK_LEITUNGSTEAM_GROUP` passen — sonst wird niemand als LT erkannt. Gemeinde Verantwortliche brauchen weiterhin die `onboarding/`-Freigabe durch LT. (Gemeinde Teamer sind lokale Accounts, siehe `teamer/` + `auth/team-login`.) Der Client-seitige PKCE-Flow (`oidc.dart`) ist gebaut, aber der volle Browser-Roundtrip ist noch nicht live getestet — dafür muss der genutzte Redirect (`http://localhost:3000/v1/auth/callback` bzw. die Prod-URL) am Authentik-Provider hinterlegt sein und ein Testaccount existieren.
|
||||
- **Teamer-Identität ist E-Mail-basiert und global eindeutig**: `User.email` ist instanzweit unique, d. h. dieselbe E-Mail kann nicht gleichzeitig Teamer in zwei KCs sein. Für den „pro KC wie Guests"-Fall in der Praxis unkritisch, aber dokumentiert.
|
||||
@@ -70,6 +71,7 @@ Vollständiges Schema: [backend/prisma/schema.prisma](backend/prisma/schema.pris
|
||||
| `onboarding/` | Selbstregistrierung Gemeinde Verantwortliche/r: öffentlicher Invite-Lookup, JIT-`User`-Anlage aus Authentik-Claims, `Membership` im Status `PENDING`; LT sieht/genehmigt/lehnt ab | `GET /api/onboarding/kc/:inviteCode`, `POST /api/onboarding/verantwortliche` (Authentik-Bearer), `GET /api/onboarding/requests?kcId=` (LT), `POST /api/onboarding/requests/:id/approve\|reject` (LT) |
|
||||
| `teamer/` | Lokale Gemeinde-Teamer-Accounts + Invites; Direkt-Anlage, Gruppen-Link und E-Mail-Invite (persönliche Invites werden per `MailService` best-effort verschickt, `emailSent` im Response); nutzbar von LT (jede Gemeinde) oder Verantwortliche/r (nur eigene Gemeinde, in `TeamerService` geprüft) | `POST/GET /api/gemeinde/:gemeindeId/teamer`, `DELETE /api/gemeinde/:gemeindeId/teamer/:userId`, `POST/GET /api/gemeinde/:gemeindeId/teamer-invites`, `DELETE .../teamer-invites/:id` |
|
||||
| `mail/` | Globale `MailProvider`-Abstraktion (log-only Default, SMTP via `MAIL_PROVIDER=smtp`); `MailService` baut die Invite-Mail inkl. Link aus `APP_BASE_URL` | – |
|
||||
| `push/` | Globale `PushProvider`-Abstraktion (log-only Default, FCM HTTP v1 via `PUSH_PROVIDER=fcm`); `PushService.notifyChannel` löst die Kanal-Zielgruppe auf → `DeviceToken`s → Versand, prunt ungültige Tokens; von `ChatService.sendMessage` best-effort ausgelöst | `POST /api/push/register`, `POST /api/push/unregister` |
|
||||
| `wahl/` | Wahl-/Workshop-Verwaltung, Force-Zuteilung, Teilnehmer-Einreichung, `ZuteilungService` (Portierung von `kc_run_zuteilung`: Force-Zuteilungen → 3 Wunschrunden → Zufallsfüllung → Konsolidierung unterbesetzter Workshops), CSV-Export | `POST/GET /api/wahl`, `POST/GET /api/wahl/:id/workshops`, `POST /api/wahl/:id/force-zuteilung`, `POST /api/wahl/:id/teilnehmer` (Guest), `GET /api/wahl/guest/overview` + `GET /api/wahl/guest/results` (Guest), `POST /api/wahl/:id/zuteilung/run`, `GET /api/wahl/:id/zuteilung(.csv)` |
|
||||
| `files/` | Upload (LT-only, multipart) mit Sichtbarkeitsstufe; Liste/Download für Team oder Guest, gefiltert nach erlaubten Sichtbarkeitsstufen; `StorageProvider`-Abstraktion (WebDAV/Nextcloud Default, S3 optional) | `POST /api/files/:kcId`, `GET /api/files/:kcId`, `GET /api/files/download/:fileId` |
|
||||
| `chat/` | Zentrale Zugriffslogik (`ChatService`) geteilt zwischen REST (`ChatController`) und WS (`ChatGateway`, Pfad `/chat`, eigene Token-Verifikation via `?token=`); Kanaltypen wie oben | `POST /api/chat/:kcId/channels`, `POST /api/chat/direct`, `GET /api/chat/:kcId/channels`, `GET /api/chat/channels/:id/messages`, WS-Events `chat:join`/`chat:send`/`chat:message` |
|
||||
@@ -118,6 +120,7 @@ Details, Setup-Anleitung und `.env`-Variablen: [backend/README.md](backend/READM
|
||||
3d. WS-Chat: Zwei-Client-E2E gegen echtes Postgres (zwei lokale Teamer im selben `GEMEINDE_GRUPPE`-Kanal, `chat:send` → der andere empfängt `chat:message`). Dabei behoben: `ChatGateway` speicherte den Caller erst nach dem asynchronen Token-Check, wodurch ein sofortiges `chat:join` mit 4001 abgewiesen wurde — jetzt wartet der Handler auf das Caller-Promise.
|
||||
3e. LT-Admin gegen echtes Postgres mit einem `isLeitungsteam`-Account (Team-Token): `POST/GET /api/kc`, `POST /api/gemeinde`, `GET /api/onboarding/requests` + PENDING-Anfrage → `approve` → `ACTIVE`; Wahl-Admin (`POST /api/wahl`, `POST /api/wahl/:id/workshops`, `POST .../zuteilung/run`, `GET .../zuteilung`, `PATCH /api/wahl/:id` `isOpen`, `GET /api/wahl/:id/teilnehmer`, CSV-Export); Teamer-Admin (`POST/GET /api/gemeinde/:id/teamer`, `POST /api/gemeinde/:id/teamer-invites`); `GET /api/onboarding/kc/:code`. (Datei-Upload `POST /api/files/:kcId` liefert 500 ohne konfiguriertes Nextcloud/S3 — erwartet.)
|
||||
3f. **Echte Authentik verifiziert**: mit einem Password-Grant-Token für ein `KC-APP-LT`-Mitglied (`hermes`) gegen `https://sso.konfi-castle.com` → `GET /api/auth/me` liefert `isLeitungsteam: true` (JWKS-Prüfung, Trailing-Slash-Issuer, JIT-`User`, `groups`→LT), `POST /api/kc` → 201. Placeholder-E-Mail-Fallback, da `hermes` keine E-Mail hat. `AUTHENTIK_LEITUNGSTEAM_GROUP="KC-APP-LT"`. **Noch offen:** nur der In-Browser-Redirect-Roundtrip (Authentik-Loginseite → Code-Tausch).
|
||||
3g. Push: gegen echtes Postgres Gerätetoken registriert (`POST /api/push/register`, Guest + Teamer), dann Gemeinde-Gruppen-Chat von einem anderen Mitglied → `PushProvider`-Log: „would push „Gemeinde-Gruppe" to 1 device". Backend serviert `index.html` mit Firebase-Bootstrap + `firebase-messaging-sw.js` (200). Echter FCM-Versand ungetestet (Secrets fehlen).
|
||||
4. Jest-Unit-Tests (Prisma/Sync/Mail gemockt, `npm test` grün, 56 Tests):
|
||||
- `src/wahl/zuteilung.service.spec.ts`: Force-Vorrang, Wunschrunden-Fallback bei voller Kapazität, Unzugeteilt-Fall, Konsolidierung unterbesetzter Workshops, Sync-Capture-Anzahl.
|
||||
- `src/auth/team-auth.service.spec.ts`: Invite-Redemption (unbekannt/widerrufen/abgelaufen/aufgebraucht, Gruppen-Link ohne E-Mail, E-Mail-Mismatch, Dublette) + Passwort-Login.
|
||||
@@ -133,5 +136,5 @@ Details, Setup-Anleitung und `.env`-Variablen: [backend/README.md](backend/READM
|
||||
1. Flutter-Client: ✅ Authentik-PKCE-Login (`oidc.dart`), LT-Admin (KC/Gemeinde), **Wahl-Verwaltung** (Wahlen/Workshops/Zuteilung + Ergebnis, öffnen/schließen, **Force-Zuteilung**, **CSV-Export** als Browser-Download), **Teamer-Verwaltung** (Konten + Invites), **Verantwortlichen-Selbstregistrierung**, **LT-Datei-Upload** (nativer `<input file>` + Sichtbarkeitsstufe). 🔜 Browser-OIDC-Roundtrip einmal live durchklicken; Mobile/Desktop-Targets (`flutter create --platforms=…`, Toolchains fehlen); Push.
|
||||
2. Authentik-Provider so konfigurieren, dass das Access-Token den `groups`-Claim trägt (Scope „groups"), LT-Gruppe = `AUTHENTIK_LEITUNGSTEAM_GROUP`. (Ops/Config, Code ist fertig.)
|
||||
3. SMTP konfigurieren (`MAIL_PROVIDER=smtp` + `SMTP_*`/`MAIL_FROM`/`APP_BASE_URL`) und Invite-Mail-Templates finalisieren (aktuell Plain-Text); optional Onboarding-Benachrichtigungen an LT.
|
||||
4. Push-Benachrichtigungen (FCM/APNs) für Chat/Ankündigungen.
|
||||
4. Push scharfschalten: Firebase-Web-App-Secrets (`apiKey`/`appId`/VAPID-Key) in `client/app/web/index.html` + `firebase-messaging-sw.js` eintragen, Service-Account-JSON hinterlegen und `PUSH_PROVIDER=fcm` setzen (`FCM_PROJECT_ID=konfi-castle-app`). Danach echten Zustellungstest.
|
||||
5. Echte Authentik- + Nextcloud/S3-Infra anbinden und die in Abschnitt 7 offenen E2E-Verifikationsschritte durchführen (lokales Postgres + Migration sind erledigt).
|
||||
|
||||
Reference in New Issue
Block a user