feat(backend): self-registration for Gemeinde Verantwortliche
New onboarding/ module. A prospective Verantwortliche/r signs in with their
Konfi-Castle-ID (Authentik), looks up a KC by invite code, picks an existing
Gemeinde, and registers:
- GET /api/onboarding/kc/:inviteCode -> KC name + its Gemeinden (public;
the invite code is the shared secret)
- POST /api/onboarding/verantwortliche -> verifies the raw Authentik bearer
token's claims (no local Membership required yet via new
TokenVerificationService.verifyAuthentikClaims), JIT-provisions the local
User, and creates a Membership with status PENDING. Idempotent per
(user, kc, gemeinde).
- GET /api/onboarding/requests?kcId= (LT) list pending
- POST /api/onboarding/requests/:id/approve|reject (LT) approve flips to
ACTIVE, reject deletes.
Schema: Membership gains status (enum MembershipStatus { ACTIVE, PENDING },
default ACTIVE). AuthentikStrategy / TokenVerificationService / TeamAuthService
now load only ACTIVE memberships, so a pending request grants nothing until
approved. Membership create/update/delete flow through the sync log.
Tests: onboarding.service.spec.ts (14 cases); npm test green at 46.
Docs (plan + backend README) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+11
-2
@@ -47,6 +47,14 @@ enum Role {
|
||||
GEMEINDE_TEAMER
|
||||
}
|
||||
|
||||
/// PENDING memberships come from self-registration and grant no rights until
|
||||
/// a Leitungsteam member approves them. Everything created by LT/Verantwortliche
|
||||
/// directly is ACTIVE from the start.
|
||||
enum MembershipStatus {
|
||||
ACTIVE
|
||||
PENDING
|
||||
}
|
||||
|
||||
/// A team member account. Leitungsteam and Gemeinde Verantwortliche are
|
||||
/// Authentik-backed (`authentikSub` set, `passwordHash` null). Gemeinde
|
||||
/// Teamer are local accounts created by a Verantwortliche/r (`passwordHash`
|
||||
@@ -70,12 +78,13 @@ model User {
|
||||
/// Scopes a User's role to a specific Kc (and Gemeinde, if applicable).
|
||||
/// LEITUNGSTEAM memberships apply to all Kcs implicitly and omit gemeindeId.
|
||||
model Membership {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
kcId String
|
||||
gemeindeId String?
|
||||
role Role
|
||||
createdAt DateTime @default(now())
|
||||
status MembershipStatus @default(ACTIVE)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
||||
|
||||
Reference in New Issue
Block a user