diff --git a/backend/app/Auth/BcryptPasswordHasher.php b/backend/app/Auth/BcryptPasswordHasher.php deleted file mode 100644 index 0bc4a46..0000000 --- a/backend/app/Auth/BcryptPasswordHasher.php +++ /dev/null @@ -1,16 +0,0 @@ - $dto->token, - 'user_id' => $dto->user->getId(), - 'created_at' => $dto->createdAt, - 'expires_at' => $dto->expiresAt, - ]); - - return new Session( - token: $dto->token, - user: $dto->user, - createdAt: $dto->createdAt, - expiresAt: $dto->expiresAt, - ); - } - - public function findByToken(string $token): ?Session - { - $model = SessionModel::find($token); - if ($model === null) { - return null; - } - $user = $this->userRepo->find($model->user_id); - if ($user === null) { - return null; - } - $utc = new DateTimeZone('UTC'); - - return new Session( - token: $model->token, - user: $user, - createdAt: new DateTimeImmutable( - $model->created_at->toDateTimeString(), - $utc - ), - expiresAt: new DateTimeImmutable( - $model->expires_at->toDateTimeString(), - $utc - ), - ); - } - - public function deleteByToken(string $token): void - { - SessionModel::where('token', $token)->delete(); - } -} diff --git a/backend/app/Auth/PasswordHasher.php b/backend/app/Auth/PasswordHasher.php deleted file mode 100644 index ab57a41..0000000 --- a/backend/app/Auth/PasswordHasher.php +++ /dev/null @@ -1,10 +0,0 @@ -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; - } -} diff --git a/backend/app/Auth/SessionModel.php b/backend/app/Auth/SessionModel.php deleted file mode 100644 index 574a142..0000000 --- a/backend/app/Auth/SessionModel.php +++ /dev/null @@ -1,44 +0,0 @@ -|SessionModel newModelQuery() - * @method static Builder|SessionModel newQuery() - * @method static Builder|SessionModel query() - * - * @mixin \Eloquent - */ -class SessionModel extends Model -{ - protected $table = 'sessions'; - - protected $primaryKey = 'token'; - - public $incrementing = false; - - protected $keyType = 'string'; - - public $timestamps = false; - - protected $fillable = [ - 'token', - 'user_id', - 'created_at', - 'expires_at', - ]; - - protected $casts = [ - 'created_at' => 'datetime', - 'expires_at' => 'datetime', - ]; -} diff --git a/backend/app/Auth/SessionRepository.php b/backend/app/Auth/SessionRepository.php deleted file mode 100644 index cabae60..0000000 --- a/backend/app/Auth/SessionRepository.php +++ /dev/null @@ -1,12 +0,0 @@ -email === null || $request->email === '') { - throw new BadRequestException('email is required'); - } - if ($request->password === null || $request->password === '') { - throw new BadRequestException('password is required'); - } - - $user = $this->userRepo->findByEmail( - new EmailAddress($request->email) - ); - if ($user === null) { - throw new UnauthorizedException('invalid credentials'); - } - - $passwordMatches = $this->hasher->verify( - $request->password, - $user->getPasswordHash(), - ); - if (! $passwordMatches) { - throw new UnauthorizedException('invalid credentials'); - } - - return $user; - } -} diff --git a/backend/app/Auth/UseCases/AuthenticateUser/AuthenticateUserRequest.php b/backend/app/Auth/UseCases/AuthenticateUser/AuthenticateUserRequest.php deleted file mode 100644 index aa8b1df..0000000 --- a/backend/app/Auth/UseCases/AuthenticateUser/AuthenticateUserRequest.php +++ /dev/null @@ -1,11 +0,0 @@ -clock->now(); - $expiresAt = $now->modify(self::SESSION_LIFETIME); - - return $this->sessionRepo->create(new CreateSessionDto( - token: $this->tokenGenerator->generate(), - user: $user, - createdAt: $now, - expiresAt: $expiresAt, - )); - } -} diff --git a/backend/app/Auth/UseCases/Logout/Logout.php b/backend/app/Auth/UseCases/Logout/Logout.php deleted file mode 100644 index 31b16de..0000000 --- a/backend/app/Auth/UseCases/Logout/Logout.php +++ /dev/null @@ -1,17 +0,0 @@ -sessionRepo->deleteByToken($token); - } -} diff --git a/backend/app/Exceptions/BadRequestException.php b/backend/app/Exceptions/BadRequestException.php deleted file mode 100644 index b900f47..0000000 --- a/backend/app/Exceptions/BadRequestException.php +++ /dev/null @@ -1,7 +0,0 @@ -cookie(self::COOKIE_NAME); - if (! is_string($token) || $token === '') { - return $this->unauthorized(); - } - - $session = $this->sessionRepo->findByToken($token); - if ($session === null) { - return $this->unauthorized(); - } - - if ($session->isExpired($this->clock->now())) { - $this->sessionRepo->deleteByToken($token); - - return $this->unauthorized(); - } - - $request->attributes->set('user', $session->getUser()); - - return $next($request); - } - - private function unauthorized(): JsonResponse - { - return new JsonResponse(['error' => 'unauthenticated'], 401); - } -} diff --git a/backend/app/Shared/ValueObject/EmailAddress.php b/backend/app/Shared/ValueObject/EmailAddress.php deleted file mode 100644 index a744918..0000000 --- a/backend/app/Shared/ValueObject/EmailAddress.php +++ /dev/null @@ -1,54 +0,0 @@ -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; - } -} diff --git a/backend/app/User/CreateUserDto.php b/backend/app/User/CreateUserDto.php deleted file mode 100644 index d10b373..0000000 --- a/backend/app/User/CreateUserDto.php +++ /dev/null @@ -1,13 +0,0 @@ - $dto->email->value(), - 'password_hash' => $dto->passwordHash, - ]); - - return $this->toDomain($model); - } - - public function findByEmail(EmailAddress $email): ?User - { - $model = UserModel::where('email', $email->value())->first(); - - return $model === null ? null : $this->toDomain($model); - } - - public function findByEmailDomain(string $domain): array - { - $models = UserModel::where('email', 'like', '%@'.$domain)->get(); - $users = []; - foreach ($models as $model) { - $users[] = $this->toDomain($model); - } - - return $users; - } - - public function find(int $id): ?User - { - $model = UserModel::find($id); - - return $model === null ? null : $this->toDomain($model); - } - - private function toDomain(UserModel $model): User - { - return new User( - id: $model->id, - email: new EmailAddress($model->email), - passwordHash: $model->password_hash, - ); - } -} diff --git a/backend/app/User/User.php b/backend/app/User/User.php deleted file mode 100644 index 3d8ed63..0000000 --- a/backend/app/User/User.php +++ /dev/null @@ -1,29 +0,0 @@ -id; - } - - public function getEmail(): EmailAddress - { - return $this->email; - } - - public function getPasswordHash(): string - { - return $this->passwordHash; - } -} diff --git a/backend/app/User/UserModel.php b/backend/app/User/UserModel.php deleted file mode 100644 index 7d8c09d..0000000 --- a/backend/app/User/UserModel.php +++ /dev/null @@ -1,27 +0,0 @@ -|UserModel newModelQuery() - * @method static \Illuminate\Database\Eloquent\Builder|UserModel newQuery() - * @method static \Illuminate\Database\Eloquent\Builder|UserModel query() - * @method static \Illuminate\Database\Eloquent\Builder|UserModel whereEmail($value) - * @method static \Illuminate\Database\Eloquent\Builder|UserModel whereId($value) - * - * @mixin \Eloquent - */ -class UserModel extends Model -{ - protected $table = 'users'; - - public $timestamps = false; - - protected $fillable = ['email', 'password_hash']; -} diff --git a/backend/app/User/UserRepository.php b/backend/app/User/UserRepository.php deleted file mode 100644 index 3a1f083..0000000 --- a/backend/app/User/UserRepository.php +++ /dev/null @@ -1,19 +0,0 @@ -withRouting( + web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) diff --git a/backend/tests/Fakes/FakeClock.php b/backend/tests/Fakes/FakeClock.php deleted file mode 100644 index f32d5a4..0000000 --- a/backend/tests/Fakes/FakeClock.php +++ /dev/null @@ -1,15 +0,0 @@ -token, - $dto->user, - $dto->createdAt, - $dto->expiresAt - ); - $this->sessionsByToken[$dto->token] = $session; - - return $session; - } - - public function findByToken(string $token): ?Session - { - if (! isset($this->sessionsByToken[$token])) { - return null; - } - - $stored = $this->sessionsByToken[$token]; - - return new Session( - $stored->getToken(), - $stored->getUser(), - $stored->getCreatedAt(), - $stored->getExpiresAt() - ); - } - - public function deleteByToken(string $token): void - { - unset($this->sessionsByToken[$token]); - } -} diff --git a/backend/tests/Fakes/FakeTokenGenerator.php b/backend/tests/Fakes/FakeTokenGenerator.php deleted file mode 100644 index dcb8819..0000000 --- a/backend/tests/Fakes/FakeTokenGenerator.php +++ /dev/null @@ -1,13 +0,0 @@ -usersById) + 1; - $user = new User($id, $dto->email, $dto->passwordHash); - $this->usersById[$id] = $user; - $this->usersByEmail[$dto->email->value()] = $user; - - return $user; - } - - public function findByEmail(EmailAddress $email): ?User - { - if (! isset($this->usersByEmail[$email->value()])) { - return null; - } - - $stored = $this->usersByEmail[$email->value()]; - - return new User( - $stored->getId(), - $stored->getEmail(), - $stored->getPasswordHash() - ); - } - - public function findByEmailDomain(string $domain): array - { - $result = []; - foreach ($this->usersByEmail as $email => $stored) { - if (str_ends_with($email, '@' . $domain)) { - $result[] = new User( - $stored->getId(), - $stored->getEmail(), - $stored->getPasswordHash() - ); - } - } - - return $result; - } - - public function find(int $id): ?User - { - if (! isset($this->usersById[$id])) { - return null; - } - - $stored = $this->usersById[$id]; - - return new User( - $stored->getId(), - $stored->getEmail(), - $stored->getPasswordHash() - ); - } -} diff --git a/backend/tests/Unit/Auth/UseCases/AuthenticateUserTest.php b/backend/tests/Unit/Auth/UseCases/AuthenticateUserTest.php deleted file mode 100644 index 8a96f41..0000000 --- a/backend/tests/Unit/Auth/UseCases/AuthenticateUserTest.php +++ /dev/null @@ -1,99 +0,0 @@ -userRepo = new FakeUserRepository(); - $this->hasher = new FakeHasher(); - - $this->authenticateUser = new AuthenticateUser($this->userRepo, $this->hasher); - } - - public function testAuthenticatesValidUser(): void - { - $email = new EmailAddress('user@example.com'); - $this->userRepo->create(new CreateUserDto($email, 'hashed-secret')); - - $request = new AuthenticateUserRequest('user@example.com', 'secret'); - $user = $this->authenticateUser->execute($request); - - $this->assertInstanceOf(User::class, $user); - $this->assertSame('user@example.com', $user->getEmail()->value()); - } - - public function testThrowsWhenEmailMissing(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('email is required'); - - $request = new AuthenticateUserRequest(null, 'secret'); - $this->authenticateUser->execute($request); - } - - public function testThrowsWhenPasswordMissing(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('password is required'); - - $request = new AuthenticateUserRequest('user@example.com', null); - $this->authenticateUser->execute($request); - } - - public function testThrowsWhenEmailEmpty(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('email is required'); - - $request = new AuthenticateUserRequest('', 'secret'); - $this->authenticateUser->execute($request); - } - - public function testThrowsWhenPasswordEmpty(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('password is required'); - - $request = new AuthenticateUserRequest('user@example.com', ''); - $this->authenticateUser->execute($request); - } - - public function testThrowsWhenUserNotFound(): void - { - $this->expectException(UnauthorizedException::class); - $this->expectExceptionMessage('invalid credentials'); - - $request = new AuthenticateUserRequest('missing@example.com', 'secret'); - $this->authenticateUser->execute($request); - } - - public function testThrowsWhenPasswordIncorrect(): void - { - $email = new EmailAddress('user@example.com'); - $this->userRepo->create(new CreateUserDto($email, 'hashed-secret')); - - $this->expectException(UnauthorizedException::class); - $this->expectExceptionMessage('invalid credentials'); - - $request = new AuthenticateUserRequest('user@example.com', 'wrong'); - $this->authenticateUser->execute($request); - } -} diff --git a/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php b/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php deleted file mode 100644 index d6368f2..0000000 --- a/backend/tests/Unit/Auth/UseCases/CreateSessionTest.php +++ /dev/null @@ -1,47 +0,0 @@ -sessionRepo = new FakeSessionRepository(); - $this->tokenGenerator = new FakeTokenGenerator(); - $this->clock = new FakeClock(); - - $this->createSession = new CreateSession($this->sessionRepo, $this->tokenGenerator, $this->clock); - } - - public function testCreatesSessionForUser(): void - { - $email = new EmailAddress('user@example.com'); - $user = new User(1, $email, 'hashed-password'); - - $session = $this->createSession->execute($user); - - $this->assertSame('fake-token-123', $session->getToken()); - $this->assertSame($user, $session->getUser()); - $this->assertFalse($session->isExpired($this->clock->now())); - $this->assertSame('2026-05-18 12:00:00', $session->getCreatedAt()->format('Y-m-d H:i:s')); - $this->assertSame('2026-05-25 12:00:00', $session->getExpiresAt()->format('Y-m-d H:i:s')); - - $stored = $this->sessionRepo->findByToken($session->getToken()); - $this->assertNotNull($stored); - $this->assertSame('fake-token-123', $stored->getToken()); - } -} diff --git a/backend/tests/Unit/Auth/UseCases/LogoutTest.php b/backend/tests/Unit/Auth/UseCases/LogoutTest.php deleted file mode 100644 index 2b28630..0000000 --- a/backend/tests/Unit/Auth/UseCases/LogoutTest.php +++ /dev/null @@ -1,46 +0,0 @@ -sessionRepo = new FakeSessionRepository(); - $this->logout = new Logout($this->sessionRepo); - } - - public function testDeletesSessionByToken(): void - { - $email = new EmailAddress('user@example.com'); - $user = new User(1, $email, 'hashed-password'); - - $session = $this->sessionRepo->create(new CreateSessionDto( - 'session-token', - $user, - new \DateTimeImmutable(), - new \DateTimeImmutable('+1 hour') - )); - - $this->logout->execute('session-token'); - - $this->assertNull($this->sessionRepo->findByToken('session-token')); - } - - public function testDeletesMissingTokenIsIdempotent(): void - { - $this->logout->execute('nonexistent-token'); - - $this->assertNull($this->sessionRepo->findByToken('nonexistent-token')); - } -} diff --git a/backend/tests/Unit/User/UserTest.php b/backend/tests/Unit/User/UserTest.php deleted file mode 100644 index 89734e9..0000000 --- a/backend/tests/Unit/User/UserTest.php +++ /dev/null @@ -1,20 +0,0 @@ -assertSame(1, $user->getId()); - $this->assertSame($email, $user->getEmail()); - $this->assertSame('hashed-password', $user->getPasswordHash()); - } -} diff --git a/process-compose.yml b/process-compose.yml index 4fcfb4e..7f71fe0 100644 --- a/process-compose.yml +++ b/process-compose.yml @@ -12,8 +12,8 @@ processes: period_seconds: 2 backend: - command: php artisan serve --host=127.0.0.1 --port=8000 - working_dir: backend + command: bash backend/bin/serve + working_dir: . depends_on: postgres: condition: process_healthy