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:
@@ -6,6 +6,7 @@ namespace App\Administration\Domain\Model\User;
|
||||
|
||||
use App\Administration\Domain\Event\CompteActive;
|
||||
use App\Administration\Domain\Event\CompteCreated;
|
||||
use App\Administration\Domain\Event\MotDePasseChange;
|
||||
use App\Administration\Domain\Exception\CompteNonActivableException;
|
||||
use App\Administration\Domain\Model\ConsentementParental\ConsentementParental;
|
||||
use App\Administration\Domain\Policy\ConsentementParentalPolicy;
|
||||
@@ -14,11 +15,11 @@ use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* Aggregate Root représentant un utilisateur dans Classeo.
|
||||
* Aggregate Root representing a user in Classeo.
|
||||
*
|
||||
* Un utilisateur appartient à un établissement (tenant) et possède un rôle.
|
||||
* Le cycle de vie du compte passe par plusieurs statuts : création → activation.
|
||||
* Les mineurs (< 15 ans) nécessitent un consentement parental avant activation.
|
||||
* A user belongs to a school (tenant) and has a role.
|
||||
* The account lifecycle goes through multiple statuses: creation → activation.
|
||||
* Minors (< 15 years) require parental consent before activation.
|
||||
*/
|
||||
final class User extends AggregateRoot
|
||||
{
|
||||
@@ -39,7 +40,7 @@ final class User extends AggregateRoot
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un nouveau compte utilisateur en attente d'activation.
|
||||
* Creates a new user account awaiting activation.
|
||||
*/
|
||||
public static function creer(
|
||||
Email $email,
|
||||
@@ -72,9 +73,9 @@ final class User extends AggregateRoot
|
||||
}
|
||||
|
||||
/**
|
||||
* Active le compte avec le mot de passe hashé.
|
||||
* Activates the account with the hashed password.
|
||||
*
|
||||
* @throws CompteNonActivableException si le compte ne peut pas être activé
|
||||
* @throws CompteNonActivableException if the account cannot be activated
|
||||
*/
|
||||
public function activer(
|
||||
string $hashedPassword,
|
||||
@@ -85,7 +86,7 @@ final class User extends AggregateRoot
|
||||
throw CompteNonActivableException::carStatutIncompatible($this->id, $this->statut);
|
||||
}
|
||||
|
||||
// Vérifier si le consentement parental est requis
|
||||
// Check if parental consent is required
|
||||
if ($consentementPolicy->estRequis($this->dateNaissance)) {
|
||||
if ($this->consentementParental === null) {
|
||||
throw CompteNonActivableException::carConsentementManquant($this->id);
|
||||
@@ -107,20 +108,20 @@ final class User extends AggregateRoot
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre le consentement parental donné par le parent.
|
||||
* Records the parental consent given by the parent.
|
||||
*/
|
||||
public function enregistrerConsentementParental(ConsentementParental $consentement): void
|
||||
{
|
||||
$this->consentementParental = $consentement;
|
||||
|
||||
// Si le compte était en attente de consentement, passer en attente d'activation
|
||||
// If the account was awaiting consent, move to awaiting activation
|
||||
if ($this->statut === StatutCompte::CONSENTEMENT_REQUIS) {
|
||||
$this->statut = StatutCompte::EN_ATTENTE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si cet utilisateur est mineur et nécessite un consentement parental.
|
||||
* Checks if this user is a minor and requires parental consent.
|
||||
*/
|
||||
public function necessiteConsentementParental(ConsentementParentalPolicy $policy): bool
|
||||
{
|
||||
@@ -128,7 +129,7 @@ final class User extends AggregateRoot
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si le compte est actif et peut se connecter.
|
||||
* Checks if the account is active and can log in.
|
||||
*/
|
||||
public function peutSeConnecter(): bool
|
||||
{
|
||||
@@ -136,9 +137,26 @@ final class User extends AggregateRoot
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstitue un User depuis le stockage.
|
||||
* Changes the user's password.
|
||||
*
|
||||
* @internal Pour usage par l'Infrastructure uniquement
|
||||
* Used during password reset.
|
||||
*/
|
||||
public function changerMotDePasse(string $hashedPassword, DateTimeImmutable $at): void
|
||||
{
|
||||
$this->hashedPassword = $hashedPassword;
|
||||
|
||||
$this->recordEvent(new MotDePasseChange(
|
||||
userId: (string) $this->id,
|
||||
email: (string) $this->email,
|
||||
tenantId: $this->tenantId,
|
||||
occurredOn: $at,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstitutes a User from storage.
|
||||
*
|
||||
* @internal For Infrastructure use only
|
||||
*/
|
||||
public static function reconstitute(
|
||||
UserId $id,
|
||||
|
||||
Reference in New Issue
Block a user