diff --git a/app/Dto/Builder/User.php b/app/Dto/Builder/User.php new file mode 100644 index 0000000..1af742f --- /dev/null +++ b/app/Dto/Builder/User.php @@ -0,0 +1,10 @@ +userBuilderDto; + } + + public function getPage(): int + { + return $this->page; + } +} diff --git a/app/Dto/Request/Private/User/StoreUpdate.php b/app/Dto/Request/Private/User/StoreUpdate.php new file mode 100644 index 0000000..a2ae078 --- /dev/null +++ b/app/Dto/Request/Private/User/StoreUpdate.php @@ -0,0 +1,42 @@ +name; + } + + public function getEmail(): string + { + return $this->email; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getRoles(): ManyRoleDto + { + return $this->roles; + } + + public function isActive(): bool + { + return $this->isActive; + } +} diff --git a/app/Dto/Request/Private/Profile/UpdatePassword.php b/app/Dto/Request/Private/User/UpdatePassword.php similarity index 86% rename from app/Dto/Request/Private/Profile/UpdatePassword.php rename to app/Dto/Request/Private/User/UpdatePassword.php index 8f6fe23..2b92bba 100644 --- a/app/Dto/Request/Private/Profile/UpdatePassword.php +++ b/app/Dto/Request/Private/User/UpdatePassword.php @@ -1,6 +1,6 @@ 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('private/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('private/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('private/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('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('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('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('users.index')->withSuccess($result->getMessage()); + } +} diff --git a/app/Http/Requests/Private/Profile/UpdatePasswordRequest.php b/app/Http/Requests/Private/Profile/UpdatePasswordRequest.php index bb7a12e..33782a4 100644 --- a/app/Http/Requests/Private/Profile/UpdatePasswordRequest.php +++ b/app/Http/Requests/Private/Profile/UpdatePasswordRequest.php @@ -3,7 +3,7 @@ namespace App\Http\Requests\Private\Profile; use App\Contracts\FormRequestDto; -use App\Dto\Request\Private\Profile\UpdatePassword; +use App\Dto\Request\Private\User\UpdatePassword; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rules\Password; diff --git a/app/Http/Requests/Private/Users/IndexRequest.php b/app/Http/Requests/Private/Users/IndexRequest.php new file mode 100644 index 0000000..d9cbf88 --- /dev/null +++ b/app/Http/Requests/Private/Users/IndexRequest.php @@ -0,0 +1,30 @@ +redirect = route('users.index'); + return [ + 'page' => ['nullable', 'numeric', 'min:1'] + ]; + } + + public function getDto(): Index + { + return new Index( + userBuilderDto: new User(), + page: (int) $this->input('page', 1) + ); + } +} diff --git a/app/Http/Requests/Private/Users/StoreUpdateRequest.php b/app/Http/Requests/Private/Users/StoreUpdateRequest.php new file mode 100644 index 0000000..b67ff6c --- /dev/null +++ b/app/Http/Requests/Private/Users/StoreUpdateRequest.php @@ -0,0 +1,44 @@ + ['required', 'max:255'], + 'email' => ['required', 'email', 'max:255'], + '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'), + isActive: (bool) $this->input('is_active', false), + roles: new ManyRoleDto($this->input('roles', [])), + password: $this->input('password', null), + ); + } +} diff --git a/app/Http/Requests/Private/Users/UpdatePasswordRequest.php b/app/Http/Requests/Private/Users/UpdatePasswordRequest.php new file mode 100644 index 0000000..fcf93c6 --- /dev/null +++ b/app/Http/Requests/Private/Users/UpdatePasswordRequest.php @@ -0,0 +1,26 @@ + ['required', Password::default()], + ]; + } + + public function getDto(): UpdatePassword + { + return new UpdatePassword(password: $this->input('password')); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 973a42a..2c0940f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,6 +4,8 @@ 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; @@ -66,4 +68,33 @@ final class User extends Authenticatable { 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(); + } + + } diff --git a/app/Policies/UserPolicy.php b/app/Policies/UserPolicy.php new file mode 100644 index 0000000..686e501 --- /dev/null +++ b/app/Policies/UserPolicy.php @@ -0,0 +1,33 @@ +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'); + } +} diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index c8022e9..785bd1a 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -2,13 +2,49 @@ 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(); + } } diff --git a/app/Services/Private/ProfileService.php b/app/Services/Private/ProfileService.php index 6cae92a..b1248f4 100644 --- a/app/Services/Private/ProfileService.php +++ b/app/Services/Private/ProfileService.php @@ -3,8 +3,8 @@ namespace App\Services\Private; use App\Dto\Request\Private\Profile\Update; -use App\Dto\Request\Private\Profile\UpdatePassword; use App\Dto\Request\Private\Profile\UpdateSettings; +use App\Dto\Request\Private\User\UpdatePassword; use App\Models\User; use App\ServiceResults\ServiceResultError; use App\ServiceResults\ServiceResultSuccess; diff --git a/app/Services/Private/UserService.php b/app/Services/Private/UserService.php new file mode 100644 index 0000000..f79a17e --- /dev/null +++ b/app/Services/Private/UserService.php @@ -0,0 +1,201 @@ +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')])] + ); + } + + try { + $modelUser = DB::transaction(function () use ($data) { + $dataUser = $this->getDataUser($data); + + $modelUser = $this->userCommandHandler->handleStore($dataUser, $data->getPassword()); + $this->userCommandHandler->handleConfirmationByEmail($modelUser); + $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')])] + ); + } + + 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 [ + 'is_active' => $data->isActive(), + 'name' => $data->getName(), + 'email' => $data->getEmail(), + ]; + } +} diff --git a/app/Services/User/BuilderCommand.php b/app/Services/User/BuilderCommand.php new file mode 100644 index 0000000..c0e99f6 --- /dev/null +++ b/app/Services/User/BuilderCommand.php @@ -0,0 +1,15 @@ +delete(); + } } diff --git a/app/View/Components/Private/Forms/Checkbox.php b/app/View/Components/Private/Forms/Checkbox.php new file mode 100644 index 0000000..729b37a --- /dev/null +++ b/app/View/Components/Private/Forms/Checkbox.php @@ -0,0 +1,57 @@ +name; + } + + private function getTitle(): string + { + return Str::ucfirst($this->title); + } + + private function getCheckboxValue(): string + { + return (string) old($this->getRequestName(), $this->checkboxValue); + } + + public function getUserValue(): string + { + return (string) $this->userValue; + } + + public function getNotCheckedValue(): ?string + { + return $this->notCheckedValue; + } + + /** + * @inheritDoc + */ + public function render(): View + { + return view('private.components.forms.checkbox', [ + 'title' => $this->getTitle(), + 'name' => $this->getName(), + 'requestName' => $this->getRequestName(), + 'checkboxValue' => $this->getCheckboxValue(), + 'userValue' => $this->getUserValue(), + 'notCheckedValue' => $this->getNotCheckedValue(), + ]); + } +} diff --git a/app/View/Components/Private/Forms/MultiCheckbox.php b/app/View/Components/Private/Forms/MultiCheckbox.php new file mode 100644 index 0000000..739b302 --- /dev/null +++ b/app/View/Components/Private/Forms/MultiCheckbox.php @@ -0,0 +1,53 @@ + 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('private.components.forms.multi_checkbox', [ + 'title' => $this->getTitle(), + 'name' => $this->getName(), + 'requestName' => $this->getRequestName(), + 'list' => $this->getList(), + 'value' => $this->getValue() + ]); + } +} diff --git a/lang/en.json b/lang/en.json index d8e3f33..6954b0e 100644 --- a/lang/en.json +++ b/lang/en.json @@ -54,5 +54,10 @@ "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" + "The group has been deleted": "The group has been deleted", + "yes": "yes", + "no": "no", + "The user was successfully created": "The user was successfully created", + "The user was successfully updated": "The user was successfully updated", + "The user has been deleted": "The user has been deleted" } diff --git a/lang/en/permissions.php b/lang/en/permissions.php index 2bdb95e..d207a51 100644 --- a/lang/en/permissions.php +++ b/lang/en/permissions.php @@ -2,6 +2,7 @@ return [ 'Role' => 'User group', + 'User' => 'Users', 'Allowed to watch' => 'Allowed to watch', 'Allowed to create' => 'Allowed to create', 'Allowed to edit' => 'Allowed to edit', diff --git a/lang/en/sections.php b/lang/en/sections.php index 4d55602..14945a5 100644 --- a/lang/en/sections.php +++ b/lang/en/sections.php @@ -3,4 +3,5 @@ return [ 'Dashboard' => 'Dashboard', 'User group' => 'User group', + 'Users' => 'Users', ]; diff --git a/lang/en/validation.php b/lang/en/validation.php index 694f5ec..f336241 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -218,5 +218,7 @@ return [ 'timezone' => 'timezone', 'code' => 'code', 'permissions' => 'permissions', + 'is_active' => 'is active', + 'roles' => 'user group', ], ]; diff --git a/lang/ru.json b/lang/ru.json index 51f9cff..ef62cf5 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -54,5 +54,10 @@ "Create": "Создать", "The group was successfully created": "Группа успешно создана", "The group was successfully updated": "Группа успешно обновлена", - "The group has been deleted": "Группа была удалена" + "The group has been deleted": "Группа была удалена", + "yes": "да", + "no": "нет", + "The user was successfully created": "Пользователь был успешно создан", + "The user was successfully updated": "Пользователь был успешно обновлен", + "The user has been deleted": "Пользователь был удален" } diff --git a/lang/ru/permissions.php b/lang/ru/permissions.php index 05dd129..275f953 100644 --- a/lang/ru/permissions.php +++ b/lang/ru/permissions.php @@ -2,6 +2,7 @@ return [ 'Role' => 'Группа пользователей', + 'User' => 'Пользователи', 'Allowed to watch' => 'Разрешено смотреть', 'Allowed to create' => 'Разрешено создать', 'Allowed to edit' => 'Разрешено редактировать', diff --git a/lang/ru/sections.php b/lang/ru/sections.php index ae16063..358c65b 100644 --- a/lang/ru/sections.php +++ b/lang/ru/sections.php @@ -3,5 +3,6 @@ return [ 'Dashboard' => 'Dashboard', 'User group' => 'Группа пользователей', + 'Users' => 'Пользователи', ]; diff --git a/lang/ru/validation.php b/lang/ru/validation.php index 058b5fd..8732c5f 100644 --- a/lang/ru/validation.php +++ b/lang/ru/validation.php @@ -218,5 +218,7 @@ return [ 'timezone' => 'часовой пояс', 'code' => 'код', 'permissions' => 'разрешения', + 'is_active' => 'активен', + 'roles' => 'группа пользователей', ], ]; diff --git a/resources/views/private/components/forms/checkbox.blade.php b/resources/views/private/components/forms/checkbox.blade.php new file mode 100644 index 0000000..a88014b --- /dev/null +++ b/resources/views/private/components/forms/checkbox.blade.php @@ -0,0 +1,12 @@ +