feat(backend): local accounts + invites for Gemeinde Teamer
Per the updated plan, Gemeinde Teamer are no longer Authentik-backed; they
are local accounts a Gemeinde Verantwortliche/r provisions per KC.
Schema:
- User.authentikSub now nullable; add passwordHash + kcId (cascade from Kc)
so one User model covers Authentik members and local Teamer.
- new TeamerInvite model: shareable group link (email null, maxUses null)
or personal invite (email pinned, single use), with expiry + revoke.
- sync log now also replicates User / Membership / TeamerInvite.
Auth:
- TeamAuthService: bcrypt password login (POST /auth/team-login) and invite
redemption (POST /auth/teamer/register) issuing a JWT signed with
TEAM_JWT_SECRET, payload typ:"team".
- TeamJwtStrategy (AuthGuard('team')) resolves it to the same
AuthenticatedUser shape as AuthentikStrategy.
- TokenVerificationService.verifyEither() also accepts team tokens (WS).
- files + chat read endpoints accept 'team' tokens; Teamer see non-Konfi
files and can use chat / start DMs.
Teamer admin (teamer/ module, under /gemeinde/:gemeindeId):
- POST/GET teamer, DELETE teamer/:userId
- POST/GET teamer-invites, DELETE teamer-invites/:inviteId
- LT may manage any Gemeinde; a Verantwortliche/r only their own
(checked in TeamerService, since RolesGuard only scopes by kcId).
Tests: TeamAuthService + TeamerService specs added (Prisma/Sync mocked),
npm test green at 32. Docs (plan + backend README) updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+41
-11
@@ -16,12 +16,14 @@ model Kc {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
gemeinden Gemeinde[]
|
||||
memberships Membership[]
|
||||
wahlen Wahl[]
|
||||
files File[]
|
||||
channels ChatChannel[]
|
||||
guests GuestAccount[]
|
||||
gemeinden Gemeinde[]
|
||||
memberships Membership[]
|
||||
wahlen Wahl[]
|
||||
files File[]
|
||||
channels ChatChannel[]
|
||||
guests GuestAccount[]
|
||||
localUsers User[]
|
||||
teamerInvites TeamerInvite[]
|
||||
}
|
||||
|
||||
/// A local congregation/community participating in one Kc.
|
||||
@@ -31,9 +33,10 @@ model Gemeinde {
|
||||
kcId String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
||||
memberships Membership[]
|
||||
guests GuestAccount[]
|
||||
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
||||
memberships Membership[]
|
||||
guests GuestAccount[]
|
||||
teamerInvites TeamerInvite[]
|
||||
|
||||
@@unique([kcId, name])
|
||||
}
|
||||
@@ -44,15 +47,21 @@ enum Role {
|
||||
GEMEINDE_TEAMER
|
||||
}
|
||||
|
||||
/// Authentik-backed user (team member with elevated rights).
|
||||
/// 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`
|
||||
/// set, `authentikSub` null, `kcId` set) and, like guests, scoped to one KC.
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
authentikSub String @unique
|
||||
authentikSub String? @unique
|
||||
email String @unique
|
||||
firstName String
|
||||
lastName String
|
||||
passwordHash String?
|
||||
kcId String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
kc Kc? @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
||||
memberships Membership[]
|
||||
messages ChatMessage[]
|
||||
chatParticipations ChatParticipant[]
|
||||
@@ -90,6 +99,27 @@ model GuestAccount {
|
||||
teilnehmer Teilnehmer[]
|
||||
}
|
||||
|
||||
/// Invitation issued by a Gemeinde Verantwortliche/r so new Gemeinde Teamer
|
||||
/// can self-register a local account for one Gemeinde. A group link leaves
|
||||
/// `email` null and may be redeemed up to `maxUses` times (null = unlimited);
|
||||
/// a personal invite pins `email` and defaults to a single use.
|
||||
model TeamerInvite {
|
||||
id String @id @default(cuid())
|
||||
kcId String
|
||||
gemeindeId String
|
||||
token String @unique
|
||||
email String?
|
||||
maxUses Int?
|
||||
usedCount Int @default(0)
|
||||
expiresAt DateTime?
|
||||
revokedAt DateTime?
|
||||
createdByUserId String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
kc Kc @relation(fields: [kcId], references: [id], onDelete: Cascade)
|
||||
gemeinde Gemeinde @relation(fields: [gemeindeId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
/// A workshop election, scoped to a Kc; name carries a date key + "Teil".
|
||||
model Wahl {
|
||||
id String @id @default(cuid())
|
||||
|
||||
Reference in New Issue
Block a user