From f3c6e2e000536caf00c2a1f34c1d0f5dfca77f52 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Wed, 6 May 2026 22:08:54 +0300 Subject: [PATCH] implement SignupUser two-step confirm flow Signup now collects only email + displayName, creates an unconfirmed user with empty password hash, mints an EmailConfirmationToken, and dispatches a confirmation email. Password is set during ConfirmUserEmail. --- .../User/UseCases/SignupUser/SignupUser.php | 41 ++++++++++++------- .../UseCases/SignupUser/SignupUserRequest.php | 1 - 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/backend/app/User/UseCases/SignupUser/SignupUser.php b/backend/app/User/UseCases/SignupUser/SignupUser.php index 7228e87..007bab0 100644 --- a/backend/app/User/UseCases/SignupUser/SignupUser.php +++ b/backend/app/User/UseCases/SignupUser/SignupUser.php @@ -2,7 +2,11 @@ namespace App\User\UseCases\SignupUser; -use App\Auth\PasswordHasher; +use App\Auth\Clock; +use App\Email\EmailConfirmationToken\CreateEmailConfirmationTokenDto; +use App\Email\EmailConfirmationToken\EmailConfirmationTokenRepository; +use App\Email\Emailer; +use App\Email\EmailFactory; use App\Exceptions\BadRequestException; use App\Shared\ValueObject\EmailAddress; use App\User\CreateUserDto; @@ -13,13 +17,17 @@ use InvalidArgumentException; class SignupUser { - private const MIN_PASSWORD_LENGTH = 8; - private const DISPLAY_NAME_PATTERN = '/^[a-z0-9_-]{3,30}$/'; + private const TOKEN_LIFETIME = '+1 day'; + public function __construct( private UserRepository $userRepo, - private PasswordHasher $hasher, + private EmailConfirmationTokenRepository $tokenRepo, + private Emailer $emailer, + private EmailFactory $emailFactory, + private Clock $clock, + private string $fromAddress, ) {} /** @@ -44,14 +52,6 @@ class SignupUser 'displayName must be 3-30 chars of [a-z0-9_-]' ); } - if ($request->password === null || $request->password === '') { - throw new BadRequestException('password is required'); - } - if (strlen($request->password) < self::MIN_PASSWORD_LENGTH) { - throw new BadRequestException( - 'password must be at least '.self::MIN_PASSWORD_LENGTH.' characters' - ); - } try { $email = new EmailAddress($request->email); @@ -66,12 +66,25 @@ class SignupUser throw new DomainException('displayName already taken'); } - return $this->userRepo->create(new CreateUserDto( + $user = $this->userRepo->create(new CreateUserDto( email: $email, displayName: $request->displayName, - passwordHash: $this->hasher->hash($request->password), + passwordHash: '', isAdmin: false, emailConfirmedAt: null, )); + + $token = $this->tokenRepo->create(new CreateEmailConfirmationTokenDto( + user: $user, + availableTo: $this->clock->now()->modify(self::TOKEN_LIFETIME), + )); + + $this->emailer->send( + $this->fromAddress, + $user->getEmail()->value(), + $this->emailFactory->makeConfirmationEmail($token->getToken()), + ); + + return $user; } } diff --git a/backend/app/User/UseCases/SignupUser/SignupUserRequest.php b/backend/app/User/UseCases/SignupUser/SignupUserRequest.php index e202131..f39d861 100644 --- a/backend/app/User/UseCases/SignupUser/SignupUserRequest.php +++ b/backend/app/User/UseCases/SignupUser/SignupUserRequest.php @@ -7,6 +7,5 @@ class SignupUserRequest public function __construct( public ?string $email, public ?string $displayName, - public ?string $password, ) {} }