Commit cebf78db authored by Jacob Priddy's avatar Jacob Priddy 👌
Browse files

Merge branch '3-create-login-route' into 'master'

Resolve "Create login route"

Closes #3

See merge request kretschmar/doorcode!8
parents b86f1216 87df45f6
Pipeline #1586 passed with stages
in 1 minute and 40 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;
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,
];
/**
......
......@@ -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(
[
......
......@@ -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);
}
}
......@@ -199,6 +199,7 @@ class User {
if (!$password || !$email) {
return false;
}
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;
use App\User;
use Carbon\Carbon;
use Source\Entities\Token;
use Basho\Riak\Node\Builder;
use Source\Exceptions\EntityNotFoundException;
class DatabaseTokensRepository implements TokensRepository {
......@@ -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;
......
......@@ -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;
}
}
......
......@@ -19,5 +19,5 @@ interface TokensRepository {
* @param string $token
* @return Token|null
*/
public function findByToken(string $token): ?Token;
public function findValidToken(string $token): ?Token;
}
......@@ -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
......
......@@ -29,7 +29,7 @@ class Authenticate implements AuthenticateUseCase {
return;
}
$found = $this->tokens->findByToken($token);
$found = $this->tokens->findValidToken($token);
if (!$found) {
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;
}
}
Supports Markdown
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