feat(#105): Statut « refusé » – enum, migration, pending/reprise, refuser, connexion

- Enum statut_utilisateur_type + valeur 'refuse' (migration + BDD.sql)
- GET /users/reprise, PATCH /users/:id/refuser (refus_compte en validations)
- PATCH /users/:id/valider accepte en_attente et refuse (reprise)
- Connexion refusée si statut refuse

Made-with: Cursor
This commit is contained in:
MARTIN Julien 2026-03-12 22:21:12 +01:00
parent dfd58d9b6c
commit 393a527c37
6 changed files with 73 additions and 3 deletions

View File

@ -29,6 +29,7 @@ export enum StatutUtilisateurType {
EN_ATTENTE = 'en_attente',
ACTIF = 'actif',
SUSPENDU = 'suspendu',
REFUSE = 'refuse',
}
export enum SituationFamilialeType {

View File

@ -96,6 +96,12 @@ export class AuthService {
throw new UnauthorizedException('Votre compte a été suspendu. Contactez un administrateur.');
}
if (user.statut === StatutUtilisateurType.REFUSE) {
throw new UnauthorizedException(
'Votre compte a été refusé. Vous pouvez corriger votre dossier et le soumettre à nouveau ; un gestionnaire pourra le réexaminer.',
);
}
return this.generateTokens(user.id, user.email, user.role);
}

View File

@ -50,6 +50,16 @@ export class UserController {
return this.userService.findPendingUsers(role);
}
// Lister les comptes refusés (à corriger / reprise)
@Get('reprise')
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE)
@ApiOperation({ summary: 'Lister les comptes refusés (reprise)' })
findRefusedUsers(
@Query('role') role?: RoleType
) {
return this.userService.findRefusedUsers(role);
}
// Lister tous les utilisateurs (super_admin uniquement)
@Get()
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
@ -112,6 +122,18 @@ export class UserController {
return this.userService.validateUser(id, currentUser, comment);
}
@Patch(':id/refuser')
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Refuser un compte (à corriger)' })
@ApiParam({ name: 'id', description: "UUID de l'utilisateur" })
refuse(
@Param('id') id: string,
@User() currentUser: Users,
@Body('comment') comment?: string,
) {
return this.userService.refuseUser(id, currentUser, comment);
}
@Patch(':id/suspendre')
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
@ApiOperation({ summary: 'Suspendre un compte utilisateur' })

View File

@ -140,6 +140,15 @@ export class UserService {
return this.usersRepository.find({ where });
}
/** Comptes refusés (à corriger) : liste pour reprise par le gestionnaire */
async findRefusedUsers(role?: RoleType): Promise<Users[]> {
const where: any = { statut: StatutUtilisateurType.REFUSE };
if (role) {
where.role = role;
}
return this.usersRepository.find({ where });
}
async findAll(): Promise<Users[]> {
return this.usersRepository.find();
}
@ -214,7 +223,7 @@ export class UserService {
return this.usersRepository.save(user);
}
// Valider un compte utilisateur
// Valider un compte utilisateur (en_attente ou refuse -> actif)
async validateUser(user_id: string, currentUser: Users, comment?: string): Promise<Users> {
if (![RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE].includes(currentUser.role)) {
throw new ForbiddenException('Accès réservé aux super admins, administrateurs et gestionnaires');
@ -223,6 +232,10 @@ export class UserService {
const user = await this.usersRepository.findOne({ where: { id: user_id } });
if (!user) throw new NotFoundException('Utilisateur introuvable');
if (user.statut !== StatutUtilisateurType.EN_ATTENTE && user.statut !== StatutUtilisateurType.REFUSE) {
throw new BadRequestException('Seuls les comptes en attente ou refusés (à corriger) peuvent être validés.');
}
user.statut = StatutUtilisateurType.ACTIF;
const savedUser = await this.usersRepository.save(user);
if (user.role === RoleType.PARENT) {
@ -270,6 +283,30 @@ export class UserService {
await this.validationRepository.save(suspend);
return savedUser;
}
/** Refuser un compte (en_attente -> refuse) ; tracé dans validations */
async refuseUser(user_id: string, currentUser: Users, comment?: string): Promise<Users> {
if (![RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR, RoleType.GESTIONNAIRE].includes(currentUser.role)) {
throw new ForbiddenException('Accès réservé aux super admins, administrateurs et gestionnaires');
}
const user = await this.usersRepository.findOne({ where: { id: user_id } });
if (!user) throw new NotFoundException('Utilisateur introuvable');
if (user.statut !== StatutUtilisateurType.EN_ATTENTE) {
throw new BadRequestException('Seul un compte en attente peut être refusé.');
}
user.statut = StatutUtilisateurType.REFUSE;
const savedUser = await this.usersRepository.save(user);
const validation = this.validationRepository.create({
user: savedUser,
type: 'refus_compte',
status: StatutValidationType.REFUSE,
validated_by: currentUser,
comment,
});
await this.validationRepository.save(validation);
return savedUser;
}
/**
* Affecter ou modifier le numéro de dossier d'un utilisateur (parent ou AM).
* Permet au gestionnaire/admin de rapprocher deux dossiers (même numéro pour plusieurs personnes).

View File

@ -11,7 +11,7 @@ DO $$ BEGIN
CREATE TYPE genre_type AS ENUM ('H', 'F');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_utilisateur_type') THEN
CREATE TYPE statut_utilisateur_type AS ENUM ('en_attente','actif','suspendu');
CREATE TYPE statut_utilisateur_type AS ENUM ('en_attente','actif','suspendu','refuse');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_enfant_type') THEN
CREATE TYPE statut_enfant_type AS ENUM ('a_naitre','actif','scolarise');

View File

@ -0,0 +1,4 @@
-- Migration #105 : Statut utilisateur « refusé » (à corriger)
-- Ajout de la valeur 'refuse' à l'enum statut_utilisateur_type.
ALTER TYPE statut_utilisateur_type ADD VALUE IF NOT EXISTS 'refuse';