From c235fd2170036488b4fce58066dec11f461c1f6e Mon Sep 17 00:00:00 2001 From: root Date: Tue, 10 Mar 2026 17:11:48 -0400 Subject: [PATCH] add role permission logic --- .../Api/RolePermissionController.php | 245 ++++++ .../Api/RoleSwitcherController.php | 44 ++ .../Requests/Roles/AssignUserRolesRequest.php | 21 + .../Requests/Roles/PermissionStoreRequest.php | 22 + .../Roles/PermissionUpdateRequest.php | 24 + app/Http/Requests/Roles/RoleListRequest.php | 24 + .../Roles/RolePermissionUpdateRequest.php | 26 + app/Http/Requests/Roles/RoleStoreRequest.php | 26 + app/Http/Requests/Roles/RoleSwitchRequest.php | 20 + app/Http/Requests/Roles/RoleUpdateRequest.php | 28 + .../Requests/Roles/UserRoleListRequest.php | 24 + .../Resources/Roles/PermissionResource.php | 19 + .../Roles/RolePermissionResource.php | 22 + app/Http/Resources/Roles/RoleResource.php | 24 + .../Resources/Roles/UserWithRolesResource.php | 25 + app/Services/Roles/PermissionCrudService.php | 60 ++ app/Services/Roles/RoleAssignmentService.php | 58 ++ app/Services/Roles/RoleCrudService.php | 107 +++ app/Services/Roles/RoleDashboardService.php | 19 + app/Services/Roles/RolePermissionService.php | 95 +++ app/Services/Roles/RoleQueryService.php | 79 ++ app/Services/Roles/RoleStaffService.php | 72 ++ app/Services/Roles/RoleSwitchService.php | 25 + app/old/RolePermissionController.php | 723 ------------------ app/old/RoleService.php | 21 - app/old/RoleSwitcherController.php | 94 --- routes/api.php | 23 + .../V1/Roles/RolePermissionControllerTest.php | 247 ++++++ .../V1/Roles/RoleSwitcherControllerTest.php | 90 +++ .../Roles/PermissionResourceTest.php | 24 + .../Roles/RolePermissionResourceTest.php | 25 + .../Unit/Resources/Roles/RoleResourceTest.php | 29 + .../Roles/UserWithRolesResourceTest.php | 26 + .../Roles/PermissionCrudServiceTest.php | 28 + .../Roles/RoleAssignmentServiceTest.php | 57 ++ .../Services/Roles/RoleCrudServiceTest.php | 32 + .../Roles/RoleDashboardServiceTest.php | 32 + .../Roles/RolePermissionServiceTest.php | 45 ++ .../Services/Roles/RoleQueryServiceTest.php | 74 ++ .../Services/Roles/RoleSwitchServiceTest.php | 55 ++ 40 files changed, 1896 insertions(+), 838 deletions(-) create mode 100644 app/Http/Controllers/Api/RolePermissionController.php create mode 100644 app/Http/Controllers/Api/RoleSwitcherController.php create mode 100644 app/Http/Requests/Roles/AssignUserRolesRequest.php create mode 100644 app/Http/Requests/Roles/PermissionStoreRequest.php create mode 100644 app/Http/Requests/Roles/PermissionUpdateRequest.php create mode 100644 app/Http/Requests/Roles/RoleListRequest.php create mode 100644 app/Http/Requests/Roles/RolePermissionUpdateRequest.php create mode 100644 app/Http/Requests/Roles/RoleStoreRequest.php create mode 100644 app/Http/Requests/Roles/RoleSwitchRequest.php create mode 100644 app/Http/Requests/Roles/RoleUpdateRequest.php create mode 100644 app/Http/Requests/Roles/UserRoleListRequest.php create mode 100644 app/Http/Resources/Roles/PermissionResource.php create mode 100644 app/Http/Resources/Roles/RolePermissionResource.php create mode 100644 app/Http/Resources/Roles/RoleResource.php create mode 100644 app/Http/Resources/Roles/UserWithRolesResource.php create mode 100644 app/Services/Roles/PermissionCrudService.php create mode 100644 app/Services/Roles/RoleAssignmentService.php create mode 100644 app/Services/Roles/RoleCrudService.php create mode 100644 app/Services/Roles/RoleDashboardService.php create mode 100644 app/Services/Roles/RolePermissionService.php create mode 100644 app/Services/Roles/RoleQueryService.php create mode 100644 app/Services/Roles/RoleStaffService.php create mode 100644 app/Services/Roles/RoleSwitchService.php delete mode 100644 app/old/RolePermissionController.php delete mode 100644 app/old/RoleService.php delete mode 100644 app/old/RoleSwitcherController.php create mode 100644 tests/Feature/Api/V1/Roles/RolePermissionControllerTest.php create mode 100644 tests/Feature/Api/V1/Roles/RoleSwitcherControllerTest.php create mode 100644 tests/Unit/Resources/Roles/PermissionResourceTest.php create mode 100644 tests/Unit/Resources/Roles/RolePermissionResourceTest.php create mode 100644 tests/Unit/Resources/Roles/RoleResourceTest.php create mode 100644 tests/Unit/Resources/Roles/UserWithRolesResourceTest.php create mode 100644 tests/Unit/Services/Roles/PermissionCrudServiceTest.php create mode 100644 tests/Unit/Services/Roles/RoleAssignmentServiceTest.php create mode 100644 tests/Unit/Services/Roles/RoleCrudServiceTest.php create mode 100644 tests/Unit/Services/Roles/RoleDashboardServiceTest.php create mode 100644 tests/Unit/Services/Roles/RolePermissionServiceTest.php create mode 100644 tests/Unit/Services/Roles/RoleQueryServiceTest.php create mode 100644 tests/Unit/Services/Roles/RoleSwitchServiceTest.php diff --git a/app/Http/Controllers/Api/RolePermissionController.php b/app/Http/Controllers/Api/RolePermissionController.php new file mode 100644 index 00000000..4b331fd4 --- /dev/null +++ b/app/Http/Controllers/Api/RolePermissionController.php @@ -0,0 +1,245 @@ +queryService->listRoles($request->validated()); + + return $this->success([ + 'roles' => RoleResource::collection($roles->items()), + 'meta' => [ + 'total' => $roles->total(), + 'per_page' => $roles->perPage(), + 'current_page' => $roles->currentPage(), + 'last_page' => $roles->lastPage(), + ], + ]); + } + + public function showRole(int $roleId): JsonResponse + { + $role = Role::query()->find($roleId); + if (!$role) { + return $this->error('Role not found.', Response::HTTP_NOT_FOUND); + } + + return $this->success([ + 'role' => new RoleResource($role), + ]); + } + + public function storeRole(RoleStoreRequest $request): JsonResponse + { + try { + $role = $this->roleCrudService->create($request->validated()); + } catch (\Throwable $e) { + Log::error('Role store failed: ' . $e->getMessage()); + return $this->error('Failed to create role.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success([ + 'role' => new RoleResource($role), + ], 'Role created successfully.', Response::HTTP_CREATED); + } + + public function updateRole(RoleUpdateRequest $request, int $roleId): JsonResponse + { + $role = Role::query()->find($roleId); + if (!$role) { + return $this->error('Role not found.', Response::HTTP_NOT_FOUND); + } + + try { + $role = $this->roleCrudService->update($role, $request->validated()); + } catch (\Throwable $e) { + Log::error('Role update failed: ' . $e->getMessage()); + return $this->error('Failed to update role.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success([ + 'role' => new RoleResource($role), + ], 'Role updated successfully.'); + } + + public function deleteRole(int $roleId): JsonResponse + { + $role = Role::query()->find($roleId); + if (!$role) { + return $this->error('Role not found.', Response::HTTP_NOT_FOUND); + } + + try { + $this->roleCrudService->delete($role); + } catch (\Throwable $e) { + Log::error('Role delete failed: ' . $e->getMessage()); + return $this->error('Failed to delete role.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success(null, 'Role deleted successfully.'); + } + + public function users(UserRoleListRequest $request): JsonResponse + { + $users = $this->queryService->listUsersWithRoles($request->validated()); + + return $this->success([ + 'users' => UserWithRolesResource::collection($users->items()), + 'meta' => [ + 'total' => $users->total(), + 'per_page' => $users->perPage(), + 'current_page' => $users->currentPage(), + 'last_page' => $users->lastPage(), + ], + ]); + } + + public function assignRoles(AssignUserRolesRequest $request, int $userId): JsonResponse + { + try { + $result = $this->assignmentService->assignRoles( + $userId, + (array) ($request->validated()['role_ids'] ?? []), + (int) (auth()->id() ?? 0) + ); + } catch (\Throwable $e) { + Log::error('Assign roles failed: ' . $e->getMessage()); + return $this->error('Failed to update roles.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + if (!($result['ok'] ?? false)) { + return $this->error($result['message'] ?? 'Failed to update roles.', Response::HTTP_UNPROCESSABLE_ENTITY); + } + + return $this->success([ + 'role_names' => $result['role_names'] ?? [], + ], 'Roles updated successfully.'); + } + + public function permissions(): JsonResponse + { + $permissions = $this->permissionService->listPermissions(); + + return $this->success([ + 'permissions' => PermissionResource::collection($permissions), + ]); + } + + public function showPermission(int $permissionId): JsonResponse + { + $permission = Permission::query()->find($permissionId); + if (!$permission) { + return $this->error('Permission not found.', Response::HTTP_NOT_FOUND); + } + + return $this->success([ + 'permission' => new PermissionResource($permission), + ]); + } + + public function storePermission(PermissionStoreRequest $request): JsonResponse + { + try { + $permission = $this->permissionCrudService->create($request->validated()); + } catch (\Throwable $e) { + Log::error('Permission store failed: ' . $e->getMessage()); + return $this->error('Failed to create permission.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success([ + 'permission' => new PermissionResource($permission), + ], 'Permission created successfully.', Response::HTTP_CREATED); + } + + public function updatePermission(PermissionUpdateRequest $request, int $permissionId): JsonResponse + { + $permission = Permission::query()->find($permissionId); + if (!$permission) { + return $this->error('Permission not found.', Response::HTTP_NOT_FOUND); + } + + try { + $permission = $this->permissionCrudService->update($permission, $request->validated()); + } catch (\Throwable $e) { + Log::error('Permission update failed: ' . $e->getMessage()); + return $this->error('Failed to update permission.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success([ + 'permission' => new PermissionResource($permission), + ], 'Permission updated successfully.'); + } + + public function deletePermission(int $permissionId): JsonResponse + { + $permission = Permission::query()->find($permissionId); + if (!$permission) { + return $this->error('Permission not found.', Response::HTTP_NOT_FOUND); + } + + try { + $this->permissionCrudService->delete($permission); + } catch (\Throwable $e) { + Log::error('Permission delete failed: ' . $e->getMessage()); + return $this->error('Failed to delete permission.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success(null, 'Permission deleted successfully.'); + } + + public function rolePermissions(int $roleId): JsonResponse + { + $rows = $this->permissionService->listRolePermissions($roleId); + + return $this->success([ + 'permissions' => RolePermissionResource::collection($rows), + ]); + } + + public function updateRolePermissions(RolePermissionUpdateRequest $request, int $roleId): JsonResponse + { + try { + $this->permissionService->saveRolePermissions($roleId, $request->validated()['permissions'] ?? []); + } catch (\Throwable $e) { + Log::error('Role permissions update failed: ' . $e->getMessage()); + return $this->error('Failed to update role permissions.', Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->success(null, 'Permissions saved successfully.'); + } +} diff --git a/app/Http/Controllers/Api/RoleSwitcherController.php b/app/Http/Controllers/Api/RoleSwitcherController.php new file mode 100644 index 00000000..5cf20f70 --- /dev/null +++ b/app/Http/Controllers/Api/RoleSwitcherController.php @@ -0,0 +1,44 @@ +id() ?? 0); + if ($userId <= 0) { + return $this->error('Please log in first.', Response::HTTP_UNAUTHORIZED); + } + + $roles = $this->switchService->getUserRoleNames($userId); + if (empty($roles)) { + return $this->error('No roles assigned to this user.', Response::HTTP_NOT_FOUND); + } + + return $this->success([ + 'roles' => $roles, + ]); + } + + public function switch(RoleSwitchRequest $request): JsonResponse + { + $role = (string) $request->validated()['role']; + $route = $this->switchService->switchRole($role); + + return $this->success([ + 'role' => $role, + 'dashboard_route' => $route, + ], 'Role switched successfully.'); + } +} diff --git a/app/Http/Requests/Roles/AssignUserRolesRequest.php b/app/Http/Requests/Roles/AssignUserRolesRequest.php new file mode 100644 index 00000000..39e31839 --- /dev/null +++ b/app/Http/Requests/Roles/AssignUserRolesRequest.php @@ -0,0 +1,21 @@ +check(); + } + + public function rules(): array + { + return [ + 'role_ids' => ['nullable', 'array'], + 'role_ids.*' => ['integer', 'min:1', 'exists:roles,id'], + ]; + } +} diff --git a/app/Http/Requests/Roles/PermissionStoreRequest.php b/app/Http/Requests/Roles/PermissionStoreRequest.php new file mode 100644 index 00000000..cd97a21c --- /dev/null +++ b/app/Http/Requests/Roles/PermissionStoreRequest.php @@ -0,0 +1,22 @@ +check(); + } + + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255', 'unique:permissions,name'], + 'description' => ['nullable', 'string', 'max:500'], + ]; + } +} diff --git a/app/Http/Requests/Roles/PermissionUpdateRequest.php b/app/Http/Requests/Roles/PermissionUpdateRequest.php new file mode 100644 index 00000000..a06fe272 --- /dev/null +++ b/app/Http/Requests/Roles/PermissionUpdateRequest.php @@ -0,0 +1,24 @@ +check(); + } + + public function rules(): array + { + $permissionId = (int) $this->route('permissionId'); + + return [ + 'name' => ['sometimes', 'string', 'max:255', Rule::unique('permissions', 'name')->ignore($permissionId)], + 'description' => ['nullable', 'string', 'max:500'], + ]; + } +} diff --git a/app/Http/Requests/Roles/RoleListRequest.php b/app/Http/Requests/Roles/RoleListRequest.php new file mode 100644 index 00000000..ece38640 --- /dev/null +++ b/app/Http/Requests/Roles/RoleListRequest.php @@ -0,0 +1,24 @@ +check(); + } + + public function rules(): array + { + return [ + 'search' => ['nullable', 'string', 'max:255'], + 'sort_by' => ['nullable', 'string', Rule::in(['name', 'slug', 'priority', 'is_active', 'created_at'])], + 'sort_dir' => ['nullable', 'string', Rule::in(['asc', 'desc'])], + 'per_page' => ['nullable', 'integer', 'min:1', 'max:200'], + ]; + } +} diff --git a/app/Http/Requests/Roles/RolePermissionUpdateRequest.php b/app/Http/Requests/Roles/RolePermissionUpdateRequest.php new file mode 100644 index 00000000..5a6c835c --- /dev/null +++ b/app/Http/Requests/Roles/RolePermissionUpdateRequest.php @@ -0,0 +1,26 @@ +check(); + } + + public function rules(): array + { + return [ + 'permissions' => ['required', 'array'], + 'permissions.*' => ['array'], + 'permissions.*.create' => ['nullable', 'boolean'], + 'permissions.*.read' => ['nullable', 'boolean'], + 'permissions.*.update' => ['nullable', 'boolean'], + 'permissions.*.delete' => ['nullable', 'boolean'], + 'permissions.*.manage' => ['nullable', 'boolean'], + ]; + } +} diff --git a/app/Http/Requests/Roles/RoleStoreRequest.php b/app/Http/Requests/Roles/RoleStoreRequest.php new file mode 100644 index 00000000..3ffd0371 --- /dev/null +++ b/app/Http/Requests/Roles/RoleStoreRequest.php @@ -0,0 +1,26 @@ +check(); + } + + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'min:3', 'max:255', 'unique:roles,name'], + 'slug' => ['nullable', 'string', 'max:64', 'unique:roles,slug'], + 'description' => ['nullable', 'string', 'max:500'], + 'dashboard_route' => ['required', 'string', 'min:1', 'max:255', 'regex:/^[a-z0-9_\\/\\-]+$/'], + 'priority' => ['required', 'integer', 'min:0', 'max:100000'], + 'is_active' => ['required', Rule::in([0, 1, '0', '1', true, false])], + ]; + } +} diff --git a/app/Http/Requests/Roles/RoleSwitchRequest.php b/app/Http/Requests/Roles/RoleSwitchRequest.php new file mode 100644 index 00000000..c2cbcd52 --- /dev/null +++ b/app/Http/Requests/Roles/RoleSwitchRequest.php @@ -0,0 +1,20 @@ +check(); + } + + public function rules(): array + { + return [ + 'role' => ['required', 'string', 'max:255'], + ]; + } +} diff --git a/app/Http/Requests/Roles/RoleUpdateRequest.php b/app/Http/Requests/Roles/RoleUpdateRequest.php new file mode 100644 index 00000000..e5ee1f32 --- /dev/null +++ b/app/Http/Requests/Roles/RoleUpdateRequest.php @@ -0,0 +1,28 @@ +check(); + } + + public function rules(): array + { + $roleId = (int) $this->route('roleId'); + + return [ + 'name' => ['sometimes', 'string', 'min:3', 'max:255', Rule::unique('roles', 'name')->ignore($roleId)], + 'slug' => ['nullable', 'string', 'max:64', Rule::unique('roles', 'slug')->ignore($roleId)], + 'description' => ['nullable', 'string', 'max:500'], + 'dashboard_route' => ['sometimes', 'string', 'min:1', 'max:255', 'regex:/^[a-z0-9_\\/\\-]+$/'], + 'priority' => ['sometimes', 'integer', 'min:0', 'max:100000'], + 'is_active' => ['sometimes', Rule::in([0, 1, '0', '1', true, false])], + ]; + } +} diff --git a/app/Http/Requests/Roles/UserRoleListRequest.php b/app/Http/Requests/Roles/UserRoleListRequest.php new file mode 100644 index 00000000..121bf88c --- /dev/null +++ b/app/Http/Requests/Roles/UserRoleListRequest.php @@ -0,0 +1,24 @@ +check(); + } + + public function rules(): array + { + return [ + 'search' => ['nullable', 'string', 'max:255'], + 'sort_by' => ['nullable', 'string', Rule::in(['firstname', 'lastname', 'email', 'account_id'])], + 'sort_dir' => ['nullable', 'string', Rule::in(['asc', 'desc'])], + 'per_page' => ['nullable', 'integer', 'min:1', 'max:200'], + ]; + } +} diff --git a/app/Http/Resources/Roles/PermissionResource.php b/app/Http/Resources/Roles/PermissionResource.php new file mode 100644 index 00000000..123a52b1 --- /dev/null +++ b/app/Http/Resources/Roles/PermissionResource.php @@ -0,0 +1,19 @@ + (int) ($this->id ?? $this['id'] ?? 0), + 'name' => (string) ($this->name ?? $this['name'] ?? ''), + 'description' => $this->description ?? $this['description'] ?? null, + 'created_at' => $this->created_at ?? $this['created_at'] ?? null, + 'updated_at' => $this->updated_at ?? $this['updated_at'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Roles/RolePermissionResource.php b/app/Http/Resources/Roles/RolePermissionResource.php new file mode 100644 index 00000000..ae51ecf1 --- /dev/null +++ b/app/Http/Resources/Roles/RolePermissionResource.php @@ -0,0 +1,22 @@ + (int) ($this['id'] ?? $this['permission_id'] ?? 0), + 'name' => (string) ($this['name'] ?? ''), + 'description' => $this['description'] ?? null, + 'can_create' => (int) ($this['can_create'] ?? 0), + 'can_read' => (int) ($this['can_read'] ?? 0), + 'can_update' => (int) ($this['can_update'] ?? 0), + 'can_delete' => (int) ($this['can_delete'] ?? 0), + 'can_manage' => (int) ($this['can_manage'] ?? 0), + ]; + } +} diff --git a/app/Http/Resources/Roles/RoleResource.php b/app/Http/Resources/Roles/RoleResource.php new file mode 100644 index 00000000..1ac8c73f --- /dev/null +++ b/app/Http/Resources/Roles/RoleResource.php @@ -0,0 +1,24 @@ + (int) ($this->id ?? $this['id'] ?? 0), + 'name' => (string) ($this->name ?? $this['name'] ?? ''), + 'slug' => (string) ($this->slug ?? $this['slug'] ?? ''), + 'description' => $this->description ?? $this['description'] ?? null, + 'dashboard_route' => (string) ($this->dashboard_route ?? $this['dashboard_route'] ?? ''), + 'priority' => (int) ($this->priority ?? $this['priority'] ?? 0), + 'is_active' => (int) ($this->is_active ?? $this['is_active'] ?? 0), + 'user_count' => $this->user_count ?? $this['user_count'] ?? null, + 'created_at' => $this->created_at ?? $this['created_at'] ?? null, + 'updated_at' => $this->updated_at ?? $this['updated_at'] ?? null, + ]; + } +} diff --git a/app/Http/Resources/Roles/UserWithRolesResource.php b/app/Http/Resources/Roles/UserWithRolesResource.php new file mode 100644 index 00000000..a5b752c9 --- /dev/null +++ b/app/Http/Resources/Roles/UserWithRolesResource.php @@ -0,0 +1,25 @@ + (int) ($this['id'] ?? 0), + 'firstname' => (string) ($this['firstname'] ?? ''), + 'lastname' => (string) ($this['lastname'] ?? ''), + 'email' => (string) ($this['email'] ?? ''), + 'account_id' => (string) ($this['account_id'] ?? ''), + 'roles' => array_values(array_filter($roles)), + ]; + } +} diff --git a/app/Services/Roles/PermissionCrudService.php b/app/Services/Roles/PermissionCrudService.php new file mode 100644 index 00000000..6c57bdb4 --- /dev/null +++ b/app/Services/Roles/PermissionCrudService.php @@ -0,0 +1,60 @@ +normalizePayload($payload); + + try { + return DB::transaction(function () use ($data) { + return Permission::query()->create($data); + }); + } catch (\Throwable $e) { + Log::error('Permission create failed: ' . $e->getMessage()); + throw $e; + } + } + + public function update(Permission $permission, array $payload): Permission + { + $data = $this->normalizePayload($payload); + + try { + return DB::transaction(function () use ($permission, $data) { + $permission->fill($data); + $permission->save(); + return $permission; + }); + } catch (\Throwable $e) { + Log::error('Permission update failed: ' . $e->getMessage()); + throw $e; + } + } + + public function delete(Permission $permission): void + { + try { + DB::transaction(function () use ($permission) { + $permission->delete(); + }); + } catch (\Throwable $e) { + Log::error('Permission delete failed: ' . $e->getMessage()); + throw $e; + } + } + + private function normalizePayload(array $payload): array + { + return [ + 'name' => trim((string) ($payload['name'] ?? '')), + 'description' => trim((string) ($payload['description'] ?? '')), + ]; + } +} diff --git a/app/Services/Roles/RoleAssignmentService.php b/app/Services/Roles/RoleAssignmentService.php new file mode 100644 index 00000000..8c55dd8e --- /dev/null +++ b/app/Services/Roles/RoleAssignmentService.php @@ -0,0 +1,58 @@ + $id > 0))); + + $user = User::query()->find($userId); + if (!$user) { + return ['ok' => false, 'message' => 'User not found.']; + } + + try { + DB::transaction(function () use ($userId, $roleIds, $adminId) { + UserRole::query()->where('user_id', $userId)->delete(); + + foreach ($roleIds as $roleId) { + UserRole::query()->create([ + 'user_id' => $userId, + 'role_id' => $roleId, + 'updated_by' => $adminId, + ]); + } + }); + } catch (\Throwable $e) { + Log::error('Role assignment failed: ' . $e->getMessage()); + throw $e; + } + + $roleNames = Role::query() + ->whereIn('id', $roleIds) + ->pluck('name') + ->map(fn ($n) => strtolower((string) $n)) + ->all(); + + $this->staffService->syncStaffRecord($user, $roleNames); + + return [ + 'ok' => true, + 'role_names' => $roleNames, + ]; + } +} diff --git a/app/Services/Roles/RoleCrudService.php b/app/Services/Roles/RoleCrudService.php new file mode 100644 index 00000000..a11b0570 --- /dev/null +++ b/app/Services/Roles/RoleCrudService.php @@ -0,0 +1,107 @@ +normalizePayload($payload); + + try { + return DB::transaction(function () use ($data) { + return Role::query()->create($data); + }); + } catch (\Throwable $e) { + Log::error('Role create failed: ' . $e->getMessage()); + throw $e; + } + } + + public function update(Role $role, array $payload): Role + { + $data = $this->normalizePayload($payload, $role->id, $role->slug ?? null); + + try { + return DB::transaction(function () use ($role, $data) { + $role->fill($data); + $role->save(); + return $role; + }); + } catch (\Throwable $e) { + Log::error('Role update failed: ' . $e->getMessage()); + throw $e; + } + } + + public function delete(Role $role): void + { + try { + DB::transaction(function () use ($role) { + $role->delete(); + }); + } catch (\Throwable $e) { + Log::error('Role delete failed: ' . $e->getMessage()); + throw $e; + } + } + + private function normalizePayload(array $payload, ?int $ignoreId = null, ?string $currentSlug = null): array + { + $rawName = trim((string) ($payload['name'] ?? '')); + $rawSlug = trim((string) ($payload['slug'] ?? '')); + $route = trim((string) ($payload['dashboard_route'] ?? '')); + $desc = trim((string) ($payload['description'] ?? '')); + $priority = (int) ($payload['priority'] ?? 0); + $isActive = !empty($payload['is_active']) ? 1 : 0; + + $name = strtolower($rawName); + $dashboardRoute = strtolower($route); + $description = strtolower($desc); + + $slug = $rawSlug !== '' ? $rawSlug : $name; + $slug = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $slug)); + $slug = trim($slug, '_'); + + if ($slug !== $currentSlug) { + $slug = $this->ensureUniqueSlug($slug, $ignoreId); + } + + return [ + 'name' => $name, + 'slug' => $slug, + 'description' => $description, + 'dashboard_route' => $dashboardRoute, + 'priority' => $priority, + 'is_active' => $isActive, + ]; + } + + private function ensureUniqueSlug(string $baseSlug, ?int $ignoreId = null): string + { + $slug = $baseSlug; + $i = 2; + + while (true) { + $query = Role::query()->where('slug', $slug); + if ($ignoreId !== null) { + $query->where('id', '!=', $ignoreId); + } + + if (!$query->exists()) { + return $slug; + } + + $slug = $baseSlug . '_' . $i; + $i++; + + if ($i > 100000) { + return $slug; + } + } + } +} diff --git a/app/Services/Roles/RoleDashboardService.php b/app/Services/Roles/RoleDashboardService.php new file mode 100644 index 00000000..f5a6f119 --- /dev/null +++ b/app/Services/Roles/RoleDashboardService.php @@ -0,0 +1,19 @@ +dashboard_route ?? '/landing_page/guest_dashboard'; + } + + return '/landing_page/guest_dashboard'; + } +} diff --git a/app/Services/Roles/RolePermissionService.php b/app/Services/Roles/RolePermissionService.php new file mode 100644 index 00000000..c299adc3 --- /dev/null +++ b/app/Services/Roles/RolePermissionService.php @@ -0,0 +1,95 @@ +orderBy('id', 'asc') + ->get() + ->all(); + } + + public function listRolePermissions(int $roleId): array + { + $rows = Permission::query() + ->leftJoin('role_permissions', function ($join) use ($roleId) { + $join->on('role_permissions.permission_id', '=', 'permissions.id') + ->where('role_permissions.role_id', '=', $roleId); + }) + ->select( + 'permissions.id', + 'permissions.name', + 'permissions.description', + 'role_permissions.can_create', + 'role_permissions.can_read', + 'role_permissions.can_update', + 'role_permissions.can_delete', + 'role_permissions.can_manage' + ) + ->orderBy('permissions.name', 'asc') + ->get() + ->map(fn ($r) => (array) $r) + ->all(); + + return $rows; + } + + public function saveRolePermissions(int $roleId, array $permissions): void + { + try { + DB::transaction(function () use ($roleId, $permissions) { + $existing = RolePermission::query() + ->where('role_id', $roleId) + ->get() + ->keyBy('permission_id'); + + foreach ($permissions as $permissionId => $crudValues) { + $permissionId = (int) $permissionId; + if ($permissionId <= 0) { + continue; + } + + $data = [ + 'can_create' => !empty($crudValues['create']) ? 1 : 0, + 'can_read' => !empty($crudValues['read']) ? 1 : 0, + 'can_update' => !empty($crudValues['update']) ? 1 : 0, + 'can_delete' => !empty($crudValues['delete']) ? 1 : 0, + 'can_manage' => !empty($crudValues['manage']) ? 1 : 0, + ]; + + if ($existing->has($permissionId)) { + RolePermission::query() + ->where('role_id', $roleId) + ->where('permission_id', $permissionId) + ->update($data); + } else { + RolePermission::query()->create(array_merge($data, [ + 'role_id' => $roleId, + 'permission_id' => $permissionId, + ])); + } + } + + foreach ($existing as $permissionId => $row) { + if (!array_key_exists((string) $permissionId, $permissions) && !array_key_exists((int) $permissionId, $permissions)) { + RolePermission::query() + ->where('role_id', $roleId) + ->where('permission_id', $permissionId) + ->delete(); + } + } + }); + } catch (\Throwable $e) { + Log::error('Role permission save failed: ' . $e->getMessage()); + throw $e; + } + } +} diff --git a/app/Services/Roles/RoleQueryService.php b/app/Services/Roles/RoleQueryService.php new file mode 100644 index 00000000..0e8276a5 --- /dev/null +++ b/app/Services/Roles/RoleQueryService.php @@ -0,0 +1,79 @@ +select('roles.*') + ->leftJoin('user_roles', 'user_roles.role_id', '=', 'roles.id') + ->selectRaw('COUNT(DISTINCT user_roles.user_id) AS user_count') + ->groupBy('roles.id'); + + if (!empty($filters['search'])) { + $search = trim((string) $filters['search']); + $query->where(function (Builder $q) use ($search) { + $q->where('roles.name', 'like', '%' . $search . '%') + ->orWhere('roles.slug', 'like', '%' . $search . '%') + ->orWhere('roles.description', 'like', '%' . $search . '%'); + }); + } + + $sortBy = (string) ($filters['sort_by'] ?? 'name'); + $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc'; + $allowed = ['name', 'slug', 'priority', 'is_active', 'created_at']; + if (!in_array($sortBy, $allowed, true)) { + $sortBy = 'name'; + } + + $perPage = (int) ($filters['per_page'] ?? 20); + $perPage = $perPage > 0 ? $perPage : 20; + + return $query->orderBy($sortBy, $sortDir)->paginate($perPage); + } + + public function listUsersWithRoles(array $filters): LengthAwarePaginator + { + $query = DB::table('users') + ->leftJoin('user_roles', 'user_roles.user_id', '=', 'users.id') + ->leftJoin('roles', 'roles.id', '=', 'user_roles.role_id') + ->select( + 'users.id', + 'users.firstname', + 'users.lastname', + 'users.email', + 'users.account_id', + DB::raw('GROUP_CONCAT(DISTINCT roles.name) as roles') + ) + ->groupBy('users.id', 'users.firstname', 'users.lastname', 'users.email', 'users.account_id'); + + if (!empty($filters['search'])) { + $search = trim((string) $filters['search']); + $query->where(function ($q) use ($search) { + $q->where('users.firstname', 'like', '%' . $search . '%') + ->orWhere('users.lastname', 'like', '%' . $search . '%') + ->orWhere('users.email', 'like', '%' . $search . '%') + ->orWhere('users.account_id', 'like', '%' . $search . '%'); + }); + } + + $sortBy = (string) ($filters['sort_by'] ?? 'lastname'); + $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc'; + $allowed = ['firstname', 'lastname', 'email', 'account_id']; + if (!in_array($sortBy, $allowed, true)) { + $sortBy = 'lastname'; + } + + $perPage = (int) ($filters['per_page'] ?? 20); + $perPage = $perPage > 0 ? $perPage : 20; + + return $query->orderBy('users.' . $sortBy, $sortDir)->paginate($perPage); + } +} diff --git a/app/Services/Roles/RoleStaffService.php b/app/Services/Roles/RoleStaffService.php new file mode 100644 index 00000000..2e4009b5 --- /dev/null +++ b/app/Services/Roles/RoleStaffService.php @@ -0,0 +1,72 @@ +where('user_id', $user->id)->first(); + $existingRoles = []; + if ($existingStaff && !empty($existingStaff->role_name)) { + $existingRoles = array_map('strtolower', array_map('trim', explode(',', $existingStaff->role_name))); + } + + $allRoles = array_unique(array_merge($existingRoles, $loweredNewRoles)); + $staffRoles = array_filter($newRoleNames, fn ($role) => !in_array(strtolower($role), $excludedRoles, true)); + $activeRole = !empty($staffRoles) ? $staffRoles[0] : 'inactive'; + + $email = $this->generateUniqueStaffEmail($user->firstname ?? '', $user->lastname ?? '', (int) $user->id); + + $payload = [ + 'user_id' => (int) $user->id, + 'firstname' => $user->firstname ?? '', + 'lastname' => $user->lastname ?? '', + 'email' => $email, + 'phone' => $user->cellphone ?? '', + 'role_name' => implode(', ', $allRoles), + 'active_role' => $activeRole, + 'school_year' => $user->school_year ?? '', + ]; + + $saved = Staff::upsertStaff($payload); + if (!$saved) { + Log::warning('Failed to upsert staff record.', ['user_id' => $user->id]); + } + } + + private function generateUniqueStaffEmail(string $firstname, string $lastname, int $userId): string + { + $domain = '@alrahmaisgl.org'; + + $firstname = strtolower(preg_replace('/[^a-z]/i', '', $firstname)); + $lastname = strtolower(preg_replace('/[^a-z]/i', '', $lastname)); + + $min = 1; + $max = max(1, strlen($firstname)); + $base = ''; + $email = ''; + + for ($i = $min; $i <= $max; $i++) { + $base = substr($firstname, 0, $i) . $lastname; + $email = $base . $domain; + + $exists = Staff::query() + ->where('email', $email) + ->where('user_id', '!=', $userId) + ->exists(); + if (!$exists) { + return $email; + } + } + + return $base . $userId . $domain; + } +} diff --git a/app/Services/Roles/RoleSwitchService.php b/app/Services/Roles/RoleSwitchService.php new file mode 100644 index 00000000..57eba73a --- /dev/null +++ b/app/Services/Roles/RoleSwitchService.php @@ -0,0 +1,25 @@ + (string) ($r['role_name'] ?? ''), $roles))); + } + + public function switchRole(string $role): string + { + Session::put('active_role', $role); + return $this->dashboardService->bestDashboardRouteFor([$role]); + } +} diff --git a/app/old/RolePermissionController.php b/app/old/RolePermissionController.php deleted file mode 100644 index a00266e4..00000000 --- a/app/old/RolePermissionController.php +++ /dev/null @@ -1,723 +0,0 @@ -roleModel = new RoleModel(); - $this->userModel = new UserModel(); - $this->userRoleModel = new UserRoleModel(); - $this->staffModel = new StaffModel(); - $this->configModel = new ConfigurationModel(); - $this->permissionModel = new PermissionModel(); - $this->rolePermissionModel = new RolePermissionModel(); - - $this->request = \Config\Services::request(); - $this->db = \Config\Database::connect(); - $this->schoolYear = $this->configModel->getConfig('school_year'); - $this->semester = $this->configModel->getConfig('semester'); - } - - public function index() - { - helper('url'); - return view('rolepermission/list_roles', [ - 'rolesEndpoint' => site_url('api/rolepermission/roles'), - ]); - } - - public function assignRole() - { - helper('url'); - return view('rolepermission/assign_role', [ - 'assignRoleEndpoint' => site_url('api/rolepermission/users'), - 'rolesEndpoint' => site_url('api/rolepermission/roles'), - ]); - } - - public function saveRole() - { - if ($this->request->getMethod(true) === 'POST') { - $userId = (int) $this->request->getPost('user_id'); - $roleIdsRaw = $this->request->getPost('role_ids'); - $roleIdsArray = is_array($roleIdsRaw) ? $roleIdsRaw : ($roleIdsRaw !== null ? [$roleIdsRaw] : []); - $roleIds = array_values(array_unique(array_filter(array_map('intval', $roleIdsArray), fn($id) => $id > 0))); - $adminId = session()->get('user_id'); - - if ($userId <= 0) { - return redirect()->to('rolepermission/assign_role')->with('error', 'Invalid user selection.'); - } - - try { - $this->db->transStart(); - - // Remove all existing roles for the user - $this->userRoleModel->where('user_id', $userId)->delete(); - - // Assign new roles - if (!empty($roleIds)) { - foreach ($roleIds as $roleId) { - $this->userRoleModel->insert([ - 'user_id' => $userId, - 'role_id' => $roleId, - 'created_at' => utc_now(), - 'updated_by' => $adminId - ]); - } - } - - $this->db->transComplete(); - - if ($this->db->transStatus() === false) { - throw new \RuntimeException('Transaction failed.'); - } - - // ✅ Load role names after saving - $assignedRoles = !empty($roleIds) - ? $this->roleModel->whereIn('id', $roleIds)->findAll() - : []; - $roleNames = array_map(fn($r) => strtolower($r['name']), $assignedRoles ?? []); - - // ✅ Update staff record if needed - $this->updateStaffRecord($userId, $roleNames); - - return redirect()->to('rolepermission/assign_role')->with('success', 'Roles updated successfully.'); - } catch (\Exception $e) { - $this->db->transRollback(); - log_message('error', 'Error updating roles: ' . $e->getMessage()); - return redirect()->to('rolepermission/assign_role')->with('error', 'Failed to update roles.'); - } - } - - return redirect()->to('rolepermission/assign_role')->with('error', 'Invalid request.'); - } - - public function usersData() - { - return $this->response->setJSON($this->buildUsersWithRolesPayload()); - } - - public function rolesData() - { - $rows = $this->roleModel - ->select('roles.id, roles.name, roles.description, COUNT(DISTINCT user_roles.user_id) AS user_count') - ->join('user_roles', 'user_roles.role_id = roles.id', 'left') - ->groupBy('roles.id, roles.name, roles.description') - ->orderBy('roles.name', 'ASC') - ->findAll(); - - $normalized = array_map(static function ($row) { - if (!is_array($row)) { - return []; - } - return [ - 'id' => (int) ($row['id'] ?? 0), - 'name' => (string) ($row['name'] ?? ''), - 'description' => $row['description'] ?? null, - 'user_count' => (int) ($row['user_count'] ?? 0), - ]; - }, $rows ?? []); - - return $this->response->setJSON(['roles' => $normalized]); - } - - private function buildUsersWithRolesPayload(): array - { - $rows = $this->userModel - ->select('users.id, users.firstname, users.lastname, users.email, users.account_id, roles.name AS role_name') - ->join('user_roles', 'users.id = user_roles.user_id', 'left') - ->join('roles', 'user_roles.role_id = roles.id', 'left') - ->orderBy('users.id', 'ASC') - ->findAll(); - - $users = []; - foreach ($rows as $row) { - if (!is_array($row)) { - continue; - } - - $id = (int) ($row['id'] ?? 0); - if ($id <= 0) { - continue; - } - - if (!isset($users[$id])) { - $users[$id] = [ - 'id' => $id, - 'firstname' => $row['firstname'] ?? '', - 'lastname' => $row['lastname'] ?? '', - 'email' => $row['email'] ?? '', - 'account_id' => $row['account_id'] ?? '', - 'roles' => [], - ]; - } - - $roleName = $row['role_name'] ?? null; - if ($roleName) { - $users[$id]['roles'][] = $roleName; - } - } - - $normalized = array_map(static function ($user) { - $roles = array_values(array_unique(array_filter($user['roles'] ?? [], 'strlen'))); - return [ - 'id' => $user['id'], - 'firstname' => $user['firstname'] ?? '', - 'lastname' => $user['lastname'] ?? '', - 'email' => $user['email'] ?? '', - 'account_id' => $user['account_id'] ?? '', - 'roles' => $roles, - ]; - }, array_values($users)); - - return ['users' => $normalized]; - } - - private function updateStaffRecord(int $userId, array $newRoleNames): void - { - $excludedRoles = ['parent', 'student', 'guest']; - $loweredNewRoles = array_map('strtolower', $newRoleNames); - - $user = $this->userModel->find($userId); - if (!$user) { - log_message('error', "updateStaffRecord: user_id {$userId} not found"); - return; - } - - // Fetch existing staff record if it exists - $existingStaff = $this->staffModel->where('user_id', $userId)->first(); - - // Extract existing roles from DB if any - $existingRoles = []; - if (!empty($existingStaff['role_name'])) { - $existingRoles = array_map('strtolower', array_map('trim', explode(',', $existingStaff['role_name']))); - } - - // Merge and deduplicate roles - $allRoles = array_unique(array_merge($existingRoles, $loweredNewRoles)); - - // Determine staff roles (excluding parent/student/guest) - $staffRoles = array_filter($newRoleNames, fn($role) => !in_array($role, $excludedRoles)); - - // Determine active role - $activeRole = !empty($staffRoles) ? $staffRoles[0] : 'inactive'; - - $email = $this->generateUniqueStaffEmail($user['firstname'], $user['lastname'], $userId); - $now = utc_now(); - - $row = [ - 'user_id' => $userId, - 'firstname' => $user['firstname'], - 'lastname' => $user['lastname'], - 'email' => $email, - 'phone' => $user['cellphone'], - 'role_name' => implode(', ', $allRoles), // historical roles - 'active_role' => $activeRole, - 'school_year' => $this->schoolYear, - 'updated_at' => $now, - ]; - - $this->staffModel->upsert($row); - } - - private function generateUniqueStaffEmail(string $firstname, string $lastname, int $userId): string - { - $domain = '@alrahmaisgl.org'; - - // Sanitize names: lowercase, letters only - $firstname = strtolower(preg_replace('/[^a-z]/i', '', $firstname)); - $lastname = strtolower(preg_replace('/[^a-z]/i', '', $lastname)); - - // Use at least 1 letter from firstname - $min = 1; - $max = strlen($firstname); - $base = ''; - $email = ''; - - for ($i = $min; $i <= $max; $i++) { - $base = substr($firstname, 0, $i) . $lastname; - $email = $base . $domain; - - // If email not taken or belongs to same user, use it - $exists = $this->staffModel->where('email', $email) - ->where('user_id !=', $userId) - ->first(); - if (!$exists) { - return $email; - } - } - - // If all combinations used, fallback to adding user ID - return $base . $userId . $domain; - } - - public function listRoles() - { - helper('url'); - return view('rolepermission/list_roles', [ - 'rolesEndpoint' => site_url('api/rolepermission/roles'), - ]); - } - - public function editRolePermissions($roleId) - { - // Fetch roles, permissions, and role permissions data - $roles = $this->roleModel->findAll(); - $permissions = $this->permissionModel->findAll(); - $rolePermissions = $this->rolePermissionModel->where('role_id', $roleId)->findAll(); - - // Convert existing role permissions to a simple array for comparison - $existingPermissions = []; - foreach ($rolePermissions as $rolePermission) { - $existingPermissions[$rolePermission['permission_id']] = [ - 'can_create' => $rolePermission['can_create'], - 'can_read' => $rolePermission['can_read'], - 'can_update' => $rolePermission['can_update'], - 'can_delete' => $rolePermission['can_delete'], - ]; - } - - // Handle POST request - if (strtolower($this->request->getMethod()) === 'post') { - // Get the posted permissions data - $postPermissions = $this->request->getPost('permissions'); - - // Call saveRolePermissions with the correct data - return $this->saveRolePermissions($roleId, $postPermissions); - } - - return view('rolepermission/edit_role_permissions', [ - 'roles' => $roles, - 'roleId' => $roleId, - 'permissions' => $permissions, - 'rolePermissions' => $rolePermissions, - 'existingPermissions' => $existingPermissions - ]); - } - - public function saveRolePermissions($roleId = null, $permissions = null) - { - // If $roleId and $permissions are not passed, retrieve them from POST - if (!$roleId) { - $roleId = $this->request->getPost('role_id'); - } - - if (!$permissions) { - $permissions = $this->request->getPost('permissions'); - } - - if (!$roleId) { - return redirect()->back()->with('error', 'Role ID is missing.'); - } - - // Fetch existing permissions for the role - $rolePermissions = $this->rolePermissionModel->where('role_id', $roleId)->findAll(); - - $existingPermissions = []; - foreach ($rolePermissions as $rolePermission) { - $existingPermissions[$rolePermission['permission_id']] = $rolePermission; - } - - try { - $this->rolePermissionModel->transStart(); - - // Process the new permissions if provided - if ($permissions && is_array($permissions)) { - foreach ($permissions as $permissionId => $crudValues) { - // Ensure CRUD values are correctly set, defaulting to 0 if not provided - $permissionData = [ - 'can_create' => isset($crudValues['create']) ? 1 : 0, - 'can_read' => isset($crudValues['read']) ? 1 : 0, - 'can_update' => isset($crudValues['update']) ? 1 : 0, - 'can_delete' => isset($crudValues['delete']) ? 1 : 0, - 'updated_at' => utc_now(), - ]; - - if (isset($existingPermissions[$permissionId])) { - // Update existing permission - $this->rolePermissionModel->where('role_id', $roleId) - ->where('permission_id', $permissionId) - ->set($permissionData) - ->update(); - } else { - // Insert new permission - $permissionData['role_id'] = $roleId; - $permissionData['permission_id'] = $permissionId; - $permissionData['created_at'] = utc_now(); - $this->rolePermissionModel->insert($permissionData); - } - } - } - - // Remove permissions that are no longer present - foreach ($existingPermissions as $permissionId => $permissionData) { - if (!isset($permissions[$permissionId])) { - $this->rolePermissionModel->where('role_id', $roleId) - ->where('permission_id', $permissionId) - ->delete(); - } - } - - $this->rolePermissionModel->transComplete(); - - if ($this->rolePermissionModel->transStatus() === false) { - throw new \RuntimeException('Transaction failed.'); - } - - return redirect()->to('/rolepermission/edit_role_permissions/' . $roleId) - ->with('status', 'Permissions saved successfully.'); - } catch (\Exception $e) { - $this->rolePermissionModel->transRollback(); - log_message('error', 'Failed to save permissions: ' . $e->getMessage()); - return redirect()->back()->with('error', 'Failed to save permissions: ' . $e->getMessage()); - } - } - - // Role management functions -public function rolesList() -{ - return $this->listRoles(); -} - - - public function addRole() - { - return view('rolepermission/add_role'); - } - -public function storeRole() -{ - $validation = \Config\Services::validation(); - - // Validation rules - $validation->setRules([ - 'name' => 'required|min_length[3]|max_length[255]|is_unique[roles.name]', - 'slug' => 'permit_empty|max_length[64]|is_unique[roles.slug]', - 'description' => 'permit_empty|max_length[500]', - 'dashboard_route' => [ - 'rules' => 'required|min_length[1]|max_length[255]|regex_match[/^[a-z0-9_\\/\\-]+$/]', - 'errors' => [ - 'regex_match' => 'Route may contain lowercase letters, numbers, /, -, and _.' - ] - ], - 'priority' => 'required|integer|greater_than_equal_to[0]|less_than_equal_to[100000]', - 'is_active' => 'required|in_list[0,1]', - ]); - - if (!$validation->withRequest($this->request)->run()) { - return redirect()->back()->withInput()->with('errors', $validation->getErrors()); - } - - // Normalize + sanitize inputs - $rawName = trim((string) $this->request->getPost('name')); - $rawSlug = trim((string) $this->request->getPost('slug')); - $route = trim((string) $this->request->getPost('dashboard_route')); - $desc = trim((string) $this->request->getPost('description')); - $priority = (int) $this->request->getPost('priority'); - $isActive = (string) $this->request->getPost('is_active') === '1' ? 1 : 0; - - // Force lowercase for consistency - $name = strtolower($rawName); - $dashboard_route = strtolower($route); - $description = strtolower($desc); - - // Build slug: from input if provided, else from name - $slug = $rawSlug !== '' ? $rawSlug : $name; - // convert spaces / invalid chars to underscores and lowercase - $slug = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $slug)); - $slug = trim($slug, '_'); - - // Ensure slug uniqueness (in case collision after normalization) - $slug = $this->ensureUniqueSlug($slug); - - $data = [ - 'name' => $name, - 'slug' => $slug, - 'description' => $description, - 'dashboard_route' => $dashboard_route, - 'priority' => $priority, - 'is_active' => $isActive, - 'created_at' => utc_now(), - 'updated_at' => utc_now(), - ]; - - if ($this->roleModel->insert($data)) { - return redirect()->to('rolepermission/roles')->with('success', 'Role created successfully!'); - } - - return redirect()->back()->withInput()->with('error', 'Failed to create role.'); -} - -/** - * Ensure slug is unique in roles.slug; if exists, append -2, -3, ... - */ -/** - * Ensure slug is unique in the roles table. - * - If $ignoreId is provided, that record is excluded (for updates). - * - Appends _2, _3, ... if duplicates exist. - */ -private function ensureUniqueSlug(string $baseSlug, ?int $ignoreId = null): string -{ - $slug = $baseSlug; - $i = 2; - - while (true) { - $builder = $this->roleModel->where('slug', $slug); - if ($ignoreId !== null) { - $builder->where('id !=', $ignoreId); - } - - $exists = $builder->countAllResults() > 0; - - if (!$exists) { - break; - } - - $slug = $baseSlug . '_' . $i; - $i++; - - if ($i > 100000) { // safety stop - break; - } - } - - return $slug; -} - - - - public function editRole($id) - { - if (!$role = $this->roleModel->find($id)) { - return redirect()->to('rolepermission/roles')->with('error', 'Role not found.'); - } - - return view('rolepermission/edit_role', ['role' => $role]); - } - -public function updateRole($id) -{ - $role = $this->roleModel->find($id); - if (!$role) { - return redirect()->to('rolepermission/roles')->with('error', 'Role not found.'); - } - - $validation = \Config\Services::validation(); - // CI4 is_unique format: is_unique[table.field,ignore_field,ignore_value] - $validation->setRules([ - 'name' => "required|min_length[3]|max_length[255]|is_unique[roles.name,id,{$id}]", - 'slug' => "permit_empty|max_length[64]|is_unique[roles.slug,id,{$id}]", - 'description' => 'permit_empty|max_length[500]', - 'dashboard_route' => [ - 'rules' => 'required|min_length[1]|max_length[255]|regex_match[/^[a-z0-9_\\/\\-]+$/]', - 'errors' => [ - 'regex_match' => 'Route may contain lowercase letters, numbers, /, -, and _.' - ] - ], - 'priority' => 'required|integer|greater_than_equal_to[0]|less_than_equal_to[100000]', - 'is_active' => 'required|in_list[0,1]', - ]); - - if (!$validation->withRequest($this->request)->run()) { - return redirect()->back()->withInput()->with('errors', $validation->getErrors()); - } - - // Normalize inputs (lowercase storage) - $rawName = trim((string) $this->request->getPost('name')); - $rawSlug = trim((string) $this->request->getPost('slug')); - $route = trim((string) $this->request->getPost('dashboard_route')); - $desc = trim((string) $this->request->getPost('description')); - $priority = (int) $this->request->getPost('priority'); - $isActive = (string) $this->request->getPost('is_active') === '1' ? 1 : 0; - - $name = strtolower($rawName); - $dashboard_route = strtolower($route); - $description = strtolower($desc); - - // slug: if blank, derive from name; else normalize - $slug = $rawSlug !== '' ? $rawSlug : $name; - $slug = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $slug)); - $slug = trim($slug, '_'); - - // Ensure unique slug if changed or blank turned into new value - if ($slug !== $role['slug']) { - $slug = $this->ensureUniqueSlug($slug, (int) $id); - } - - $data = [ - 'name' => $name, - 'slug' => $slug, - 'description' => $description, - 'dashboard_route' => $dashboard_route, - 'priority' => $priority, - 'is_active' => $isActive, - 'updated_at' => utc_now(), - ]; - - if ($this->roleModel->update($id, $data)) { - return redirect()->to('rolepermission/roles')->with('success', 'Role updated successfully!'); - } - - return redirect()->back()->withInput()->with('error', 'Failed to update role.'); -} - - - - - - public function deleteRole($id) - { - // Check if role exists - $role = $this->roleModel->find($id); - if (!$role) { - return redirect()->to(site_url('rolepermission/roles')) - ->with('error', 'Role not found.'); - } - - // Try deleting - if ($this->roleModel->delete($id)) { - return redirect()->to(site_url('rolepermission/roles')) - ->with('success', 'Role deleted successfully.'); - } else { - return redirect()->to(site_url('rolepermission/roles')) - ->with('error', 'Failed to delete role.'); - } - } - - // ADD: show form + handle submit - public function addPermission() - { - if (strtolower($this->request->getMethod()) === 'post') { - $name = trim((string) $this->request->getPost('name')); - $desc = trim((string) $this->request->getPost('description')); - - // Basic guard - if ($name === '') { - return redirect()->back() - ->with('error', 'Permission name is required.') - ->withInput(); - } - - // Check existence (collation is case-insensitive: utf8mb4_unicode_ci) - $existing = $this->permissionModel->where('name', $name)->first(); - - if ($existing) { - // Idempotent: do nothing if already exists - return redirect()->to(site_url('rolepermission/list_permissions')) - ->with('success', 'Permission already exists. No changes made.'); - } - - // Create new - $data = [ - 'name' => $name, - 'description' => $desc, - ]; - - if (!$this->permissionModel->save($data)) { - return redirect()->back() - ->with('error', implode(' ', $this->permissionModel->errors())) - ->withInput(); - } - - return redirect()->to(site_url('rolepermission/list_permissions')) - ->with('success', 'Permission added successfully.'); - } - - // GET -> form - return view('rolepermission/add_permission'); - } - - - // EDIT: show form + handle submit - public function editPermission($id = null) - { - $permission = $this->permissionModel->find($id); - if (!$permission) { - return redirect()->to(site_url('roles'))->with('error', 'Permission not found.'); - } - - if (strtolower($this->request->getMethod()) === 'post') { - $data = [ - 'id' => $id, // needed for is_unique ignore - 'name' => trim($this->request->getPost('name')), - 'description' => trim($this->request->getPost('description')), - ]; - - if (!$this->permissionModel->save($data)) { - return redirect()->back() - ->with('error', implode(' ', $this->permissionModel->errors())) - ->withInput(); - } - - return redirect()->to(site_url('rolepermission/list_permissions')) - ->with('success', 'Permission updated successfully.'); - } - - return view('rolepermission/edit_permission', ['permission' => $permission]); - } - - // Optional delete - public function deletePermission($id = null) - { - if (!$this->permissionModel->find($id)) { - return redirect()->to(site_url('rolepermission/list_permissions'))->with('error', 'Permission not found.'); - } - - $this->permissionModel->delete($id); - return redirect()->to(site_url('rolepermission/list_permissions'))->with('success', 'Permission deleted.'); - } - - public function listPermissions() - { - helper('url'); - return view('rolepermission/permissionList', [ - 'permissionsEndpoint' => site_url('api/rolepermission/permissions'), - ]); - } - - public function permissionsData() - { - $rows = $this->permissionModel - ->select('id, name, description, created_at, updated_at') - ->orderBy('id', 'ASC') - ->findAll(); - - $normalized = array_map(static function ($row) { - if (!is_array($row)) { - return []; - } - return [ - 'id' => (int) ($row['id'] ?? 0), - 'name' => (string) ($row['name'] ?? ''), - 'description' => $row['description'] ?? null, - 'created_at' => $row['created_at'] ?? null, - 'updated_at' => $row['updated_at'] ?? null, - ]; - }, $rows ?? []); - - return $this->response->setJSON(['permissions' => $normalized]); - } -} diff --git a/app/old/RoleService.php b/app/old/RoleService.php deleted file mode 100644 index f496ec53..00000000 --- a/app/old/RoleService.php +++ /dev/null @@ -1,21 +0,0 @@ -roles->findByNamesOrSlugs($roleKeys); - if (!empty($rows)) { - // first row is highest priority due to orderBy('priority','ASC') - return $rows[0]['dashboard_route'] ?? '/landing_page/guest_dashboard'; - } - return '/landing_page/guest_dashboard'; - } -} diff --git a/app/old/RoleSwitcherController.php b/app/old/RoleSwitcherController.php deleted file mode 100644 index 2351d6e1..00000000 --- a/app/old/RoleSwitcherController.php +++ /dev/null @@ -1,94 +0,0 @@ -db = \Config\Database::connect(); - // Check if the database connection is established - if (!$this->db->connect()) { - log_message('error', 'Database connection failed.'); - throw new \Exception('Database connection failed.'); - } else { - log_message('info', 'Database connection successful.'); - } - $this->configModel = new ConfigurationModel(); - $this->userRoleModel = new UserRoleModel(); - $this->semester = $this->configModel->getConfig('semester'); - $this->schoolYear = $this->configModel->getConfig('school_year'); - } - - public function index() - { - $userId = session()->get('user_id'); - - if (!$userId) { - return redirect()->to('/login')->with('error', 'Please log in first.'); - } - $roles = $this->userRoleModel - ->select('roles.name') - ->join('roles', 'roles.id = user_roles.role_id') - ->where('user_roles.user_id', $userId) - ->findAll(); - - if (empty($roles)) { - return redirect()->back()->with('error', 'No roles assigned to this user.'); - } - - $roleNames = array_column($roles, 'name'); - - if (count($roleNames) === 1) { - return $this->redirectToDashboard($roleNames[0]); - } - - return view('account/choose_account', ['roles' => $roleNames]); - } - - public function switchTo($role) - { - session()->set('active_role', $role); - return $this->redirectToDashboard($role); - } - -/* - private function redirectToDashboard($role) - { - return match ($role) { - 'administrator' => redirect()->to('administrator/administratordashboard'), - 'administrative staff' => redirect()->to('/landing_page/admin_dashboard'), - 'teacher' => redirect()->to('/teacher_dashboard'), - 'student' => redirect()->to('/landing_page/student_dashboard'), - 'parent' => redirect()->to('/parent_dashboard'), - 'head_of_department' => redirect()->to('/hod/dashboard'), - default => redirect()->to('/')->with('error', 'Unknown role.'), - }; - } -*/ - - -private function redirectToDashboard($role) -{ - $key = is_string($role) ? $role : (string) $role; - - $route = (new RoleModel())->getRouteByNameOrSlug($key) - ?? '/landing_page/guest_dashboard'; - - log_message('debug', 'Redirecting user to: ' . $route); - return redirect()->to($route); -} - -} diff --git a/routes/api.php b/routes/api.php index 5d80a4ca..6cd2d855 100755 --- a/routes/api.php +++ b/routes/api.php @@ -187,6 +187,29 @@ Route::prefix('v1')->group(function () { Route::get('check-timeout', [SessionTimeoutController::class, 'check'])->name('session.timeout.check'); Route::post('ping', [SessionTimeoutController::class, 'ping'])->name('session.timeout.ping'); }); + Route::prefix('role-permissions')->group(function () { + Route::get('roles', [RolePermissionController::class, 'roles']); + Route::post('roles', [RolePermissionController::class, 'storeRole']); + Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole']); + Route::patch('roles/{roleId}', [RolePermissionController::class, 'updateRole']); + Route::delete('roles/{roleId}', [RolePermissionController::class, 'deleteRole']); + + Route::get('users', [RolePermissionController::class, 'users']); + Route::post('users/{userId}/roles', [RolePermissionController::class, 'assignRoles']); + + Route::get('permissions', [RolePermissionController::class, 'permissions']); + Route::post('permissions', [RolePermissionController::class, 'storePermission']); + Route::get('permissions/{permissionId}', [RolePermissionController::class, 'showPermission']); + Route::patch('permissions/{permissionId}', [RolePermissionController::class, 'updatePermission']); + Route::delete('permissions/{permissionId}', [RolePermissionController::class, 'deletePermission']); + + Route::get('roles/{roleId}/permissions', [RolePermissionController::class, 'rolePermissions']); + Route::put('roles/{roleId}/permissions', [RolePermissionController::class, 'updateRolePermissions']); + }); + Route::prefix('role-switcher')->group(function () { + Route::get('/', [RoleSwitcherController::class, 'index']); + Route::post('switch', [RoleSwitcherController::class, 'switch']); + }); Route::prefix('settings/school-calendar')->group(function () { Route::get('options', [SchoolCalendarController::class, 'options']); Route::get('events', [SchoolCalendarController::class, 'index']); diff --git a/tests/Feature/Api/V1/Roles/RolePermissionControllerTest.php b/tests/Feature/Api/V1/Roles/RolePermissionControllerTest.php new file mode 100644 index 00000000..e38c0f0e --- /dev/null +++ b/tests/Feature/Api/V1/Roles/RolePermissionControllerTest.php @@ -0,0 +1,247 @@ +seedUser(); + $this->seedRole(); + Sanctum::actingAs($user); + + $response = $this->getJson('/api/v1/role-permissions/roles'); + + $response->assertOk(); + $response->assertJsonPath('status', true); + $this->assertNotEmpty($response->json('data.roles')); + } + + public function test_store_role_creates_role(): void + { + $user = $this->seedUser(); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/role-permissions/roles', [ + 'name' => 'coach', + 'slug' => 'coach', + 'description' => 'Coach role', + 'dashboard_route' => 'coach/dashboard', + 'priority' => 5, + 'is_active' => 1, + ]); + + $response->assertStatus(201); + $this->assertDatabaseHas('roles', ['name' => 'coach']); + } + + public function test_update_role_updates_role(): void + { + $user = $this->seedUser(); + $roleId = $this->seedRole(); + Sanctum::actingAs($user); + + $response = $this->patchJson('/api/v1/role-permissions/roles/' . $roleId, [ + 'description' => 'updated', + ]); + + $response->assertOk(); + $this->assertDatabaseHas('roles', ['id' => $roleId, 'description' => 'updated']); + } + + public function test_delete_role_removes_role(): void + { + $user = $this->seedUser(); + $roleId = $this->seedRole(); + Sanctum::actingAs($user); + + $response = $this->deleteJson('/api/v1/role-permissions/roles/' . $roleId); + + $response->assertOk(); + $this->assertDatabaseMissing('roles', ['id' => $roleId]); + } + + public function test_users_index_returns_users_with_roles(): void + { + $user = $this->seedUser(); + $roleId = $this->seedRole(); + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + Sanctum::actingAs($user); + $response = $this->getJson('/api/v1/role-permissions/users'); + + $response->assertOk(); + $response->assertJsonPath('status', true); + $this->assertNotEmpty($response->json('data.users')); + } + + public function test_assign_roles_updates_user_roles(): void + { + $user = $this->seedUser(); + $roleId = $this->seedRole(); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/role-permissions/users/' . $user->id . '/roles', [ + 'role_ids' => [$roleId], + ]); + + $response->assertOk(); + $this->assertDatabaseHas('user_roles', [ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + $this->assertDatabaseHas('staff', [ + 'user_id' => $user->id, + ]); + } + + public function test_permissions_crud(): void + { + $user = $this->seedUser(); + Sanctum::actingAs($user); + + $create = $this->postJson('/api/v1/role-permissions/permissions', [ + 'name' => 'manage_settings', + 'description' => 'Manage settings', + ]); + $create->assertStatus(201); + $permissionId = (int) $create->json('data.permission.id'); + + $show = $this->getJson('/api/v1/role-permissions/permissions/' . $permissionId); + $show->assertOk(); + + $update = $this->patchJson('/api/v1/role-permissions/permissions/' . $permissionId, [ + 'description' => 'Updated', + ]); + $update->assertOk(); + + $delete = $this->deleteJson('/api/v1/role-permissions/permissions/' . $permissionId); + $delete->assertOk(); + $this->assertDatabaseMissing('permissions', ['id' => $permissionId]); + } + + public function test_role_permissions_update(): void + { + $user = $this->seedUser(); + $roleId = $this->seedRole(); + $permissionId = $this->seedPermission(); + + Sanctum::actingAs($user); + $response = $this->putJson('/api/v1/role-permissions/roles/' . $roleId . '/permissions', [ + 'permissions' => [ + $permissionId => ['create' => true, 'read' => true], + ], + ]); + + $response->assertOk(); + $this->assertDatabaseHas('role_permissions', [ + 'role_id' => $roleId, + 'permission_id' => $permissionId, + 'can_create' => 1, + 'can_read' => 1, + ]); + } + + public function test_validation_rejects_invalid_role_payload(): void + { + $user = $this->seedUser(); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/role-permissions/roles', [ + 'name' => 'a', + ]); + + $response->assertStatus(422); + } + + public function test_store_role_returns_error_on_service_exception(): void + { + $user = $this->seedUser(); + Sanctum::actingAs($user); + + $this->app->bind(\App\Services\Roles\RoleCrudService::class, function () { + return new class { + public function create(array $payload) + { + throw new \RuntimeException('fail'); + } + }; + }); + + $response = $this->postJson('/api/v1/role-permissions/roles', [ + 'name' => 'admin', + 'slug' => 'admin', + 'description' => 'Admin', + 'dashboard_route' => 'administrator/administratordashboard', + 'priority' => 1, + 'is_active' => 1, + ]); + + $response->assertStatus(500); + $response->assertJsonPath('status', false); + } + + public function test_requires_authentication(): void + { + $response = $this->getJson('/api/v1/role-permissions/roles'); + + $response->assertStatus(401); + } + + private function seedUser(): User + { + $userId = DB::table('users')->insertGetId([ + 'firstname' => 'Admin', + 'lastname' => 'User', + 'cellphone' => '9999999999', + 'email' => 'roles@example.com', + 'address_street' => '123 Street', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'password' => bcrypt('password'), + 'user_type' => 'primary', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + 'status' => 'Active', + ]); + + return User::query()->findOrFail($userId); + } + + private function seedRole(): int + { + return DB::table('roles')->insertGetId([ + 'name' => 'admin', + 'slug' => 'admin', + 'description' => 'Admin', + 'dashboard_route' => 'administrator/administratordashboard', + 'priority' => 1, + 'is_active' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + private function seedPermission(): int + { + return DB::table('permissions')->insertGetId([ + 'name' => 'manage_roles', + 'description' => 'Manage roles', + 'created_at' => now(), + 'updated_at' => now(), + ]); + } +} diff --git a/tests/Feature/Api/V1/Roles/RoleSwitcherControllerTest.php b/tests/Feature/Api/V1/Roles/RoleSwitcherControllerTest.php new file mode 100644 index 00000000..7954da27 --- /dev/null +++ b/tests/Feature/Api/V1/Roles/RoleSwitcherControllerTest.php @@ -0,0 +1,90 @@ +seedUser(); + $roleId = $this->seedRole(); + + DB::table('user_roles')->insert([ + 'user_id' => $user->id, + 'role_id' => $roleId, + ]); + + Sanctum::actingAs($user); + $response = $this->getJson('/api/v1/role-switcher'); + + $response->assertOk(); + $response->assertJsonPath('status', true); + $this->assertNotEmpty($response->json('data.roles')); + } + + public function test_switch_sets_role(): void + { + $user = $this->seedUser(); + $this->seedRole(); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/role-switcher/switch', [ + 'role' => 'admin', + ]); + + $response->assertOk(); + $response->assertJsonPath('status', true); + $response->assertJsonPath('data.role', 'admin'); + } + + public function test_requires_authentication(): void + { + $response = $this->getJson('/api/v1/role-switcher'); + + $response->assertStatus(401); + } + + private function seedUser(): User + { + $userId = DB::table('users')->insertGetId([ + 'firstname' => 'Admin', + 'lastname' => 'User', + 'cellphone' => '9999999999', + 'email' => 'switch@example.com', + 'address_street' => '123 Street', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'password' => bcrypt('password'), + 'user_type' => 'primary', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + 'status' => 'Active', + ]); + + return User::query()->findOrFail($userId); + } + + private function seedRole(): int + { + return DB::table('roles')->insertGetId([ + 'name' => 'admin', + 'slug' => 'admin', + 'description' => 'Admin', + 'dashboard_route' => 'administrator/administratordashboard', + 'priority' => 1, + 'is_active' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } +} diff --git a/tests/Unit/Resources/Roles/PermissionResourceTest.php b/tests/Unit/Resources/Roles/PermissionResourceTest.php new file mode 100644 index 00000000..c53d92fe --- /dev/null +++ b/tests/Unit/Resources/Roles/PermissionResourceTest.php @@ -0,0 +1,24 @@ + 1, + 'name' => 'manage_roles', + 'description' => 'Manage roles', + ]); + + $payload = $resource->toArray(request()); + + $this->assertArrayHasKey('id', $payload); + $this->assertArrayHasKey('name', $payload); + $this->assertArrayHasKey('description', $payload); + } +} diff --git a/tests/Unit/Resources/Roles/RolePermissionResourceTest.php b/tests/Unit/Resources/Roles/RolePermissionResourceTest.php new file mode 100644 index 00000000..59ddc94b --- /dev/null +++ b/tests/Unit/Resources/Roles/RolePermissionResourceTest.php @@ -0,0 +1,25 @@ + 1, + 'name' => 'manage_roles', + 'can_create' => 1, + 'can_read' => 0, + ]); + + $payload = $resource->toArray(request()); + + $this->assertArrayHasKey('permission_id', $payload); + $this->assertArrayHasKey('can_create', $payload); + $this->assertArrayHasKey('can_read', $payload); + } +} diff --git a/tests/Unit/Resources/Roles/RoleResourceTest.php b/tests/Unit/Resources/Roles/RoleResourceTest.php new file mode 100644 index 00000000..d9e32557 --- /dev/null +++ b/tests/Unit/Resources/Roles/RoleResourceTest.php @@ -0,0 +1,29 @@ + 1, + 'name' => 'admin', + 'slug' => 'admin', + 'description' => 'Admin', + 'dashboard_route' => 'administrator/administratordashboard', + 'priority' => 1, + 'is_active' => 1, + ]); + + $payload = $resource->toArray(request()); + + $this->assertArrayHasKey('id', $payload); + $this->assertArrayHasKey('name', $payload); + $this->assertArrayHasKey('slug', $payload); + $this->assertArrayHasKey('dashboard_route', $payload); + } +} diff --git a/tests/Unit/Resources/Roles/UserWithRolesResourceTest.php b/tests/Unit/Resources/Roles/UserWithRolesResourceTest.php new file mode 100644 index 00000000..42266464 --- /dev/null +++ b/tests/Unit/Resources/Roles/UserWithRolesResourceTest.php @@ -0,0 +1,26 @@ + 1, + 'firstname' => 'Admin', + 'lastname' => 'User', + 'email' => 'admin@example.com', + 'account_id' => 'A1', + 'roles' => 'admin,teacher', + ]); + + $payload = $resource->toArray(request()); + + $this->assertArrayHasKey('roles', $payload); + $this->assertCount(2, $payload['roles']); + } +} diff --git a/tests/Unit/Services/Roles/PermissionCrudServiceTest.php b/tests/Unit/Services/Roles/PermissionCrudServiceTest.php new file mode 100644 index 00000000..d56462a7 --- /dev/null +++ b/tests/Unit/Services/Roles/PermissionCrudServiceTest.php @@ -0,0 +1,28 @@ +create([ + 'name' => 'manage_roles', + 'description' => 'Manage roles', + ]); + + $this->assertInstanceOf(Permission::class, $permission); + $this->assertDatabaseHas('permissions', ['name' => 'manage_roles']); + + $service->update($permission, ['description' => 'Updated']); + $this->assertDatabaseHas('permissions', ['id' => $permission->id, 'description' => 'Updated']); + } +} diff --git a/tests/Unit/Services/Roles/RoleAssignmentServiceTest.php b/tests/Unit/Services/Roles/RoleAssignmentServiceTest.php new file mode 100644 index 00000000..e826162d --- /dev/null +++ b/tests/Unit/Services/Roles/RoleAssignmentServiceTest.php @@ -0,0 +1,57 @@ +insertGetId([ + 'firstname' => 'Admin', + 'lastname' => 'User', + 'cellphone' => '9999999999', + 'email' => 'assign@example.com', + 'address_street' => '123 Street', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'password' => bcrypt('password'), + 'user_type' => 'primary', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + 'status' => 'Active', + ]); + + $roleId = DB::table('roles')->insertGetId([ + 'name' => 'admin', + 'slug' => 'admin', + 'description' => 'Admin', + 'dashboard_route' => 'administrator/administratordashboard', + 'priority' => 1, + 'is_active' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $service = new RoleAssignmentService(new RoleStaffService()); + $result = $service->assignRoles($userId, [$roleId], $userId); + + $this->assertTrue($result['ok']); + $this->assertDatabaseHas('user_roles', [ + 'user_id' => $userId, + 'role_id' => $roleId, + ]); + $this->assertDatabaseHas('staff', [ + 'user_id' => $userId, + ]); + } +} diff --git a/tests/Unit/Services/Roles/RoleCrudServiceTest.php b/tests/Unit/Services/Roles/RoleCrudServiceTest.php new file mode 100644 index 00000000..ad2db6da --- /dev/null +++ b/tests/Unit/Services/Roles/RoleCrudServiceTest.php @@ -0,0 +1,32 @@ +create([ + 'name' => 'admin', + 'slug' => 'admin', + 'description' => 'Admin', + 'dashboard_route' => 'administrator/administratordashboard', + 'priority' => 1, + 'is_active' => 1, + ]); + + $this->assertInstanceOf(Role::class, $role); + $this->assertDatabaseHas('roles', ['name' => 'admin']); + + $service->update($role, ['description' => 'updated']); + $this->assertDatabaseHas('roles', ['id' => $role->id, 'description' => 'updated']); + } +} diff --git a/tests/Unit/Services/Roles/RoleDashboardServiceTest.php b/tests/Unit/Services/Roles/RoleDashboardServiceTest.php new file mode 100644 index 00000000..95933bf9 --- /dev/null +++ b/tests/Unit/Services/Roles/RoleDashboardServiceTest.php @@ -0,0 +1,32 @@ +insert([ + 'name' => 'admin', + 'slug' => 'admin', + 'description' => 'Admin', + 'dashboard_route' => 'administrator/administratordashboard', + 'priority' => 1, + 'is_active' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $service = new RoleDashboardService(); + $route = $service->bestDashboardRouteFor(['admin']); + + $this->assertSame('administrator/administratordashboard', $route); + } +} diff --git a/tests/Unit/Services/Roles/RolePermissionServiceTest.php b/tests/Unit/Services/Roles/RolePermissionServiceTest.php new file mode 100644 index 00000000..0be8754a --- /dev/null +++ b/tests/Unit/Services/Roles/RolePermissionServiceTest.php @@ -0,0 +1,45 @@ +insertGetId([ + 'name' => 'admin', + 'slug' => 'admin', + 'description' => 'Admin', + 'dashboard_route' => 'administrator/administratordashboard', + 'priority' => 1, + 'is_active' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + $permissionId = DB::table('permissions')->insertGetId([ + 'name' => 'manage_roles', + 'description' => 'Manage roles', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $service = new RolePermissionService(); + $service->saveRolePermissions($roleId, [ + $permissionId => ['create' => true, 'read' => true], + ]); + + $this->assertDatabaseHas('role_permissions', [ + 'role_id' => $roleId, + 'permission_id' => $permissionId, + 'can_create' => 1, + 'can_read' => 1, + ]); + } +} diff --git a/tests/Unit/Services/Roles/RoleQueryServiceTest.php b/tests/Unit/Services/Roles/RoleQueryServiceTest.php new file mode 100644 index 00000000..299387a6 --- /dev/null +++ b/tests/Unit/Services/Roles/RoleQueryServiceTest.php @@ -0,0 +1,74 @@ +insert([ + 'name' => 'admin', + 'slug' => 'admin', + 'description' => 'Admin', + 'dashboard_route' => 'administrator/administratordashboard', + 'priority' => 1, + 'is_active' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $service = new RoleQueryService(); + $page = $service->listRoles([]); + + $this->assertCount(1, $page->items()); + } + + public function test_list_users_with_roles_returns_roles(): void + { + $userId = DB::table('users')->insertGetId([ + 'firstname' => 'Admin', + 'lastname' => 'User', + 'cellphone' => '9999999999', + 'email' => 'role.query@example.com', + 'address_street' => '123 Street', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'password' => bcrypt('password'), + 'user_type' => 'primary', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + 'status' => 'Active', + ]); + + $roleId = DB::table('roles')->insertGetId([ + 'name' => 'admin', + 'slug' => 'admin', + 'description' => 'Admin', + 'dashboard_route' => 'administrator/administratordashboard', + 'priority' => 1, + 'is_active' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $userId, + 'role_id' => $roleId, + ]); + + $service = new RoleQueryService(); + $page = $service->listUsersWithRoles([]); + + $this->assertCount(1, $page->items()); + $this->assertStringContainsString('admin', $page->items()[0]->roles); + } +} diff --git a/tests/Unit/Services/Roles/RoleSwitchServiceTest.php b/tests/Unit/Services/Roles/RoleSwitchServiceTest.php new file mode 100644 index 00000000..09632972 --- /dev/null +++ b/tests/Unit/Services/Roles/RoleSwitchServiceTest.php @@ -0,0 +1,55 @@ +insertGetId([ + 'firstname' => 'Admin', + 'lastname' => 'User', + 'cellphone' => '9999999999', + 'email' => 'switch.unit@example.com', + 'address_street' => '123 Street', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'password' => bcrypt('password'), + 'user_type' => 'primary', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + 'status' => 'Active', + ]); + + $roleId = DB::table('roles')->insertGetId([ + 'name' => 'admin', + 'slug' => 'admin', + 'description' => 'Admin', + 'dashboard_route' => 'administrator/administratordashboard', + 'priority' => 1, + 'is_active' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $userId, + 'role_id' => $roleId, + ]); + + $service = new RoleSwitchService(new RoleDashboardService()); + $roles = $service->getUserRoleNames($userId); + + $this->assertSame(['admin'], $roles); + } +}