Files
KC-APP/plan-kcAppMultiTenantPlatform.prompt.md
T
linusandClaude Sonnet 5 100f5bc2af feat(backend): add GemeindeController for LT congregation CRUD
Fills the plan's known gap where Gemeinde existed only as a Prisma model.
GemeindeModule exposes Leitungsteam-only create/list/get/update/delete
under /api/gemeinde, each mutation captured into the sync log like the
other feature services. Unique-name-per-KC violations surface as 409.
Docs (plan + backend README) updated to drop the gap and next-step item.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 16:25:14 +02:00

12 KiB
Raw Blame History

Plan: KC-App Multi-Tenant Event-, Wahl- und Kommunikationsplattform

Neuentwicklung, die das WordPress-Plugin Workshop-Wahlen (Wahlen/Workshops/Teilnehmer/Zuteilungslogik) ablöst und um Rollen-/Rechteverwaltung via Authentik, gestaffelte Dateifreigabe, mehrstufigen Chat und eine Hybrid-Server-Architektur (online + lokal mit Sync) erweitert. Backend: NestJS 10 + PostgreSQL + Prisma 5. Client: Flutter (eine Codebase für Mobile, Web, Desktop) — bis Flutter verfügbar ist, liefert das Backend selbst einen minimalen Platzhalter-Web-Client aus.

Status (Stand dieser Session): Alle geplanten Backend-Phasen (06) sind implementiert und verifiziert (Typecheck, Build, Boot-Test). Offen ist ausschließlich der Flutter-Client (Mobile/Desktop), da Flutter in dieser Umgebung nicht installiert ist.


1. Domänenmodell

  • KC (Konfi-Castle-Event) = oberster Mandant. Eine App-Instanz verwaltet mehrere KCs parallel. Felder: name, inviteCode (eindeutig, Basis für QR/Code-Einstieg), isActive.
  • Gemeinde: lokale Gemeinde/Kirchengemeinde innerhalb eines KC (kcId + name, eindeutig pro KC).
  • Rollenmodell (Enum Role, Authentik-gestützt):
    • Leitungsteam (LT) global über alle KCs hinweg (Authentik-Gruppe); bleibt LT auf jedem KC, bis die Authentik-Gruppenmitgliedschaft entfernt wird.
    • Gemeinde Verantwortliche pro Gemeinde/KC; verwalten nur Nutzer ihrer eigenen Gemeinde (legen Teamer an); Wahlen/Workshops macht ausschließlich LT.
    • Gemeinde Teamer von Verantwortlichen angelegt, ebenfalls Authentik-Account.
    • Guest/Konfi optionaler, rein lokaler Account auf dem jeweiligen Server (kein Authentik), Vor-/Nachname Pflicht, temporär pro KC/Event (nicht wiederverwendbar über mehrere Events).
  • Membership: verknüpft User (Authentik) ⇄ Kc (+ optional Gemeinde) ⇄ Role. LT-Memberships lassen gemeindeId leer und gelten global.
  • Einstieg/Onboarding: LT vergibt pro KC einen Code/QR (enthält den KC-Key). Damit erhält man sofortigen Guest-Zugang oder kann sich vorab als Gemeinde Verantwortlicher/Teamer einer Gemeinde registrieren.
  • Wahl (Workshop-Wahl): von LT pro KC angelegt; Name trägt datumsSchluessel + teil (Bewusste Vereinfachung ggü. Original-Plugin: dort gibt es mehrere "Phasen" innerhalb einer Wahl via Teilnehmer.phase; hier ist stattdessen eine Wahl = ein Teil/Phase, gemäß expliziter Nutzer-Klarstellung).
    • Workshop: kapazitaet, minTeilnehmer (für Konsolidierung unterbesetzter Workshops).
    • Teilnehmer: Guest übermittelt prioritaeten (geordnete Workshop-ID-Liste, max. 3 entspricht wunsch1..wunsch3 im Original).
    • ForceZuteilung: manuelle LT-Override vor Algorithmus-Lauf, hat Vorrang.
    • Zuteilung: Ergebnis pro Teilnehmer (workshopId nullable = unzugeteilt, wunschRang, isForced).
  • Datei-Sichtbarkeit (Enum FileVisibility): ALLE / ALLE_AUSSER_KONFIS / NUR_LT. Dateien werden vom LT hochgeladen, teilbar je nach KC-übergreifend/eingeschränkt gemäß Sichtbarkeitsstufe.
  • Chat (Enum ChatChannelType): GEMEINDE_GRUPPE, DIREKT (1:1, explizite ChatParticipant-Zuordnung), LT_UEBERGREIFEND, BROADCAST (Konfis nur lesend).
  • Sync-Infrastruktur: SyncLogEntry (Append-only-Replikationslog: model, recordId, operation, payload, originId, autoincrement sequence) + SyncCursor (pro Peer: lastPushedSequence/lastPulledSequence).

Vollständiges Schema: backend/prisma/schema.prisma.


2. Architekturentscheidungen

Bereich Entscheidung Begründung
Backend NestJS 10 + PostgreSQL + Prisma 5 bestätigt vom Nutzer; Nest 10 statt CLI-Default (siehe unten)
Client Flutter, eine Codebase Mobile/Web/Desktop vom Nutzer delegiert; noch nicht scaffoldbar (Flutter fehlt lokal)
Web-Interimslösung Backend liefert client/web/ (reines HTML/CSS/JS, kein Build-Schritt) über ServeStaticModule aus; REST-API liegt unter /api/* Nutzerwunsch: "Server soll auch Web-Client bereitstellen"; vermeidet Kollision zwischen API-Routen und statischen Dateien
Auth (Team) Authentik als OIDC Resource Server (JWKS-Verifikation), kein lokaler Autorisierungscode-Flow im Backend Clients machen Authorization Code + PKCE direkt gegen Authentik; Backend validiert nur Access Token + löst lokale Membership auf
Auth (Guest) Rein lokale JWTs (GUEST_JWT_SECRET), kein Authentik explizite Nutzervorgabe: Konfi-Accounts sind nie in Authentik
Datei-Storage Provider-Abstraktion (StorageProvider), Default Nextcloud/WebDAV, umschaltbar auf S3 via STORAGE_PROVIDER=s3 Nutzer bestätigte: Nextcloud-Zugangsdaten kommen aus .env
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
Rollen-Scope-Guard RolesGuard behandelt LEITUNGSTEAM-Memberships als global (kcId-Check wird übersprungen) Spiegelt die Anforderung "LT bleibt LT auf allen KCs" direkt in der Autorisierungslogik

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).
  • 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: Wenn ein Gemeinde Verantwortlicher einen Teamer anlegt, muss dieser aktuell weiterhin manuell (oder über eine noch zu bauende Authentik-Admin-API-Integration) in Authentik angelegt werden — das Backend erwartet, dass der User mit passendem authentikSub bereits existiert, bevor er sich einloggen kann.

3. Umgesetzte Backend-Module (Stand: alle Phasen abgeschlossen)

Modul Kernfunktion Wichtige Endpunkte
prisma/ Geteilter PrismaClient-Provider
auth/ Authentik-Resource-Server-Strategie (AuthGuard('authentik')), Guest-Login (AuthGuard('guest')), TokenVerificationService für manuelle Verifikation außerhalb des HTTP/Passport-Pfads (WS-Handshake) POST /api/auth/guest
kc/ KC-Verwaltung, Leitungsteam-only POST /api/kc, GET /api/kc
gemeinde/ Gemeinde-CRUD pro KC, Leitungsteam-only POST /api/gemeinde, GET /api/gemeinde?kcId=, GET/PATCH/DELETE /api/gemeinde/:id
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), 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
sync/ Append-only Replikationslog + Peer-Sync (lokal ⇄ Cloud), SyncSchedulerService (alle 30s, wenn SYNC_ENABLED=true) POST /api/sync/ingest, GET /api/sync/export, POST /api/sync/trigger (LT-only)
common/ Role-Enum, @Roles()-Decorator, RolesGuard (KC-scoped, LT global)
Web-Client-Hosting ServeStaticModule liefert client/web/ aus; API unter globalem Prefix /api GET / (index.html), /app.js, /style.css

Details, Setup-Anleitung und .env-Variablen: backend/README.md.


4. Tech-Stack-Stolpersteine (dokumentiert für Nachvollziehbarkeit)

  • npx @nestjs/cli new mit aktuellen Defaults (Nest v12-Beta, ESM, Vitest, @nestjs/observe) löste einen reproduzierbaren npm-Arborist-Bug aus (Cannot read properties of null (reading 'edgesOut')). Workaround: backend/package.json wurde von Hand mit gepinnten, stabilen Versionen (Nest 10.x, Jest, CommonJS, TypeScript 5.x) erstellt statt über den CLI-Generator.
  • Bei zusätzlichen offiziellen @nestjs/*-Paketen (serve-static, schedule) wurden die Peer-Dependencies vor der Installation geprüft (npm view <pkg>@<version> peerDependencies), da die jeweils neuesten Majors bereits Nest 11/12 voraussetzen und sonst mit ERESOLVE fehlschlagen. Gepinnt: @nestjs/serve-static@4.0.2, @nestjs/schedule@4.1.1.
  • multer wurde von 1.x (bekannte CVEs) auf 2.x aktualisiert.

5. Phasenübersicht (Referenz, ursprüngliche Reihenfolge)

  1. Fundament Monorepo-Skeleton, Datenmodell, Authentik-OIDC-Integration.
  2. Multi-Tenancy & Auth Invite/QR-Code-Fluss, Permission-Guards, Guest-Login.
  3. Workshop-Wahl-Engine Wahlen/Workshops/Zuteilungslogik/CSV-Export.
  4. Dateifreigabe Storage-Abstraktion, Sichtbarkeitsstufen.
  5. Kommunikation Chat (Gruppen/DM/LT/Broadcast), WebSocket. (Push-Integration noch offen)
  6. Hybrid Lokal/Cloud-Server & Sync Replikationslog, Scheduler, Shared-Secret-Auth.
  7. Flutter-Clients gemeinsame Codebase; Screens: Invite/Login, rollenspezifisches Dashboard, Wahl-Formular/Ergebnis, Dateien, Chat, Admin/Nutzerverwaltung. offen (Flutter nicht installiert); Web-Interimslösung siehe Abschnitt 3.

6. Relevante Referenz

  • WP-Plugin als fachliche Vorlage für Zuteilungslogik: includes/zuteilungslogik.php (kc_run_zuteilung), Admin-Module admin-wahlen.php, admin-workshops.php, admin-teilnehmer.php, admin-teamer.php, admin-zuteilungen.php, Frontend-Shortcodes in frontend-form.php/frontend-ergebnis.php (git.konfi-castle.com/linus/Workshop-Wahlen).

7. Verifikation (durchgeführt je Phase)

  1. Nach jeder Phase: npx tsc -p tsconfig.build.json --noEmit, npx nest build, sowie ein kurzer Boot-Test (node dist/main.js) zur Prüfung, dass der DI-Graph auflöst und alle Routen korrekt gemappt werden (ohne Live-DB/Authentik/Nextcloud).
  2. Sync-Modul: manuell verifiziert, dass SyncSecretGuard Requests ohne x-sync-secret mit 403 ablehnt und mit korrektem Secret durchlässt (DB-Fehler in der Sandbox ist erwartet, da kein Postgres läuft).
  3. Web-Client: GET / liefert die statische Seite (200), GET /api/kc trifft die echte, geschützte API (401 ohne Token).
  4. Noch ausstehend (sobald echte Infrastruktur verfügbar ist): Zuteilungslogik gegen bekannte Testdaten aus dem alten Plugin validieren; Ende-zu-Ende-Rollenmatrix (LT/Verantwortlicher/Teamer/Guest) über alle Kernfeatures; echter Sync-Test zwischen zwei laufenden Instanzen (lokal + Cloud).

8. Nächste Schritte

  1. Sobald Flutter verfügbar ist: Client-Grundgerüst aufsetzen (Mobile + Web + Desktop, eine Codebase), beginnend mit Invite/Login-Flow gegen die bestehende API.
  2. Authentik-Admin-API-Integration für automatische Teamer-Provisionierung durch Gemeinde Verantwortliche.
  3. Push-Benachrichtigungen (FCM/APNs) für Chat/Ankündigungen.
  4. Echte Infrastruktur (Postgres, Authentik, Nextcloud/S3) aufsetzen und die in Abschnitt 7 offenen Verifikationsschritte durchführen.