- 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
119 lines
4.5 KiB
TypeScript
119 lines
4.5 KiB
TypeScript
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';
|
|
import { AuthenticatedRequest } from '../auth/authenticated-request';
|
|
import { GuestJwtPayload } from '../auth/guest-auth.service';
|
|
import { resolveChatCaller } from './caller.util';
|
|
|
|
type ChatRequest = AuthenticatedRequest & { user?: AuthenticatedRequest['user'] | GuestJwtPayload };
|
|
|
|
@Controller('chat')
|
|
export class ChatController {
|
|
constructor(
|
|
private readonly chat: ChatService,
|
|
private readonly gateway: ChatGateway,
|
|
) {}
|
|
|
|
/// Channel administration (Gemeinde-Gruppen, LT-Kanäle, Broadcasts) is Leitungsteam-only.
|
|
@Post(':kcId/channels')
|
|
@UseGuards(AuthGuard('authentik'), RolesGuard)
|
|
@Roles(Role.LEITUNGSTEAM)
|
|
createChannel(@Param('kcId') kcId: string, @Body() dto: CreateChannelDto) {
|
|
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')
|
|
@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', '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', 'team', 'guest']))
|
|
listMessages(@Param('channelId') channelId: string, @Req() req: ChatRequest) {
|
|
return this.chat.listMessages(channelId, resolveChatCaller(req.user!));
|
|
}
|
|
}
|