From 4305dfdb82812a63f297b5d3994a2bf37c25e7f4 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Tue, 27 Jan 2026 16:13:07 +0100 Subject: [PATCH] =?UTF-8?q?feat(#31):=20API=20Changement=20MDP=20obligatoi?= =?UTF-8?q?re=20premi=C3=A8re=20connexion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Endpoint POST /api/v1/auth/change-password-required - DTO avec validation (min 8 car, 1 majuscule, 1 chiffre) - Vérification flag changement_mdp_obligatoire - Vérification mot de passe actuel - Empêche réutilisation de l'ancien MDP - Mise à jour flag après changement réussi Closes #31 --- backend/src/routes/auth/auth.controller.ts | 29 +++++++++- backend/src/routes/auth/auth.service.ts | 54 +++++++++++++++++++ .../routes/auth/dto/change-password.dto.ts | 23 ++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 backend/src/routes/auth/dto/change-password.dto.ts diff --git a/backend/src/routes/auth/auth.controller.ts b/backend/src/routes/auth/auth.controller.ts index ef7cd90..3de3fc1 100644 --- a/backend/src/routes/auth/auth.controller.ts +++ b/backend/src/routes/auth/auth.controller.ts @@ -1,10 +1,11 @@ -import { Body, Controller, Get, Post, Req, UnauthorizedException, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Post, 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 { RegisterParentDto } from './dto/register-parent.dto'; import { RegisterParentCompletDto } from './dto/register-parent-complet.dto'; +import { ChangePasswordRequiredDto } from './dto/change-password.dto'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { AuthGuard } from 'src/common/guards/auth.guard'; import type { Request } from 'express'; @@ -97,5 +98,31 @@ export class AuthController { 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, + ); + } } diff --git a/backend/src/routes/auth/auth.service.ts b/backend/src/routes/auth/auth.service.ts index 0dbdfd5..1c9985e 100644 --- a/backend/src/routes/auth/auth.service.ts +++ b/backend/src/routes/auth/auth.service.ts @@ -455,6 +455,60 @@ export class AuthService { return `/uploads/photos/${nomFichierUnique}`; } + /** + * Changement de mot de passe obligatoire (première connexion) + */ + async changePasswordRequired( + userId: string, + motDePasseActuel: string, + nouveauMotDePasse: string, + ) { + const user = await this.usersRepo.findOne({ where: { id: userId } }); + + if (!user) { + throw new UnauthorizedException('Utilisateur introuvable'); + } + + // Vérifier que le changement est bien obligatoire + if (!user.changement_mdp_obligatoire) { + throw new BadRequestException( + 'Le changement de mot de passe n\'est pas requis pour cet utilisateur', + ); + } + + // Vérifier que l'utilisateur a un mot de passe + if (!user.password) { + throw new BadRequestException('Compte non activé'); + } + + // Vérifier le mot de passe actuel + const motDePasseValide = await bcrypt.compare(motDePasseActuel, user.password); + if (!motDePasseValide) { + throw new BadRequestException('Mot de passe actuel incorrect'); + } + + // Vérifier que le nouveau mot de passe est différent de l'ancien + const memeMotDePasse = await bcrypt.compare(nouveauMotDePasse, user.password); + if (memeMotDePasse) { + throw new BadRequestException( + 'Le nouveau mot de passe doit être différent de l\'ancien', + ); + } + + // Hasher et sauvegarder le nouveau mot de passe + const sel = await bcrypt.genSalt(12); + user.password = await bcrypt.hash(nouveauMotDePasse, sel); + user.changement_mdp_obligatoire = false; + user.modifie_le = new Date(); + + await this.usersRepo.save(user); + + return { + success: true, + message: 'Mot de passe changé avec succès', + }; + } + async logout(userId: string) { return { success: true, message: 'Deconnexion'} } diff --git a/backend/src/routes/auth/dto/change-password.dto.ts b/backend/src/routes/auth/dto/change-password.dto.ts new file mode 100644 index 0000000..a3e692c --- /dev/null +++ b/backend/src/routes/auth/dto/change-password.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, MinLength, Matches } from 'class-validator'; + +export class ChangePasswordRequiredDto { + @ApiProperty({ description: 'Mot de passe actuel' }) + @IsString() + mot_de_passe_actuel: string; + + @ApiProperty({ + description: 'Nouveau mot de passe (min 8 caractères, 1 majuscule, 1 chiffre)', + minLength: 8 + }) + @IsString() + @MinLength(8, { message: 'Le mot de passe doit contenir au moins 8 caractères' }) + @Matches(/^(?=.*[A-Z])(?=.*\d)/, { + message: 'Le mot de passe doit contenir au moins une majuscule et un chiffre' + }) + nouveau_mot_de_passe: string; + + @ApiProperty({ description: 'Confirmation du nouveau mot de passe' }) + @IsString() + confirmation_mot_de_passe: string; +} -- 2.47.2