From 2ad7dc11700670e22eb4c72d79993520c2f9bca0 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 6 Sep 2026 17:11:03 -0400 Subject: [PATCH] fix roles access issue --- ...06-000100_GrantRequestedRolePageAccess.php | 364 ++++++++++++++++++ app/Filters/AuthFilter.php | 64 +++ app/Models/RoleModel.php | 12 +- 3 files changed, 438 insertions(+), 2 deletions(-) create mode 100644 app/Database/Migrations/2026-09-06-000100_GrantRequestedRolePageAccess.php diff --git a/app/Database/Migrations/2026-09-06-000100_GrantRequestedRolePageAccess.php b/app/Database/Migrations/2026-09-06-000100_GrantRequestedRolePageAccess.php new file mode 100644 index 0000000..75b0250 --- /dev/null +++ b/app/Database/Migrations/2026-09-06-000100_GrantRequestedRolePageAccess.php @@ -0,0 +1,364 @@ +grantNavAccess(); + $this->grantNamedPermissions(); + cache()->clean(); + } + + public function down(): void + { + } + + private function grantNavAccess(): void + { + if (! $this->db->tableExists('nav_items') || ! $this->db->tableExists('role_nav_items')) { + return; + } + + $this->grantNavItemsToRoles( + ['head of csm', 'head_of_department_communication', 'csm_contributor'], + [ + ['label' => 'Communication'], + ['url' => 'admin/enrollment/new-students'], + ['url' => 'whatsapp/'], + ['url' => 'admin/print-requests'], + ['url' => '/administrator/absence'], + ] + ); + + $financialNavItems = array_merge( + [['label' => 'Financial']], + $this->findChildNavSpecsForParentLabel('Financial'), + [ + ['label' => 'Event Management'], + ['url' => 'administrator/events'], + ['url' => 'admin/print-requests'], + ['url' => '/administrator/absence'], + ] + ); + + $this->grantNavItemsToRoles( + ['head of fa', 'head_of_fa', 'head of department (finance)', 'head of department finance'], + $financialNavItems + ); + + $this->grantNavItemsToRoles( + ['financial_contributor'], + [ + ['label' => 'Financial'], + ['url' => 'payment/manual_pay'], + ['url' => '/payment/manual'], + ['url' => 'admin/print-requests'], + ['url' => '/administrator/absence'], + ] + ); + } + + private function grantNamedPermissions(): void + { + if ( + ! $this->db->tableExists('roles') + || ! $this->db->tableExists('permissions') + || ! $this->db->tableExists('role_permissions') + ) { + return; + } + + $this->grantPermissionsToRoles( + ['head of csm', 'head_of_department_communication', 'csm_contributor'], + [ + 'view_new_students' => ['read' => true], + ] + ); + + $this->grantPermissionsToRoles( + ['head of fa', 'head_of_fa', 'head of department (finance)', 'head of department finance'], + [ + 'view_invoice' => ['read' => true], + 'view_payment' => ['read' => true], + 'view_financial_reports' => ['create' => true, 'read' => true, 'update' => true], + 'create_invoice' => ['create' => true, 'read' => true], + 'update_invoice' => ['read' => true, 'update' => true], + 'create_payment' => ['create' => true, 'read' => true], + 'update_payment' => ['read' => true, 'update' => true], + 'oversee_financial_aid' => ['create' => true, 'read' => true, 'update' => true], + ] + ); + + $this->grantPermissionsToRoles( + ['financial_contributor'], + [ + 'view_invoice' => ['read' => true], + 'view_payment' => ['read' => true], + 'create_payment' => ['create' => true, 'read' => true], + 'update_payment' => ['read' => true, 'update' => true], + ] + ); + } + + /** + * @param list $roleKeys + * @param list $navSpecs + */ + private function grantNavItemsToRoles(array $roleKeys, array $navSpecs): void + { + $navIds = $this->resolveNavItemIds($navSpecs); + if ($navIds === []) { + return; + } + + $now = date('Y-m-d H:i:s'); + + if ($this->db->fieldExists('role_id', 'role_nav_items') && $this->db->tableExists('roles')) { + foreach ($this->resolveRoleIds($roleKeys) as $roleId) { + foreach ($navIds as $navId) { + $exists = $this->db->table('role_nav_items') + ->where('role_id', $roleId) + ->where('nav_item_id', $navId) + ->countAllResults() > 0; + + if (! $exists) { + $this->db->table('role_nav_items')->insert([ + 'role_id' => $roleId, + 'nav_item_id' => $navId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } + } + + if ($this->db->fieldExists('role', 'role_nav_items')) { + foreach ($roleKeys as $role) { + foreach ($navIds as $navId) { + $exists = $this->db->table('role_nav_items') + ->where('LOWER(role)', strtolower($role)) + ->where('nav_item_id', $navId) + ->countAllResults() > 0; + + if (! $exists) { + $insert = [ + 'role' => strtolower($role), + 'nav_item_id' => $navId, + 'created_at' => $now, + ]; + if ($this->db->fieldExists('updated_at', 'role_nav_items')) { + $insert['updated_at'] = $now; + } + $this->db->table('role_nav_items')->insert($insert); + } + } + } + } + } + + /** + * @param list $roleKeys + * @param array $permissions + */ + private function grantPermissionsToRoles(array $roleKeys, array $permissions): void + { + $roleIds = $this->resolveRoleIds($roleKeys); + if ($roleIds === []) { + return; + } + + $now = date('Y-m-d H:i:s'); + + foreach ($permissions as $permissionName => $flags) { + $permissionId = $this->resolvePermissionId($permissionName); + if ($permissionId <= 0) { + continue; + } + + foreach ($roleIds as $roleId) { + $existing = $this->db->table('role_permissions') + ->where('role_id', $roleId) + ->where('permission_id', $permissionId) + ->get() + ->getRowArray(); + + $grant = [ + 'can_create' => ! empty($flags['create']) ? 1 : 0, + 'can_read' => ! empty($flags['read']) ? 1 : 0, + 'can_update' => ! empty($flags['update']) ? 1 : 0, + 'can_delete' => ! empty($flags['delete']) ? 1 : 0, + 'updated_at' => $now, + ]; + + if ($this->db->fieldExists('can_manage', 'role_permissions')) { + $grant['can_manage'] = 0; + } + + if ($existing === null) { + $grant['role_id'] = $roleId; + $grant['permission_id'] = $permissionId; + $grant['created_at'] = $now; + $this->db->table('role_permissions')->insert($grant); + continue; + } + + $this->db->table('role_permissions') + ->where('id', (int) $existing['id']) + ->update([ + 'can_create' => max((int) ($existing['can_create'] ?? 0), $grant['can_create']), + 'can_read' => max((int) ($existing['can_read'] ?? 0), $grant['can_read']), + 'can_update' => max((int) ($existing['can_update'] ?? 0), $grant['can_update']), + 'can_delete' => max((int) ($existing['can_delete'] ?? 0), $grant['can_delete']), + 'updated_at' => $now, + ]); + } + } + } + + /** + * @param list $roleKeys + * @return list + */ + private function resolveRoleIds(array $roleKeys): array + { + if (! $this->db->tableExists('roles')) { + return []; + } + + $normalized = array_values(array_unique(array_map('strtolower', $roleKeys))); + if ($normalized === []) { + return []; + } + + $builder = $this->db->table('roles')->select('id'); + $builder->groupStart()->whereIn('LOWER(name)', $normalized); + if ($this->db->fieldExists('slug', 'roles')) { + $builder->orWhereIn('LOWER(slug)', $normalized); + } + $rows = $builder->groupEnd()->get()->getResultArray(); + + return array_values(array_unique(array_map(static fn (array $row): int => (int) $row['id'], $rows))); + } + + private function resolvePermissionId(string $permissionName): int + { + $permission = $this->db->table('permissions') + ->select('id') + ->where('LOWER(name)', strtolower($permissionName)) + ->get() + ->getRowArray(); + + if ($permission !== null) { + return (int) $permission['id']; + } + + $now = date('Y-m-d H:i:s'); + $insert = [ + 'name' => $permissionName, + 'created_at' => $now, + 'updated_at' => $now, + ]; + if ($this->db->fieldExists('description', 'permissions')) { + $insert['description'] = 'Seeded route permission.'; + } + + $this->db->table('permissions')->insert($insert); + + return (int) $this->db->insertID(); + } + + /** + * @param list $navSpecs + * @return list + */ + private function resolveNavItemIds(array $navSpecs): array + { + $ids = []; + foreach ($navSpecs as $spec) { + if (isset($spec['url'])) { + $url = $this->normalizePath($spec['url']); + $rows = $this->db->table('nav_items') + ->select('id, url') + ->where('url IS NOT NULL', null, false) + ->get() + ->getResultArray(); + + foreach ($rows as $row) { + if ($this->normalizePath((string) ($row['url'] ?? '')) === $url) { + $ids[] = (int) $row['id']; + } + } + + continue; + } elseif (isset($spec['label'])) { + $builder = $this->db->table('nav_items')->select('id'); + $builder->where('LOWER(label)', strtolower($spec['label'])); + } else { + continue; + } + + foreach ($builder->get()->getResultArray() as $row) { + $ids[] = (int) $row['id']; + } + } + + return array_values(array_unique(array_filter($ids))); + } + + /** + * @return list + */ + private function findChildNavSpecsForParentLabel(string $parentLabel): array + { + $parentColumn = $this->parentColumn(); + if ($parentColumn === null) { + return []; + } + + $parents = $this->db->table('nav_items') + ->select('id') + ->where('LOWER(label)', strtolower($parentLabel)) + ->get() + ->getResultArray(); + + $parentIds = array_values(array_filter(array_map(static fn (array $row): int => (int) $row['id'], $parents))); + if ($parentIds === []) { + return []; + } + + $children = $this->db->table('nav_items') + ->select('url') + ->whereIn($parentColumn, $parentIds) + ->where('url IS NOT NULL', null, false) + ->get() + ->getResultArray(); + + return array_values(array_filter(array_map( + static fn (array $row): array => ['url' => (string) $row['url']], + $children + ), static fn (array $spec): bool => trim($spec['url']) !== '')); + } + + private function parentColumn(): ?string + { + if ($this->db->fieldExists('menu_parent_id', 'nav_items')) { + return 'menu_parent_id'; + } + + if ($this->db->fieldExists('parent_id', 'nav_items')) { + return 'parent_id'; + } + + return null; + } + + private function normalizePath(string $path): string + { + return trim(preg_replace('#/+#', '/', $path), '/'); + } +} diff --git a/app/Filters/AuthFilter.php b/app/Filters/AuthFilter.php index 50dcfd5..6b941fd 100644 --- a/app/Filters/AuthFilter.php +++ b/app/Filters/AuthFilter.php @@ -81,6 +81,10 @@ class AuthFilter implements FilterInterface return; } + if ($this->isAllowedByGrantedNavItem($request, $roleIds)) { + return; + } + return $this->deny($request, "You don't have permission to use this feature."); } @@ -271,4 +275,64 @@ class AuthFilter implements FilterInterface return true; } + + private function isAllowedByGrantedNavItem(RequestInterface $request, array $roleIds): bool + { + if ( + empty($roleIds) + || ! $this->db->tableExists('role_nav_items') + || ! $this->db->tableExists('nav_items') + || ! $this->db->fieldExists('role_id', 'role_nav_items') + ) { + return false; + } + + $path = $this->normalizePath($request->getUri()->getPath()); + if ($path === '') { + return false; + } + + $rows = $this->db->table('role_nav_items rni') + ->select('ni.url') + ->join('nav_items ni', 'ni.id = rni.nav_item_id') + ->whereIn('rni.role_id', $roleIds) + ->where('ni.url IS NOT NULL', null, false) + ->where('ni.is_enabled', 1) + ->get() + ->getResultArray(); + + foreach ($rows as $row) { + $url = $this->normalizePath((string) ($row['url'] ?? '')); + if ($url === '') { + continue; + } + + if ($path === $url || str_starts_with($path . '/', rtrim($url, '/') . '/')) { + return true; + } + + if (str_starts_with($url, 'admin/')) { + $withoutAdminPrefix = substr($url, 6); + if ($path === $withoutAdminPrefix || str_starts_with($path . '/', rtrim($withoutAdminPrefix, '/') . '/')) { + return true; + } + } + } + + return false; + } + + private function normalizePath(string $path): string + { + $path = trim(preg_replace('#/+#', '/', $path), '/'); + if ($path === 'index.php') { + return ''; + } + + if (str_starts_with($path, 'index.php/')) { + return substr($path, 10); + } + + return $path; + } } diff --git a/app/Models/RoleModel.php b/app/Models/RoleModel.php index 0efd2c4..22a1117 100644 --- a/app/Models/RoleModel.php +++ b/app/Models/RoleModel.php @@ -63,8 +63,16 @@ class RoleModel extends Model $names = array_values(array_filter(array_map('strval', $names))); if (empty($names)) return []; - // collation is usually case-insensitive; if not, add LOWER() both sides - $ids = $this->select('id')->whereIn('name', $names)->findColumn('id'); + $lower = array_values(array_unique(array_map('strtolower', $names))); + $builder = $this->select('id') + ->groupStart() + ->whereIn('LOWER(name)', $lower); + + if ($this->db->fieldExists('slug', $this->table)) { + $builder->orWhereIn('LOWER(slug)', $lower); + } + + $ids = $builder->groupEnd()->findColumn('id'); return array_map('intval', $ids ?? []); } }