import { Body, Controller, Get, HttpCode, HttpStatus, Patch, Post, Query, Req, UnauthorizedException, BadRequestException, UseGuards, } from '@nestjs/common'; import { LoginDto } from './dto/login.dto'; import { AuthService } from './auth.service'; import { Public } from 'src/common/decorators/public.decorator'; import { RegisterDto } from './dto/register.dto'; import { RegisterParentCompletDto } from './dto/register-parent-complet.dto'; import { RegisterAMCompletDto } from './dto/register-am-complet.dto'; import { RegisterAmResponseDto } from './dto/register-am-response.dto'; import { ChangePasswordRequiredDto } from './dto/change-password.dto'; import { CreatePasswordDto } from './dto/create-password.dto'; import { ForgotPasswordDto } from './dto/forgot-password.dto'; import { ResetPasswordDto } from './dto/reset-password.dto'; import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AuthGuard } from 'src/common/guards/auth.guard'; import type { Request } from 'express'; import { UserService } from '../user/user.service'; import { ProfileResponseDto } from './dto/profile_response.dto'; import { RefreshTokenDto } from './dto/refresh_token.dto'; import { ResoumettreRepriseDto } from './dto/resoumettre-reprise.dto'; import { RepriseIdentifyBodyDto } from './dto/reprise-identify.dto'; import { User } from 'src/common/decorators/user.decorator'; import { Users } from 'src/entities/users.entity'; import { RepriseDossierDto } from './dto/reprise-dossier.dto'; @ApiTags('Authentification') @Controller('auth') export class AuthController { constructor( private readonly authService: AuthService, private readonly userService: UserService, ) { } @Public() @ApiOperation({ summary: 'Connexion' }) @Post('login') async login(@Body() dto: LoginDto) { return this.authService.login(dto); } @Public() @Post('register') @ApiOperation({ summary: 'Inscription (OBSOLÈTE - utiliser /register/parent)' }) @ApiResponse({ status: 409, description: 'Email déjà utilisé' }) async register(@Body() dto: RegisterDto) { return this.authService.register(dto); } @Public() @Post('register/parent') @ApiOperation({ summary: 'Inscription Parent COMPLÈTE - Workflow 6 étapes', description: 'Crée Parent 1 + Parent 2 (opt) + Enfants + Présentation + CGU en une transaction' }) @ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation' }) @ApiResponse({ status: 400, description: 'Données invalides ou CGU non acceptées' }) @ApiResponse({ status: 409, description: 'Email déjà utilisé' }) async inscrireParentComplet(@Body() dto: RegisterParentCompletDto) { return this.authService.inscrireParentComplet(dto); } @Public() @Post('register/am') @ApiOperation({ summary: 'Inscription Assistante Maternelle COMPLÈTE', description: 'Crée User AM + entrée assistantes_maternelles (identité + infos pro + photo + CGU) en une transaction. Si photo_base64 est fourni sans photo_filename, le serveur utilise le nom par défaut photo_am.jpg (extension du fichier stocké = type du data-URL).', }) @ApiResponse({ status: 201, description: 'Inscription réussie - Dossier en attente de validation', type: RegisterAmResponseDto }) @ApiResponse({ status: 400, description: 'Données invalides ou CGU non acceptées' }) @ApiResponse({ status: 409, description: 'Email déjà utilisé' }) async inscrireAMComplet(@Body() dto: RegisterAMCompletDto): Promise { return this.authService.inscrireAMComplet(dto); } @Public() @Get('reprise-dossier') @ApiOperation({ summary: 'Dossier pour reprise (token seul)' }) @ApiQuery({ name: 'token', required: true, description: 'Token reprise (lien email)' }) @ApiResponse({ status: 200, description: 'Données dossier pour préremplir', type: RepriseDossierDto }) @ApiResponse({ status: 404, description: 'Token invalide ou expiré' }) async getRepriseDossier(@Query('token') token: string): Promise { return this.authService.getRepriseDossier(token); } @Public() @Patch('reprise-resoumettre') @ApiOperation({ summary: 'Resoumettre le dossier (mise à jour + statut en_attente, invalide le token)' }) @ApiResponse({ status: 200, description: 'Dossier resoumis' }) @ApiResponse({ status: 404, description: 'Token invalide ou expiré' }) async resoumettreReprise(@Body() dto: ResoumettreRepriseDto) { return this.authService.resoumettreReprise(dto); } @Public() @Post('reprise-identify') @ApiOperation({ summary: 'Modale reprise : numéro + email → type + token' }) @ApiResponse({ status: 201, description: 'type (parent/AM) + token pour GET reprise-dossier / PUT reprise-resoumettre' }) @ApiResponse({ status: 404, description: 'Aucun dossier en reprise pour ce numéro et email' }) async repriseIdentify(@Body() dto: RepriseIdentifyBodyDto) { return this.authService.identifyReprise(dto.numero_dossier, dto.email); } @Public() @Post('refresh') @ApiBearerAuth('refresh_token') @ApiResponse({ status: 200, description: 'Nouveaux tokens générés avec succès.' }) @ApiResponse({ status: 401, description: 'Token de rafraîchissement invalide ou expiré.' }) @ApiOperation({ summary: 'Rafraichir les tokens' }) async refresh(@Body() dto: RefreshTokenDto) { return this.authService.refreshTokens(dto.refresh_token); } @Public() @Get('verify-token') @ApiOperation({ summary: 'Vérifier un token de création de mot de passe' }) @ApiQuery({ name: 'token', required: true, description: 'Token UUID reçu par email' }) @ApiResponse({ status: 200, description: 'Token valide' }) @ApiResponse({ status: 404, description: 'Token invalide, expiré, ou déjà utilisé' }) async verifyCreatePasswordToken(@Query('token') token: string) { return this.authService.verifyCreatePasswordToken(token); } @Public() @Post('create-password') @ApiOperation({ summary: 'Créer le mot de passe initial via token email' }) @ApiResponse({ status: 201, description: 'Mot de passe créé avec succès' }) @ApiResponse({ status: 400, description: 'Confirmation invalide ou mot de passe invalide' }) @ApiResponse({ status: 404, description: 'Token invalide, expiré, ou déjà utilisé' }) async createPassword(@Body() dto: CreatePasswordDto) { if (dto.password !== dto.password_confirmation) { throw new BadRequestException('Les mots de passe ne correspondent pas'); } return this.authService.createPasswordWithToken(dto.token, dto.password); } @Public() @Post('forgot-password') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Demande de réinitialisation du mot de passe (ticket #127)', description: 'Réponse identique que l’e-mail existe ou non (anti-énumération). Si un compte avec mot de passe existe, un e-mail avec lien /reset-password est envoyé.', }) @ApiResponse({ status: 200, description: 'Message générique' }) async forgotPassword(@Body() dto: ForgotPasswordDto) { return this.authService.requestPasswordReset(dto.email ?? ''); } @Public() @Post('reset-password') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Réinitialiser le mot de passe via token e-mail (ticket #127)', description: 'Distinct de POST /auth/create-password (inscription). Utilise password_reset_token.', }) @ApiResponse({ status: 200, description: 'Mot de passe mis à jour' }) @ApiResponse({ status: 400, description: 'Confirmation ou règles de mot de passe' }) @ApiResponse({ status: 404, description: 'Token invalide, expiré, ou déjà utilisé' }) async resetPassword(@Body() dto: ResetPasswordDto) { if (dto.password !== dto.password_confirmation) { throw new BadRequestException('Les mots de passe ne correspondent pas'); } return this.authService.resetPasswordWithResetToken(dto.token, dto.password); } @Get('me') @UseGuards(AuthGuard) @ApiBearerAuth('access-token') @ApiOperation({ summary: "Récupérer le profil complet de l'utilisateur connecté" }) @ApiResponse({ status: 200, type: ProfileResponseDto }) async getProfile(@Req() req: Request): Promise { if (!req.user || !req.user.sub) { throw new UnauthorizedException('Utilisateur non authentifié'); } const user = await this.userService.findOne(req.user.sub); return { id: user.id, email: user.email, role: user.role, prenom: user.prenom ?? '', nom: user.nom ?? '', statut: user.statut, changement_mdp_obligatoire: user.changement_mdp_obligatoire, }; } @UseGuards(AuthGuard) @ApiBearerAuth('access-token') @Post('logout') logout(@User() currentUser: Users) { return this.authService.logout(currentUser.id); } @Post('change-password-required') @UseGuards(AuthGuard) @ApiBearerAuth('access-token') @ApiOperation({ summary: 'Changement de mot de passe obligatoire', description: 'Permet de changer le mot de passe lors de la première connexion (flag changement_mdp_obligatoire)' }) @ApiResponse({ status: 200, description: 'Mot de passe changé avec succès' }) @ApiResponse({ status: 400, description: 'Mot de passe actuel incorrect ou confirmation non correspondante' }) @ApiResponse({ status: 403, description: 'Changement de mot de passe non requis pour cet utilisateur' }) async changePasswordRequired( @User() currentUser: Users, @Body() dto: ChangePasswordRequiredDto, ) { // Vérifier que les mots de passe correspondent if (dto.nouveau_mot_de_passe !== dto.confirmation_mot_de_passe) { throw new BadRequestException('Les mots de passe ne correspondent pas'); } return this.authService.changePasswordRequired( currentUser.id, dto.mot_de_passe_actuel, dto.nouveau_mot_de_passe, ); } }