import { Controller, Get, Headers, MessageEvent, Query, Sse, UnauthorizedException, UseGuards, } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags, } from '@nestjs/swagger'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { Observable } from 'rxjs'; import { AuthGuard } from 'src/common/guards/auth.guard'; import { RolesGuard } from 'src/common/guards/roles.guard'; import { Roles } from 'src/common/decorators/roles.decorator'; import { Public } from 'src/common/decorators/public.decorator'; import { User } from 'src/common/decorators/user.decorator'; import { RoleType } from 'src/entities/users.entity'; import { CardsRealtimeService } from './cards-realtime.service'; /** * SSE Cartes — #195 * Auth : Bearer (recommandé) ou `?access_token=` (EventSource navigateur). */ @ApiTags('Cartes') @Controller('cards') export class CardsRealtimeController { constructor( private readonly realtime: CardsRealtimeService, private readonly jwtService: JwtService, private readonly configService: ConfigService, ) {} @Sse('stream') @Public() @ApiOperation({ summary: 'Flux SSE bulles (card.created|updated|deleted, response.added) — #195', }) @ApiQuery({ name: 'access_token', required: false, description: 'JWT si pas de header Authorization (EventSource)', }) @ApiBearerAuth('access-token') async stream( @Headers('authorization') authorization?: string, @Query('access_token') accessToken?: string, ): Promise> { const userId = await this.resolveUserId(authorization, accessToken); return this.realtime.streamFor(userId); } /** Variante gardée (clients qui envoient Bearer correctement). */ @Get('stream/info') @UseGuards(AuthGuard, RolesGuard) @Roles(RoleType.PARENT, RoleType.ASSISTANTE_MATERNELLE) @ApiBearerAuth('access-token') @ApiOperation({ summary: 'Debug : abonnés SSE pour mon user — #195' }) info(@User('id') userId: string) { return { user_id: userId, subscribers: this.realtime.subscriberCount(userId), }; } private async resolveUserId( authorization?: string, accessToken?: string, ): Promise { let token = accessToken?.trim(); if (!token && authorization?.startsWith('Bearer ')) { token = authorization.slice(7).trim(); } if (!token) { throw new UnauthorizedException('Token manquant (Bearer ou access_token)'); } try { const payload = await this.jwtService.verifyAsync<{ sub: string }>(token, { secret: this.configService.get('jwt.accessSecret'), }); if (!payload?.sub) throw new UnauthorizedException('Token invalide'); return payload.sub; } catch { throw new UnauthorizedException('Token invalide ou expiré'); } } }