feat(chat): free-form GRUPPE channels with mutable participants
- ChatChannelType.GRUPPE: created by Leitungsteam (any KC) or a Gemeinde Verantwortliche/r (own KC), mixing team users and guests/Konfis as explicit ChatParticipant rows (unlike GEMEINDE_GRUPPE, membership is not derived from Gemeinde) - POST /chat/:kcId/gruppen to create, GET participant-candidates, and POST/DELETE /chat/gruppen/:channelId/participants to manage membership (creator, LT, or Verantwortliche/r of that KC) - ChatGateway broadcasts chat:participants-changed on membership change - PushService updated for nullable ChatParticipant.userId + new guestAccountId column - SyncService now replicates ChatParticipant - Prisma migration + 14 new unit tests (75/75 passing), tsc clean - CI: add .gitea/workflows/cybedefend-scan.yml + .cybedefend project config
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { Body, Controller, Get, Param, Post, Req, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Param, Post, Req, UseGuards } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { ChatService } from './chat.service';
|
||||
import { ChatGateway } from './chat.gateway';
|
||||
import { CreateChannelDto } from './dto/create-channel.dto';
|
||||
import { CreateDirectChannelDto } from './dto/create-direct-channel.dto';
|
||||
import { AddParticipantDto } from './dto/add-participant.dto';
|
||||
import { Roles } from '../common/roles.decorator';
|
||||
import { RolesGuard } from '../common/roles.guard';
|
||||
import { Role } from '../common/role.enum';
|
||||
@@ -14,7 +16,10 @@ type ChatRequest = AuthenticatedRequest & { user?: AuthenticatedRequest['user']
|
||||
|
||||
@Controller('chat')
|
||||
export class ChatController {
|
||||
constructor(private readonly chat: ChatService) {}
|
||||
constructor(
|
||||
private readonly chat: ChatService,
|
||||
private readonly gateway: ChatGateway,
|
||||
) {}
|
||||
|
||||
/// Channel administration (Gemeinde-Gruppen, LT-Kanäle, Broadcasts) is Leitungsteam-only.
|
||||
@Post(':kcId/channels')
|
||||
@@ -24,6 +29,73 @@ export class ChatController {
|
||||
return this.chat.createChannel(kcId, dto.type, dto.gemeindeId);
|
||||
}
|
||||
|
||||
/// Free-form group chat ("Gruppenchat"): a Leitungsteam member (any KC) or
|
||||
/// a Gemeinde Verantwortliche/r (their own KC, enforced by RolesGuard's
|
||||
/// kcId scoping) can create one and pick any mix of team users and Konfis
|
||||
/// (guests) from this KC as initial participants.
|
||||
@Post(':kcId/gruppen')
|
||||
@UseGuards(AuthGuard(['authentik', 'team']), RolesGuard)
|
||||
@Roles(Role.LEITUNGSTEAM, Role.GEMEINDE_VERANTWORTLICHER)
|
||||
createGruppe(
|
||||
@Param('kcId') kcId: string,
|
||||
@Body() dto: CreateChannelDto,
|
||||
@Req() req: AuthenticatedRequest,
|
||||
) {
|
||||
return this.chat.createGruppe(kcId, dto.name, req.user!.userId, {
|
||||
userIds: dto.participantUserIds,
|
||||
guestIds: dto.participantGuestIds,
|
||||
});
|
||||
}
|
||||
|
||||
/// Candidates (team users + Konfis) a caller may add to a Gruppenchat in
|
||||
/// this KC. Allowed for LT or a Verantwortliche/r of this KC.
|
||||
@Get(':kcId/gruppen/participant-candidates')
|
||||
@UseGuards(AuthGuard(['authentik', 'team']))
|
||||
listPossibleParticipants(@Param('kcId') kcId: string, @Req() req: AuthenticatedRequest) {
|
||||
return this.chat.listPossibleParticipants(kcId, req.user!);
|
||||
}
|
||||
|
||||
/// Add a team user or Konfi to a Gruppenchat. Allowed for the channel's
|
||||
/// creator, any Leitungsteam member, or a Verantwortliche/r of that KC.
|
||||
@Post('gruppen/:channelId/participants')
|
||||
@UseGuards(AuthGuard(['authentik', 'team']))
|
||||
addParticipant(
|
||||
@Param('channelId') channelId: string,
|
||||
@Body() dto: AddParticipantDto,
|
||||
@Req() req: AuthenticatedRequest,
|
||||
) {
|
||||
return this.chat
|
||||
.addParticipant(
|
||||
channelId,
|
||||
{ kind: 'user', user: req.user! },
|
||||
{ userId: dto.userId, guestId: dto.guestId },
|
||||
)
|
||||
.then((result) => {
|
||||
this.gateway.notifyParticipantsChanged(channelId);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/// Remove a team user or Konfi from a Gruppenchat. Same authorization as add.
|
||||
@Delete('gruppen/:channelId/participants')
|
||||
@UseGuards(AuthGuard(['authentik', 'team']))
|
||||
removeParticipant(
|
||||
@Param('channelId') channelId: string,
|
||||
@Body() dto: AddParticipantDto,
|
||||
@Req() req: AuthenticatedRequest,
|
||||
) {
|
||||
return this.chat
|
||||
.removeParticipant(
|
||||
channelId,
|
||||
{ kind: 'user', user: req.user! },
|
||||
{ userId: dto.userId, guestId: dto.guestId },
|
||||
)
|
||||
.then((result) => {
|
||||
this.gateway.notifyParticipantsChanged(channelId);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/// Any two team members of the same KC can start a direct conversation
|
||||
/// (Authentik-backed members and local Gemeinde Teamer alike).
|
||||
@Post('direct')
|
||||
|
||||
Reference in New Issue
Block a user