From 87df45f6aefb04c1bc1e428443b3ffa157abee80 Mon Sep 17 00:00:00 2001 From: dakriy Date: Tue, 18 Feb 2020 08:55:18 -0800 Subject: [PATCH] add login route --- .../app/Http/Controllers/AuthController.php | 45 ++++++++++++++++ .../app/Providers/AppServiceProvider.php | 2 + src/web/backend/routes/api.php | 3 ++ src/web/backend/src/Entities/Token.php | 15 ++++++ src/web/backend/src/Entities/User.php | 1 + .../src/Exceptions/AuthorizationException.php | 14 +++++ .../Tokens/DatabaseTokensRepository.php | 14 +++-- .../Tokens/InMemoryTokensRepository.php | 8 ++- .../src/Gateways/Tokens/TokensRepository.php | 2 +- .../backend/src/UseCases/BasePresenter.php | 13 +++++ .../Token/Authenticate/Authenticate.php | 2 +- .../Users/Authenticate/APIPresenter.php | 26 +++++++++ .../Users/Authenticate/Authenticate.php | 53 +++++++++++++++++++ .../Authenticate/AuthenticateUseCase.php | 20 +++++++ .../AuthenticateUseCaseServiceProvider.php | 42 +++++++++++++++ .../UseCases/Users/Authenticate/Presenter.php | 16 ++++++ .../Users/Authenticate/ResponseModel.php | 41 ++++++++++++++ 17 files changed, 309 insertions(+), 8 deletions(-) create mode 100644 src/web/backend/app/Http/Controllers/AuthController.php create mode 100644 src/web/backend/src/Exceptions/AuthorizationException.php create mode 100644 src/web/backend/src/UseCases/Users/Authenticate/APIPresenter.php create mode 100644 src/web/backend/src/UseCases/Users/Authenticate/Authenticate.php create mode 100644 src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCase.php create mode 100644 src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCaseServiceProvider.php create mode 100644 src/web/backend/src/UseCases/Users/Authenticate/Presenter.php create mode 100644 src/web/backend/src/UseCases/Users/Authenticate/ResponseModel.php diff --git a/src/web/backend/app/Http/Controllers/AuthController.php b/src/web/backend/app/Http/Controllers/AuthController.php new file mode 100644 index 00000000..6ebb7083 --- /dev/null +++ b/src/web/backend/app/Http/Controllers/AuthController.php @@ -0,0 +1,45 @@ +request = $request; + } + + /** + * @param AuthenticateUseCase $authenticateUseCase + * @return JsonResponse + * @throws ValidationException + * @throws AuthenticationException + * @throws EntityNotFoundException + */ + public function login(AuthenticateUseCase $authenticateUseCase): JsonResponse { + $this->validate($this->request, [ + 'email' => 'required', + 'password' => 'required' + ]); + + $presenter = new APIPresenter(); + + try { + $authenticateUseCase->attempt($presenter, $this->request->all()); + } catch (AuthorizationException $e) { + throw new AuthenticationException(); + } + + return $this->respondWithData($presenter->getViewModel()); + } +} diff --git a/src/web/backend/app/Providers/AppServiceProvider.php b/src/web/backend/app/Providers/AppServiceProvider.php index e7f7a7cf..87672f3a 100644 --- a/src/web/backend/app/Providers/AppServiceProvider.php +++ b/src/web/backend/app/Providers/AppServiceProvider.php @@ -13,6 +13,7 @@ use Source\UseCases\Users\UpdateUser\UpdateUserUseCaseServiceProvider; use Source\UseCases\Users\GetAllUsers\GetAllUsersUseCaseServiceProvider; use Source\UseCases\Token\Authenticate\AuthenticateUseCaseServiceProvider; use Source\UseCases\Doors\Authenticate\AuthenticateUseCaseServiceProvider as DoorAuthenticateUseCaseServiceProvider; +use Source\UseCases\Users\Authenticate\AuthenticateUseCaseServiceProvider as UserAuthenticateUseCaseServiceProvider; class AppServiceProvider extends ServiceProvider { @@ -38,6 +39,7 @@ class AppServiceProvider extends ServiceProvider AuthenticateUseCaseServiceProvider::class, DoorAuthenticateUseCaseServiceProvider::class, + UserAuthenticateUseCaseServiceProvider::class, ]; /** diff --git a/src/web/backend/routes/api.php b/src/web/backend/routes/api.php index 8015670d..4d057cbe 100644 --- a/src/web/backend/routes/api.php +++ b/src/web/backend/routes/api.php @@ -2,6 +2,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; +use App\Http\Controllers\AuthController; use App\Http\Controllers\UsersController; /* @@ -14,6 +15,8 @@ use App\Http\Controllers\UsersController; | is assigned the "api" middleware group. Enjoy building your API! | */ +Route::post('login', [AuthController::class, 'login']); + Route::group(['middleware' => 'auth:api'], static function () { Route::group( [ diff --git a/src/web/backend/src/Entities/Token.php b/src/web/backend/src/Entities/Token.php index 004449d3..466bd3d9 100644 --- a/src/web/backend/src/Entities/Token.php +++ b/src/web/backend/src/Entities/Token.php @@ -128,4 +128,19 @@ class Token { public function getUpdatedAt(): ?Carbon { return $this->updatedAt; } + + /** + * @param int $id + */ + public function setId(int $id): void { + $this->id = $id; + } + + /** + * @param Carbon $date + * @return bool + */ + public function isValidAtTime(Carbon $date): bool { + return $this->expiresAt === null || $this->expiresAt->isAfter($date); + } } diff --git a/src/web/backend/src/Entities/User.php b/src/web/backend/src/Entities/User.php index 5b133489..9d51bca8 100644 --- a/src/web/backend/src/Entities/User.php +++ b/src/web/backend/src/Entities/User.php @@ -199,6 +199,7 @@ class User { if (!$password || !$email) { return false; } + return $this->getEmail() === $email && $this->getPassword() === $password; } diff --git a/src/web/backend/src/Exceptions/AuthorizationException.php b/src/web/backend/src/Exceptions/AuthorizationException.php new file mode 100644 index 00000000..0e63e5a7 --- /dev/null +++ b/src/web/backend/src/Exceptions/AuthorizationException.php @@ -0,0 +1,14 @@ +tokens()->create( [ - 'name' => $token->getName(), - 'api_token' => $token->getTokenString(), + 'name' => $token->getName(), + 'api_token' => $token->getTokenString(), 'expires_at' => $token->getExpiresAt(), ] ); @@ -31,8 +33,12 @@ class DatabaseTokensRepository implements TokensRepository { /** * @inheritDoc */ - public function findByToken(string $token): ?Token { - $found = \App\Token::where('api_token', $token)->first(); + public function findValidToken(string $token): ?Token { + $found = \App\Token::where('api_token', $token)->andWhere('expired_at', '<', Carbon::now())->orWhere( + static function (Builder $query) use ($token) { + $query->where('api_token', $token)->andWhere('expired_at', null); + } + )->first(); if (!$found) { return null; diff --git a/src/web/backend/src/Gateways/Tokens/InMemoryTokensRepository.php b/src/web/backend/src/Gateways/Tokens/InMemoryTokensRepository.php index 9c70fb7d..db9de552 100644 --- a/src/web/backend/src/Gateways/Tokens/InMemoryTokensRepository.php +++ b/src/web/backend/src/Gateways/Tokens/InMemoryTokensRepository.php @@ -4,6 +4,7 @@ namespace Source\Gateways\Tokens; +use Carbon\Carbon; use Source\Entities\Token; class InMemoryTokensRepository implements TokensRepository { @@ -12,17 +13,20 @@ class InMemoryTokensRepository implements TokensRepository { */ protected array $tokens = []; + protected static int $id = 1; + /** @inheritDoc */ public function create(Token $token): Token { + $token->setId(static::$id++); $this->tokens[] = $token; return $token; } /** @inheritDoc */ - public function findByToken(string $tokenToMatch): ?Token { + public function findValidToken(string $tokenToMatch): ?Token { foreach ($this->tokens as $token) { - if ($token->matches($tokenToMatch)) { + if ($token->matches($tokenToMatch) && $token->isValidAtTime(Carbon::now())) { return $token; } } diff --git a/src/web/backend/src/Gateways/Tokens/TokensRepository.php b/src/web/backend/src/Gateways/Tokens/TokensRepository.php index 0bf7ba52..eb5f8da5 100644 --- a/src/web/backend/src/Gateways/Tokens/TokensRepository.php +++ b/src/web/backend/src/Gateways/Tokens/TokensRepository.php @@ -19,5 +19,5 @@ interface TokensRepository { * @param string $token * @return Token|null */ - public function findByToken(string $token): ?Token; + public function findValidToken(string $token): ?Token; } diff --git a/src/web/backend/src/UseCases/BasePresenter.php b/src/web/backend/src/UseCases/BasePresenter.php index 90c41143..c51666d8 100644 --- a/src/web/backend/src/UseCases/BasePresenter.php +++ b/src/web/backend/src/UseCases/BasePresenter.php @@ -48,6 +48,19 @@ abstract class BasePresenter { return $time->format($format); } + /** + * @param Carbon|null $datetime + * @param string $format + * @return string|null + */ + public function formatDateTime(?Carbon $datetime, string $format = 'c'): ?string { + if ($datetime === null) { + return null; + } + + return $datetime->format($format); + } + /** * @param User $user * @return array diff --git a/src/web/backend/src/UseCases/Token/Authenticate/Authenticate.php b/src/web/backend/src/UseCases/Token/Authenticate/Authenticate.php index 64018589..fe096cbf 100644 --- a/src/web/backend/src/UseCases/Token/Authenticate/Authenticate.php +++ b/src/web/backend/src/UseCases/Token/Authenticate/Authenticate.php @@ -29,7 +29,7 @@ class Authenticate implements AuthenticateUseCase { return; } - $found = $this->tokens->findByToken($token); + $found = $this->tokens->findValidToken($token); if (!$found) { return; diff --git a/src/web/backend/src/UseCases/Users/Authenticate/APIPresenter.php b/src/web/backend/src/UseCases/Users/Authenticate/APIPresenter.php new file mode 100644 index 00000000..19418ba4 --- /dev/null +++ b/src/web/backend/src/UseCases/Users/Authenticate/APIPresenter.php @@ -0,0 +1,26 @@ +getUser(); + $token = $responseModel->getToken(); + + $this->viewModel['user'] = $this->formatUser($user); + $this->viewModel['token'] = [ + 'value' => $token->getTokenString(), + 'expires_at' => $this->formatDateTime($token->getExpiresAt()), + ]; + } + + /** @inheritDoc */ + public function getViewModel(): array { + return $this->viewModel; + } +} diff --git a/src/web/backend/src/UseCases/Users/Authenticate/Authenticate.php b/src/web/backend/src/UseCases/Users/Authenticate/Authenticate.php new file mode 100644 index 00000000..95304fd0 --- /dev/null +++ b/src/web/backend/src/UseCases/Users/Authenticate/Authenticate.php @@ -0,0 +1,53 @@ +users = $users; + $this->tokens = $tokens; + } + + /** + * @inheritDoc + */ + public function attempt(Presenter $presenter, array $credentials): void { + $email = $credentials['email'] ?? null; + $password = $credentials['password'] ?? null; + + if (!$email || !$password) { + throw new AuthorizationException(); + } + + $user = $this->users->findByCredentials(strtolower($email), $password); + + if (!$user) { + throw new AuthorizationException(); + } + + $token = $this->tokens->create( + new Token( + 0, + $user->getId(), + Str::random(60), + null, + Carbon::now()->days(2) + ) + ); + + $response = new ResponseModel($user, $token); + + $presenter->present($response); + } +} diff --git a/src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCase.php b/src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCase.php new file mode 100644 index 00000000..c74589e0 --- /dev/null +++ b/src/web/backend/src/UseCases/Users/Authenticate/AuthenticateUseCase.php @@ -0,0 +1,20 @@ +app->bind(AuthenticateUseCase::class, static function (Application $app) { + return new Authenticate($app->make(UsersRepository::class), $app->make(TokensRepository::class)); + }); + } + + /** + * Bootstrap any application services. + * + * @return void + */ + public function boot(): void { + } + + /** + * @return array + */ + public function provides() { + return [AuthenticateUseCase::class]; + } +} diff --git a/src/web/backend/src/UseCases/Users/Authenticate/Presenter.php b/src/web/backend/src/UseCases/Users/Authenticate/Presenter.php new file mode 100644 index 00000000..68b040e8 --- /dev/null +++ b/src/web/backend/src/UseCases/Users/Authenticate/Presenter.php @@ -0,0 +1,16 @@ +user = $user; + $this->token = $token; + } + + /** + * @return User + */ + public function getUser(): User { + return $this->user; + } + + /** + * @return Token + */ + public function getToken(): Token { + return $this->token; + } +} -- GitLab