Add captcha tokens management.

This commit is contained in:
2023-08-22 00:15:35 +06:00
parent d2b29e2225
commit 742b0feaf0
38 changed files with 832 additions and 6 deletions

View File

@@ -0,0 +1,11 @@
<?php declare(strict_types=1);
namespace App\Contracts;
use Illuminate\Database\Eloquent\Builder;
interface GenerateTokenCommand
{
public function execute(): string;
public function unique(Builder $builder, string $field, int $numberAttempts = 10): string;
}

View File

@@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder;
final readonly class CaptchaToken
{
public function __construct(
) { }
}

View File

@@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\CaptchaToken;
use App\Dto\Builder\CaptchaToken;
use App\Dto\Request\Dto;
final readonly class Index extends Dto
{
public function __construct(
private CaptchaToken $captchaTokenDto,
private int $page
) { }
public function getCaptchaTokenDto(): CaptchaToken
{
return $this->captchaTokenDto;
}
public function getPage(): int
{
return $this->page;
}
}

View File

@@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\CaptchaToken;
use App\Dto\Request\Dto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $title
) { }
public function getTitle(): string
{
return $this->title;
}
}

View File

@@ -6,10 +6,24 @@ enum Permission: string
{
case Role = 'role';
case User = 'user';
case CaptchaToken = 'captcha_token';
public function getPermissions(): array
{
return $this->getBasePermissions();
$permissions = match ($this) {
self::CaptchaToken => [
'view' => __('permissions.Allowed to watch all tokens'),
'view_own' => __('permissions.Allowed to view own tokens'),
'create' => __('permissions.Allowed to create tokens'),
'update' => __('permissions.Allowed to edit all tokens'),
'update_own' => __('permissions.Allowed to edit own tokens'),
'delete' => __('permissions.Allowed to delete all tokens'),
'delete_own' => __('permissions.Allowed to delete own tokens'),
],
default => $this->getBasePermissions()
};
return $permissions;
}
public function getTitle(): string

View File

@@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Exceptions\Service;
final class GenerateTokenCommandException extends \Exception
{
}

View File

@@ -0,0 +1,93 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Dto\QuerySettingsDto;
use App\Http\Requests\Private\CaptchaTokens\IndexRequest;
use App\Http\Requests\Private\CaptchaTokens\StoreUpdateRequest;
use App\Services\Private\CaptchaTokenService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class CaptchaTokensController extends Controller
{
public function __construct(
private readonly CaptchaTokenService $captchaTokenService
) { }
public function index(IndexRequest $request): View
{
$user = $request->user();
$data = $request->getDto();
$querySettingsDto = new QuerySettingsDto(
limit: 20,
page: $data->getPage(),
queryWith: []
);
$result = $this->captchaTokenService->index($data->getCaptchaTokenDto(), $querySettingsDto, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/captcha_tokens/index', $result->getData());
}
public function create(Request $request): View
{
$user = $request->user();
$result = $this->captchaTokenService->create($user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/captcha_tokens/create', $result->getData());
}
public function edit(int $id, Request $request): View
{
$user = $request->user();
$result = $this->captchaTokenService->edit($id, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/captcha_tokens/edit', $result->getData());
}
public function store(StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->captchaTokenService->store($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('captcha-tokens.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function update(int $id, StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->captchaTokenService->update($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('captcha-tokens.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function destroy(int $id, Request $request): RedirectResponse
{
$user = $request->user();
$result = $this->captchaTokenService->destroy($id, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('captcha-tokens.index')->withSuccess($result->getMessage());
}
}

View File

@@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use Illuminate\View\View;
final class DashboardController extends Controller
{
public function index(): View
{
return view('private/dashboard/index');
}
}

View File

@@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\CaptchaTokens;
use App\Contracts\FormRequestDto;
use App\Dto\Builder\CaptchaToken;
use App\Dto\Request\Private\CaptchaToken\Index;
use Illuminate\Foundation\Http\FormRequest;
final class IndexRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$this->redirect = route('users.index');
return [
'page' => ['nullable', 'numeric', 'min:1']
];
}
public function getDto(): Index
{
return new Index(
captchaTokenDto: new CaptchaToken(),
page: (int) $this->input('page', 1)
);
}
}

View File

@@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\CaptchaTokens;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\CaptchaToken\StoreUpdate;
use Illuminate\Foundation\Http\FormRequest;
final class StoreUpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'title' => ['required', 'max:255'],
];
}
public function getDto(): StoreUpdate
{
return new StoreUpdate(
title: $this->input('title'),
);
}
}

View File

@@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
final class CaptchaToken extends Model
{
use HasFactory, SoftDeletes;
protected $table = 'captcha_tokens';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'title',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'public_token',
'private_token',
];
}

View File

@@ -0,0 +1,47 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\CaptchaToken;
use App\Models\User;
final readonly class CaptchaTokenPolicy extends Policy
{
public function viewAny(User $user): bool
{
return $user->hasPermission('captcha_token.view') || $user->hasPermission('captcha_token.view_own');
}
public function viewAnyAll(User $user): bool
{
return $user->hasPermission('captcha_token.view');
}
public function viewAnyOwn(User $user): bool
{
return $user->hasPermission('captcha_token.view_own');
}
public function view(User $user, CaptchaToken $captchaToken): bool
{
return $user->hasPermission('captcha_token.view') ||
$user->hasPermission('captcha_token.view_own') && $captchaToken->user_id === $user->id;
}
public function create(User $user): bool
{
return $user->hasPermission('captcha_token.create');
}
public function update(User $user, CaptchaToken $captchaToken): bool
{
return $user->hasPermission('captcha_token.update') ||
$user->hasPermission('captcha_token.update_own') && $captchaToken->user_id === $user->id;
}
public function delete(User $user, CaptchaToken $captchaToken): bool
{
return $user->hasPermission('captcha_token.delete') ||
$user->hasPermission('captcha_token.delete_own') && $captchaToken->user_id === $user->id;
}
}

View File

@@ -11,6 +11,9 @@ use App\Captcha\Images\Head;
use App\Captcha\Images\ImageManager;
use App\Captcha\Images\Lines;
use App\Services\Api\V1\CaptchaService;
use App\Services\CaptchaToken\CaptchaTokenHandler;
use App\Services\GenerateTokenCommand\GenerateTokenUlidCommand;
use App\Services\GenerateTokenCommand\GenerateTokenUuidCommand;
use App\Services\Search\CreateSearchInstanceCommand;
use App\Services\Search\Search;
use Illuminate\Contracts\Foundation\Application;
@@ -43,6 +46,13 @@ final class AppServiceProvider extends ServiceProvider
imageBody: $app->make(ImageBody::class)
);
});
$this->app->bind(CaptchaTokenHandler::class, function (Application $app) {
return new CaptchaTokenHandler(
generatePublicTokenCommand: $app->make(GenerateTokenUlidCommand::class),
generatePrivateTokenCommand: $app->make(GenerateTokenUuidCommand::class)
);
});
}
/**

View File

@@ -1,11 +1,11 @@
<?php
<?php declare(strict_types=1);
namespace App\Providers;
// use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
final class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
@@ -13,7 +13,9 @@ class AuthServiceProvider extends ServiceProvider
* @var array<class-string, class-string>
*/
protected $policies = [
\App\Models\Role::class => \App\Policies\RolePolicy::class,
\App\Models\Role::class => \App\Policies\RolePolicy::class,
\App\Models\User::class => \App\Policies\UserPolicy::class,
\App\Models\CaptchaToken::class => \App\Policies\CaptchaTokenPolicy::class,
];
/**

View File

@@ -0,0 +1,39 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Contracts\Search;
use App\Models\CaptchaToken;
use App\Services\CaptchaToken\BuilderCommand;
use App\Services\Search\CreateSearchInstanceCommand;
use App\Dto\Builder\CaptchaToken as CaptchaTokenDto;
use Illuminate\Database\Eloquent\Builder;
final readonly class CaptchaTokenRepository
{
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
private BuilderCommand $builderCommand
) { }
public function getCaptchaTokenById(int $id): ?CaptchaToken
{
return CaptchaToken::query()->where('id', $id)->first();
}
public function getCaptchaTokens(CaptchaTokenDto $captchaTokenDto, array $with = [], ?int $userId = null): Search
{
$query = CaptchaToken::query()
->when($userId, function (Builder $query, int $userId) {
$query->where('user_id', $userId);
})
->with($with);
$query = $this->builderCommand->execute(
query: $query,
captchaTokenDto: $captchaTokenDto
);
return $this->createSearchInstanceCommand->execute($query);
}
}

View File

@@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\Services\CaptchaToken;
use App\Dto\Builder\CaptchaToken as CaptchaTokenDto;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
final readonly class BuilderCommand
{
public function execute(Relation | Builder $query, CaptchaTokenDto $captchaTokenDto): Relation | Builder
{
return $query;
}
}

View File

@@ -0,0 +1,39 @@
<?php declare(strict_types=1);
namespace App\Services\CaptchaToken;
use App\Contracts\GenerateTokenCommand;
use App\Models\CaptchaToken;
use App\Models\User;
final readonly class CaptchaTokenHandler
{
public function __construct(
private GenerateTokenCommand $generatePublicTokenCommand,
private GenerateTokenCommand $generatePrivateTokenCommand
) { }
public function handleStore(array $data, User $user): CaptchaToken
{
$captchaToken = new CaptchaToken();
$captchaToken->public_token = $this->generatePublicTokenCommand->unique(CaptchaToken::query(), 'public_token');
$captchaToken->private_token = $this->generatePrivateTokenCommand->unique(CaptchaToken::query(), 'private_token');
$captchaToken->user_id = $user->id;
$captchaToken->fill($data)->save();
return $captchaToken;
}
public function handleUpdate(CaptchaToken $captchaToken, array $data): CaptchaToken
{
$captchaToken->update($data);
$captchaToken->touch();
return $captchaToken;
}
public function handleDestroy(CaptchaToken $captchaToken): void
{
$captchaToken->delete();
}
}

View File

@@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace App\Services\GenerateTokenCommand;
use App\Contracts\GenerateTokenCommand as GenerateTokenCommandContract;
use App\Exceptions\Service\GenerateTokenCommandException;
use Illuminate\Database\Eloquent\Builder;
abstract readonly class GenerateTokenCommand implements GenerateTokenCommandContract
{
/**
* @return string
*/
abstract public function execute(): string;
/**
* @param Builder $builder
* @param string $field
* @param int $numberAttempts
* @return string
* @throws GenerateTokenCommandException
*/
public function unique(Builder $builder, string $field, int $numberAttempts = 10): string
{
for ($attempt = 0; $attempt < $numberAttempts; ++$attempt) {
$token = $this->execute();
if ($builder->where($field, '=', $token)->doesntExist()) {
return $token;
}
}
throw new GenerateTokenCommandException(__('Failed to generate token.'));
}
}

View File

@@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace App\Services\GenerateTokenCommand;
use Illuminate\Support\Str;
final readonly class GenerateTokenUlidCommand extends GenerateTokenCommand
{
public function execute(): string
{
return (string) Str::ulid();
}
}

View File

@@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace App\Services\GenerateTokenCommand;
use Illuminate\Support\Str;
final readonly class GenerateTokenUuidCommand extends GenerateTokenCommand
{
public function execute(): string
{
return (string) Str::orderedUuid();
}
}

View File

@@ -0,0 +1,154 @@
<?php declare(strict_types=1);
namespace App\Services\Private;
use App\Dto\Builder\CaptchaToken as CaptchaTokenDto;
use App\Dto\QuerySettingsDto;
use App\Dto\Request\Private\CaptchaToken\StoreUpdate;
use App\Models\User;
use App\Models\CaptchaToken;
use App\Repositories\CaptchaTokenRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\StoreUpdateResult;
use App\Services\CaptchaToken\CaptchaTokenHandler;
use App\Services\Service;
use Illuminate\Support\Facades\DB;
final class CaptchaTokenService extends Service
{
public function __construct(
private readonly CaptchaTokenRepository $captchaTokenRepository,
private readonly CaptchaTokenHandler $captchaTokenHandler,
) { }
public function index(CaptchaTokenDto $captchaTokenDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('viewAny', CaptchaToken::class)) {
return $this->errFobidden(__('Access is denied'));
}
$userId = null;
if ($user->cannot('viewAnyAll', CaptchaToken::class)) {
$userId = $user->id;
}
$captchaTokens = $this->captchaTokenRepository->getCaptchaTokens(
$captchaTokenDto,
$querySettingsDto->getQueryWith(),
$userId
)->pagination(
$querySettingsDto->getLimit(),
$querySettingsDto->getPage()
);
return $this->result([
'captchaTokens' => $captchaTokens,
]);
}
public function create(User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('create', CaptchaToken::class)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'captchaToken' => new CaptchaToken(),
]);
}
public function edit(int $id, User $user): ServiceResultError | ServiceResultArray
{
$modelCaptchaToken = $this->captchaTokenRepository->getCaptchaTokenById($id);
if (is_null($modelCaptchaToken)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('view', $modelCaptchaToken)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'captchaToken' => $modelCaptchaToken,
]);
}
public function store(StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
if ($user->cannot('create', CaptchaToken::class)) {
return $this->errFobidden(__('Access is denied'));
}
try {
$modelCaptchaToken = DB::transaction(function () use ($data, $user) {
$dataCaptchaToken = $this->getDataCaptchaToken($data);
return $this->captchaTokenHandler->handleStore($dataCaptchaToken, $user);
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($modelCaptchaToken, __('Captcha token created successfully'));
}
public function update(int $id, StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
$modelCaptchaToken = $this->captchaTokenRepository->getCaptchaTokenById($id);
if (is_null($modelCaptchaToken)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('update', $modelCaptchaToken)) {
return $this->errFobidden(__('Access is denied'));
}
try {
$modelCaptchaToken = DB::transaction(function () use ($data, $modelCaptchaToken) {
$dataCaptchaToken = $this->getDataCaptchaToken($data);
return $this->captchaTokenHandler->handleUpdate($modelCaptchaToken, $dataCaptchaToken);
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($modelCaptchaToken, __('Captcha token updated successfully'));
}
public function destroy(int $id, User $user): ServiceResultError | ServiceResultSuccess
{
$modelCaptchaToken = $this->captchaTokenRepository->getCaptchaTokenById($id);
if (is_null($modelCaptchaToken)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('delete', $modelCaptchaToken)) {
return $this->errFobidden(__('Access is denied'));
}
try {
DB::transaction(function () use ($modelCaptchaToken) {
$this->captchaTokenHandler->handleDestroy($modelCaptchaToken);
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->ok(__('The captcha token has been removed'));
}
private function getDataCaptchaToken(StoreUpdate $data): array
{
return [
'title' => $data->getTitle(),
];
}
}