role = model(Role::class); $this->permission = model(Permission::class); $this->rolePermission = model(RolePermission::class); $this->user = model(User::class); $this->userRole = model(UserRole::class); $this->staff = model(Staff::class); $this->config = model(Configuration::class); $this->schoolYear = (string) ($this->config->getConfig('school_year') ?? ''); $this->semester = (string) ($this->config->getConfig('semester') ?? ''); } /** * GET /api/v1/rolepermission/roles * List all roles with user counts */ public function roles() { $rows = DB::table('roles') ->select('roles.id', 'roles.name', 'roles.description', DB::raw('COUNT(DISTINCT user_roles.user_id) AS user_count')) ->leftJoin('user_roles', 'user_roles.role_id', '=', 'roles.id') ->groupBy('roles.id', 'roles.name', 'roles.description') ->orderBy('roles.name', 'ASC') ->get() ->toArray(); $normalized = array_map(static function ($row) { $row = (array) $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->success(['roles' => $normalized], 'Roles retrieved successfully'); } /** * GET /api/v1/rolepermission/roles/{id} * Get a single role */ public function showRole($id = null) { $role = $this->role->find($id); if (!$role) { return $this->error('Role not found', Response::HTTP_NOT_FOUND); } return $this->success($role, 'Role retrieved successfully'); } /** * POST /api/v1/rolepermission/roles * Create a new role */ public function storeRole() { $payload = $this->payloadData(); if (empty($payload)) { return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST); } $rules = [ 'name' => 'required|min:3|max:255|unique:roles,name', 'slug' => 'nullable|max:64|unique:roles,slug', 'description' => 'nullable|max:500', 'dashboard_route' => ['required', 'min:1', 'max:255', 'regex:/^[a-z0-9_\/\-]+$/'], 'priority' => 'required|integer|min:0|max:100000', 'is_active' => 'required|in:0,1', ]; $errors = $this->validateRequest($payload, $rules); if (!empty($errors)) { return $this->respondValidationError($errors); } $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 = (string) ($payload['is_active'] ?? '0') === '1' ? 1 : 0; $name = strtolower($rawName); $dashboard_route = strtolower($route); $description = strtolower($desc); // Build slug: from input if provided, else from name $slug = $rawSlug !== '' ? $rawSlug : $name; $slug = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $slug)); $slug = trim($slug, '_'); $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(), ]; try { $role = $this->role->create($data); return $this->success($role->toArray(), 'Role created successfully', Response::HTTP_CREATED); } catch (\Throwable $e) { log_message('error', 'Role creation error: ' . $e->getMessage()); return $this->respondError('Failed to create role', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * PATCH /api/v1/rolepermission/roles/{id} * Update a role */ public function updateRole($id = null) { $role = $this->role->find($id); if (!$role) { return $this->error('Role not found', Response::HTTP_NOT_FOUND); } $payload = $this->payloadData(); if (empty($payload)) { return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST); } $rules = [ 'name' => ['required', 'min:3', 'max:255', Rule::unique('roles', 'name')->ignore($id)], 'slug' => ['nullable', 'max:64', Rule::unique('roles', 'slug')->ignore($id)], 'description' => 'nullable|max:500', 'dashboard_route' => ['required', 'min:1', 'max:255', 'regex:/^[a-z0-9_\/\-]+$/'], 'priority' => 'required|integer|min:0|max:100000', 'is_active' => 'required|in:0,1', ]; $errors = $this->validateRequest($payload, $rules); if (!empty($errors)) { return $this->respondValidationError($errors); } $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 = (string) ($payload['is_active'] ?? '0') === '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 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(), ]; try { $this->role->update($id, $data); $updatedRole = $this->role->find($id); return $this->success($updatedRole, 'Role updated successfully'); } catch (\Throwable $e) { log_message('error', 'Role update error: ' . $e->getMessage()); return $this->respondError('Failed to update role', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * DELETE /api/v1/rolepermission/roles/{id} * Delete a role */ public function deleteRole($id = null) { $role = $this->role->find($id); if (!$role) { return $this->error('Role not found', Response::HTTP_NOT_FOUND); } try { $this->role->delete($id); return $this->success(null, 'Role deleted successfully'); } catch (\Throwable $e) { log_message('error', 'Role deletion error: ' . $e->getMessage()); return $this->respondError('Failed to delete role', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/rolepermission/permissions * List all permissions */ public function permissions() { $rows = $this->permission ->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->success(['permissions' => $normalized], 'Permissions retrieved successfully'); } /** * POST /api/v1/rolepermission/permissions * Create a new permission */ public function storePermission() { $payload = $this->payloadData(); if (empty($payload)) { return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST); } $name = trim((string) ($payload['name'] ?? '')); if ($name === '') { return $this->respondError('Permission name is required', Response::HTTP_BAD_REQUEST); } // Check existence $existing = $this->permission->where('name', $name)->first(); if ($existing) { return $this->success($existing, 'Permission already exists. No changes made.'); } $data = [ 'name' => $name, 'description' => trim((string) ($payload['description'] ?? '')), ]; try { $permission = $this->permission->create($data); return $this->success($permission->toArray(), 'Permission created successfully', Response::HTTP_CREATED); } catch (\Throwable $e) { log_message('error', 'Permission creation error: ' . $e->getMessage()); return $this->respondError('Failed to create permission', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * PATCH /api/v1/rolepermission/permissions/{id} * Update a permission */ public function updatePermission($id = null) { $permission = $this->permission->find($id); if (!$permission) { return $this->error('Permission not found', Response::HTTP_NOT_FOUND); } $payload = $this->payloadData(); if (empty($payload)) { return $this->error('Invalid request data', Response::HTTP_BAD_REQUEST); } $data = [ 'id' => $id, 'name' => trim((string) ($payload['name'] ?? '')), 'description' => trim((string) ($payload['description'] ?? '')), ]; try { $this->permission->update($id, $data); $updatedPermission = $this->permission->find($id); return $this->success($updatedPermission, 'Permission updated successfully'); } catch (\Throwable $e) { log_message('error', 'Permission update error: ' . $e->getMessage()); return $this->respondError('Failed to update permission', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * DELETE /api/v1/rolepermission/permissions/{id} * Delete a permission */ public function deletePermission($id = null) { $permission = $this->permission->find($id); if (!$permission) { return $this->error('Permission not found', Response::HTTP_NOT_FOUND); } try { $this->permission->delete($id); return $this->success(null, 'Permission deleted successfully'); } catch (\Throwable $e) { log_message('error', 'Permission deletion error: ' . $e->getMessage()); return $this->respondError('Failed to delete permission', Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/rolepermission/roles/{roleId}/permissions * Get permissions for a specific role */ public function rolePermissions($roleId = null) { $role = $this->role->find($roleId); if (!$role) { return $this->error('Role not found', Response::HTTP_NOT_FOUND); } $permissions = $this->rolePermission ->where('role_id', $roleId) ->findAll(); // Get all permissions with role permission data $allPermissions = $this->permission->findAll(); $existingPermissions = []; foreach ($permissions 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'], ]; } return $this->success([ 'role' => $role, 'permissions' => $allPermissions, 'role_permissions' => $existingPermissions, ], 'Role permissions retrieved successfully'); } /** * POST /api/v1/rolepermission/roles/{roleId}/permissions * Save role permissions */ public function saveRolePermissions($roleId = null) { $role = $this->role->find($roleId); if (!$role) { return $this->error('Role not found', Response::HTTP_NOT_FOUND); } $payload = $this->payloadData(); $permissions = $payload['permissions'] ?? null; if (!$permissions || !is_array($permissions)) { return $this->respondError('Permissions data is required', Response::HTTP_BAD_REQUEST); } // Fetch existing permissions for the role $rolePermissions = $this->rolePermission->where('role_id', $roleId)->findAll(); $existingPermissions = []; foreach ($rolePermissions as $rolePermission) { $existingPermissions[$rolePermission['permission_id']] = $rolePermission; } try { DB::beginTransaction(); // Process the new permissions foreach ($permissions as $permissionId => $crudValues) { $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->rolePermission->where('role_id', $roleId) ->where('permission_id', $permissionId) ->update($permissionData); } else { // Insert new permission $permissionData['role_id'] = $roleId; $permissionData['permission_id'] = $permissionId; $permissionData['created_at'] = utc_now(); $this->rolePermission->insert($permissionData); } } // Remove permissions that are no longer present foreach ($existingPermissions as $permissionId => $permissionData) { if (!isset($permissions[$permissionId])) { $this->rolePermission->where('role_id', $roleId) ->where('permission_id', $permissionId) ->delete(); } } DB::commit(); return $this->success(null, 'Permissions saved successfully'); } catch (\Throwable $e) { DB::rollBack(); log_message('error', 'Failed to save permissions: ' . $e->getMessage()); return $this->respondError('Failed to save permissions: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/rolepermission/users * Get users with their roles */ public function users() { $users = $this->buildUsersWithRolesPayload(); return $this->success($users, 'Users with roles retrieved successfully'); } /** * POST /api/v1/rolepermission/users/{userId}/roles * Assign roles to a user */ public function assignRole($userId = null) { $user = $this->user->find($userId); if (!$user) { return $this->error('User not found', Response::HTTP_NOT_FOUND); } $payload = $this->payloadData(); $roleIdsRaw = $payload['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 = $this->getCurrentUserId(); if (!$adminId) { return $this->respondError('Authentication required', Response::HTTP_UNAUTHORIZED); } try { DB::beginTransaction(); // Remove all existing roles for the user $this->userRole->where('user_id', $userId)->delete(); // Assign new roles if (!empty($roleIds)) { foreach ($roleIds as $roleId) { $this->userRole->insert([ 'user_id' => $userId, 'role_id' => $roleId, 'created_at' => utc_now(), 'updated_by' => $adminId ]); } } // Load role names after saving $assignedRoles = !empty($roleIds) ? $this->role->whereIn('id', $roleIds)->findAll() : []; $roleNames = array_map(fn($r) => strtolower($r['name']), $assignedRoles ?? []); // Update staff record if needed $this->updateStaffRecord($userId, $roleNames); DB::commit(); return $this->success(null, 'Roles updated successfully'); } catch (\Throwable $e) { DB::rollBack(); log_message('error', 'Error updating roles: ' . $e->getMessage()); return $this->respondError('Failed to update roles: ' . $e->getMessage(), Response::HTTP_INTERNAL_SERVER_ERROR); } } /** * GET /api/v1/rolepermission/admins * Get admin users */ public function admins() { $admins = $this->user->newQuery() ->select(['users.id', 'users.firstname', 'users.lastname', 'users.email']) ->join('user_roles', 'user_roles.user_id', '=', 'users.id') ->join('roles', 'roles.id', '=', 'user_roles.role_id') ->where('roles.name', 'admin') ->groupBy('users.id', 'users.firstname', 'users.lastname', 'users.email') ->get() ->toArray(); return $this->success($admins, 'Admin users retrieved successfully'); } /** * Helper: Build users with roles payload */ private function buildUsersWithRolesPayload(): array { $rows = DB::table('users') ->select('users.id', 'users.firstname', 'users.lastname', 'users.email', 'users.account_id', 'roles.name AS role_name') ->leftJoin('user_roles', 'users.id', '=', 'user_roles.user_id') ->leftJoin('roles', 'user_roles.role_id', '=', 'roles.id') ->orderBy('users.id', 'ASC') ->get() ->toArray(); $users = []; foreach ($rows as $row) { $row = (array) $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]; } /** * Helper: Update staff record based on roles */ private function updateStaffRecord(int $userId, array $newRoleNames): void { $excludedRoles = ['parent', 'student', 'guest']; $loweredNewRoles = array_map('strtolower', $newRoleNames); $user = $this->user->find($userId); if (!$user) { log_message('error', "updateStaffRecord: user_id {$userId} not found"); return; } // Fetch existing staff record if it exists $existingStaff = $this->staff->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'] ?? null, 'role_name' => implode(', ', $allRoles), 'active_role' => $activeRole, 'school_year' => $this->schoolYear, 'updated_at' => $now, ]; $this->staff->upsert($row); } /** * Helper: Generate unique staff email */ 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->staff->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; } /** * Helper: Ensure slug is unique */ private function ensureUniqueSlug(string $baseSlug, ?int $ignoreId = null): string { $slug = $baseSlug; $i = 2; while (true) { $builder = $this->role->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; } }