feat(#31): API Changement MDP obligatoire première connexion (#77)

Co-authored-by: Julien Martin <julien.martin@ptits-pas.fr>
Co-committed-by: Julien Martin <julien.martin@ptits-pas.fr>
This commit is contained in:
MARTIN Julien 2026-01-27 15:13:45 +00:00 committed by MARTIN Julien
parent c5028c3b22
commit 95d1c3741b
3 changed files with 105 additions and 1 deletions

View File

@ -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 { LoginDto } from './dto/login.dto';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { Public } from 'src/common/decorators/public.decorator'; import { Public } from 'src/common/decorators/public.decorator';
import { RegisterDto } from './dto/register.dto'; import { RegisterDto } from './dto/register.dto';
import { RegisterParentDto } from './dto/register-parent.dto'; import { RegisterParentDto } from './dto/register-parent.dto';
import { RegisterParentCompletDto } from './dto/register-parent-complet.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 { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from 'src/common/guards/auth.guard'; import { AuthGuard } from 'src/common/guards/auth.guard';
import type { Request } from 'express'; import type { Request } from 'express';
@ -97,5 +98,31 @@ export class AuthController {
logout(@User() currentUser: Users) { logout(@User() currentUser: Users) {
return this.authService.logout(currentUser.id); 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,
);
}
} }

View File

@ -455,6 +455,60 @@ export class AuthService {
return `/uploads/photos/${nomFichierUnique}`; 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) { async logout(userId: string) {
return { success: true, message: 'Deconnexion'} return { success: true, message: 'Deconnexion'}
} }

View File

@ -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;
}