Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,5 @@ VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

JWT_SECRET=
10 changes: 6 additions & 4 deletions .env.testing
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ APP_DEBUG=true
APP_URL=http://127.0.0.1:8000

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3307
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=cutcode_shop_test
DB_USERNAME=root
DB_PASSWORD=root
DB_USERNAME=shop
DB_PASSWORD=secret

JWT_SECRET=S8ucPBMPTn0RfAZwXBK4vwpa4z7Vb3ekgBxgw6nhqyE=
37 changes: 37 additions & 0 deletions app/Actions/Api/CreateTokenAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace App\Actions\Api;

use App\Dto\AuthenticateDto;
use App\Guards\JWTGuard;
use Domain\Auth\JWT;
use Illuminate\Contracts\Auth\Factory;

final class CreateTokenAction
{
public function __construct(
private JWT $jwt,
private Factory $auth,
) {
}

public function handle(AuthenticateDto $dto): ?array
{
/** @var JWTGuard $guard */
$guard = $this->auth->guard();

$id = $guard->retrieveIdByCredentials(
$dto->getEmail(),
$dto->getPassword()
);

if ($id === null) {
return null;
}

return [
$this->jwt->create($id),
$this->jwt->create($id, refresh: true),
];
}
}
27 changes: 27 additions & 0 deletions app/Actions/Api/RefreshTokenAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace App\Actions\Api;

use Domain\Auth\JWT;

class RefreshTokenAction
{
public function __construct(
private JWT $jwt,
) {
}

public function handle(string $refreshToken): ?array
{
$id = $this->jwt->parse($refreshToken);

if ($id === null) {
return null;
}

return [
$this->jwt->create($id),
$this->jwt->create($id, refresh: true),
];
}
}
21 changes: 21 additions & 0 deletions app/Console/Commands/RedisSubscribe.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;

class RedisSubscribe extends Command
{
protected $signature = 'redis:subscribe';

protected $description = 'Subscribe to a Redis channel';

public function handle(): void
{
$this->info('start listening');
Redis::subscribe(['product_change'], function ($message) {
print_r($message . "\n");
});
}
}
13 changes: 13 additions & 0 deletions app/Contracts/Api/ResponderContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace App\Contracts\Api;


use Symfony\Component\HttpFoundation\Response;
use Throwable;

interface ResponderContract
{
public function respond(ResponseResolverContract $resolver): Response;
public function error(Throwable $e): Response;
}
9 changes: 9 additions & 0 deletions app/Contracts/Api/ResponseResolverContract.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace App\Contracts\Api;

interface ResponseResolverContract
{
public function with(mixed $data = null): static;
public function resolve(): mixed;
}
32 changes: 32 additions & 0 deletions app/Dto/AuthenticateDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace App\Dto;

use SensitiveParameter;

final readonly class AuthenticateDto
{
public function __construct(
private string $email,
#[SensitiveParameter]
private string $password,
) {
}

public function getEmail(): string
{
return $this->email;
}

public function getPassword(): string
{
return $this->password;
}

public function toArray(): array
{
return [
'email' => $this->email,
];
}
}
21 changes: 21 additions & 0 deletions app/Enums/ApiErrorCode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Enums;

enum ApiErrorCode: string
{
case TOKEN_EXPIRED = 'token_expired';
case TOKEN_INVALID = 'token_invalid';
case TOKEN_REFRESH_FAILED = 'token_refresh_failed';
case CREDENTIALS_INVALID = 'credentials_invalid';

public function toString(): string
{
return match ($this) {
self::TOKEN_EXPIRED => 'Token expired',
self::TOKEN_INVALID => 'Token invalid',
self::TOKEN_REFRESH_FAILED => 'Token refresh failed',
self::CREDENTIALS_INVALID => 'Credentials invalid',
};
}
}
4 changes: 0 additions & 4 deletions app/Events/AfterSessionRegenerated.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

Expand Down
27 changes: 27 additions & 0 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@

namespace App\Exceptions;

use App\Enums\ApiErrorCode;
use App\Http\Responses\Api\TokenResponse;
use Domain\Auth\Exceptions\JWTExpiredException;
use Domain\Auth\Exceptions\JWTParserException;
use Domain\Auth\Exceptions\JWTValidatorException;
use DomainException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpFoundation\Response;
use Throwable;

class Handler extends ExceptionHandler
Expand Down Expand Up @@ -57,5 +63,26 @@ public function register(): void
? back()
: redirect()->route('home');
});

$this->renderable(function (JWTExpiredException $ex) {
return app(TokenResponse::class)->toFailure(
ApiErrorCode::TOKEN_EXPIRED,
Response::HTTP_UNAUTHORIZED
);
});

$this->renderable(function (JWTValidatorException $ex) {
return app(TokenResponse::class)->toFailure(
ApiErrorCode::TOKEN_INVALID,
Response::HTTP_UNAUTHORIZED
);
});

$this->renderable(function (JWTParserException $ex) {
return app(TokenResponse::class)->toFailure(
ApiErrorCode::TOKEN_INVALID,
Response::HTTP_UNAUTHORIZED
);
});
}
}
5 changes: 3 additions & 2 deletions app/Filters/PriceFilter.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Filters;

use Domain\Catalog\Filters\AbstractFilter;
use Domain\Product\Models\Product;
use Illuminate\Contracts\Database\Eloquent\Builder;

class PriceFilter extends AbstractFilter
Expand All @@ -23,7 +24,7 @@ public function apply(Builder $query): Builder
return $query->when($this->requestValue(), function (Builder $q) {
$q->whereBetween('price', [
$this->requestValue('from', 0) * 100,
$this->requestValue('to', 100000) * 100,
$this->requestValue('to', Product::query()->max('price')) * 100,
]);
});
}
Expand All @@ -32,7 +33,7 @@ public function values(): array
{
return [
'from' => 0,
'to' => 100000
'to' => Product::query()->max('price'),
];
}

Expand Down
80 changes: 80 additions & 0 deletions app/Guards/JWTGuard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace App\Guards;

use Domain\Auth\JWT;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;

class JWTGuard implements Guard
{
use GuardHelpers;

public const BLACKLIST_KEY = 'blacklist_token_';

private bool $blackListChecked = false;

public function __construct(
private JWT $jwt,
UserProvider $provider,
) {
$this->provider = $provider;
}

public function user(): ?Authenticatable
{
$token = request()->bearerToken();

if ($token === null) {
return null;
}

if (!$this->blackListChecked && cache()->has(self::BLACKLIST_KEY . $token)) {
return null;
}

$this->blackListChecked = true;

if ($this->user !== null) {
return $this->user;
}

$id = $this->jwt->parse($token);

return $this->user = $this->provider->retrieveById($id);
}

public function logout(): void
{
$token = request()->bearerToken();

cache()->put(self::BLACKLIST_KEY . $token, $token, $this->jwt->getExpiresAt());

$this->blackListChecked = false;
$this->user = null;
}

public function retrieveIdByCredentials(string $email, string $password): ?string
{
$credentials = ['email' => $email, 'password' => $password];

$user = $this->validate($credentials);

if ($user === null) {
return null;
}

if (!$this->provider->validateCredentials($user, $credentials)) {
return null;
}

return (string) $user->getAuthIdentifier();
}

public function validate(array $credentials = []): bool|Authenticatable|null
{
return $this->provider->retrieveByCredentials($credentials);
}
}
Loading