feat(auth): #127 forgot-password + reset-password API (BDD + mail)

Made-with: Cursor
This commit is contained in:
2026-04-23 18:17:39 +02:00
parent e51e625d93
commit 15123f691c
11 changed files with 399 additions and 3 deletions
@@ -68,4 +68,20 @@ describe('MailService (ticket #28)', () => {
expect(html).toContain('2025-000001');
expect(html).toContain(`https://app.test/create-password?token=${encodeURIComponent(token)}`);
});
it('sendPasswordResetEmail utilise Handlebars + lien /reset-password', async () => {
const token = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
await service.sendPasswordResetEmail({
email: 'u@test.fr',
prenom: 'Marie',
nom: 'Curie',
token,
});
expect(sendEmailSpy).toHaveBeenCalledTimes(1);
const [, subject, html] = sendEmailSpy.mock.calls[0];
expect(subject).toContain('Réinitialisation');
expect(html).toContain('Marie');
expect(html).toContain(`https://app.test/reset-password?token=${encodeURIComponent(token)}`);
expect(html).not.toContain('create-password');
});
});
+27
View File
@@ -238,4 +238,31 @@ export class MailService {
await this.sendEmail(to, subject, html);
}
/**
* Ticket #127 — lien application `/reset-password?token=` (pas create-password).
*/
async sendPasswordResetEmail(recipient: {
email: string;
prenom: string;
nom: string;
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') || '').replace(/\/+$/, '');
const resetPasswordUrl = `${appUrl}/reset-password?token=${encodeURIComponent(recipient.token)}`;
const filePath = join(__dirname, 'templates', 'password-reset.hbs');
const source = readFileSync(filePath, 'utf8');
const compiled = Handlebars.compile(source);
const html = compiled({
prenom: recipient.prenom || '',
nom: recipient.nom || '',
appName,
resetPasswordUrl,
}) as string;
const subject = `${appName} — Réinitialisation de votre mot de passe`;
await this.sendEmail(recipient.email, subject, html);
}
}
@@ -0,0 +1,12 @@
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #2D6A4F;">Bonjour {{prenom}} {{nom}},</h2>
<p>Vous avez demandé à <strong>réinitialiser votre mot de passe</strong> sur <strong>{{appName}}</strong>.</p>
<p>Cliquez sur le bouton ci-dessous pour en choisir un nouveau. Ce lien est valable une durée limitée.</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{{resetPasswordUrl}}}" style="background-color: #2D6A4F; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold;">Réinitialiser mon mot de passe</a>
</div>
<p style="color: #666; font-size: 13px;">Si le bouton ne fonctionne pas, copiez ce lien dans votre navigateur :<br /><span style="word-break: break-all;">{{{resetPasswordUrl}}}</span></p>
<p style="color: #666; font-size: 12px;">Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer cet e-mail.</p>
<hr style="border: 1px solid #eee; margin: 20px 0;" />
<p style="color: #666; font-size: 12px;">Cet email a été envoyé automatiquement par {{appName}}. Merci de ne pas y répondre.</p>
</div>