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
192 lines
5.7 KiB
PHP
192 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
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;
|
|
use App\Shared\Domain\AggregateRoot;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
|
|
/**
|
|
* Aggregate Root representing a user in Classeo.
|
|
*
|
|
* 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
|
|
{
|
|
public private(set) ?string $hashedPassword = null;
|
|
public private(set) ?DateTimeImmutable $activatedAt = null;
|
|
public private(set) ?ConsentementParental $consentementParental = null;
|
|
|
|
private function __construct(
|
|
public private(set) UserId $id,
|
|
public private(set) Email $email,
|
|
public private(set) Role $role,
|
|
public private(set) TenantId $tenantId,
|
|
public private(set) string $schoolName,
|
|
public private(set) StatutCompte $statut,
|
|
public private(set) ?DateTimeImmutable $dateNaissance,
|
|
public private(set) DateTimeImmutable $createdAt,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Creates a new user account awaiting activation.
|
|
*/
|
|
public static function creer(
|
|
Email $email,
|
|
Role $role,
|
|
TenantId $tenantId,
|
|
string $schoolName,
|
|
?DateTimeImmutable $dateNaissance,
|
|
DateTimeImmutable $createdAt,
|
|
): self {
|
|
$user = new self(
|
|
id: UserId::generate(),
|
|
email: $email,
|
|
role: $role,
|
|
tenantId: $tenantId,
|
|
schoolName: $schoolName,
|
|
statut: StatutCompte::EN_ATTENTE,
|
|
dateNaissance: $dateNaissance,
|
|
createdAt: $createdAt,
|
|
);
|
|
|
|
$user->recordEvent(new CompteCreated(
|
|
userId: $user->id,
|
|
email: (string) $user->email,
|
|
role: $user->role->value,
|
|
tenantId: $user->tenantId,
|
|
occurredOn: $createdAt,
|
|
));
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* Activates the account with the hashed password.
|
|
*
|
|
* @throws CompteNonActivableException if the account cannot be activated
|
|
*/
|
|
public function activer(
|
|
string $hashedPassword,
|
|
DateTimeImmutable $at,
|
|
ConsentementParentalPolicy $consentementPolicy,
|
|
): void {
|
|
if (!$this->statut->peutActiver()) {
|
|
throw CompteNonActivableException::carStatutIncompatible($this->id, $this->statut);
|
|
}
|
|
|
|
// Check if parental consent is required
|
|
if ($consentementPolicy->estRequis($this->dateNaissance)) {
|
|
if ($this->consentementParental === null) {
|
|
throw CompteNonActivableException::carConsentementManquant($this->id);
|
|
}
|
|
}
|
|
|
|
$this->hashedPassword = $hashedPassword;
|
|
$this->statut = StatutCompte::ACTIF;
|
|
$this->activatedAt = $at;
|
|
|
|
$this->recordEvent(new CompteActive(
|
|
userId: (string) $this->id,
|
|
email: (string) $this->email,
|
|
tenantId: $this->tenantId,
|
|
role: $this->role->value,
|
|
occurredOn: $at,
|
|
aggregateId: $this->id->value,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Records the parental consent given by the parent.
|
|
*/
|
|
public function enregistrerConsentementParental(ConsentementParental $consentement): void
|
|
{
|
|
$this->consentementParental = $consentement;
|
|
|
|
// If the account was awaiting consent, move to awaiting activation
|
|
if ($this->statut === StatutCompte::CONSENTEMENT_REQUIS) {
|
|
$this->statut = StatutCompte::EN_ATTENTE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if this user is a minor and requires parental consent.
|
|
*/
|
|
public function necessiteConsentementParental(ConsentementParentalPolicy $policy): bool
|
|
{
|
|
return $policy->estRequis($this->dateNaissance);
|
|
}
|
|
|
|
/**
|
|
* Checks if the account is active and can log in.
|
|
*/
|
|
public function peutSeConnecter(): bool
|
|
{
|
|
return $this->statut->peutSeConnecter();
|
|
}
|
|
|
|
/**
|
|
* Changes the user's password.
|
|
*
|
|
* 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,
|
|
Email $email,
|
|
Role $role,
|
|
TenantId $tenantId,
|
|
string $schoolName,
|
|
StatutCompte $statut,
|
|
?DateTimeImmutable $dateNaissance,
|
|
DateTimeImmutable $createdAt,
|
|
?string $hashedPassword,
|
|
?DateTimeImmutable $activatedAt,
|
|
?ConsentementParental $consentementParental,
|
|
): self {
|
|
$user = new self(
|
|
id: $id,
|
|
email: $email,
|
|
role: $role,
|
|
tenantId: $tenantId,
|
|
schoolName: $schoolName,
|
|
statut: $statut,
|
|
dateNaissance: $dateNaissance,
|
|
createdAt: $createdAt,
|
|
);
|
|
|
|
$user->hashedPassword = $hashedPassword;
|
|
$user->activatedAt = $activatedAt;
|
|
$user->consentementParental = $consentementParental;
|
|
|
|
return $user;
|
|
}
|
|
}
|