Files
petitspas/backend/src/routes/auth/auth.controller.ts
T
jmartinandCursor b99745e0fe feat(#112): reprise après refus — dossier complet, email resoumission
- GET/PATCH reprise-dossier enrichis (parents, enfants, motivation, fiche AM)
- Front: lien mail, modale identify login, wizards préremplis, PATCH complet
- Email accusé resoumission aux parents avec n° de dossier
- Fixes préremplissage AM (dates, places, ValueKey étape 2)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 19:19:55 +02:00

235 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<RegisterAmResponseDto> {
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<RepriseDossierDto> {
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 le-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<ProfileResponseDto> {
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,
);
}
}