diff --git a/src/app/Domain/Role/Actions/DestroyAction.php b/src/app/Domain/Role/Actions/DestroyAction.php new file mode 100644 index 0000000..6acc595 --- /dev/null +++ b/src/app/Domain/Role/Actions/DestroyAction.php @@ -0,0 +1,34 @@ +roleRepository->whereUuid($request->role_uuid)->firstOrFail(); + + // Нельзя удалить роль, если к ней привязаны пользователи + if ($role->users()->count() > 0) { + AppException::new( + 'role_in_use', + 'Нельзя удалить роль, к которой привязаны пользователи', + Response::HTTP_CONFLICT + ); + } + + $role->delete(); + } +} \ No newline at end of file diff --git a/src/app/Domain/Role/Actions/IndexAction.php b/src/app/Domain/Role/Actions/IndexAction.php new file mode 100644 index 0000000..fc06a90 --- /dev/null +++ b/src/app/Domain/Role/Actions/IndexAction.php @@ -0,0 +1,31 @@ +roleRepository + ->withCount('users') + ->orderBy($request->sort_by ?? 'name', $request->sort_dir ?? 'asc'); + + $paginator = $query->paginate( + perPage: $request->per_page ?? 15, + page: $request->page ?? 1, + ); + + return IndexResponseData::fromPaginator($paginator); + } +} \ No newline at end of file diff --git a/src/app/Domain/Role/Actions/ShowAction.php b/src/app/Domain/Role/Actions/ShowAction.php new file mode 100644 index 0000000..3228c82 --- /dev/null +++ b/src/app/Domain/Role/Actions/ShowAction.php @@ -0,0 +1,23 @@ +roleRepository->whereUuid($request->role_uuid)->firstOrFail(); + return ShowResponseData::fromModel($result); + } +} \ No newline at end of file diff --git a/src/app/Domain/Role/Actions/StoreAction.php b/src/app/Domain/Role/Actions/StoreAction.php new file mode 100644 index 0000000..2c00f5f --- /dev/null +++ b/src/app/Domain/Role/Actions/StoreAction.php @@ -0,0 +1,26 @@ +roleRepository->create([ + 'name' => $request->name, + 'code' => $request->code, + 'description' => $request->description, + ]); + } +} \ No newline at end of file diff --git a/src/app/Domain/Role/Actions/UpdateAction.php b/src/app/Domain/Role/Actions/UpdateAction.php new file mode 100644 index 0000000..d9d63ff --- /dev/null +++ b/src/app/Domain/Role/Actions/UpdateAction.php @@ -0,0 +1,22 @@ +roleRepository->whereUuid($request->role_uuid)->firstOrFail(); + $role->update($request->getFilledFields()); + } +} \ No newline at end of file diff --git a/src/app/Domain/Role/Data/DestroyRequest.php b/src/app/Domain/Role/Data/DestroyRequest.php new file mode 100644 index 0000000..b975b96 --- /dev/null +++ b/src/app/Domain/Role/Data/DestroyRequest.php @@ -0,0 +1,23 @@ + */ + public readonly DataCollection $roles, + public readonly int $current_page, + public readonly int $last_page, + public readonly int $per_page, + public readonly int $total, + ) { + } + + public static function fromPaginator(LengthAwarePaginator $paginator): self + { + $items = $paginator->getCollection()->map( + fn (Role $role) => IndexRoleItemData::fromModel($role) + ); + + return new self( + roles: new DataCollection(IndexRoleItemData::class, $items), + current_page: $paginator->currentPage(), + last_page: $paginator->lastPage(), + per_page: $paginator->perPage(), + total: $paginator->total(), + ); + } +} \ No newline at end of file diff --git a/src/app/Domain/Role/Data/IndexRoleItemData.php b/src/app/Domain/Role/Data/IndexRoleItemData.php new file mode 100644 index 0000000..98abe1c --- /dev/null +++ b/src/app/Domain/Role/Data/IndexRoleItemData.php @@ -0,0 +1,33 @@ +uuid->toString(), + name: $model->name, + code: $model->code, + description: $model->description, + users_count: $model->users_count ?? $model->users()->count(), + created_at: $model->created_at->toDateTimeString(), + ); + } +} \ No newline at end of file diff --git a/src/app/Domain/Role/Data/ShowRequest.php b/src/app/Domain/Role/Data/ShowRequest.php new file mode 100644 index 0000000..6b503a4 --- /dev/null +++ b/src/app/Domain/Role/Data/ShowRequest.php @@ -0,0 +1,23 @@ +uuid->toString(), + name: $model->name, + code: $model->code, + description: $model->description, + users_count: $model->users()->count(), + created_at: $model->created_at->toDateTimeString(), + updated_at: $model->updated_at->toDateTimeString(), + ); + } +} \ No newline at end of file diff --git a/src/app/Domain/Role/Data/StoreRequest.php b/src/app/Domain/Role/Data/StoreRequest.php new file mode 100644 index 0000000..d58c14b --- /dev/null +++ b/src/app/Domain/Role/Data/StoreRequest.php @@ -0,0 +1,26 @@ + $this->name, + 'code' => $this->code, + 'description' => $this->description, + ], fn ($value) => $value !== null); + } +} \ No newline at end of file diff --git a/src/app/Domain/Role/Repositories/RoleRepository.php b/src/app/Domain/Role/Repositories/RoleRepository.php new file mode 100644 index 0000000..85a9b9c --- /dev/null +++ b/src/app/Domain/Role/Repositories/RoleRepository.php @@ -0,0 +1,13 @@ +uuid->toString() === $request->user_uuid) { + AppException::new('self_delete', 'Нельзя удалить самого себя', Response::HTTP_FORBIDDEN); + } + + $user = $this->userRepository->whereUuid($request->user_uuid)->firstOrFail(); + $user->delete(); + } +} \ No newline at end of file diff --git a/src/app/Domain/User/Actions/IndexAction.php b/src/app/Domain/User/Actions/IndexAction.php new file mode 100644 index 0000000..98413a0 --- /dev/null +++ b/src/app/Domain/User/Actions/IndexAction.php @@ -0,0 +1,43 @@ +userRepository + ->with('role') + ->orderBy($request->sort_by ?? 'created_at', $request->sort_dir ?? 'desc'); + + if ($request->search) { + $search = $request->search; + $query->where(function ($q) use ($search): void { + $q->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); + }); + } + + if ($request->role) { + $query->where('role_uuid', $request->role); + } + + $paginator = $query->paginate( + perPage: $request->per_page ?? 15, + page: $request->page ?? 1, + ); + + return IndexResponseData::fromPaginator($paginator); + } +} \ No newline at end of file diff --git a/src/app/Domain/User/Actions/StoreAction.php b/src/app/Domain/User/Actions/StoreAction.php new file mode 100644 index 0000000..d459dcf --- /dev/null +++ b/src/app/Domain/User/Actions/StoreAction.php @@ -0,0 +1,28 @@ +userRepository->create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => bcrypt($request->password), + 'role_uuid' => $request->role_uuid, + 'email_verified_at' => now(), + ]); + } +} \ No newline at end of file diff --git a/src/app/Domain/User/Actions/UpdateAction.php b/src/app/Domain/User/Actions/UpdateAction.php index 003b6c3..a514d42 100644 --- a/src/app/Domain/User/Actions/UpdateAction.php +++ b/src/app/Domain/User/Actions/UpdateAction.php @@ -6,7 +6,10 @@ namespace App\Domain\User\Actions; use App\Domain\User\Data\UpdateRequest; use App\Domain\User\Repositories\UserRepository; -use Illuminate\Http\JsonResponse; +use App\Domain\Shared\Exceptions\AppException; +use App\Models\User; +use Illuminate\Support\Facades\Auth; +use Symfony\Component\HttpFoundation\Response; class UpdateAction { @@ -18,6 +21,26 @@ class UpdateAction public function execute(UpdateRequest $request) { + // Получаем текущего аутентифицированного пользователя + /** @var User|null $currentUser */ + $currentUser = Auth::user(); + + // Проверяем, что пользователь аутентифицирован + if (!$currentUser) { + AppException::new('UNAUTHORIZED', 'UNAUTHORIZED', Response::HTTP_UNAUTHORIZED); + } + + // Загружаем роль текущего пользователя + $currentUser->load('role'); + + // Проверяем, является ли пользователь администратором + $isAdmin = $currentUser->role && $currentUser->role->code === 'admin'; + + // Если пользователь не администратор и пытается редактировать не себя, запрещаем доступ + if (!$isAdmin && $currentUser->uuid->toString() !== $request->user_uuid) { + AppException::new('forbidden', 'Недостаточно прав для редактирования этого пользователя', Response::HTTP_FORBIDDEN); + } + $user = $this->userRepository->whereUuid($request->user_uuid)->firstOrFail(); $user->update($request->getFilledFields()); } diff --git a/src/app/Domain/User/Data/DestroyRequest.php b/src/app/Domain/User/Data/DestroyRequest.php new file mode 100644 index 0000000..498f05a --- /dev/null +++ b/src/app/Domain/User/Data/DestroyRequest.php @@ -0,0 +1,23 @@ + */ + public readonly DataCollection $users, + public readonly int $current_page, + public readonly int $last_page, + public readonly int $per_page, + public readonly int $total, + ) { + } + + public static function fromPaginator(LengthAwarePaginator $paginator): self + { + $items = $paginator->getCollection()->map( + fn (User $user) => IndexUserItemData::fromModel($user) + ); + + return new self( + users: new DataCollection(IndexUserItemData::class, $items), + current_page: $paginator->currentPage(), + last_page: $paginator->lastPage(), + per_page: $paginator->perPage(), + total: $paginator->total(), + ); + } +} \ No newline at end of file diff --git a/src/app/Domain/User/Data/IndexUserItemData.php b/src/app/Domain/User/Data/IndexUserItemData.php new file mode 100644 index 0000000..658e6b3 --- /dev/null +++ b/src/app/Domain/User/Data/IndexUserItemData.php @@ -0,0 +1,37 @@ +uuid->toString(), + name: $model->name, + email: $model->email, + email_verified: $model->email_verified_at !== null, + role: new RoleData( + uuid: $model->role->uuid->toString(), + name: $model->role->name, + ), + created_at: $model->created_at->toDateTimeString(), + ); + } +} \ No newline at end of file diff --git a/src/app/Domain/User/Data/StoreRequest.php b/src/app/Domain/User/Data/StoreRequest.php new file mode 100644 index 0000000..9176a5e --- /dev/null +++ b/src/app/Domain/User/Data/StoreRequest.php @@ -0,0 +1,35 @@ + $this->name, 'email' => $this->email, + 'role_uuid' => $this->role_uuid, 'password' => $this->password ? bcrypt($this->password) : null, ]; diff --git a/src/app/Helpers/MenuHelper.php b/src/app/Helpers/MenuHelper.php index be57b2d..79c0885 100644 --- a/src/app/Helpers/MenuHelper.php +++ b/src/app/Helpers/MenuHelper.php @@ -12,13 +12,33 @@ class MenuHelper [ 'icon' => 'dashboard', 'name' => 'Dashboard', - 'path' => route('dashboard') + 'path' => route('dashboard'), ], [ 'icon' => 'user-profile', 'name' => 'User Profile', - 'path' => auth()->check() ? route('users.show', ['user_uuid' => auth()->user()->uuid]) : '#' - ] + 'path' => auth()->check() ? route('users.show', ['user_uuid' => auth()->user()->uuid]) : '#', + ], + ]; + } + + public static function getAdminNavItems(): array + { + if (!auth()->check() || auth()->user()->role->code !== 'admin') { + return []; + } + + return [ + [ + 'icon' => 'users', + 'name' => 'Users', + 'path' => route('users.index'), + ], + [ + 'icon' => 'roles', + 'name' => 'Roles', + 'path' => route('roles.index'), + ], ]; } @@ -34,23 +54,29 @@ class MenuHelper 'title' => 'Menu', 'items' => self::getMainNavItems(), ], + [ + 'title' => 'Administration', + 'items' => self::getAdminNavItems(), + ], [ 'title' => 'Others', - 'items' => self::getOterItems() - ] + 'items' => self::getOterItems(), + ], ]; } - public static function isActive($path) + public static function isActive($path): bool { - return request()->is($path, '/'); + return request()->is(ltrim($path, '/'), '/'); } - public static function getIconSvg($iconName) + public static function getIconSvg($iconName): string { $icons = [ 'dashboard' => '', - 'user-profile' => '' + 'user-profile' => '', + 'users' => '', + 'roles' => '', ]; return $icons[$iconName] ?? ''; } diff --git a/src/app/Http/Controllers/RoleController.php b/src/app/Http/Controllers/RoleController.php index 4507fa4..95c9936 100644 --- a/src/app/Http/Controllers/RoleController.php +++ b/src/app/Http/Controllers/RoleController.php @@ -4,7 +4,51 @@ declare(strict_types=1); namespace App\Http\Controllers; +use App\Domain\Role\Actions\DestroyAction; +use App\Domain\Role\Actions\IndexAction; +use App\Domain\Role\Actions\ShowAction; +use App\Domain\Role\Actions\StoreAction; +use App\Domain\Role\Actions\UpdateAction; +use App\Domain\Role\Data\DestroyRequest; +use App\Domain\Role\Data\IndexRequest; +use App\Domain\Role\Data\ShowRequest; +use App\Domain\Role\Data\StoreRequest; +use App\Domain\Role\Data\UpdateRequest; +use Illuminate\Contracts\View\View; +use Illuminate\Http\JsonResponse; + class RoleController extends Controller { - // + public function index(IndexRequest $request, IndexAction $action): View + { + $data = $action->execute($request); + return view('pages.role.index', ['data' => $data]); + } + + public function show(ShowRequest $request, ShowAction $action): View + { + $data = $action->execute($request); + return view('pages.role.show', ['data' => $data]); + } + + public function store(StoreRequest $request, StoreAction $action): JsonResponse + { + $action->execute($request); + + return response()->json(['success' => true, 'message' => 'Role created successfully']); + } + + public function update(UpdateRequest $request, UpdateAction $action): JsonResponse + { + $action->execute($request); + + return response()->json(['success' => true, 'message' => 'Role updated successfully']); + } + + public function destroy(DestroyRequest $request, DestroyAction $action): JsonResponse + { + $action->execute($request); + + return response()->json(['success' => true, 'message' => 'Role deleted successfully']); + } } diff --git a/src/app/Http/Controllers/UserController.php b/src/app/Http/Controllers/UserController.php index bde9402..54c44bd 100644 --- a/src/app/Http/Controllers/UserController.php +++ b/src/app/Http/Controllers/UserController.php @@ -4,17 +4,25 @@ declare(strict_types=1); namespace App\Http\Controllers; +use App\Domain\User\Actions\DestroyAction; +use App\Domain\User\Actions\IndexAction; use App\Domain\User\Actions\ShowAction; +use App\Domain\User\Actions\StoreAction; use App\Domain\User\Actions\UpdateAction; +use App\Domain\User\Data\DestroyRequest; +use App\Domain\User\Data\IndexRequest; use App\Domain\User\Data\ShowRequest; +use App\Domain\User\Data\StoreRequest; use App\Domain\User\Data\UpdateRequest; use Illuminate\Contracts\View\View; +use Illuminate\Http\JsonResponse; class UserController extends Controller { - public function index(): View + public function index(IndexRequest $request, IndexAction $action): View { - return view('pages.user.index', []); + $data = $action->execute($request); + return view('pages.user.index', ['data' => $data]); } public function show(ShowRequest $request, ShowAction $action): View @@ -22,11 +30,25 @@ class UserController extends Controller $data = $action->execute($request); return view('pages.user.show', ['data' => $data]); } - - public function update(UpdateRequest $request, UpdateAction $action) + + public function store(StoreRequest $request, StoreAction $action): JsonResponse { - $data = $action->execute($request); - + $action->execute($request); + + return response()->json(['success' => true, 'message' => 'User created successfully']); + } + + public function update(UpdateRequest $request, UpdateAction $action): JsonResponse + { + $action->execute($request); + return response()->json(['success' => true, 'message' => 'Profile updated successfully']); } + + public function destroy(DestroyRequest $request, DestroyAction $action): JsonResponse + { + $action->execute($request); + + return response()->json(['success' => true, 'message' => 'User deleted successfully']); + } } diff --git a/src/app/Http/Middleware/CheckRoleMiddleware.php b/src/app/Http/Middleware/CheckRoleMiddleware.php index ca4f02a..8180849 100644 --- a/src/app/Http/Middleware/CheckRoleMiddleware.php +++ b/src/app/Http/Middleware/CheckRoleMiddleware.php @@ -33,13 +33,10 @@ class CheckRoleMiddleware AppException::new('UNAUTHORIZED', 'UNAUTHORIZED', Response::HTTP_UNAUTHORIZED); } - $userRole = $user->load('role')->role(); + $user->load('role'); - $hasRole = $userRole->whereIn('code', $roles) - ->count(); - - if (!$hasRole) { - AppException::new('forbidden', 'Недостаточно прав'); + if (!$user->role || !in_array($user->role->code, $roles, true)) { + AppException::new('forbidden', 'Недостаточно прав', Response::HTTP_FORBIDDEN); } return $next($request); diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php index 8325058..20b69b1 100644 --- a/src/app/Providers/AppServiceProvider.php +++ b/src/app/Providers/AppServiceProvider.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace App\Providers; +use App\Domain\Role\Repositories\RoleRepository; +use App\Domain\User\Repositories\UserRepository; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -13,7 +15,13 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + $this->app->singleton(UserRepository::class, function () { + return new UserRepository(); + }); + + $this->app->singleton(RoleRepository::class, function () { + return new RoleRepository(); + }); } /** diff --git a/src/app/Providers/FortifyServiceProvider.php b/src/app/Providers/FortifyServiceProvider.php index 8da5c3f..a09eff3 100644 --- a/src/app/Providers/FortifyServiceProvider.php +++ b/src/app/Providers/FortifyServiceProvider.php @@ -50,7 +50,7 @@ class FortifyServiceProvider extends ServiceProvider // Custom routes defined specifically Fortify::registerView(function () { - return view('auth.register'); + return view('pages.auth.register'); }); Fortify::loginView(function () { @@ -58,23 +58,23 @@ class FortifyServiceProvider extends ServiceProvider }); Fortify::verifyEmailView(function () { - return view('auth.verify-mail'); + return view('pages.auth.verify-mail'); }); Fortify::requestPasswordResetLinkView(function () { - return view('auth.forgot-password'); + return view('pages.auth.forgot-password'); }); Fortify::resetPasswordView(function (Request $request) { - return view('auth.reset-password', ['request' => $request]); + return view('pages.auth.reset-password', ['request' => $request]); }); Fortify::confirmPasswordView(function () { - return view('auth.confirm-password'); + return view('pages.auth.confirm-password'); }); Fortify::twoFactorChallengeView(function () { - return view('auth.two-factor-challange'); + return view('pages.auth.two-factor-challange'); }); } } diff --git a/src/database/factories/UserFactory.php b/src/database/factories/UserFactory.php index 2d3445e..5938825 100644 --- a/src/database/factories/UserFactory.php +++ b/src/database/factories/UserFactory.php @@ -43,4 +43,12 @@ class UserFactory extends Factory 'email_verified_at' => null, ]); } + + public function hasRole(Role|Factory|null $role = null): self + { + return $this->for( + $role ?? Role::factory(), + 'role' + ); + } } diff --git a/src/resources/views/components/profile/profile-card.blade.php b/src/resources/views/components/profile/profile-card.blade.php index be1e41e..4069ad7 100644 --- a/src/resources/views/components/profile/profile-card.blade.php +++ b/src/resources/views/components/profile/profile-card.blade.php @@ -3,6 +3,7 @@ name: '{{ $user->name }}', email: '{{ $user->email }}', password: '{{ $user->password ?? null }}', + role_uuid: '{{ $user->role->uuid ?? null }}', errors: {}, saveProfile() { // Очищаем предыдущие ошибки @@ -22,6 +23,10 @@ data.email = this.email; } + if (this.role_uuid) { + data.role_uuid = this.role_uuid; + } + if (this.password) { data.password = this.password; } @@ -187,10 +192,13 @@ @if(auth()->user()->role->code === 'admin')
{{ __('Total Users') }}
+{{ \App\Models\User::count() }}
+{{ __('Total Roles') }}
+{{ \App\Models\Role::count() }}
+{{ __('Verified Users') }}
+{{ \App\Models\User::whereNotNull('email_verified_at')->count() }}
++ {{ round((\App\Models\User::whereNotNull('email_verified_at')->count() / max(\App\Models\User::count(), 1)) * 100) }}% {{ __('verified') }} +
+| {{ __('Name') }} | +{{ __('Code') }} | +{{ __('Description') }} | +{{ __('Users') }} | +{{ __('Created') }} | +{{ __('Actions') }} | +
|---|---|---|---|---|---|
| + {{ $role->name }} + | ++ + {{ $role->code }} + + | +{{ $role->description }} | +{{ $role->users_count }} | +{{ $role->created_at }} | +
+
+ @if(auth()->user()?->role?->code === 'admin' && $role->users_count === 0)
+
+ @endif
+
+ |
+
| + {{ __('No roles found') }} + | +|||||
{{ $data->description }}
+{{ $data->users_count }}
+{{ $data->created_at }}
+{{ $data->updated_at }}
+| {{ __('Name') }} | +{{ __('Email') }} | +{{ __('Role') }} | +{{ __('Verified') }} | +{{ __('Created') }} | +{{ __('Actions') }} | +
|---|---|---|---|---|---|
| + + {{ $user->name }} + + | +{{ $user->email }} | ++ + {{ $user->role->name }} + + | ++ @if($user->email_verified) + ✓ + @else + ✗ + @endif + | +{{ $user->created_at }} | +
+
+
+ {{ __('View') }}
+
+ @if(auth()->user()?->role?->code === 'admin' && auth()->user()?->uuid !== $user->uuid)
+
+ @endif
+
+ |
+
| + {{ __('No users found') }} + | +|||||