add domain layer, config, and entry point
Domain: User, Session, EmailAddress, DTOs, repositories, services (PasswordHasher, TokenGenerator, Clock). Config: PHP-DI container definitions and Slim routes. Entry point: public/index.php with slim-bridge.
This commit is contained in:
parent
e54197f8a5
commit
9703f82788
18 changed files with 335 additions and 0 deletions
16
backend/app/Auth/BcryptPasswordHasher.php
Normal file
16
backend/app/Auth/BcryptPasswordHasher.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Auth;
|
||||||
|
|
||||||
|
class BcryptPasswordHasher implements PasswordHasher
|
||||||
|
{
|
||||||
|
public function hash(string $password): string
|
||||||
|
{
|
||||||
|
return password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verify(string $password, string $hash): bool
|
||||||
|
{
|
||||||
|
return password_verify($password, $hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
backend/app/Auth/Clock.php
Normal file
10
backend/app/Auth/Clock.php
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Auth;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
|
||||||
|
interface Clock
|
||||||
|
{
|
||||||
|
public function now(): DateTimeImmutable;
|
||||||
|
}
|
||||||
16
backend/app/Auth/CreateSessionDto.php
Normal file
16
backend/app/Auth/CreateSessionDto.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Auth;
|
||||||
|
|
||||||
|
use App\User\User;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
|
||||||
|
class CreateSessionDto
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $token,
|
||||||
|
public User $user,
|
||||||
|
public DateTimeImmutable $createdAt,
|
||||||
|
public DateTimeImmutable $expiresAt,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
10
backend/app/Auth/PasswordHasher.php
Normal file
10
backend/app/Auth/PasswordHasher.php
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Auth;
|
||||||
|
|
||||||
|
interface PasswordHasher
|
||||||
|
{
|
||||||
|
public function hash(string $password): string;
|
||||||
|
|
||||||
|
public function verify(string $password, string $hash): bool;
|
||||||
|
}
|
||||||
11
backend/app/Auth/RandomTokenGenerator.php
Normal file
11
backend/app/Auth/RandomTokenGenerator.php
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Auth;
|
||||||
|
|
||||||
|
class RandomTokenGenerator implements TokenGenerator
|
||||||
|
{
|
||||||
|
public function generate(): string
|
||||||
|
{
|
||||||
|
return bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
}
|
||||||
41
backend/app/Auth/Session.php
Normal file
41
backend/app/Auth/Session.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Auth;
|
||||||
|
|
||||||
|
use App\User\User;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
|
||||||
|
class Session
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private string $token,
|
||||||
|
private User $user,
|
||||||
|
private DateTimeImmutable $createdAt,
|
||||||
|
private DateTimeImmutable $expiresAt,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getToken(): string
|
||||||
|
{
|
||||||
|
return $this->token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUser(): User
|
||||||
|
{
|
||||||
|
return $this->user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreatedAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpiresAt(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isExpired(DateTimeImmutable $now): bool
|
||||||
|
{
|
||||||
|
return $now >= $this->expiresAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
backend/app/Auth/SessionRepository.php
Normal file
12
backend/app/Auth/SessionRepository.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Auth;
|
||||||
|
|
||||||
|
interface SessionRepository
|
||||||
|
{
|
||||||
|
public function create(CreateSessionDto $dto): Session;
|
||||||
|
|
||||||
|
public function findByToken(string $token): ?Session;
|
||||||
|
|
||||||
|
public function deleteByToken(string $token): void;
|
||||||
|
}
|
||||||
14
backend/app/Auth/SystemClock.php
Normal file
14
backend/app/Auth/SystemClock.php
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Auth;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeZone;
|
||||||
|
|
||||||
|
class SystemClock implements Clock
|
||||||
|
{
|
||||||
|
public function now(): DateTimeImmutable
|
||||||
|
{
|
||||||
|
return new DateTimeImmutable('now', new DateTimeZone('UTC'));
|
||||||
|
}
|
||||||
|
}
|
||||||
8
backend/app/Auth/TokenGenerator.php
Normal file
8
backend/app/Auth/TokenGenerator.php
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Auth;
|
||||||
|
|
||||||
|
interface TokenGenerator
|
||||||
|
{
|
||||||
|
public function generate(): string;
|
||||||
|
}
|
||||||
7
backend/app/Exceptions/BadRequestException.php
Normal file
7
backend/app/Exceptions/BadRequestException.php
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use DomainException;
|
||||||
|
|
||||||
|
class BadRequestException extends DomainException {}
|
||||||
7
backend/app/Exceptions/UnauthorizedException.php
Normal file
7
backend/app/Exceptions/UnauthorizedException.php
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use DomainException;
|
||||||
|
|
||||||
|
class UnauthorizedException extends DomainException {}
|
||||||
54
backend/app/Shared/ValueObject/EmailAddress.php
Normal file
54
backend/app/Shared/ValueObject/EmailAddress.php
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Shared\ValueObject;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final readonly class EmailAddress
|
||||||
|
{
|
||||||
|
private string $normalized;
|
||||||
|
|
||||||
|
private string $domain;
|
||||||
|
|
||||||
|
private const ERROR_MESSAGE = 'Invalid email address:';
|
||||||
|
|
||||||
|
public function __construct(string $email)
|
||||||
|
{
|
||||||
|
|
||||||
|
$trimmed = trim($email);
|
||||||
|
|
||||||
|
if ($trimmed === '' || ! str_contains($trimmed, '@')) {
|
||||||
|
throw new InvalidArgumentException(self::ERROR_MESSAGE." $email");
|
||||||
|
}
|
||||||
|
|
||||||
|
[$local, $domain] = explode('@', $trimmed, 2);
|
||||||
|
$this->domain = mb_strtolower($domain);
|
||||||
|
$normalized = $local.'@'.$this->domain;
|
||||||
|
|
||||||
|
if (filter_var($normalized, FILTER_VALIDATE_EMAIL) === false) {
|
||||||
|
throw new InvalidArgumentException(self::ERROR_MESSAGE." $email");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->normalized = $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function value(): string
|
||||||
|
{
|
||||||
|
return $this->normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function equals(self $other): bool
|
||||||
|
{
|
||||||
|
return $this->normalized === $other->normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDomain(): string
|
||||||
|
{
|
||||||
|
return $this->domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
backend/app/User/CreateUserDto.php
Normal file
13
backend/app/User/CreateUserDto.php
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User;
|
||||||
|
|
||||||
|
use App\Shared\ValueObject\EmailAddress;
|
||||||
|
|
||||||
|
class CreateUserDto
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public EmailAddress $email,
|
||||||
|
public string $passwordHash,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
29
backend/app/User/User.php
Normal file
29
backend/app/User/User.php
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User;
|
||||||
|
|
||||||
|
use App\Shared\ValueObject\EmailAddress;
|
||||||
|
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private int $id,
|
||||||
|
private EmailAddress $email,
|
||||||
|
private string $passwordHash,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getId(): int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEmail(): EmailAddress
|
||||||
|
{
|
||||||
|
return $this->email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPasswordHash(): string
|
||||||
|
{
|
||||||
|
return $this->passwordHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
backend/app/User/UserRepository.php
Normal file
14
backend/app/User/UserRepository.php
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User;
|
||||||
|
|
||||||
|
use App\Shared\ValueObject\EmailAddress;
|
||||||
|
|
||||||
|
interface UserRepository
|
||||||
|
{
|
||||||
|
public function create(CreateUserDto $dto): User;
|
||||||
|
|
||||||
|
public function findByEmail(EmailAddress $email): ?User;
|
||||||
|
|
||||||
|
public function find(int $id): ?User;
|
||||||
|
}
|
||||||
37
backend/config/container.php
Normal file
37
backend/config/container.php
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Auth\BcryptPasswordHasher;
|
||||||
|
use App\Auth\Clock;
|
||||||
|
use App\Auth\PasswordHasher;
|
||||||
|
use App\Auth\RandomTokenGenerator;
|
||||||
|
use App\Auth\SystemClock;
|
||||||
|
use App\Auth\TokenGenerator;
|
||||||
|
use App\Auth\UseCases\AuthenticateUser\AuthenticateUser;
|
||||||
|
use App\Auth\UseCases\CreateSession\CreateSession;
|
||||||
|
use App\Auth\UseCases\Logout\Logout;
|
||||||
|
use App\Controllers\AuthController;
|
||||||
|
use App\Middleware\AuthMiddleware;
|
||||||
|
use DI\ContainerBuilder;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
|
||||||
|
$builder = new ContainerBuilder();
|
||||||
|
|
||||||
|
$builder->addDefinitions([
|
||||||
|
|
||||||
|
// Services
|
||||||
|
PasswordHasher::class => DI\create(BcryptPasswordHasher::class),
|
||||||
|
TokenGenerator::class => DI\create(RandomTokenGenerator::class),
|
||||||
|
Clock::class => DI\create(SystemClock::class),
|
||||||
|
|
||||||
|
// Use cases
|
||||||
|
AuthenticateUser::class => DI\autowire(),
|
||||||
|
CreateSession::class => DI\autowire(),
|
||||||
|
Logout::class => DI\autowire(),
|
||||||
|
|
||||||
|
// HTTP layer
|
||||||
|
AuthController::class => DI\autowire(),
|
||||||
|
AuthMiddleware::class => DI\autowire(),
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $builder->build();
|
||||||
17
backend/config/routes.php
Normal file
17
backend/config/routes.php
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Controllers\AuthController;
|
||||||
|
use App\Middleware\AuthMiddleware;
|
||||||
|
use Slim\App;
|
||||||
|
use Slim\Routing\RouteCollectorProxy;
|
||||||
|
|
||||||
|
return function (App $app): void {
|
||||||
|
|
||||||
|
$app->get('/me', [AuthController::class, 'me'])
|
||||||
|
->add(AuthMiddleware::class);
|
||||||
|
|
||||||
|
$app->post('/login', [AuthController::class, 'login']);
|
||||||
|
|
||||||
|
$app->post('/logout', [AuthController::class, 'logout'])
|
||||||
|
->add(AuthMiddleware::class);
|
||||||
|
};
|
||||||
19
backend/public/index.php
Normal file
19
backend/public/index.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use DI\Bridge\Slim\Bridge;
|
||||||
|
use Slim\App;
|
||||||
|
|
||||||
|
(static function (): void {
|
||||||
|
$root = dirname(__DIR__);
|
||||||
|
|
||||||
|
require $root.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
$container = require $root.'/config/container.php';
|
||||||
|
|
||||||
|
/** @var App $app */
|
||||||
|
$app = Bridge::create($container);
|
||||||
|
|
||||||
|
(require $root.'/config/routes.php')($app);
|
||||||
|
|
||||||
|
$app->run();
|
||||||
|
})();
|
||||||
Loading…
Add table
Add a link
Reference in a new issue