feat: Réinitialisation de mot de passe avec tokens sécurisés

Implémentation complète du flux de réinitialisation de mot de passe (Story 1.5):

Backend:
- Aggregate PasswordResetToken avec TTL 1h, UUID v7, usage unique
- Endpoint POST /api/password/forgot avec rate limiting (3/h par email, 10/h par IP)
- Endpoint POST /api/password/reset avec validation token
- Templates email (demande + confirmation)
- Repository Redis avec TTL 2h pour distinguer expiré/invalide

Frontend:
- Page /mot-de-passe-oublie avec message générique (anti-énumération)
- Page /reset-password/[token] avec validation temps réel des critères
- Gestion erreurs: token invalide, expiré, déjà utilisé

Tests:
- 14 tests unitaires PasswordResetToken
- 7 tests unitaires RequestPasswordResetHandler
- 7 tests unitaires ResetPasswordHandler
- Tests E2E Playwright pour le flux complet
This commit is contained in:
2026-02-01 23:15:01 +01:00
parent b7354b8448
commit affad287f9
71 changed files with 4829 additions and 222 deletions

View File

@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mot de passe modifié - Classeo</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
padding: 20px 0;
border-bottom: 2px solid #4f46e5;
}
.header h1 {
color: #4f46e5;
margin: 0;
font-size: 28px;
}
.content {
padding: 30px 0;
}
.success-icon {
text-align: center;
padding: 20px;
}
.success-icon span {
display: inline-block;
width: 60px;
height: 60px;
background-color: #10b981;
border-radius: 50%;
line-height: 60px;
color: white;
font-size: 30px;
}
.info-box {
background-color: #f3f4f6;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.info-box p {
margin: 5px 0;
}
.warning-box {
background-color: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 8px;
padding: 15px;
margin: 20px 0;
}
.warning-box p {
margin: 0;
color: #92400e;
font-size: 14px;
}
.button {
display: inline-block;
background-color: #4f46e5;
color: white;
text-decoration: none;
padding: 12px 24px;
border-radius: 6px;
font-weight: 500;
}
.button:hover {
background-color: #4338ca;
}
.footer {
text-align: center;
padding: 20px 0;
border-top: 1px solid #e5e7eb;
color: #6b7280;
font-size: 14px;
}
</style>
</head>
<body>
<div class="header">
<h1>Classeo</h1>
</div>
<div class="content">
<div class="success-icon">
<span>✓</span>
</div>
<h2 style="text-align: center;">Mot de passe modifié</h2>
<p>Bonjour,</p>
<p>Nous vous confirmons que le mot de passe de votre compte Classeo associé à l'adresse <strong>{{ email }}</strong> a été modifié avec succès.</p>
<div class="info-box">
<p><strong>Date du changement :</strong> {{ changedAt }}</p>
</div>
<div class="warning-box">
<p><strong>Vous n'êtes pas à l'origine de ce changement ?</strong><br>
Si vous n'avez pas demandé cette modification, votre compte pourrait être compromis.
Contactez immédiatement votre établissement ou faites une nouvelle demande de réinitialisation.</p>
</div>
<p>Vous pouvez maintenant vous connecter avec votre nouveau mot de passe.</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{ loginUrl }}" class="button">Se connecter</a>
</p>
<p><strong>Conseils de sécurité :</strong></p>
<ul>
<li>Ne partagez jamais votre mot de passe</li>
<li>Utilisez un mot de passe unique pour chaque service</li>
<li>Déconnectez-vous des appareils partagés</li>
</ul>
</div>
<div class="footer">
<p>Cet email a été envoyé automatiquement par Classeo.</p>
<p>Si vous avez des questions, contactez votre établissement.</p>
</div>
</body>
</html>