add email signup flow

This commit is contained in:
Yisroel Baum 2026-08-03 20:26:08 +03:00
parent 9945163a2f
commit 3dc9204979
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
27 changed files with 629 additions and 24 deletions

View file

@ -0,0 +1,15 @@
<?php
namespace App\Email\EmailConfirmationToken;
use App\User\User;
use DateTimeImmutable;
final readonly class CreateEmailConfirmationTokenDto
{
public function __construct(
public User $user,
public DateTimeImmutable $availableTo,
public string $token,
) {}
}

View file

@ -0,0 +1,68 @@
<?php
namespace App\Email\EmailConfirmationToken;
use App\User\User;
use App\User\UserRepository;
use DomainException;
class EloquentEmailConfirmationTokenRepository implements
EmailConfirmationTokenRepository
{
public function __construct(
private UserRepository $userRepository,
) {}
public function create(
CreateEmailConfirmationTokenDto $dto,
): EmailConfirmationToken {
$model = EmailConfirmationTokenModel::create([
'user_id' => $dto->user->getId(),
'token' => $dto->token,
'available_to' => $dto->availableTo,
]);
return $this->toDomain($model);
}
public function findByToken(string $token): ?EmailConfirmationToken
{
$model = EmailConfirmationTokenModel::query()
->where('token', $token)
->first();
return $model === null ? null : $this->toDomain($model);
}
public function findByUser(User $user): ?EmailConfirmationToken
{
$model = EmailConfirmationTokenModel::query()
->where('user_id', $user->getId())
->first();
return $model === null ? null : $this->toDomain($model);
}
public function delete(int $id): void
{
EmailConfirmationTokenModel::where('id', $id)->delete();
}
private function toDomain(
EmailConfirmationTokenModel $model,
): EmailConfirmationToken {
$user = $this->userRepository->find($model->user_id);
if ($user === null) {
throw new DomainException(
"User with id {$model->user_id} not found",
);
}
return new EmailConfirmationToken(
id: $model->id,
user: $user,
availableTo: $model->available_to,
token: $model->token,
);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Email\EmailConfirmationToken;
use App\User\User;
use DateTimeImmutable;
final readonly class EmailConfirmationToken
{
public function __construct(
private int $id,
private User $user,
private DateTimeImmutable $availableTo,
private string $token,
) {}
public function getId(): int
{
return $this->id;
}
public function getUser(): User
{
return $this->user;
}
public function getAvailableTo(): DateTimeImmutable
{
return $this->availableTo;
}
public function getToken(): string
{
return $this->token;
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace App\Email\EmailConfirmationToken;
use DateTimeImmutable;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @property int $user_id
* @property string $token
* @property DateTimeImmutable $available_to
*
* @method static Builder<static>|EmailConfirmationTokenModel newModelQuery()
* @method static Builder<static>|EmailConfirmationTokenModel newQuery()
* @method static Builder<static>|EmailConfirmationTokenModel query()
*
* @mixin \Eloquent
*/
#[Fillable([
'user_id',
'token',
'available_to',
])]
class EmailConfirmationTokenModel extends Model
{
protected $table = 'email_confirmation_tokens';
public $timestamps = false;
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'available_to' => 'immutable_datetime',
];
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Email\EmailConfirmationToken;
use App\User\User;
interface EmailConfirmationTokenRepository
{
public function create(
CreateEmailConfirmationTokenDto $dto,
): EmailConfirmationToken;
public function findByToken(string $token): ?EmailConfirmationToken;
public function findByUser(User $user): ?EmailConfirmationToken;
public function delete(int $id): void;
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Email\EmailConfirmationToken\UseCases;
use App\Auth\Clock;
use App\Auth\TokenGenerator;
use App\Email\EmailConfirmationToken\CreateEmailConfirmationTokenDto;
use App\Email\EmailConfirmationToken\EmailConfirmationToken;
use App\Email\EmailConfirmationToken\EmailConfirmationTokenRepository;
use App\Exceptions\BadRequestException;
class CreateEmailConfirmationToken
{
public function __construct(
private EmailConfirmationTokenRepository $tokenRepository,
private Clock $clock,
private TokenGenerator $tokenGenerator,
) {}
/**
* @throws BadRequestException
*/
public function execute(
CreateEmailConfirmationTokenRequest $request,
): EmailConfirmationToken {
if ($request->user === null) {
throw new BadRequestException('user is required');
}
if ($request->minuteOffset === null) {
throw new BadRequestException('minuteOffset is required');
}
$existingToken = $this->tokenRepository->findByUser($request->user);
if ($existingToken !== null) {
$this->tokenRepository->delete($existingToken->getId());
}
return $this->tokenRepository->create(
new CreateEmailConfirmationTokenDto(
user: $request->user,
availableTo: $this->clock->now()->modify(
"+{$request->minuteOffset} minutes",
),
token: $this->tokenGenerator->generate(),
),
);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace App\Email\EmailConfirmationToken\UseCases;
use App\User\User;
final readonly class CreateEmailConfirmationTokenRequest
{
public function __construct(
public ?User $user,
public ?int $minuteOffset,
) {}
}