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:
@@ -24,21 +24,22 @@ export class ChatController {
|
||||
return this.chat.createChannel(kcId, dto.type, dto.gemeindeId);
|
||||
}
|
||||
|
||||
/// Any two team members of the same KC can start a direct conversation.
|
||||
/// Any two team members of the same KC can start a direct conversation
|
||||
/// (Authentik-backed members and local Gemeinde Teamer alike).
|
||||
@Post('direct')
|
||||
@UseGuards(AuthGuard('authentik'))
|
||||
@UseGuards(AuthGuard(['authentik', 'team']))
|
||||
createDirectChannel(@Body() dto: CreateDirectChannelDto, @Req() req: AuthenticatedRequest) {
|
||||
return this.chat.getOrCreateDirectChannel(dto.kcId, req.user!.userId, dto.otherUserId);
|
||||
}
|
||||
|
||||
@Get(':kcId/channels')
|
||||
@UseGuards(AuthGuard(['authentik', 'guest']))
|
||||
@UseGuards(AuthGuard(['authentik', 'team', 'guest']))
|
||||
listChannels(@Param('kcId') kcId: string, @Req() req: ChatRequest) {
|
||||
return this.chat.listChannelsForCaller(kcId, resolveChatCaller(req.user!));
|
||||
}
|
||||
|
||||
@Get('channels/:channelId/messages')
|
||||
@UseGuards(AuthGuard(['authentik', 'guest']))
|
||||
@UseGuards(AuthGuard(['authentik', 'team', 'guest']))
|
||||
listMessages(@Param('channelId') channelId: string, @Req() req: ChatRequest) {
|
||||
return this.chat.listMessages(channelId, resolveChatCaller(req.user!));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user