linusandClaude Sonnet 5 081a9aa241 test(backend): unit-test ZuteilungService assignment algorithm
First automated tests in the backend. Fakes Prisma + SyncService in
memory and asserts on the zuteilung.createMany payload:
- Force-Zuteilung wins over participant wishes
- wish-round fallback when a workshop hits capacity
- participant left unassigned when nothing is free
- underfilled-workshop consolidation reassigns via remaining wishes
- workshop exactly meeting minTeilnehmer is kept
- one CREATE sync entry captured per resulting Zuteilung

npm test green (9 tests). Plan verification section updated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 16:29:02 +02:00
2026-09-09 08:30:18 +00:00

KC-App Backend

NestJS API for the KC-App platform (see repo root README + plan for architecture context).

Setup

npm install
cp .env.example .env   # then fill in DATABASE_URL / AUTHENTIK_ISSUER_URL / GUEST_JWT_SECRET
npx prisma generate
npx prisma migrate dev --name init   # requires a running PostgreSQL instance
npm run start:dev

The API is served under /api (see app.setGlobalPrefix('api') in main.ts); everything else (/, /app.js, ...) is served statically from ../client/web via ServeStaticModule, so the backend doubles as the web client's host - no separate web server is needed.

Auth model

  • Team members (Leitungsteam, Gemeinde Verantwortliche, Gemeinde Teamer) are provisioned in Authentik; this API acts as an OIDC resource server, verifying access tokens against Authentik's JWKS (AuthentikStrategy) and then resolving local Membership rows to determine role + KC/Gemeinde scope. Clients perform the actual Authorization Code + PKCE flow against Authentik directly.
  • Guests/Konfis get a temporary local account (first/last name required, no Authentik) created via POST /auth/guest with a KC invite code, returning a JWT signed with GUEST_JWT_SECRET.

Modules implemented so far

  • prisma/ — shared PrismaClient provider.
  • auth/ — Authentik resource-server strategy (AuthGuard('authentik')) + guest invite-code login issuing a locally-signed JWT (AuthGuard('guest')).
  • kc/ — KC (event) creation/listing, Leitungsteam-only.
  • gemeinde/ — Gemeinde (congregation) CRUD per KC (POST /gemeinde, GET /gemeinde?kcId=, GET/PATCH/DELETE /gemeinde/:id), Leitungsteam-only. Gemeinde Verantwortliche/Teamer get their own Gemeinde from their Membership, not from this endpoint.
  • 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 → up to 3 wish rounds → random fill → consolidation of workshops that stay below minTeilnehmer), plus CSV export (GET /wahl/:id/zuteilung/csv).
  • files/ — Leitungsteam-only upload (POST /files/:kcId, multipart) tagged with a FileVisibility tier; list/download (GET /files/:kcId, GET /files/download/:fileId) accept either an Authentik or a guest token and filter by the caller's allowed visibility tiers. Storage is behind a StorageProvider abstraction: defaults to Nextcloud via WebDAV (WEBDAV_* env vars), switchable to S3-compatible storage with STORAGE_PROVIDER=s3 (S3_* env vars).
  • chat/ — Gemeinde-Gruppenchat, 1:1 Direktnachrichten, Leitungsteam-über- greifende Kanäle und Broadcast (Konfis lesen nur). Channel administration and message history are plain REST (ChatController); real-time send/ receive is a raw ws gateway (ChatGateway, path /chat) since passport guards don't apply to WS upgrades — auth happens once via ?token= at connect time (TokenVerificationService tries Authentik JWKS, then falls back to a guest token). Access rules live in ChatService and are shared between the REST and WS entry points.
  • sync/ — replicates mutations between the local (on-site) and cloud server. SyncService.capture() is called by feature services right after a write, appending an entry to the append-only SyncLogEntry log tagged with this server's SERVER_ID. The local server (set SYNC_ENABLED=true, SYNC_PEER_URL) periodically pushes its new entries to the cloud's POST /sync/ingest and pulls the cloud's via GET /sync/export (SyncSchedulerService, every 30s), both guarded by SYNC_SHARED_SECRET (SyncSecretGuard) rather than user auth. No conflict resolution is implemented by design — the local server is the sole source of truth while an event is live. POST /sync/trigger lets a Leitungsteam member force an immediate push+pull. Known gap: only entity metadata is replicated; uploaded file bytes only resolve on both sides if local and cloud share the same Nextcloud/S3 backend.
  • common/Role enum, @Roles() decorator, RolesGuard (KC-scoped, Leitungsteam roles are global across all KCs).

All planned backend phases are implemented; remaining work is the Flutter clients (see repo root README).

S
Description
No description provided
Readme GPL-3.0
456 KiB
Languages
TypeScript 98.2%
JavaScript 1%
Dockerfile 0.8%