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

@@ -17,15 +17,15 @@ use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* Charge les utilisateurs depuis le domaine pour l'authentification Symfony.
* Loads users from the domain for Symfony authentication.
*
* Ce provider fait le pont entre Symfony Security et notre Domain Layer.
* Il ne révèle jamais si un utilisateur existe ou non pour des raisons de sécurité.
* Les utilisateurs sont isolés par tenant (établissement).
* This provider bridges Symfony Security with our Domain Layer.
* It never reveals whether a user exists or not for security reasons.
* Users are isolated by tenant (school).
*
* @implements UserProviderInterface<SecurityUser>
*
* @see Story 1.4 - Connexion utilisateur (AC2: pas de révélation d'existence du compte)
* @see Story 1.4 - User login (AC2: no account existence disclosure)
*/
final readonly class DatabaseUserProvider implements UserProviderInterface
{
@@ -50,12 +50,12 @@ final readonly class DatabaseUserProvider implements UserProviderInterface
$user = $this->userRepository->findByEmail($email, $tenantId);
// Message générique pour ne pas révéler l'existence du compte
// Generic message to not reveal account existence
if ($user === null) {
throw new SymfonyUserNotFoundException();
}
// Ne pas permettre la connexion si le compte n'est pas actif
// Do not allow login if the account is not active
if (!$user->peutSeConnecter()) {
throw new SymfonyUserNotFoundException();
}