feat(backend): #28 templates email validation compte (Handlebars)
- Templates .hbs parent principal, co-parent, AM + lien create-password - app_name / app_url / expéditeur via ConfigService (sendEmail) - validateUser: renouvelle token création MDP + envoi email si sans password - Dist: assets *.hbs (nest-cli) - Tests unitaires MailService (mock sendEmail) - Doc liste tickets #28 terminé Made-with: Cursor
This commit is contained in:
@@ -1,12 +1,80 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import * as Handlebars from 'handlebars';
|
||||
import { AppConfigService } from '../config/config.service';
|
||||
|
||||
/** Ticket #28 — quel template d'email envoyer après validation de compte */
|
||||
export type ValidationAccountEmailKind = 'parent_primary' | 'parent_coparent' | 'am';
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
private readonly logger = new Logger(MailService.name);
|
||||
|
||||
/** Cache des templates Handlebars compilés (fichiers .hbs) */
|
||||
private readonly compiledTemplates = new Map<ValidationAccountEmailKind, Handlebars.TemplateDelegate>();
|
||||
|
||||
constructor(private readonly configService: AppConfigService) {}
|
||||
|
||||
private templateBasename(kind: ValidationAccountEmailKind): string {
|
||||
const map: Record<ValidationAccountEmailKind, string> = {
|
||||
parent_primary: 'account-validated-parent-primary',
|
||||
parent_coparent: 'account-validated-parent-coparent',
|
||||
am: 'account-validated-am',
|
||||
};
|
||||
return map[kind];
|
||||
}
|
||||
|
||||
private getCompiledTemplate(kind: ValidationAccountEmailKind): Handlebars.TemplateDelegate {
|
||||
let compiled = this.compiledTemplates.get(kind);
|
||||
if (!compiled) {
|
||||
const filePath = join(__dirname, 'templates', `${this.templateBasename(kind)}.hbs`);
|
||||
const source = readFileSync(filePath, 'utf8');
|
||||
compiled = Handlebars.compile(source);
|
||||
this.compiledTemplates.set(kind, compiled);
|
||||
}
|
||||
return compiled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email post-validation : lien création MDP (token existant ou régénéré côté appelant).
|
||||
* Ticket #28 — app_name, app_url, expéditeur via ConfigService (sendEmail).
|
||||
*/
|
||||
async sendValidatedAccountPasswordSetupEmail(
|
||||
recipient: {
|
||||
email: string;
|
||||
prenom: string;
|
||||
nom: string;
|
||||
token: string;
|
||||
numeroDossier?: string | null;
|
||||
},
|
||||
kind: ValidationAccountEmailKind,
|
||||
): 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') || '').replace(/\/+$/, '');
|
||||
|
||||
const createPasswordUrl = `${appUrl}/create-password?token=${encodeURIComponent(recipient.token)}`;
|
||||
|
||||
const data: Record<string, string> = {
|
||||
prenom: recipient.prenom || '',
|
||||
nom: recipient.nom || '',
|
||||
appName,
|
||||
appUrl,
|
||||
createPasswordUrl,
|
||||
numeroDossier: recipient.numeroDossier || '',
|
||||
};
|
||||
|
||||
const html = this.getCompiledTemplate(kind)(data) as string;
|
||||
|
||||
const subjects: Record<ValidationAccountEmailKind, string> = {
|
||||
parent_primary: `${appName} — Compte validé : créez votre mot de passe`,
|
||||
parent_coparent: `${appName} — Co-parent : créez votre mot de passe`,
|
||||
am: `${appName} — Inscription validée : créez votre mot de passe`,
|
||||
};
|
||||
|
||||
await this.sendEmail(recipient.email, subjects[kind], html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoi d'un email générique
|
||||
* @param to Destinataire
|
||||
|
||||
Reference in New Issue
Block a user