feat(#110): Refus sans suppression – token reprise + email

- Colonnes token_reprise, token_reprise_expire_le (migration + BDD.sql)
- refuser: génère token (7j), enregistre, trace validations, envoie email (template refus + lien reprise)
- MailService.sendRefusEmail ; échec email ne bloque pas le refus

Made-with: Cursor
This commit is contained in:
2026-03-12 22:56:27 +01:00
parent dbcb3611d4
commit 86d8189038
6 changed files with 93 additions and 3 deletions
+2
View File
@@ -10,6 +10,7 @@ import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entit
import { AssistantesMaternellesModule } from '../assistantes_maternelles/assistantes_maternelles.module';
import { Parents } from 'src/entities/parents.entity';
import { GestionnairesModule } from './gestionnaires/gestionnaires.module';
import { MailModule } from 'src/modules/mail/mail.module';
@Module({
imports: [TypeOrmModule.forFeature(
@@ -22,6 +23,7 @@ import { GestionnairesModule } from './gestionnaires/gestionnaires.module';
ParentsModule,
AssistantesMaternellesModule,
GestionnairesModule,
MailModule,
],
controllers: [UserController],
providers: [UserService],
+30 -3
View File
@@ -1,4 +1,4 @@
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { RoleType, StatutUtilisateurType, Users } from "src/entities/users.entity";
import { In, Repository } from "typeorm";
@@ -9,9 +9,13 @@ import * as bcrypt from 'bcrypt';
import { StatutValidationType, Validation } from "src/entities/validations.entity";
import { Parents } from "src/entities/parents.entity";
import { AssistanteMaternelle } from "src/entities/assistantes_maternelles.entity";
import { MailService } from "src/modules/mail/mail.service";
import * as crypto from 'crypto';
@Injectable()
export class UserService {
private readonly logger = new Logger(UserService.name);
constructor(
@InjectRepository(Users)
private readonly usersRepository: Repository<Users>,
@@ -23,7 +27,9 @@ export class UserService {
private readonly parentsRepository: Repository<Parents>,
@InjectRepository(AssistanteMaternelle)
private readonly assistantesRepository: Repository<AssistanteMaternelle>
private readonly assistantesRepository: Repository<AssistanteMaternelle>,
private readonly mailService: MailService,
) { }
async createUser(dto: CreateUserDto, currentUser?: Users): Promise<Users> {
@@ -284,7 +290,7 @@ export class UserService {
return savedUser;
}
/** Refuser un compte (en_attente -> refuse) ; tracé dans validations */
/** Refuser un compte (en_attente -> refuse) ; tracé validations, token reprise, email. Ticket #110 */
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');
@@ -294,8 +300,16 @@ export class UserService {
if (user.statut !== StatutUtilisateurType.EN_ATTENTE) {
throw new BadRequestException('Seul un compte en attente peut être refusé.');
}
const tokenReprise = crypto.randomUUID();
const expireLe = new Date();
expireLe.setDate(expireLe.getDate() + 7);
user.statut = StatutUtilisateurType.REFUSE;
user.token_reprise = tokenReprise;
user.token_reprise_expire_le = expireLe;
const savedUser = await this.usersRepository.save(user);
const validation = this.validationRepository.create({
user: savedUser,
type: 'refus_compte',
@@ -304,6 +318,19 @@ export class UserService {
comment,
});
await this.validationRepository.save(validation);
try {
await this.mailService.sendRefusEmail(
savedUser.email,
savedUser.prenom ?? '',
savedUser.nom ?? '',
comment,
tokenReprise,
);
} catch (err) {
this.logger.warn(`Envoi email refus échoué pour ${savedUser.email}`, err);
}
return savedUser;
}