Commit 87df45f6 authored by Jacob Priddy's avatar Jacob Priddy 👌

add login route

parent b86f1216
Pipeline #1585 passed with stages
in 1 minute and 38 seconds
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Auth\AuthenticationException;
use Source\Exceptions\AuthorizationException;
use Illuminate\Validation\ValidationException;
use Source\Exceptions\EntityNotFoundException;
use Source\UseCases\Users\Authenticate\APIPresenter;
use Source\UseCases\Users\Authenticate\AuthenticateUseCase;
class AuthController extends ApiController
{
protected Request $request;
public function __construct(Request $request) {
$this->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());
}
}
...@@ -13,6 +13,7 @@ use Source\UseCases\Users\UpdateUser\UpdateUserUseCaseServiceProvider; ...@@ -13,6 +13,7 @@ use Source\UseCases\Users\UpdateUser\UpdateUserUseCaseServiceProvider;
use Source\UseCases\Users\GetAllUsers\GetAllUsersUseCaseServiceProvider; use Source\UseCases\Users\GetAllUsers\GetAllUsersUseCaseServiceProvider;
use Source\UseCases\Token\Authenticate\AuthenticateUseCaseServiceProvider; use Source\UseCases\Token\Authenticate\AuthenticateUseCaseServiceProvider;
use Source\UseCases\Doors\Authenticate\AuthenticateUseCaseServiceProvider as DoorAuthenticateUseCaseServiceProvider; use Source\UseCases\Doors\Authenticate\AuthenticateUseCaseServiceProvider as DoorAuthenticateUseCaseServiceProvider;
use Source\UseCases\Users\Authenticate\AuthenticateUseCaseServiceProvider as UserAuthenticateUseCaseServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
...@@ -38,6 +39,7 @@ class AppServiceProvider extends ServiceProvider ...@@ -38,6 +39,7 @@ class AppServiceProvider extends ServiceProvider
AuthenticateUseCaseServiceProvider::class, AuthenticateUseCaseServiceProvider::class,
DoorAuthenticateUseCaseServiceProvider::class, DoorAuthenticateUseCaseServiceProvider::class,
UserAuthenticateUseCaseServiceProvider::class,
]; ];
/** /**
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\UsersController; use App\Http\Controllers\UsersController;
/* /*
...@@ -14,6 +15,8 @@ use App\Http\Controllers\UsersController; ...@@ -14,6 +15,8 @@ use App\Http\Controllers\UsersController;
| is assigned the "api" middleware group. Enjoy building your API! | 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(['middleware' => 'auth:api'], static function () {
Route::group( Route::group(
[ [
......
...@@ -128,4 +128,19 @@ class Token { ...@@ -128,4 +128,19 @@ class Token {
public function getUpdatedAt(): ?Carbon { public function getUpdatedAt(): ?Carbon {
return $this->updatedAt; 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);
}
} }
...@@ -199,6 +199,7 @@ class User { ...@@ -199,6 +199,7 @@ class User {
if (!$password || !$email) { if (!$password || !$email) {
return false; return false;
} }
return $this->getEmail() === $email && $this->getPassword() === $password; return $this->getEmail() === $email && $this->getPassword() === $password;
} }
......
<?php
namespace Source\Exceptions;
use Exception;
use Throwable;
class AuthorizationException extends Exception {
public function __construct($message = 'Unauthorized', $code = 0, Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
}
...@@ -5,7 +5,9 @@ namespace Source\Gateways\Tokens; ...@@ -5,7 +5,9 @@ namespace Source\Gateways\Tokens;
use App\User; use App\User;
use Carbon\Carbon;
use Source\Entities\Token; use Source\Entities\Token;
use Basho\Riak\Node\Builder;
use Source\Exceptions\EntityNotFoundException; use Source\Exceptions\EntityNotFoundException;
class DatabaseTokensRepository implements TokensRepository { class DatabaseTokensRepository implements TokensRepository {
...@@ -21,8 +23,8 @@ class DatabaseTokensRepository implements TokensRepository { ...@@ -21,8 +23,8 @@ class DatabaseTokensRepository implements TokensRepository {
return $user->tokens()->create( return $user->tokens()->create(
[ [
'name' => $token->getName(), 'name' => $token->getName(),
'api_token' => $token->getTokenString(), 'api_token' => $token->getTokenString(),
'expires_at' => $token->getExpiresAt(), 'expires_at' => $token->getExpiresAt(),
] ]
); );
...@@ -31,8 +33,12 @@ class DatabaseTokensRepository implements TokensRepository { ...@@ -31,8 +33,12 @@ class DatabaseTokensRepository implements TokensRepository {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function findByToken(string $token): ?Token { public function findValidToken(string $token): ?Token {
$found = \App\Token::where('api_token', $token)->first(); $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) { if (!$found) {
return null; return null;
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
namespace Source\Gateways\Tokens; namespace Source\Gateways\Tokens;
use Carbon\Carbon;
use Source\Entities\Token; use Source\Entities\Token;
class InMemoryTokensRepository implements TokensRepository { class InMemoryTokensRepository implements TokensRepository {
...@@ -12,17 +13,20 @@ class InMemoryTokensRepository implements TokensRepository { ...@@ -12,17 +13,20 @@ class InMemoryTokensRepository implements TokensRepository {
*/ */
protected array $tokens = []; protected array $tokens = [];
protected static int $id = 1;
/** @inheritDoc */ /** @inheritDoc */
public function create(Token $token): Token { public function create(Token $token): Token {
$token->setId(static::$id++);
$this->tokens[] = $token; $this->tokens[] = $token;
return $token; return $token;
} }
/** @inheritDoc */ /** @inheritDoc */
public function findByToken(string $tokenToMatch): ?Token { public function findValidToken(string $tokenToMatch): ?Token {
foreach ($this->tokens as $token) { foreach ($this->tokens as $token) {
if ($token->matches($tokenToMatch)) { if ($token->matches($tokenToMatch) && $token->isValidAtTime(Carbon::now())) {
return $token; return $token;
} }
} }
......
...@@ -19,5 +19,5 @@ interface TokensRepository { ...@@ -19,5 +19,5 @@ interface TokensRepository {
* @param string $token * @param string $token
* @return Token|null * @return Token|null
*/ */
public function findByToken(string $token): ?Token; public function findValidToken(string $token): ?Token;
} }
...@@ -48,6 +48,19 @@ abstract class BasePresenter { ...@@ -48,6 +48,19 @@ abstract class BasePresenter {
return $time->format($format); 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 * @param User $user
* @return array * @return array
......
...@@ -29,7 +29,7 @@ class Authenticate implements AuthenticateUseCase { ...@@ -29,7 +29,7 @@ class Authenticate implements AuthenticateUseCase {
return; return;
} }
$found = $this->tokens->findByToken($token); $found = $this->tokens->findValidToken($token);
if (!$found) { if (!$found) {
return; return;
......
<?php
namespace Source\UseCases\Users\Authenticate;
use Source\UseCases\BasePresenter;
class APIPresenter extends BasePresenter implements Presenter {
protected array $viewModel = [];
/** @inheritDoc */
public function present(ResponseModel $responseModel): void {
$user = $responseModel->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;
}
}
<?php
namespace Source\UseCases\Users\Authenticate;
use Carbon\Carbon;
use Source\Entities\Token;
use Illuminate\Support\Str;
use Source\Gateways\Users\UsersRepository;
use Source\Gateways\Tokens\TokensRepository;
use Source\Exceptions\AuthorizationException;
class Authenticate implements AuthenticateUseCase {
protected UsersRepository $users;
protected TokensRepository $tokens;
public function __construct(UsersRepository $users, TokensRepository $tokens) {
$this->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);
}
}
<?php
namespace Source\UseCases\Users\Authenticate;
use Source\Exceptions\AuthorizationException;
use Source\Exceptions\EntityNotFoundException;
interface AuthenticateUseCase {
/**
* Attempt an auth with credentials
*
* @param Presenter $presenter
* @param array $credentials
* @throws AuthorizationException
* @throws EntityNotFoundException
*/
public function attempt(Presenter $presenter, array $credentials): void;
}
<?php
namespace Source\UseCases\Users\Authenticate;
use Source\Gateways\Users\UsersRepository;
use Source\Gateways\Tokens\TokensRepository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Support\ServiceProvider;
/**
* Service provider must be registered in AppServiceProvider
*/
class AuthenticateUseCaseServiceProvider extends ServiceProvider implements DeferrableProvider {
/**
* Register any application services.
*
* @return void
*/
public function register() {
$this->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];
}
}
<?php
namespace Source\UseCases\Users\Authenticate;
interface Presenter {
/**
* @param ResponseModel $responseModel
* @return void
*/
public function present(ResponseModel $responseModel): void;
/**
* @return array
*/
public function getViewModel(): array;
}
<?php
namespace Source\UseCases\Users\Authenticate;
use Source\Entities\User;
use Source\Entities\Token;
class ResponseModel {
/**
* @var User
*/
protected User $user;
/**
* @var Token
*/
protected Token $token;
/**
* @param User $user
* @param Token $token
*/
public function __construct(User $user, Token $token) {
$this->user = $user;
$this->token = $token;
}
/**
* @return User
*/
public function getUser(): User {
return $this->user;
}
/**
* @return Token
*/
public function getToken(): Token {
return $this->token;
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment