Added the ability to manage a group of users.

This commit is contained in:
Leonid Nikitin 2023-07-16 19:21:09 +06:00
parent ba7e52f8ac
commit 4083e2ec5e
Signed by: kor-elf
GPG Key ID: 7DE8F80C5CEC2C0D
45 changed files with 1173 additions and 10 deletions

View File

@ -41,7 +41,7 @@ final class CreateUserAdmin extends Command
$data = $validator->valid(); $data = $validator->valid();
try { try {
$role = $roleRepository->getRoleBySlug(SystemRole::Admin->value); $role = $roleRepository->getRoleByCode(SystemRole::Admin->value);
if (is_null($role)) { if (is_null($role)) {
$this->errorMessageAndStop('Administrator role not found.'); $this->errorMessageAndStop('Administrator role not found.');
} }

18
app/Contracts/Search.php Normal file
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

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

10
app/Dto/Builder/Role.php Normal file
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,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,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\Role;
use App\Dto\Builder\Role;
use App\Dto\Request\Dto;
final readonly class Index extends Dto
{
public function __construct(
private Role $roleBuilderDto,
private int $page
) { }
public function getRoleBuilderDto(): Role
{
return $this->roleBuilderDto;
}
public function getPage(): int
{
return $this->page;
}
}

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\Role;
use App\Dto\Request\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;
}
}

52
app/Enums/Permission.php Normal file
View File

@ -0,0 +1,52 @@
<?php declare(strict_types=1);
namespace App\Enums;
enum Permission: string
{
case Role = 'role';
public function getPermissions(): array
{
return $this->getBasePermissions();
}
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\Exceptions\Rule;
final class RoleSyncPermissionsCommandHandlerException extends \Exception
{
}

View File

@ -0,0 +1,92 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Dto\QuerySettingsDto;
use App\Http\Requests\Private\Roles\IndexRequest;
use App\Http\Requests\Private\Roles\StoreUpdateRequest;
use App\Services\Private\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('private/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('private/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('private/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('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('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('roles.index')->withSuccess($result->getMessage());
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\Roles;
use App\Contracts\FormRequestDto;
use App\Dto\Builder\Role;
use App\Dto\Request\Private\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('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\Private\Roles;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\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,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] ?? '';
}
}

24
app/Policies/Policy.php Normal file
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;
/**
* @param User $user
* @return bool|null
*/
final public function before(User $user): ?bool
{
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

@ -11,7 +11,10 @@ use App\Captcha\Images\Head;
use App\Captcha\Images\ImageManager; use App\Captcha\Images\ImageManager;
use App\Captcha\Images\Lines; use App\Captcha\Images\Lines;
use App\Services\Api\V1\CaptchaService; use App\Services\Api\V1\CaptchaService;
use App\Services\Search\CreateSearchInstanceCommand;
use App\Services\Search\Search;
use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Foundation\Application;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password; use Illuminate\Validation\Rules\Password;
@ -29,6 +32,10 @@ final class AppServiceProvider extends ServiceProvider
$this->app->bind(ImageBody::class, Body::class); $this->app->bind(ImageBody::class, Body::class);
$this->app->bind(ImageLines::class, Lines::class); $this->app->bind(ImageLines::class, Lines::class);
$this->app->bind(CreateSearchInstanceCommand::class, function () {
return new CreateSearchInstanceCommand(Search::class);
});
$this->app->bind(CaptchaService::class, function (Application $app) { $this->app->bind(CaptchaService::class, function (Application $app) {
return new CaptchaService( return new CaptchaService(
config: config('captcha', []), config: config('captcha', []),
@ -54,5 +61,7 @@ final class AppServiceProvider extends ServiceProvider
->uncompromised() ->uncompromised()
: $rule; : $rule;
}); });
Paginator::useBootstrapFive();
} }
} }

View File

@ -13,7 +13,7 @@ class AuthServiceProvider extends ServiceProvider
* @var array<class-string, class-string> * @var array<class-string, class-string>
*/ */
protected $policies = [ protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy', \App\Models\Role::class => \App\Policies\RolePolicy::class,
]; ];
/** /**

View File

@ -2,12 +2,48 @@
namespace App\Repositories; namespace App\Repositories;
use App\Contracts\Search;
use App\Models\Role; 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 final readonly class RoleRepository
{ {
public function getRoleBySlug(string $slug): ?Role final public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
private BuilderCommand $builderCommand
) { }
public function getRoleById(int $id): ?Role
{ {
return Role::query()->where('slug', $slug)->first(); 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 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();
} }
} }

55
app/Rules/Permission.php Normal file
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

@ -30,6 +30,15 @@ final class ServiceResultError extends ServiceResult implements ServiceResultErr
return $this->errors; return $this->errors;
} }
public function getErrorsOrMessage(): array|string
{
if (!empty($this->getErrors())) {
return $this->getErrors();
}
return $this->getMessage();
}
public function getData(): array public function getData(): array
{ {
return [ return [

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\Private;
use App\Dto\Builder\Role as RoleBuilderDto;
use App\Dto\QuerySettingsDto;
use App\Dto\Request\Private\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,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;
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\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,80 @@
<?php declare(strict_types=1);
namespace App\Services\Search;
use App\Models\Role;
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

@ -6,6 +6,8 @@ namespace App\Services;
use App\ServiceResults\ServiceResultArray; use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError; use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess; use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\StoreUpdateResult;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Response; use Illuminate\Http\Response;
abstract class Service abstract class Service
@ -45,6 +47,11 @@ abstract class Service
return new ServiceResultSuccess($message); 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 final protected function result(array $data = []): ServiceResultArray
{ {
return new ServiceResultArray(data: $data); return new ServiceResultArray(data: $data);

View File

@ -0,0 +1,62 @@
<?php declare(strict_types=1);
namespace App\View\Components\Private\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;
}
/**
* @return string
*/
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
/**
* @return array
*/
private function getValue(): Collection
{
$value = old($this->getRequestName(), $this->value);
return collect($value);
}
/**
* @return Role
*/
private function getRole(): Role
{
return $this->role;
}
public function render(): View
{
return view('private.components.forms.permissions_for_role', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'permissions' => Permission::cases(),
'role' => $this->getRole(),
'value' => $this->getValue(),
]);
}
}

View File

@ -47,5 +47,12 @@
"Profile saved successfully": "Profile saved successfully", "Profile saved successfully": "Profile saved successfully",
"The password has been changed": "The password has been changed", "The password has been changed": "The password has been changed",
"Default": "default", "Default": "default",
"The settings have been saved": "The settings have been saved" "The settings have been saved": "The settings have been saved",
"Access is denied": "Access is denied!",
"Delete": "Delete",
"Do you want to delete?": "Do you want to delete?",
"Create": "Create",
"The group was successfully created": "The group was successfully created",
"The group was successfully updated": "The group was successfully updated",
"The group has been deleted": "The group has been deleted"
} }

9
lang/en/permissions.php Normal file
View File

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
return [
'Role' => 'User group',
'Allowed to watch' => 'Allowed to watch',
'Allowed to create' => 'Allowed to create',
'Allowed to edit' => 'Allowed to edit',
'Allowed to delete' => 'Allowed to delete',
];

6
lang/en/sections.php Normal file
View File

@ -0,0 +1,6 @@
<?php declare(strict_types=1);
return [
'Dashboard' => 'Dashboard',
'User group' => 'User group',
];

View File

@ -141,6 +141,7 @@ return [
'uppercase' => 'The :attribute must be uppercase.', 'uppercase' => 'The :attribute must be uppercase.',
'url' => 'The :attribute format is invalid.', 'url' => 'The :attribute format is invalid.',
'uuid' => 'The :attribute must be a valid UUID.', 'uuid' => 'The :attribute must be a valid UUID.',
'no_type' => 'The :attribute can only use: :type.',
'attributes' => [ 'attributes' => [
'address' => 'address', 'address' => 'address',
'age' => 'age', 'age' => 'age',
@ -215,5 +216,7 @@ return [
'year' => 'year', 'year' => 'year',
'lang' => 'language', 'lang' => 'language',
'timezone' => 'timezone', 'timezone' => 'timezone',
'code' => 'code',
'permissions' => 'permissions',
], ],
]; ];

View File

@ -47,5 +47,12 @@
"Profile saved successfully": "Профиль успешно сохранен", "Profile saved successfully": "Профиль успешно сохранен",
"The password has been changed": "Пароль был изменен", "The password has been changed": "Пароль был изменен",
"Default": "По умолчанию", "Default": "По умолчанию",
"The settings have been saved": "Настройки были сохранены" "The settings have been saved": "Настройки были сохранены",
"Access is denied": "Доступ запрещён!",
"Delete": "Удалить",
"Do you want to delete?": "Вы хотите удалить?",
"Create": "Создать",
"The group was successfully created": "Группа успешно создана",
"The group was successfully updated": "Группа успешно обновлена",
"The group has been deleted": "Группа была удалена"
} }

9
lang/ru/permissions.php Normal file
View File

@ -0,0 +1,9 @@
<?php declare(strict_types=1);
return [
'Role' => 'Группа пользователей',
'Allowed to watch' => 'Разрешено смотреть',
'Allowed to create' => 'Разрешено создать',
'Allowed to edit' => 'Разрешено редактировать',
'Allowed to delete' => 'Разрешено удалять',
];

7
lang/ru/sections.php Normal file
View File

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
return [
'Dashboard' => 'Dashboard',
'User group' => 'Группа пользователей',
];

View File

@ -141,6 +141,7 @@ return [
'uppercase' => 'Значение поля :attribute должно быть в верхнем регистре.', 'uppercase' => 'Значение поля :attribute должно быть в верхнем регистре.',
'url' => 'Значение поля :attribute имеет ошибочный формат URL.', 'url' => 'Значение поля :attribute имеет ошибочный формат URL.',
'uuid' => 'Значение поля :attribute должно быть корректным UUID.', 'uuid' => 'Значение поля :attribute должно быть корректным UUID.',
'no_type' => 'Значение поля :attribute может использовать только: :type.',
'attributes' => [ 'attributes' => [
'address' => 'адрес', 'address' => 'адрес',
'age' => 'возраст', 'age' => 'возраст',
@ -215,5 +216,7 @@ return [
'year' => 'год', 'year' => 'год',
'lang' => 'язык', 'lang' => 'язык',
'timezone' => 'часовой пояс', 'timezone' => 'часовой пояс',
'code' => 'код',
'permissions' => 'разрешения',
], ],
]; ];

View File

@ -0,0 +1,24 @@
<div class="pt-3">
<div class="h5 pb-3">{{ $title }}</div>
@error($requestName)
<span class="invalid-feedback d-block pb-3">{{ $message }}</span>
@enderror
@foreach($permissions as $mainPermission)
<div class="row ps-3">
<div class="mb-3">
<span class="h6 fw-bold">{{ $mainPermission->getTitle() }}</span>
</div>
<ul>
@foreach($mainPermission->getPermissions() as $keyPermission => $permission)
<li class="form-check">
<input class="form-check-input" type="checkbox" value="{{ $mainPermission->formatValue($keyPermission) }}" name="{{ $name }}" @checked($role->is_admin || $value->search($mainPermission->formatValue($keyPermission)) !== false) @disabled($role->is_admin) id="form-permission-{{ $requestName }}-{{ $mainPermission->name }}-{{ $keyPermission }}">
<label class="form-check-label" for="form-permission-{{ $requestName }}-{{ $mainPermission->name }}-{{ $keyPermission }}">
{{ $permission }}
</label>
</li>
@endforeach
</ul>
<hr>
</div>
@endforeach
</div>

View File

@ -7,12 +7,28 @@
<span class="mt-1 ms-1 sidebar-text">Captcha service</span> <span class="mt-1 ms-1 sidebar-text">Captcha service</span>
</a> </a>
</li> </li>
<li class="nav-item {{ request()->route()->named('home') ? 'active' : '' }}"> <li @class([
'nav-item',
'active' => request()->route()->named('home')
])>
<a href="{{ route('home') }}" class="nav-link"> <a href="{{ route('home') }}" class="nav-link">
<span class="sidebar-icon"> <span class="sidebar-icon">
<svg class="icon icon-xs me-2" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path><path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z"></path></svg> <svg class="icon icon-xs me-2" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path><path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z"></path></svg>
</span> </span>
<span class="sidebar-text">Dashboard</span> <span class="sidebar-text">{{ __('sections.Dashboard') }}</span>
</a> </a>
</li> </li>
@can('viewAny', \App\Models\Role::class)
<li @class([
'nav-item',
'active' => request()->route()->named('roles.*'),
])>
<a href="{{ route('roles.index') }}" class="nav-link">
<span class="sidebar-icon">
<svg class="icon icon-xs me-2" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"></path></svg>
</span>
<span class="sidebar-text">{{ __('sections.User group') }}</span>
</a>
</li>
@endcan
</ul> </ul>

View File

@ -0,0 +1,5 @@
@csrf
<x-private.forms.input :title="__('validation.attributes.name')" name="name" type="text" :value="$role->name" required autofocus />
<x-private.forms.permissions_for_role :title="__('validation.attributes.permissions')" name="permissions[]" :value="$role->permissions->pluck('permission')->toArray()" :role="$role" />
<x-private.forms.input :title="__('validation.attributes.code')" name="code" type="text" :value="$role->code" :disabled="!empty($role->id)" required />
<button class="btn btn-primary" type="submit">{{ __('Save') }}</button>

View File

@ -0,0 +1,8 @@
@can('create', \App\Models\Role::class)
<div class="mb-4">
<a href="{{ route('roles.create') }}" class="btn btn-secondary d-inline-flex align-items-center me-2">
<svg class="icon icon-xs me-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
{{ __('Create') }}
</a>
</div>
@endcan

View File

@ -0,0 +1,16 @@
@section('meta_title', __('sections.User group'))
@section('h1', __('sections.User group'))
<x-private.layout>
@include('private.roles._top')
<div class="row">
<div class="col-12 mb-4">
<div class="card border-0 shadow components-section">
<div class="card-body">
<form method="post" action="{{ route('roles.store') }}">
@include('private.roles._from')
</form>
</div>
</div>
</div>
</div>
</x-private.layout>

View File

@ -0,0 +1,17 @@
@section('meta_title', __('sections.User group'))
@section('h1', __('sections.User group'))
<x-private.layout>
@include('private.roles._top')
<div class="row">
<div class="col-12 mb-4">
<div class="card border-0 shadow components-section">
<div class="card-body">
<form method="post" action="{{ route('roles.update', $role) }}">
@method('PUT')
@include('private.roles._from')
</form>
</div>
</div>
</div>
</div>
</x-private.layout>

View File

@ -0,0 +1,54 @@
@section('meta_title', __('sections.User group'))
@section('h1', __('sections.User group'))
<x-private.layout>
@include('private.roles._top')
<div class="card border-0 shadow mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-centered table-nowrap mb-0 rounded">
<thead class="thead-light">
<tr>
<th class="border-0">{{ __('validation.attributes.name') }}</th>
<th class="border-0">{{ __('validation.attributes.code') }}</th>
<th class="border-0 rounded-end" style="width: 150px"></th>
</tr>
</thead>
<tbody>
@foreach($roles as $role)
<tr>
<td>
<a href="{{ route('roles.edit', $role) }}" class="fw-bold">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="align-text-top" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{{ $role->name }}
</a>
</td>
<td>{{ $role->code }}</td>
<td>
@if($role->is_remove)
<form method="post" action="{{ route('roles.destroy', $role) }}">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger click-confirm">
{{ __('Delete') }}
</button>
</form>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="card-footer border-0 d-flex flex-column flex-lg-row align-items-center justify-content-between">
<nav aria-label="Пример навигации по странице">
{{ $roles->links() }}
</nav>
</div>
</div>
</div>
</div>
@push('scripts')
@include('private._scripts._click-confirm', ['alert' => __('Do you want to delete?')])
@endpush
</x-private.layout>

View File

@ -28,4 +28,7 @@ Route::middleware(['auth', 'verified', 'user.locale'])->group(function () {
Route::get('settings', [\App\Http\Controllers\Private\ProfileController::class, 'settings'])->name('settings'); Route::get('settings', [\App\Http\Controllers\Private\ProfileController::class, 'settings'])->name('settings');
Route::put('settings', [\App\Http\Controllers\Private\ProfileController::class, 'updateSettings'])->name('update-settings'); Route::put('settings', [\App\Http\Controllers\Private\ProfileController::class, 'updateSettings'])->name('update-settings');
}); });
Route::resource('users', \App\Http\Controllers\Private\UsersController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['user' => '[0-9]+']);
Route::resource('roles', \App\Http\Controllers\Private\RolesController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['role' => '[0-9]+']);
}); });