diff --git a/app/Contracts/GenerateTokenCommand.php b/app/Contracts/GenerateTokenCommand.php new file mode 100644 index 0000000..e7fabf2 --- /dev/null +++ b/app/Contracts/GenerateTokenCommand.php @@ -0,0 +1,11 @@ +captchaTokenDto; + } + + public function getPage(): int + { + return $this->page; + } +} diff --git a/app/Dto/Request/Private/CaptchaToken/StoreUpdate.php b/app/Dto/Request/Private/CaptchaToken/StoreUpdate.php new file mode 100644 index 0000000..086014f --- /dev/null +++ b/app/Dto/Request/Private/CaptchaToken/StoreUpdate.php @@ -0,0 +1,17 @@ +title; + } +} diff --git a/app/Enums/Permission.php b/app/Enums/Permission.php index 383f347..cc36e2b 100644 --- a/app/Enums/Permission.php +++ b/app/Enums/Permission.php @@ -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 diff --git a/app/Exceptions/Service/GenerateTokenCommandException.php b/app/Exceptions/Service/GenerateTokenCommandException.php new file mode 100644 index 0000000..c19f6de --- /dev/null +++ b/app/Exceptions/Service/GenerateTokenCommandException.php @@ -0,0 +1,8 @@ +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()); + } +} diff --git a/app/Http/Controllers/Private/DashboardController.php b/app/Http/Controllers/Private/DashboardController.php new file mode 100644 index 0000000..8d1cade --- /dev/null +++ b/app/Http/Controllers/Private/DashboardController.php @@ -0,0 +1,13 @@ +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) + ); + } +} diff --git a/app/Http/Requests/Private/CaptchaTokens/StoreUpdateRequest.php b/app/Http/Requests/Private/CaptchaTokens/StoreUpdateRequest.php new file mode 100644 index 0000000..01cf143 --- /dev/null +++ b/app/Http/Requests/Private/CaptchaTokens/StoreUpdateRequest.php @@ -0,0 +1,28 @@ + ['required', 'max:255'], + ]; + } + + + public function getDto(): StoreUpdate + { + return new StoreUpdate( + title: $this->input('title'), + ); + } +} diff --git a/app/Models/CaptchaToken.php b/app/Models/CaptchaToken.php new file mode 100644 index 0000000..71544be --- /dev/null +++ b/app/Models/CaptchaToken.php @@ -0,0 +1,33 @@ + + */ + protected $hidden = [ + 'public_token', + 'private_token', + ]; +} diff --git a/app/Policies/CaptchaTokenPolicy.php b/app/Policies/CaptchaTokenPolicy.php new file mode 100644 index 0000000..c02fc5a --- /dev/null +++ b/app/Policies/CaptchaTokenPolicy.php @@ -0,0 +1,47 @@ +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; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index d7a5122..f9a56fd 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -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) + ); + }); } /** diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index f4c0ae9..63964b0 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -1,11 +1,11 @@ - */ 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, ]; /** diff --git a/app/Repositories/CaptchaTokenRepository.php b/app/Repositories/CaptchaTokenRepository.php new file mode 100644 index 0000000..d09f7ce --- /dev/null +++ b/app/Repositories/CaptchaTokenRepository.php @@ -0,0 +1,39 @@ +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); + } +} diff --git a/app/Services/CaptchaToken/BuilderCommand.php b/app/Services/CaptchaToken/BuilderCommand.php new file mode 100644 index 0000000..ca70171 --- /dev/null +++ b/app/Services/CaptchaToken/BuilderCommand.php @@ -0,0 +1,15 @@ +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(); + } +} diff --git a/app/Services/GenerateTokenCommand/GenerateTokenCommand.php b/app/Services/GenerateTokenCommand/GenerateTokenCommand.php new file mode 100644 index 0000000..27ee1d4 --- /dev/null +++ b/app/Services/GenerateTokenCommand/GenerateTokenCommand.php @@ -0,0 +1,34 @@ +execute(); + if ($builder->where($field, '=', $token)->doesntExist()) { + return $token; + } + } + + throw new GenerateTokenCommandException(__('Failed to generate token.')); + } +} diff --git a/app/Services/GenerateTokenCommand/GenerateTokenUlidCommand.php b/app/Services/GenerateTokenCommand/GenerateTokenUlidCommand.php new file mode 100644 index 0000000..cd2d35d --- /dev/null +++ b/app/Services/GenerateTokenCommand/GenerateTokenUlidCommand.php @@ -0,0 +1,13 @@ +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(), + ]; + } +} diff --git a/database/migrations/2023_08_03_145020_create_captcha_tokens_table.php b/database/migrations/2023_08_03_145020_create_captcha_tokens_table.php new file mode 100644 index 0000000..b36701f --- /dev/null +++ b/database/migrations/2023_08_03_145020_create_captcha_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('title'); + $table->ulid('public_token')->unique(); + $table->uuid('private_token')->unique(); + $table->unsignedBigInteger('user_id')->index(); + $table->foreign('user_id')->references('id')->on('users'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('captcha_tokens'); + } +}; diff --git a/lang/en.json b/lang/en.json index bd47dd9..c9cf9e4 100644 --- a/lang/en.json +++ b/lang/en.json @@ -58,5 +58,9 @@ "no": "no", "The user was successfully created": "The user was successfully created", "The user was successfully updated": "The user was successfully updated", - "The user has been deleted": "The user has been deleted" + "The user has been deleted": "The user has been deleted", + "Captcha token created successfully": "Captcha token created successfully", + "Captcha token updated successfully": "Captcha token updated successfully", + "The captcha token has been removed": "The captcha token has been removed", + "Failed to generate token.": "Failed to generate token." } diff --git a/lang/en/permissions.php b/lang/en/permissions.php index d207a51..2029569 100644 --- a/lang/en/permissions.php +++ b/lang/en/permissions.php @@ -7,4 +7,11 @@ return [ 'Allowed to create' => 'Allowed to create', 'Allowed to edit' => 'Allowed to edit', 'Allowed to delete' => 'Allowed to delete', + 'Allowed to watch all tokens' => 'Allowed to watch all tokens', + 'Allowed to view own tokens' => 'Allowed to view own tokens', + 'Allowed to create tokens' => 'Allowed to create tokens', + 'Allowed to edit all tokens' => 'Allowed to edit all tokens', + 'Allowed to edit own tokens' => 'Allowed to edit own tokens', + 'Allowed to delete own tokens' => 'Allowed to delete own tokens', + 'Allowed to delete all tokens' => 'Allowed to delete all tokens', ]; diff --git a/lang/en/sections.php b/lang/en/sections.php index 14945a5..73fc2f6 100644 --- a/lang/en/sections.php +++ b/lang/en/sections.php @@ -4,4 +4,5 @@ return [ 'Dashboard' => 'Dashboard', 'User group' => 'User group', 'Users' => 'Users', + 'Captcha tokens' => 'Captcha tokens', ]; diff --git a/lang/en/validation.php b/lang/en/validation.php index f336241..0e251fb 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -220,5 +220,7 @@ return [ 'permissions' => 'permissions', 'is_active' => 'is active', 'roles' => 'user group', + 'public_token' => 'public token', + 'private_token' => 'private token', ], ]; diff --git a/lang/ru.json b/lang/ru.json index 8479ec6..81b8482 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -58,5 +58,9 @@ "no": "нет", "The user was successfully created": "Пользователь был успешно создан", "The user was successfully updated": "Пользователь был успешно обновлен", - "The user has been deleted": "Пользователь был удален" + "The user has been deleted": "Пользователь был удален", + "Captcha token created successfully": "Токен капчи успешно создан", + "Captcha token updated successfully": "Токен капчи успешно обновлен", + "The captcha token has been removed": "Токен капчи был удален", + "Failed to generate token.": "Не удалось сгенерировать токен." } diff --git a/lang/ru/permissions.php b/lang/ru/permissions.php index 275f953..a64c97f 100644 --- a/lang/ru/permissions.php +++ b/lang/ru/permissions.php @@ -7,4 +7,11 @@ return [ 'Allowed to create' => 'Разрешено создать', 'Allowed to edit' => 'Разрешено редактировать', 'Allowed to delete' => 'Разрешено удалять', + 'Allowed to watch all tokens' => 'Разрешено смотреть все токены', + 'Allowed to view own tokens' => 'Разрешено просматривать собственные токены', + 'Allowed to create tokens' => 'Разрешено создавать токены', + 'Allowed to edit all tokens' => 'Разрешено редактировать все токены', + 'Allowed to edit own tokens' => 'Разрешено редактировать собственные токены', + 'Allowed to delete all tokens' => 'Разрешено удалять все токены', + 'Allowed to delete own tokens' => 'Разрешено удалять собственные токены', ]; diff --git a/lang/ru/sections.php b/lang/ru/sections.php index 358c65b..216d1b6 100644 --- a/lang/ru/sections.php +++ b/lang/ru/sections.php @@ -4,5 +4,6 @@ return [ 'Dashboard' => 'Dashboard', 'User group' => 'Группа пользователей', 'Users' => 'Пользователи', + 'Captcha tokens' => 'Токены от капчи', ]; diff --git a/lang/ru/validation.php b/lang/ru/validation.php index 8732c5f..3a75b75 100644 --- a/lang/ru/validation.php +++ b/lang/ru/validation.php @@ -220,5 +220,7 @@ return [ 'permissions' => 'разрешения', 'is_active' => 'активен', 'roles' => 'группа пользователей', + 'public_token' => 'публичный токен', + 'private_token' => 'приватный токен', ], ]; diff --git a/resources/views/private/captcha_tokens/_from.blade.php b/resources/views/private/captcha_tokens/_from.blade.php new file mode 100644 index 0000000..db1ab7a --- /dev/null +++ b/resources/views/private/captcha_tokens/_from.blade.php @@ -0,0 +1,5 @@ +@csrf + +@canany(['create', 'update'], $captchaToken) + +@endcanany diff --git a/resources/views/private/captcha_tokens/_top.blade.php b/resources/views/private/captcha_tokens/_top.blade.php new file mode 100644 index 0000000..01bcf37 --- /dev/null +++ b/resources/views/private/captcha_tokens/_top.blade.php @@ -0,0 +1,8 @@ +@can('create', \App\Models\CaptchaToken::class) +
+ + + {{ __('Create') }} + +
+@endcan diff --git a/resources/views/private/captcha_tokens/create.blade.php b/resources/views/private/captcha_tokens/create.blade.php new file mode 100644 index 0000000..ad2abbe --- /dev/null +++ b/resources/views/private/captcha_tokens/create.blade.php @@ -0,0 +1,16 @@ +@section('meta_title', __('sections.Captcha tokens')) +@section('h1', __('sections.Captcha tokens')) + + @include('private.captcha_tokens._top') +
+
+
+
+
+ @include('private.captcha_tokens._from') +
+
+
+
+
+
diff --git a/resources/views/private/captcha_tokens/edit.blade.php b/resources/views/private/captcha_tokens/edit.blade.php new file mode 100644 index 0000000..e04c5e1 --- /dev/null +++ b/resources/views/private/captcha_tokens/edit.blade.php @@ -0,0 +1,19 @@ +@section('meta_title', __('sections.Captcha tokens')) +@section('h1', __('sections.Captcha tokens')) + + @include('private.captcha_tokens._top') +
+
+
+
+
+ @method('PUT') + + + @include('private.captcha_tokens._from') + +
+
+
+
+
diff --git a/resources/views/private/captcha_tokens/index.blade.php b/resources/views/private/captcha_tokens/index.blade.php new file mode 100644 index 0000000..896307b --- /dev/null +++ b/resources/views/private/captcha_tokens/index.blade.php @@ -0,0 +1,50 @@ +@section('meta_title', __('sections.Captcha tokens')) +@section('h1', __('sections.Captcha tokens')) + + @include('private.captcha_tokens._top') +
+
+
+ + + + + + + + + @foreach($captchaTokens as $captchaToken) + + + + + @endforeach + +
{{ __('validation.attributes.title') }}
+ + + + + {{ $captchaToken->title }} + + + @can('delete', $captchaToken) +
+ @csrf + @method('DELETE') + +
+ @endcan +
+ +
+
+
+ @push('scripts') + @include('private._scripts._click-confirm', ['alert' => __('Do you want to delete?')]) + @endpush +
diff --git a/resources/views/private/dashboard/index.blade.php b/resources/views/private/dashboard/index.blade.php new file mode 100644 index 0000000..057ac8b --- /dev/null +++ b/resources/views/private/dashboard/index.blade.php @@ -0,0 +1,5 @@ +@section('meta_title', __('sections.Dashboard')) +@section('h1', __('sections.Dashboard')) + + + diff --git a/resources/views/private/layout/_navigation.blade.php b/resources/views/private/layout/_navigation.blade.php index 85594ff..17f9e92 100644 --- a/resources/views/private/layout/_navigation.blade.php +++ b/resources/views/private/layout/_navigation.blade.php @@ -19,6 +19,20 @@ + @can('viewAny', \App\Models\CaptchaToken::class) +
  • request()->route()->named('captcha-tokens.*'), + ])> + + + + + {{ __('sections.Captcha tokens') }} + +
  • + @endcan + @can('viewAny', \App\Models\User::class)
  • group(function () { Route::put('users/{id}/password', [\App\Http\Controllers\Private\UsersController::class, 'updatePassword'])->name('users.update-password')->where(['id' => '[0-9]+']); Route::resource('roles', \App\Http\Controllers\Private\RolesController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['role' => '[0-9]+']); + Route::resource('captcha-tokens', \App\Http\Controllers\Private\CaptchaTokensController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['captcha_token' => '[0-9]+']); });