Init git.

This commit is contained in:
Leonid Nikitin 2024-06-18 22:35:54 +05:00
commit a8d656148a
Signed by: kor-elf
GPG Key ID: 3C0F720C170F6E1D
282 changed files with 25996 additions and 0 deletions

10
.env.example Normal file
View File

@ -0,0 +1,10 @@
DOCKER_APP_PORT=8080
DOCKER_CAPTCHA_PORT=8081
DOCKER_CAPTCHA_WEBSOCKET_PORT=8082
DOCKER_DB_PORT=3306
MYSQL_ROOT_PASSWORD=root_pass
DB_DATABASE=registry
DB_USERNAME=registry
DB_PASSWORD=registry_pass
UID=1000
GID=1000

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.env
Homestead.yaml
Homestead.json
/.vagrant
.phpunit.result.cache

9
LICENSE.md Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 - 2024 Leonid Nikitin (kor-elf)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

19
README.md Normal file
View File

@ -0,0 +1,19 @@
## О проекте MDHub
Docker Hub отличный сервис, но, а что если он Вас заблокирует или перестанет по другим причинам работать?! Я так же задумался, а вдруг образы от моих проектов, кто-то не сможет скачать с Docker Hub? Цель проекта сделать возможность загружать свои образы в своё хранилища и разрешить скачивать их без авторизации. Так же была возможность загружать и приватные образы, где нужна авторизация. Типа Docker Hub на минималках.
[Сайт проекта](https://my-docker-repository.projects.kor-elf.net/)
## Зависимости
php 8.3 (модули: redis)
redis
mysql 8
[Service Captcha](https://git.kor-elf.net/kor-elf/service-captcha)
## Лицензия
[MIT license](https://opensource.org/licenses/MIT).

14
app/.dockerignore Normal file
View File

@ -0,0 +1,14 @@
**/.env
**/*.env
**/.env.example
**/storage/app/*
**/storage/debugbar
**/storage/framework/cache/*
**/storage/framework/sessions/*
**/storage/framework/views/*
**/storage/framework/testing/*
**/storage/logs/*
**/vendor/
**/node_modules/
**/public/build/
**/public/storage

View File

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4

View File

@ -0,0 +1,76 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:LAXuVFnfcrPlPcYftpV/hJ7mEO1TlbriMaDN7hT5WOo=
APP_DEBUG=true
DEBUGBAR_OPEN_STORAGE=true
APP_TIMEZONE=UTC
APP_URL=
APP_CAPTCHA=false
CAPTCHA_API_DOMAIN=http://your-domain-captcha-or-IP:8081
CAPTCHA_PRIVATE_TOKEN=
CAPTCHA_STATIC_PATH=http://your-domain-captcha-or-IP:8081/captcha
CAPTCHA_PUBLIC_TOKEN=
APP_FORCE_HTTPS=false
APP_DEFAULT_LOCALE=ru
APP_FAKER_LOCALE=ru_RU
APP_DEFAULT_USER_TIMEZONE=Asia/Almaty
APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database
LOGIN_MAX_REQUEST=50
LOGIN_MAX_EMAIL_REQUEST=10
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=registry
DB_USERNAME=registry
DB_PASSWORD=registry_pass
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=true
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
CACHE_STORE=redis
CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=app-redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
app/application/.gitattributes vendored Normal file
View File

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

20
app/application/.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode

66
app/application/README.md Normal file
View File

@ -0,0 +1,66 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@ -0,0 +1,95 @@
<?php declare(strict_types=1);
namespace App\Console\Commands;
use App\Dto\Service\User\ManyRoleDto;
use App\Enums\SystemRole;
use App\Repositories\RoleRepository;
use App\Rules\Username;
use App\Services\User\UserCommandHandler;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator as ValidatorFacade;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\Validator;
final class CreateUserAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:create-user-admin {email} {username} {password}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create admin user.';
/**
* Execute the console command.
*/
public function handle(UserCommandHandler $userCommandHandler, RoleRepository $roleRepository): void
{
$validator = $this->getData();
if ($validator->fails()) {
$this->errorMessageAndStop($validator->errors()->all());
}
$data = $validator->valid();
try {
$role = $roleRepository->getRoleByCode(SystemRole::Admin->value);
if (is_null($role)) {
$this->errorMessageAndStop('Administrator role not found.');
}
DB::transaction(function () use($data, $userCommandHandler, $role) {
$data['name'] = 'Administrator';
$user = $userCommandHandler->handleStore($data, $data['password']);
$userCommandHandler->handleConfirmationByEmail($user);
$roles = new ManyRoleDto([$role->id]);
$userCommandHandler->handleSyncRoles($user, $roles);
});
} catch (\Throwable $e) {
$this->errorMessageAndStop($e->getMessage());
}
$this->info('The command was successful!');
}
private function getData(): Validator
{
return ValidatorFacade::make([
'email' => $this->argument('email'),
'username' => $this->argument('username'),
'password' => $this->argument('password'),
], [
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
'username' => ['required', 'string', new Username(), 'unique:users,username'],
'password' => ['required', Password::default()],
]);
}
private function stop(): never
{
exit;
}
private function errorMessageAndStop(string | array $error): never
{
$this->info('User not created. See error messages below:');
if (is_array($error)) {
foreach ($error as $err) {
$this->error($err);
}
} else {
$this->error($error);
}
$this->stop();
}
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Contracts;
use App\Dto\Service\Dto;
interface FormRequestDto
{
public function getDto(): Dto;
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Contracts;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
interface Search
{
public function __construct(Relation | Builder $query);
public function all(): Collection;
public function get(int $limit): Collection;
public function pagination(int $limit, int $page = 1): LengthAwarePaginator;
public function cursorPaginate(int $limit): CursorPaginator;
}

View File

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
namespace App\Contracts;
interface ServiceResult
{
public function isSuccess(): bool;
public function isError(): bool;
}

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
namespace App\Contracts;
interface ServiceResultError
{
public function getCode(): ?int;
public function getMessage(): string;
public function getErrors(): array;
public function getErrorsOrMessage(): array|string;
public function getData(): array;
}

View File

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

View File

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

View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Dto;
final readonly class QuerySettingsDto
{
public function __construct(
private int $limit,
private int $page = 1,
private array $queryWith = []
) { }
public function getLimit(): int
{
return $this->limit;
}
public function getPage(): int
{
return $this->page;
}
public function getQueryWith(): array
{
return $this->queryWith;
}
}

View File

@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Role;
use App\Dto\Builder\Role;
use App\Dto\Service\Pages;
final readonly class Index extends Pages
{
public function __construct(
private Role $roleBuilderDto,
int $page
) {
parent::__construct($page);
}
public function getRoleBuilderDto(): Role
{
return $this->roleBuilderDto;
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\Role;
use App\Dto\Service\Dto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $name,
private ?string $code,
private array $permissions,
) { }
public function getName(): string
{
return $this->name;
}
public function getCode(): ?string
{
return $this->code;
}
public function getPermissions(): array
{
return $this->permissions;
}
}

View File

@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\User;
use App\Dto\Builder\User;
use App\Dto\Service\Pages;
final readonly class Index extends Pages
{
public function __construct(
private User $userBuilderDto,
int $page
) {
parent::__construct($page);
}
public function getUserBuilderDto(): User
{
return $this->userBuilderDto;
}
}

View File

@ -0,0 +1,48 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Admin\User;
use App\Dto\Service\Dto;
use App\Dto\Service\User\ManyRoleDto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $name,
private string $email,
private string $username,
private bool $isActive,
private ManyRoleDto $roles,
private ?string $password = null
) { }
public function getName(): string
{
return $this->name;
}
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): ?string
{
return $this->password;
}
public function getRoles(): ManyRoleDto
{
return $this->roles;
}
public function getUsername(): string
{
return $this->username;
}
public function isActive(): bool
{
return $this->isActive;
}
}

View File

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace App\Dto\Service;
final readonly class Authorization extends Dto
{
public function __construct(
private string $email,
private string $password,
private bool $remember = false
) { }
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): string
{
return $this->password;
}
public function getRemember(): bool
{
return $this->remember;
}
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Dto\Service;
abstract readonly class Dto
{
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\Dto\Service;
abstract readonly class Pages extends Dto
{
public function __construct(
private int $page
) { }
public function getPage(): int
{
return $this->page;
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Site\Profile;
use App\Dto\Service\Dto;
final readonly class Update extends Dto
{
public function __construct(
private string $name
) { }
public function getName(): string
{
return $this->name;
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\Site\Profile;
use App\Dto\Service\Dto;
use App\Enums\Lang;
final readonly class UpdateSettings extends Dto
{
public function __construct(
private ?Lang $lang,
private ?string $timezone
) { }
public function getLang(): ?Lang
{
return $this->lang;
}
public function getTimezone(): ?string
{
return $this->timezone;
}
}

View File

@ -0,0 +1,32 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\User;
use App\Exceptions\Dto\Service\User\ManyRoleDtoException;
final class ManyRoleDto
{
private array $roles = [];
public function __construct(array $roles = []) {
foreach ($roles as $role) {
if (!is_numeric($role) || is_float($role)) {
throw new ManyRoleDtoException('Not an integer: ' . $role . '.');
}
$this->add((int) $role);
}
}
public function add(int $id): void
{
if ($id < 1) {
throw new ManyRoleDtoException('Only Integer > 0.');
}
$this->roles[] = $id;
}
public function toArray(): array
{
return $this->roles;
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\Dto\Service\User;
use App\Dto\Service\Dto;
final readonly class UpdatePassword extends Dto
{
public function __construct(
private string $password
) { }
public function getPassword(): string
{
return $this->password;
}
}

View File

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace App\Enums;
use Illuminate\Support\Collection;
enum Lang: int
{
case Ru = 1;
case En = 2;
public function getTitle(): string
{
return match ($this) {
self::Ru => 'Русский',
self::En => 'English'
};
}
public function getLocale(): string
{
return match ($this) {
self::Ru => 'ru',
self::En => 'en'
};
}
public static function toArray(): array
{
$choices = [];
foreach (self::cases() as $lang) {
$choices[] = [
'name' => $lang->name,
'value' => $lang->value,
'title' => $lang->getTitle(),
'locale' => $lang->getLocale()
];
}
return $choices;
}
public static function toCollection(): Collection
{
return collect(self::toArray());
}
}

View File

@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace App\Enums;
enum Permission: string
{
case AdminPanel = 'allow-admin-panel';
case Role = 'role';
case User = 'user';
public function getPermissions(): array
{
$permissions = match ($this) {
self::AdminPanel => [
'view' => __('permissions.Administrative panel allowed'),
],
default => $this->getBasePermissions()
};
return $permissions;
}
public function getTitle(): string
{
return __('permissions.' . $this->name);
}
public function formatValue(string $permission): string
{
return $this->value . '.' . $permission;
}
public static function toArrayList(): array
{
$permissions = [];
foreach (self::cases() as $permissionEnum) {
foreach ($permissionEnum->getPermissions() as $permissionName => $permissionTitle) {
$name = $permissionEnum->formatValue($permissionName);
$title = $permissionEnum->getTitle() . ' - ' . $permissionTitle;
$permissions[$name] = $title;
}
}
return $permissions;
}
public static function toArrayListCodes(): array
{
return \array_keys(self::toArrayList());
}
private function getBasePermissions(): array
{
return [
'view' => __('permissions.Allowed to watch'),
'create' => __('permissions.Allowed to create'),
'update' => __('permissions.Allowed to edit'),
'delete' => __('permissions.Allowed to delete'),
];
}
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Enums;
enum SystemRole: string
{
case Admin = 'admin';
}

View File

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

View File

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

View File

@ -0,0 +1,35 @@
<?php declare(strict_types=1);
namespace App\Helpers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
final readonly class Helpers
{
public static function getTimeZoneList(): Collection
{
return Cache::rememberForever('timezones_list_collection', function () {
$timezone = [];
foreach (timezone_identifiers_list(\DateTimeZone::ALL) as $key => $value) {
$timezone[$value] = $value . ' (UTC ' . now($value)->format('P') . ')';
}
return collect($timezone)->sortKeys();
});
}
/**
* $name = 'field[key]' return 'field.key'
*/
public static function formatAttributeNameToRequestName(string $name): string
{
return Str::of($name)
->replace(
['.', '[', ']'],
['_', '.', ''],
)
->rtrim('.')
->value();
}
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller as BaseController;
abstract class Controller extends BaseController
{
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,49 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers;
use App\Http\Requests\AuthorizationRequest;
use App\Services\AuthService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth;
final class AuthController extends Controller
{
public function __construct(
private readonly AuthService $authService
) { }
public function login(): View
{
return view('login', [
'captcha' => config('app.captcha', false),
]);
}
public function authorization(AuthorizationRequest $request): RedirectResponse
{
$authorization = $request->getDto();
$result = $this->authService->authorization($authorization);
if ($result->isError()) {
if ($result->getCode() === Response::HTTP_UNAUTHORIZED) {
Log::warning('Unauthorized ' . $authorization->getEmail() . ' [' . $request->getClientIp() . ']');
}
return redirect()->route('login')->withInput()->withErrors($result->getMessage());
}
$request->session()->regenerate();
Log::notice('Logged in ' . $authorization->getEmail() . ' [' . $request->getClientIp() . ']');
return redirect()->route('home');
}
public function logout(Request $request): RedirectResponse
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect(route('login'));
}
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Site;
abstract class Controller
{
//
}

View File

@ -0,0 +1,13 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Site;
use Illuminate\View\View;
final class HomeController extends Controller
{
public function index(): View
{
return \view('site.home.index');
}
}

View File

@ -0,0 +1,72 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Site;
use App\Enums\Lang;
use App\Helpers\Helpers;
use App\Http\Requests\Site\Profile\UpdatePasswordRequest;
use App\Http\Requests\Site\Profile\UpdateRequest;
use App\Http\Requests\Site\Profile\UpdateSettingsRequest;
use App\Services\Site\ProfileService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
final class ProfileController extends Controller
{
public function __construct(
private readonly ProfileService $profileService
) { }
public function profile(Request $request): View
{
return view('site.profile.profile', [
'user' => $request->user()
]);
}
public function settings(Request $request): View
{
return view('site.profile.settings', [
'user' => $request->user(),
'languages' => Lang::toCollection()->pluck(value: 'title', key: 'value')->toArray(),
'timezone' => Helpers::getTimeZoneList()->toArray(),
]);
}
public function update(UpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->profileService->update($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getMessage());
}
return redirect()->route('profile.edit')->withSuccess($result->getMessage());
}
public function updatePassword(UpdatePasswordRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->profileService->updatePassword($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getMessage());
}
return redirect()->route('profile.edit')->withSuccess($result->getMessage());
}
public function updateSettings(UpdateSettingsRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->profileService->updateSettings($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getMessage());
}
return redirect()->route('profile.settings')->withSuccess($result->getMessage());
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class AdminPanel
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (
$request->user() === null || $request->user()->cannot('AdminPanel')
) {
abort(403);
}
return $next($request);
}
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
final class UserIsActive
{
public function handle(Request $request, \Closure $next)
{
if ($request->user()->is_active === false) {
\abort(Response::HTTP_FORBIDDEN, 'User disabled');
}
return $next($request);
}
}

View File

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
final class UserLocale
{
public function handle(Request $request, Closure $next)
{
if ($request->user() && $request->user()->lang) {
App::setLocale($request->user()->lang->getLocale());
}
return $next($request);
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Roles;
use App\Contracts\FormRequestDto;
use App\Dto\Builder\Role;
use App\Dto\Service\Admin\Role\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('admin.roles.index');
return [
'page' => ['nullable', 'numeric', 'min:1']
];
}
public function getDto(): Index
{
return new Index(
roleBuilderDto: new Role(),
page: (int) $this->input('page', 1)
);
}
}

View File

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Roles;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\Role\StoreUpdate;
use App\Rules\Permission;
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
{
$rules = [
'name' => ['required', 'max:255'],
'permissions' => ['array', new Permission()]
];
if ($this->getMethod() === 'POST') {
$rules['code'] = ['required', 'min:3', 'max:255', 'regex:/^[a-z0-9_-]+$/i'];
}
return $rules;
}
public function getDto(): StoreUpdate
{
return new StoreUpdate(
name: $this->input('name'),
code: $this->input('code', null),
permissions: $this->input('permissions', [])
);
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Users;
use App\Contracts\FormRequestDto;
use App\Dto\Builder\User;
use App\Dto\Service\Admin\User\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('admin.users.index');
return [
'page' => ['nullable', 'numeric', 'min:1']
];
}
public function getDto(): Index
{
return new Index(
userBuilderDto: new User(),
page: (int) $this->input('page', 1)
);
}
}

View File

@ -0,0 +1,47 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Users;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Admin\User\StoreUpdate;
use App\Dto\Service\User\ManyRoleDto;
use App\Rules\Username;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
final class StoreUpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'name' => ['required', 'max:255'],
'email' => ['required', 'email', 'max:255'],
'username' => ['required', 'string', new Username()],
'is_active' => ['required', 'boolean'],
'roles' => ['array', Rule::exists('roles', 'id')],
];
if ($this->getMethod() === 'POST') {
$rules['password'] = ['required', Password::default()];
}
return $rules;
}
public function getDto(): StoreUpdate
{
return new StoreUpdate(
name: $this->input('name'),
email: $this->input('email'),
username: $this->input('username'),
isActive: (bool) $this->input('is_active', false),
roles: new ManyRoleDto($this->input('roles', [])),
password: $this->input('password', null),
);
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Admin\Users;
use App\Contracts\FormRequestDto;
use App\Dto\Service\User\UpdatePassword;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
final class UpdatePasswordRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'password' => ['required', Password::default()],
];
}
public function getDto(): UpdatePassword
{
return new UpdatePassword(password: $this->input('password'));
}
}

View File

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace App\Http\Requests;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Authorization;
use Illuminate\Foundation\Http\FormRequest;
final class AuthorizationRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'email' => ['required', 'email', 'max:255'],
'password' => ['required', 'min:3'],
'remember' => ['nullable', 'boolean'],
];
if (config('app.captcha', false)) {
$rules['captcha-verified'] = ['captcha'];
}
return $rules;
}
public function getDto(): Authorization
{
return new Authorization(
email: $this->input('email'),
password: $this->input('password'),
remember: (bool) $this->input('remember', false)
);
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Site\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Service\User\UpdatePassword;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
final class UpdatePasswordRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'password' => ['required', 'confirmed', Password::default()],
];
}
public function getDto(): UpdatePassword
{
return new UpdatePassword(password: $this->input('password'));
}
}

View File

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

View File

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Site\Profile;
use App\Contracts\FormRequestDto;
use App\Dto\Service\Site\Profile\UpdateSettings;
use App\Enums\Lang;
use App\Helpers\Helpers;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
final class UpdateSettingsRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'lang' => ['nullable', new Enum(Lang::class)],
'timezone' => ['nullable', Rule::in(Helpers::getTimeZoneList()->keys()->toArray())]
];
}
public function getDto(): UpdateSettings
{
$lang = $this->input('lang', null);
if (!is_null($lang)) {
$lang = Lang::from((int) $lang);
}
return new UpdateSettings(
lang: $lang,
timezone: $this->input('timezone', null),
);
}
}

View File

@ -0,0 +1,55 @@
<?php declare(strict_types=1);
namespace App\Models;
use App\Enums\SystemRole as SystemRoleEnum;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\hasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
final class Role extends Model
{
use HasFactory, SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'code'
];
public function scopeLatest(Builder $query): Builder
{
return $query->orderBy('id', 'desc');
}
public function scopeAlphavit(Builder $query): Builder
{
return $query->orderBy('name', 'asc');
}
public function permissions(): hasMany
{
return $this->hasMany(RolePermission::class, 'role_id', 'id');
}
protected function isRemove(): Attribute
{
return Attribute::make(
get: fn ($dontRemove) => ( SystemRoleEnum::tryFrom($this->code) === null ),
)->shouldCache();
}
protected function isAdmin(): Attribute
{
return Attribute::make(
get: fn () => ( $this->code === SystemRoleEnum::Admin->value ),
)->shouldCache();
}
}

View File

@ -0,0 +1,21 @@
<?php declare(strict_types=1);
namespace App\Models;
use App\Enums\Permission;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
final class RolePermission extends Model
{
use HasFactory;
protected $table = 'role_permission';
public function getPermissionTitleAttribute(): string
{
$pemissions = Permission::toArrayList();
return $pemissions[$this->permission] ?? '';
}
}

View File

@ -0,0 +1,102 @@
<?php declare(strict_types=1);
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\Lang;
use App\Enums\SystemRole;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
final class User extends Authenticatable
{
use HasFactory, Notifiable, SoftDeletes;
/**
* The model's default values for attributes.
*
* @var array
*/
protected $attributes = [
'is_active' => true,
];
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'username',
'email',
'password',
'timezone',
'lang',
'is_active',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'lang' => Lang::class,
'is_active' => 'boolean',
];
}
/**
* Return the user's roles
*/
public function roles(): belongsToMany
{
return $this->belongsToMany(Role::class);
}
public function hasRole(string $role): bool
{
return $this->roles->where('code', $role)->isNotEmpty();
}
public function hasPermission(string $permission): bool
{
return $this->permissions->search($permission) !== false;
}
protected function isAdmin(): Attribute
{
return Attribute::make(
get: fn () => $this->hasRole(SystemRole::Admin->value),
)->shouldCache();
}
protected function permissions(): Attribute
{
return Attribute::make(
get: function () {
$roles = $this->roles->modelKeys();
return RolePermission::whereIn('role_id', $roles)->select('permission')->pluck('permission');
},
)->shouldCache();
}
}

View File

@ -0,0 +1,14 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Enums\SystemRole;
use App\Models\User;
readonly class AdminPanel extends Policy
{
public function view(User $user): bool
{
return $user->hasPermission('allow-admin-panel.view');
}
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
abstract readonly class Policy
{
use HandlesAuthorization;
final public function before(User $user): ?bool
{
if ($user->is_active !== true) {
return false;
}
if ($user->is_admin) {
return true;
}
return null;
}
}

View File

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\Role;
use App\Models\User;
final readonly class RolePolicy extends Policy
{
public function viewAny(User $user): bool
{
return $user->hasPermission('role.view');
}
public function view(User $user, Role $role): bool
{
return $user->hasPermission('role.view');
}
public function create(User $user): bool
{
return $user->hasPermission('role.create');
}
public function update(User $user, Role $role): bool
{
return $user->hasPermission('role.update');
}
public function delete(User $user, Role $role): bool
{
return $user->hasPermission('role.delete');
}
}

View File

@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\User;
final readonly class UserPolicy extends Policy
{
public function viewAny(User $user): bool
{
return $user->hasPermission('user.view');
}
public function view(User $user, User $userView): bool
{
return $user->hasPermission('user.view');
}
public function create(User $user): bool
{
return $user->hasPermission('user.create');
}
public function update(User $user, User $userUpdate): bool
{
return $user->hasPermission('user.update');
}
public function delete(User $user, User $userDelete): bool
{
return $user->hasPermission('user.delete');
}
}

View File

@ -0,0 +1,71 @@
<?php declare(strict_types=1);
namespace App\Providers;
use App\Services\Search\CreateSearchInstanceCommand;
use App\Services\Search\Search;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
$this->app->bind(CreateSearchInstanceCommand::class, function () {
return new CreateSearchInstanceCommand(Search::class);
});
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
if (config('app.force_https') === true) {
URL::forceScheme('https');
}
$this->passwordDefaults();
$this->configureRateLimiting();
Gate::define('AdminPanel', [\App\Policies\AdminPanel::class, 'view']);
}
/**
* Configure the rate limiters for the application.
*/
private function configureRateLimiting(): void
{
RateLimiter::for('login', function (Request $request) {
return [
Limit::perHour(config('rate_limiting.login_max_request', 50))->by($request->getClientIp()),
Limit::perHour(config('rate_limiting.login_max_email_request', 10))->by($request->getClientIp() . '-' . $request->input('email')),
];
});
}
private function passwordDefaults(): void
{
Password::defaults(function () {
$rule = Password::min(8);
if ($this->app->isProduction()) {
$rule->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised();
}
return $rule;
});
}
}

View File

@ -0,0 +1,59 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Contracts\Search;
use App\Models\Role;
use App\Dto\Builder\Role as RoleBuilderDto;
use App\Services\Role\BuilderCommand;
use App\Services\Search\CreateSearchInstanceCommand;
use Illuminate\Database\Eloquent\Builder;
final readonly class RoleRepository
{
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
private BuilderCommand $builderCommand
) { }
public function getRoleById(int $id): ?Role
{
return Role::query()->where('id', $id)->first();
}
public function getRoleByCode(string $code): ?Role
{
return Role::query()->where('code', $code)->first();
}
public function getRoles(RoleBuilderDto $roleBuilderDto, array $with = []): Search
{
$query = $this->builderCommand->execute(
query: Role::query()->with($with),
roleBuilderDto: $roleBuilderDto
);
return $this->createSearchInstanceCommand->execute($query);
}
public function getRolesForSelect(array $withExcepts = []): array
{
return Role::query()
->when($withExcepts, function (Builder $query, array $withExcepts) {
$query->withTrashed()->whereNull('deleted_at')->orWhereIn('id', $withExcepts);
})
->pluck('name', 'id')
->toArray();
}
public function isExistsCode(string $code, ?int $exceptId = null): bool
{
return Role::query()
->where('code', $code)
->when($exceptId, function (Builder $query, int $exceptId) {
$query->where('id', '!=', $exceptId);
})
->withTrashed()
->exists();
}
}

View File

@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace App\Repositories;
use App\Contracts\Search;
use App\Models\User;
use App\Dto\Builder\User as UserBuilderDto;
use App\Services\User\BuilderCommand;
use App\Services\Search\CreateSearchInstanceCommand;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Builder;
final readonly class UserRepository
{
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
private BuilderCommand $builderCommand
) { }
public function getUserById(int $id): ?User
{
return User::query()->where('id', $id)->first();
}
public function getUserByEmail(string $email): ?User
{
return User::query()->where('email', Str::lower($email))->first();
}
public function getUsers(UserBuilderDto $userBuilderDto, array $with = []): Search
{
$query = $this->builderCommand->execute(
query: User::query()->with($with),
userBuilderDto: $userBuilderDto
);
return $this->createSearchInstanceCommand->execute($query);
}
public function isExistsEmail(string $email, ?int $exceptId = null): bool
{
return User::query()
->where('email', Str::lower($email))
->when($exceptId, function (Builder $query, int $exceptId) {
$query->where('id', '!=', $exceptId);
})
->withTrashed()
->exists();
}
public function isExistsUsername(string $username, ?int $exceptId = null): bool
{
return User::query()
->where('username', Str::lower($username))
->when($exceptId, function (Builder $query, int $exceptId) {
$query->where('id', '!=', $exceptId);
})
->withTrashed()
->exists();
}
}

View File

@ -0,0 +1,55 @@
<?php declare(strict_types=1);
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
final readonly class Permission implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_string($value) && !is_array($value)) {
$fail('validation.no_type')->translate(['type' => 'string, array']);
return;
}
if (is_string($value)) {
$this->validatePermission($value, $fail);
}
if (is_array($value)) {
foreach ($value as $item) {
$this->validatePermission($item, $fail);
}
}
}
private function validatePermission(string $value, Closure $fail): void
{
$value = explode('.', $value, 2);
if (count($value) !== 2) {
$fail('validation.enum')->translate();
return;
}
list($permissionEnum, $permission) = $value;
$permissionEnum = \App\Enums\Permission::tryFrom($permissionEnum);
if (is_null($permissionEnum)) {
$fail('validation.enum')->translate();
return;
}
$permissions = $permissionEnum->getPermissions();
if (!isset($permissions[$permission])) {
$fail('validation.enum')->translate();
return;
}
}
}

View File

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace App\Rules;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Validator;
use Closure;
final readonly class Repository implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_string($value) && !is_array($value)) {
$fail('validation.no_type')->translate(['type' => 'string, array']);
return;
}
$validator = Validator::make([
'slug' => $value,
], [
'slug' => 'min:1|max:150|regex:/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/',
]);
if ($validator->fails()) {
foreach ($validator->errors()->all() as $error) {
$fail($error);
}
}
}
}

View File

@ -0,0 +1,34 @@
<?php declare(strict_types=1);
namespace App\Rules;
use Illuminate\Contracts\Validation\ValidationRule;
use Closure;
use Illuminate\Support\Facades\Validator;
final readonly class Username implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_string($value) && !is_array($value)) {
$fail('validation.no_type')->translate(['type' => 'string, array']);
return;
}
$validator = Validator::make([
'username' => $value,
], [
'username' => 'min:1|max:70|regex:/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/',
]);
if ($validator->fails()) {
foreach ($validator->errors()->all() as $error) {
$fail($error);
}
}
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\ServiceResults;
use App\Contracts\ServiceResult as ServiceResultContract;
use App\Contracts\ServiceResultError as ServiceResultErrorContract;
abstract class ServiceResult implements ServiceResultContract
{
public function isSuccess(): bool
{
return $this->isError() === false;
}
public function isError(): bool
{
return $this instanceof ServiceResultErrorContract === true;
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\ServiceResults;
final class ServiceResultArray extends ServiceResult
{
public function __construct(
private readonly array $data,
) { }
public function getData(): array
{
return $this->data;
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\ServiceResults;
use App\Contracts\ServiceResultError as ServiceResultErrorContract;
final class ServiceResultError extends ServiceResult implements ServiceResultErrorContract
{
public function __construct(
private readonly string $message,
private readonly array $errors = [],
private readonly ?int $code = null,
) {
}
public function getMessage(): string
{
return $this->message;
}
public function getCode(): ?int
{
return $this->code;
}
public function getErrors(): array
{
return $this->errors;
}
public function getErrorsOrMessage(): array|string
{
if (!empty($this->getErrors())) {
return $this->getErrors();
}
return $this->getMessage();
}
public function getData(): array
{
return [
'message' => $this->getMessage(),
'errors' => $this->errors
];
}
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\ServiceResults;
final class ServiceResultSuccess extends ServiceResult
{
public function __construct(
private readonly string $message
) { }
public function getMessage(): string
{
return $this->message;
}
}

View File

@ -0,0 +1,23 @@
<?php declare(strict_types=1);
namespace App\ServiceResults;
use Illuminate\Database\Eloquent\Model;
final class StoreUpdateResult extends ServiceResult
{
public function __construct(
private readonly Model $model,
private readonly string $message
) { }
public function getModel(): Model
{
return $this->model;
}
public function getMessage(): string
{
return $this->message;
}
}

View File

@ -0,0 +1,165 @@
<?php declare(strict_types=1);
namespace App\Services\Admin;
use App\Dto\Builder\Role as RoleBuilderDto;
use App\Dto\QuerySettingsDto;
use App\Dto\Service\Admin\Role\StoreUpdate;
use App\Models\Role;
use App\Models\User;
use App\Repositories\RoleRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\StoreUpdateResult;
use App\Services\Role\RoleCommandHandler;
use App\Services\Role\RoleSyncPermissionsCommandHandler;
use App\Services\Service;
use Illuminate\Support\Facades\DB;
final class RoleService extends Service
{
public function __construct(
private readonly RoleRepository $roleRepository,
private readonly RoleCommandHandler $roleCommandHandler,
private readonly RoleSyncPermissionsCommandHandler $roleSyncPermissionsCommandHandler
) { }
public function index(RoleBuilderDto $roleBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('viewAny', Role::class)) {
return $this->errFobidden(__('Access is denied'));
}
$roles = $this->roleRepository->getRoles(
$roleBuilderDto,
$querySettingsDto->getQueryWith()
)->pagination(
$querySettingsDto->getLimit(),
$querySettingsDto->getPage()
);
return $this->result([
'roles' => $roles
]);
}
public function create(User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('create', Role::class)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'role' => new Role(),
]);
}
public function edit(int $id, User $user): ServiceResultError | ServiceResultArray
{
$role = $this->roleRepository->getRoleById($id);
if (is_null($role)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('view', $role)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'role' => $role,
]);
}
public function store(StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
if ($user->cannot('create', Role::class)) {
return $this->errFobidden(__('Access is denied'));
}
if ($this->roleRepository->isExistsCode($data->getCode())) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.code')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.code')])]
);
}
try {
$role = DB::transaction(function () use ($data) {
$dataRole = $this->getDataRole($data);
$dataRole['code'] = $data->getCode();
$role = $this->roleCommandHandler->handleStore($dataRole);
$role = $this->roleSyncPermissionsCommandHandler->handle($role, $data->getPermissions());
return $role;
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($role, __('The group was successfully created'));
}
public function update(int $id, StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
$role = $this->roleRepository->getRoleById($id);
if (is_null($role)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('update', $role)) {
return $this->errFobidden(__('Access is denied'));
}
try {
$role = DB::transaction(function () use ($data, $role) {
$dataRole = $this->getDataRole($data);
$role = $this->roleCommandHandler->handleUpdate($role, $dataRole);
$role = $this->roleSyncPermissionsCommandHandler->handle($role, $data->getPermissions());
return $role;
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($role, __('The group was successfully updated'));
}
public function destroy(int $id, User $user): ServiceResultError|ServiceResultSuccess
{
$role = $this->roleRepository->getRoleById($id);
if (is_null($role)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('delete', $role)) {
return $this->errFobidden(__('Access is denied'));
}
try {
DB::transaction(function () use ($role) {
$this->roleCommandHandler->handleDestroy($role);
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->ok(__('The group has been deleted'));
}
private function getDataRole(StoreUpdate $data): array
{
return [
'name' => $data->getName(),
];
}
}

View File

@ -0,0 +1,216 @@
<?php declare(strict_types=1);
namespace App\Services\Admin;
use App\Dto\Builder\User as UserBuilderDto;
use App\Dto\Service\Admin\User\StoreUpdate;
use App\Dto\Service\User\UpdatePassword;
use App\Helpers\Helpers;
use App\Models\User;
use App\Dto\QuerySettingsDto;
use App\Repositories\RoleRepository;
use App\Repositories\UserRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\StoreUpdateResult;
use App\Services\Service;
use App\Services\User\UserCommandHandler;
use Illuminate\Support\Facades\DB;
final class UserService extends Service
{
public function __construct(
private readonly UserRepository $userRepository,
private readonly UserCommandHandler $userCommandHandler,
private readonly RoleRepository $roleRepository
) { }
public function index(UserBuilderDto $userBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('viewAny', User::class)) {
return $this->errFobidden(__('Access is denied'));
}
$users = $this->userRepository->getUsers(
$userBuilderDto,
$querySettingsDto->getQueryWith()
)->pagination(
$querySettingsDto->getLimit(),
$querySettingsDto->getPage()
);
return $this->result([
'users' => $users,
]);
}
public function create(User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('create', User::class)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'user' => new User(),
'roles' => $this->roleRepository->getRolesForSelect(),
'userRoles' => [],
]);
}
public function edit(int $id, User $user): ServiceResultError | ServiceResultArray
{
$modelUser = $this->userRepository->getUserById($id);
if (is_null($modelUser)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('view', $modelUser)) {
return $this->errFobidden(__('Access is denied'));
}
$userRoles = $modelUser->roles()->withTrashed()->pluck('id')->toArray();
return $this->result([
'user' => $modelUser,
'roles' => $this->roleRepository->getRolesForSelect($userRoles),
'userRoles' => $userRoles,
]);
}
public function store(StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
if ($user->cannot('create', User::class)) {
return $this->errFobidden(__('Access is denied'));
}
if ($this->userRepository->isExistsEmail($data->getEmail())) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.email')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.email')])]
);
}
if ($this->userRepository->isExistsUsername($data->getUsername())) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.username')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.username')])]
);
}
try {
$modelUser = DB::transaction(function () use ($data) {
$dataUser = $this->getDataUser($data);
$modelUser = $this->userCommandHandler->handleStore($dataUser, $data->getPassword());
$this->userCommandHandler->handleSyncRoles($modelUser, $data->getRoles());
return $modelUser;
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($modelUser, __('The user was successfully created'));
}
public function update(int $id, StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
$modelUser = $this->userRepository->getUserById($id);
if (is_null($modelUser)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('update', $modelUser)) {
return $this->errFobidden(__('Access is denied'));
}
if ($this->userRepository->isExistsEmail($data->getEmail(), $modelUser->id)) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.email')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.email')])]
);
}
if ($this->userRepository->isExistsUsername($data->getUsername(), $modelUser->id)) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.username')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.username')])]
);
}
try {
$modelUser = DB::transaction(function () use ($data, $modelUser) {
$dataUser = $this->getDataUser($data);
$modelUser = $this->userCommandHandler->handleUpdate($modelUser, $dataUser);
$this->userCommandHandler->handleSyncRoles($modelUser, $data->getRoles());
return $modelUser;
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($modelUser, __('The user was successfully updated'));
}
public function updatePassword(int $id, UpdatePassword $data, User $user): ServiceResultError | StoreUpdateResult
{
$modelUser = $this->userRepository->getUserById($id);
if (is_null($modelUser)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('update', $modelUser)) {
return $this->errFobidden(__('Access is denied'));
}
try {
$this->userCommandHandler->handleUpdatePassword($modelUser, $data->getPassword());
} catch (\Throwable $e) {
report($e->getMessage());
return $this->errService($e->getMessage());
}
return $this->resultStoreUpdateModel($modelUser, __('The password has been changed'));
}
public function destroy(int $id, User $user): ServiceResultError | ServiceResultSuccess
{
$modelUser = $this->userRepository->getUserById($id);
if (is_null($modelUser)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('delete', $modelUser)) {
return $this->errFobidden(__('Access is denied'));
}
try {
DB::transaction(function () use ($modelUser) {
$this->userCommandHandler->handleDestroy($modelUser);
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->ok(__('The user has been deleted'));
}
private function getDataUser(StoreUpdate $data): array
{
return [
'name' => $data->getName(),
'email' => $data->getEmail(),
'username' => $data->getUsername(),
'is_active' => $data->isActive(),
];
}
}

View File

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace App\Services;
use App\Dto\Service\Authorization;
use App\Repositories\UserRepository;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
final class AuthService extends Service
{
public function __construct(
private readonly UserRepository $userRepository
) { }
public function authorization(Authorization $authorization): ServiceResultError | ServiceResultSuccess
{
$user = $this->userRepository->getUserByEmail($authorization->getEmail());
if (is_null($user)) {
return $this->errUnauthorized(__('auth.failed'));
}
if (Hash::check($authorization->getPassword(), $user->password) !== true) {
return $this->errUnauthorized(__('auth.password'));
}
try {
Auth::login($user, $authorization->getRemember());
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->ok(__('auth.success'));
}
}

View File

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

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Services\Role;
use App\Models\Role;
final readonly class RoleCommandHandler
{
public function handleStore(array $data): Role
{
return Role::create($data);
}
public function handleUpdate(Role $role, array $data): Role
{
$role->update($data);
$role->touch();
return $role;
}
public function handleDestroy(Role $role): void
{
$role->delete();
}
}

View File

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\Services\Role;
use App\Enums\Permission;
use App\Exceptions\Services\Rule\RoleSyncPermissionsCommandHandlerException;
use App\Models\Role;
use App\Models\RolePermission;
final readonly class RoleSyncPermissionsCommandHandler
{
public function handle(Role $role, array $dataPermissions): Role
{
$rolePermissions = $role->permissions->pluck('id', 'permission')->toArray();
$data = $this->getInsertDeleteData($role->id, $rolePermissions, $dataPermissions);
if (!empty($data['insert'])) {
RolePermission::query()->insert($data['insert']);
}
if (!empty($data['delete'])) {
RolePermission::query()->whereIn('id', $data['delete'])->delete();
}
return $role;
}
private function getInsertDeleteData(int $roleId, array $rolePermissions, array $permissionsData): array
{
$data = [
'insert' => [],
'delete' => []
];
$permissions = Permission::toArrayListCodes();
foreach ($permissionsData as $permission) {
if (array_search($permission, $permissions) === false) {
throw new RoleSyncPermissionsCommandHandlerException('Таких разрешений в системе нет: ' . $permission);
}
if (isset($rolePermissions[$permission])) {
unset($rolePermissions[$permission]);
continue;
}
$data['insert'][] = [
'role_id' => $roleId,
'permission' => $permission,
];
}
$data['delete'] = array_values($rolePermissions);
return $data;
}
}

View File

@ -0,0 +1,19 @@
<?php declare(strict_types=1);
namespace App\Services\Search;
use App\Contracts\Search;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
final readonly class CreateSearchInstanceCommand
{
public function __construct(
private string $abstract
) { }
public function execute(Relation | Builder $query): Search
{
return new $this->abstract($query);
}
}

View File

@ -0,0 +1,79 @@
<?php declare(strict_types=1);
namespace App\Services\Search;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Pagination\CursorPaginator;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
use App\Contracts\Search as SearchContract;
final readonly class Search implements SearchContract
{
public function __construct(
private Relation|Builder $query
) { }
public function all(): Collection
{
return $this->query->get();
}
public function get(int $limit): Collection
{
return $this->query->limit($limit)->get();
}
public function pagination(int $limit, int $page = 1): LengthAwarePaginator
{
if ($page > 100) {
return $this->paginationPerfomance($limit, $page);
}
return $this->query->paginate($limit, page: $page)->withQueryString();
}
public function cursorPaginate(int $limit): CursorPaginator
{
return $this->query->cursorPaginate($limit);
}
private function paginationPerfomance(int $limit, int $page = 1): LengthAwarePaginator
{
$total = $this->query->clone()->count();
$options = [
'path' => Paginator::resolveCurrentPath(),
'pageName' => 'page',
];
$result = collect();
if ($total > 0) {
$result = $this->subQuery($limit, $page);
}
$pagination = Container::getInstance()->makeWith(LengthAwarePaginator::class, [
'items' => $result,
'total' => $total,
'perPage' => $limit,
'currentPage' => $page,
'options' => $options
]);
return $pagination->withQueryString();
}
private function subQuery(int $limit, int $page): Collection
{
$table = $this->query->getModel()->getTable();
return $this->query->getModel()::query()
->select($table.'.*')
->with($this->query->getEagerLoads())
->from(
clone $this->query->select('id')->forPage($page, $limit),
'q'
)->join($table.' as '.$table, $table.'.id', '=', 'q.id')
->get();
}
}

View File

@ -0,0 +1,74 @@
<?php declare(strict_types=1);
namespace App\Services;
use App\Models\Project;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\Site\PagePossibleWithoutTranslation;
use App\ServiceResults\StoreUpdateResult;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Response;
abstract class Service
{
final protected function errValidate(string $message, array $errors = []): ServiceResultError
{
return $this->error(Response::HTTP_UNPROCESSABLE_ENTITY, $message, $errors);
}
final protected function errFobidden(string $message): ServiceResultError
{
return $this->error(Response::HTTP_FORBIDDEN, $message);
}
final protected function errNotFound(string $message): ServiceResultError
{
return $this->error(Response::HTTP_NOT_FOUND, $message);
}
final protected function errService(string $message): ServiceResultError
{
return $this->error(Response::HTTP_INTERNAL_SERVER_ERROR, $message);
}
final protected function notAcceptable(string $message): ServiceResultError
{
return $this->error(Response::HTTP_NOT_ACCEPTABLE, $message);
}
final protected function errUnauthorized(string $message): ServiceResultError
{
return $this->error(Response::HTTP_UNAUTHORIZED, $message);
}
final protected function ok(string $message = 'OK'): ServiceResultSuccess
{
return new ServiceResultSuccess($message);
}
final protected function resultStoreUpdateModel(Model $model, string $message = 'OK'): StoreUpdateResult
{
return new StoreUpdateResult($model, $message);
}
final protected function result(array $data = []): ServiceResultArray
{
return new ServiceResultArray(data: $data);
}
final protected function resultSitePage(Project $project, WebsiteTranslations $websiteTranslations, array $data, bool $isTranslation): PagePossibleWithoutTranslation
{
return new PagePossibleWithoutTranslation($project, $websiteTranslations, $data, $isTranslation);
}
final protected function error(int $code, string $message, array $errors = []): ServiceResultError
{
return new ServiceResultError(
message: $message,
errors: $errors,
code: $code
);
}
}

View File

@ -0,0 +1,59 @@
<?php declare(strict_types=1);
namespace App\Services\Site;
use App\Dto\Service\Site\Profile\Update;
use App\Dto\Service\Site\Profile\UpdateSettings;
use App\Dto\Service\User\UpdatePassword;
use App\Models\User;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\Services\Service;
use App\Services\User\UserCommandHandler;
final class ProfileService extends Service
{
public function __construct(
private readonly UserCommandHandler $userCommandHandler
) { }
public function update(Update $update, User $user): ServiceResultError | ServiceResultSuccess
{
try {
$data = [
'name' => $update->getName()
];
$this->userCommandHandler->handleUpdate($user, $data);
} catch (\Throwable $e) {
report($e->getMessage());
return $this->errService($e->getMessage());
}
return $this->ok(__('Profile saved successfully'));
}
public function updatePassword(UpdatePassword $update, User $user): ServiceResultError | ServiceResultSuccess
{
try {
$this->userCommandHandler->handleUpdatePassword($user, $update->getPassword());
} catch (\Throwable $e) {
report($e->getMessage());
return $this->errService($e->getMessage());
}
return $this->ok(__('The password has been changed'));
}
public function updateSettings(UpdateSettings $update, User $user): ServiceResultError | ServiceResultSuccess
{
try {
$data = [
'lang' => $update->getLang(),
'timezone' => $update->getTimezone(),
];
$this->userCommandHandler->handleUpdate($user, $data);
} catch (\Throwable $e) {
report($e->getMessage());
return $this->errService($e->getMessage());
}
return $this->ok(__('The settings have been saved'));
}
}

View File

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

View File

@ -0,0 +1,78 @@
<?php declare(strict_types=1);
namespace App\Services\User;
use App\Dto\Service\User\ManyRoleDto;
use App\Models\Role;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
final readonly class UserCommandHandler
{
public function handleStore(array $data, int|string $password): User
{
$data['email'] = Str::lower($data['email']);
$data['username'] = Str::lower($data['username']);
$data['password'] = $this->hashPassword($password);
$user = User::create($data);
return $user;
}
public function handleUpdate(User $user, array $data): User
{
if (isset($data['email'])) {
$data['email'] = Str::lower($data['email']);
}
if (isset($data['username'])) {
$data['username'] = Str::lower($data['username']);
}
if (isset($data['password'])) {
unset($data['password']);
}
$user->update($data);
$user->touch();
return $user;
}
public function handleConfirmationByEmail(User $user): void
{
$user->update(['email_verified_at' => new Carbon('NOW')]);
}
public function handleUpdatePassword(User $user, int|string $password): void
{
$user->update(['password' => $this->hashPassword($password)]);
}
public function handleSyncRoles(User $user, ManyRoleDto $roles): void
{
$user->roles()->sync($roles->toArray());
}
public function attachRole(User $user, Role $role): void
{
$user->roles()->attach($role);
}
public function detachRole(User $user, Role $role): void
{
$user->roles()->detach($user);
}
private function hashPassword(int|string $password): string
{
return Hash::make($password);
}
public function handleDestroy(User $user): void
{
$user->delete();
}
}

View File

@ -0,0 +1,17 @@
<?php declare(strict_types=1);
namespace App\View\Components\Layout;
use Illuminate\View\Component;
use Illuminate\View\View;
final class Admin extends Component
{
/**
* @inheritDoc
*/
public function render(): View
{
return view('layout.admin');
}
}

View File

@ -0,0 +1,18 @@
<?php declare(strict_types=1);
namespace App\View\Components\Layout;
use Illuminate\View\Component;
use Illuminate\View\View;
final class Site extends Component
{
/**
* @inheritDoc
*/
public function render(): View
{
return view('layout.site');
}
}

View File

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\View\Components\Site\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class Checkbox extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly string $checkboxValue,
private readonly ?string $userValue = '',
private readonly ?string $notCheckedValue = null
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getCheckboxValue(): string
{
return (string) $this->checkboxValue;
}
private function getUserValue(): string
{
return (string) old($this->getRequestName(), $this->userValue);
}
private function getNotCheckedValue(): ?string
{
return $this->notCheckedValue;
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('components.site.forms.checkbox', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'checkboxValue' => $this->getCheckboxValue(),
'userValue' => $this->getUserValue(),
'notCheckedValue' => $this->getNotCheckedValue(),
]);
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\View\Components\Site\Forms;
use App\Helpers\Helpers;
use Illuminate\View\Component;
use Illuminate\View\View;
abstract class Form extends Component
{
private ?string $requestName = null;
abstract protected function getName(): string;
abstract public function render(): View;
protected function getRequestName(): string
{
if (!is_null($this->requestName)) {
return $this->requestName;
}
$this->requestName = Helpers::formatAttributeNameToRequestName($this->getName());
return $this->requestName;
}
}

View File

@ -0,0 +1,50 @@
<?php declare(strict_types=1);
namespace App\View\Components\Site\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class Input extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly string $type = 'text',
private readonly ?string $value = '',
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getType(): string
{
return $this->type;
}
private function getValue(): string
{
return (string) old($this->getRequestName(), $this->value);
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('components.site.forms.input', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'type' => $this->getType(),
'value' => $this->getValue(),
]);
}
}

View File

@ -0,0 +1,54 @@
<?php declare(strict_types=1);
namespace App\View\Components\Site\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class Select extends Form
{
/**
* @param string $title
* @param string $name
* @param array $list = [ [key => value], ... ]
* @param null|string $value
*/
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly array $list,
private readonly ?string $value = ''
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getValue(): string
{
return (string) old($this->getRequestName(), $this->value);
}
private function getList(): array
{
return $this->list;
}
public function render(): View
{
return view('components.site.forms.select', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'list' => $this->getList(),
'value' => $this->getValue()
]);
}
}

View File

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class Checkbox extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly string $checkboxValue,
private readonly ?string $userValue = '',
private readonly ?string $notCheckedValue = null
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getCheckboxValue(): string
{
return (string) $this->checkboxValue;
}
private function getUserValue(): string
{
return (string) old($this->getRequestName(), $this->userValue);
}
private function getNotCheckedValue(): ?string
{
return $this->notCheckedValue;
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('components.volt.forms.checkbox', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'checkboxValue' => $this->getCheckboxValue(),
'userValue' => $this->getUserValue(),
'notCheckedValue' => $this->getNotCheckedValue(),
]);
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use App\Helpers\Helpers;
use Illuminate\View\Component;
use Illuminate\View\View;
abstract class Form extends Component
{
private ?string $requestName = null;
abstract protected function getName(): string;
abstract public function render(): View;
protected function getRequestName(): string
{
if (!is_null($this->requestName)) {
return $this->requestName;
}
$this->requestName = Helpers::formatAttributeNameToRequestName($this->getName());
return $this->requestName;
}
}

View File

@ -0,0 +1,64 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class Input extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly string $type = 'text',
private readonly ?string $value = '',
private readonly ?string $example = null,
private readonly ?string $allowedCharacters = null,
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getType(): string
{
return $this->type;
}
private function getValue(): string
{
return (string) old($this->getRequestName(), $this->value);
}
private function getExample(): ?string
{
return $this->example;
}
public function getAllowedCharacters(): ?string
{
return $this->allowedCharacters;
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('components.volt.forms.input', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'type' => $this->getType(),
'value' => $this->getValue(),
'example' => $this->getExample(),
'allowedCharacters' => $this->getAllowedCharacters(),
]);
}
}

View File

@ -0,0 +1,53 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class MultiCheckbox extends Form
{
/**
* @param string $title
* @param string $name
* @param array $list = [ [key => value], ... ]
* @param array $value
*/
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly array $list,
private readonly array $value = []
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getValue(): array
{
return old($this->getRequestName(), $this->value);
}
private function getList(): array
{
return $this->list;
}
public function render(): View
{
return view('components.volt.forms.multi_checkbox', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'list' => $this->getList(),
'value' => $this->getValue()
]);
}
}

View File

@ -0,0 +1,53 @@
<?php declare(strict_types=1);
namespace App\View\Components\Volt\Forms;
use App\Enums\Permission;
use App\Models\Role;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class PermissionsForRole extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly Role $role,
private readonly array $value
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getValue(): Collection
{
$value = old($this->getRequestName(), $this->value);
return collect($value);
}
private function getRole(): Role
{
return $this->role;
}
public function render(): View
{
return view('components.volt.forms.permissions_for_role', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'permissions' => Permission::cases(),
'role' => $this->getRole(),
'value' => $this->getValue(),
]);
}
}

15
app/application/artisan Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env php
<?php
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
$status = (require_once __DIR__.'/bootstrap/app.php')
->handleCommand(new ArgvInput);
exit($status);

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Support\Facades\Route;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
commands: __DIR__.'/../routes/console.php',
using: function () {
Route::middleware(['web', \App\Http\Middleware\UserLocale::class])
->group(base_path('routes/web.php'));
Route::middleware(['web', 'auth', 'verified', \App\Http\Middleware\UserIsActive::class, \App\Http\Middleware\AdminPanel::class, \App\Http\Middleware\UserLocale::class])
->prefix('admin')
->as('admin.')
->group(base_path('routes/admin.php'));
},
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();

2
app/application/bootstrap/cache/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
*
!.gitignore

Some files were not shown because too many files have changed in this diff Show More