Merge branch 'develop' (squash) – Refus sans suppression #110
Made-with: Cursor
This commit is contained in:
parent
aa4e240ad1
commit
7e32eef0a7
@ -119,6 +119,13 @@ export class Users {
|
||||
@Column({ type: 'timestamptz', nullable: true, name: 'token_creation_mdp_expire_le' })
|
||||
token_creation_mdp_expire_le?: Date;
|
||||
|
||||
/** Token pour reprise après refus (lien email), ticket #110 */
|
||||
@Column({ nullable: true, name: 'token_reprise', length: 255 })
|
||||
token_reprise?: string;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true, name: 'token_reprise_expire_le' })
|
||||
token_reprise_expire_le?: Date;
|
||||
|
||||
@Column({ nullable: true, name: 'ville' })
|
||||
ville?: string;
|
||||
|
||||
|
||||
@ -97,4 +97,41 @@ export class MailService {
|
||||
|
||||
await this.sendEmail(to, subject, html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Email de refus de dossier avec lien reprise (token).
|
||||
* Ticket #110 – Refus sans suppression
|
||||
*/
|
||||
async sendRefusEmail(
|
||||
to: string,
|
||||
prenom: string,
|
||||
nom: string,
|
||||
comment: string | undefined,
|
||||
token: string,
|
||||
): Promise<void> {
|
||||
const appName = this.configService.get<string>('app_name', "P'titsPas");
|
||||
const appUrl = this.configService.get<string>('app_url', 'https://app.ptits-pas.fr');
|
||||
const repriseLink = `${appUrl}/reprise?token=${encodeURIComponent(token)}`;
|
||||
|
||||
const subject = `Votre dossier – compléments demandés`;
|
||||
const commentBlock = comment
|
||||
? `<p><strong>Message du gestionnaire :</strong></p><p>${comment.replace(/</g, '<').replace(/>/g, '>')}</p>`
|
||||
: '';
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #333;">Bonjour ${prenom} ${nom},</h2>
|
||||
<p>Votre dossier d'inscription sur <strong>${appName}</strong> n'a pas pu être validé en l'état.</p>
|
||||
${commentBlock}
|
||||
<p>Vous pouvez corriger les éléments indiqués et soumettre à nouveau votre dossier en cliquant sur le lien ci-dessous.</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${repriseLink}" style="background-color: #2196F3; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold;">Reprendre mon dossier</a>
|
||||
</div>
|
||||
<p style="color: #666; font-size: 12px;">Ce lien est valable 7 jours. Si vous n'avez pas demandé cette reprise, vous pouvez ignorer cet email.</p>
|
||||
<hr style="border: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #666; font-size: 12px;">Cet email a été envoyé automatiquement. Merci de ne pas y répondre.</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await this.sendEmail(to, subject, html);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -369,6 +369,13 @@ CREATE INDEX IF NOT EXISTS idx_utilisateurs_numero_dossier ON utilisateurs(numer
|
||||
CREATE INDEX IF NOT EXISTS idx_assistantes_maternelles_numero_dossier ON assistantes_maternelles(numero_dossier) WHERE numero_dossier IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_parents_numero_dossier ON parents(numero_dossier) WHERE numero_dossier IS NOT NULL;
|
||||
|
||||
-- ==========================================================
|
||||
-- Ticket #110 : Token reprise après refus (lien email)
|
||||
-- ==========================================================
|
||||
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS token_reprise VARCHAR(255) NULL;
|
||||
ALTER TABLE utilisateurs ADD COLUMN IF NOT EXISTS token_reprise_expire_le TIMESTAMPTZ NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_utilisateurs_token_reprise ON utilisateurs(token_reprise) WHERE token_reprise IS NOT NULL;
|
||||
|
||||
-- ==========================================================
|
||||
-- Seed : Documents légaux génériques v1
|
||||
-- ==========================================================
|
||||
|
||||
10
database/migrations/2026_token_reprise_refus.sql
Normal file
10
database/migrations/2026_token_reprise_refus.sql
Normal file
@ -0,0 +1,10 @@
|
||||
-- Migration #110 : Token reprise après refus (lien email)
|
||||
-- Permet à l'utilisateur refusé de corriger et resoumettre via un lien sécurisé.
|
||||
|
||||
ALTER TABLE utilisateurs
|
||||
ADD COLUMN IF NOT EXISTS token_reprise VARCHAR(255) NULL,
|
||||
ADD COLUMN IF NOT EXISTS token_reprise_expire_le TIMESTAMPTZ NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_utilisateurs_token_reprise
|
||||
ON utilisateurs(token_reprise)
|
||||
WHERE token_reprise IS NOT NULL;
|
||||
Loading…
x
Reference in New Issue
Block a user