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:
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetTokenId;
|
||||
use RuntimeException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class PasswordResetTokenAlreadyUsedException extends RuntimeException
|
||||
{
|
||||
public static function forToken(PasswordResetTokenId $tokenId): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Password reset token "%s" has already been used.',
|
||||
$tokenId,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetTokenId;
|
||||
use RuntimeException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class PasswordResetTokenExpiredException extends RuntimeException
|
||||
{
|
||||
public static function forToken(PasswordResetTokenId $tokenId): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Password reset token "%s" has expired.',
|
||||
$tokenId,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetTokenId;
|
||||
use RuntimeException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class PasswordResetTokenNotFoundException extends RuntimeException
|
||||
{
|
||||
public static function withId(PasswordResetTokenId $tokenId): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Password reset token with ID "%s" not found.',
|
||||
$tokenId,
|
||||
));
|
||||
}
|
||||
|
||||
public static function withTokenValue(string $tokenValue): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Password reset token with value "%s" not found.',
|
||||
$tokenValue,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Thrown when a token consumption is already in progress.
|
||||
*
|
||||
* This indicates a concurrent request is processing the same token,
|
||||
* and the client should retry after a short delay.
|
||||
*/
|
||||
final class TokenConsumptionInProgressException extends RuntimeException
|
||||
{
|
||||
public function __construct(string $tokenValue)
|
||||
{
|
||||
parent::__construct(
|
||||
sprintf('Token consumption in progress for token "%s". Please retry.', $tokenValue)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user