From 485341875a67c18b0ed4c322af77fac50c8c11ac Mon Sep 17 00:00:00 2001 From: root Date: Sun, 6 Sep 2026 20:49:25 -0400 Subject: [PATCH] fix head fa access issue --- app/Controllers/View/FilesController.php | 2 +- app/Controllers/View/InvoiceController.php | 4 + app/Controllers/View/PaymentController.php | 2 +- app/Controllers/View/RefundController.php | 2 +- ...06-000100_GrantRequestedRolePageAccess.php | 9 +- ...000100_BackfillHeadFaNewStudentsAccess.php | 195 ++++++++++++++++++ app/Filters/AuthFilter.php | 72 +++++++ 7 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 app/Database/Migrations/2026-09-07-000100_BackfillHeadFaNewStudentsAccess.php diff --git a/app/Controllers/View/FilesController.php b/app/Controllers/View/FilesController.php index baee46b..7d47218 100644 --- a/app/Controllers/View/FilesController.php +++ b/app/Controllers/View/FilesController.php @@ -522,7 +522,7 @@ class FilesController extends Controller $roles[] = $activeRole; } - foreach (['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant'] as $role) { + foreach (['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant', 'head fa', 'head of fa', 'head_of_fa', 'financial_contributor'] as $role) { if (in_array($role, $roles, true)) { return true; } diff --git a/app/Controllers/View/InvoiceController.php b/app/Controllers/View/InvoiceController.php index 9b2ad10..744f3f9 100644 --- a/app/Controllers/View/InvoiceController.php +++ b/app/Controllers/View/InvoiceController.php @@ -2293,6 +2293,10 @@ private function getGradeLevel($grade): array 'administrative staff', 'principal', 'admin', + 'head fa', + 'head of fa', + 'head_of_fa', + 'financial_contributor', ]); if ($userId <= 0 || (! $isStaff && $userId !== $parentId)) { diff --git a/app/Controllers/View/PaymentController.php b/app/Controllers/View/PaymentController.php index c2df443..57c832f 100644 --- a/app/Controllers/View/PaymentController.php +++ b/app/Controllers/View/PaymentController.php @@ -1685,7 +1685,7 @@ class PaymentController extends ResourceController $roles[] = $activeRole; } - $staffRoles = ['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant']; + $staffRoles = ['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant', 'head fa', 'head of fa', 'head_of_fa', 'financial_contributor']; foreach ($staffRoles as $role) { if (in_array($role, $roles, true)) { return true; diff --git a/app/Controllers/View/RefundController.php b/app/Controllers/View/RefundController.php index a35a989..22f9ea8 100644 --- a/app/Controllers/View/RefundController.php +++ b/app/Controllers/View/RefundController.php @@ -1414,7 +1414,7 @@ class RefundController extends BaseController $roles[] = $activeRole; } - foreach (['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant'] as $role) { + foreach (['administrator', 'administrative staff', 'principal', 'teacher', 'teacher_assistant', 'head fa', 'head of fa', 'head_of_fa', 'financial_contributor'] as $role) { if (in_array($role, $roles, true)) { return true; } diff --git a/app/Database/Migrations/2026-09-06-000100_GrantRequestedRolePageAccess.php b/app/Database/Migrations/2026-09-06-000100_GrantRequestedRolePageAccess.php index 75b0250..a23154e 100644 --- a/app/Database/Migrations/2026-09-06-000100_GrantRequestedRolePageAccess.php +++ b/app/Database/Migrations/2026-09-06-000100_GrantRequestedRolePageAccess.php @@ -46,8 +46,10 @@ class GrantRequestedRolePageAccess extends Migration ); $this->grantNavItemsToRoles( - ['head of fa', 'head_of_fa', 'head of department (finance)', 'head of department finance'], - $financialNavItems + ['head fa', 'head of fa', 'head_of_fa', 'head of department (finance)', 'head of department finance'], + array_merge($financialNavItems, [ + ['url' => 'admin/enrollment/new-students'], + ]) ); $this->grantNavItemsToRoles( @@ -80,8 +82,9 @@ class GrantRequestedRolePageAccess extends Migration ); $this->grantPermissionsToRoles( - ['head of fa', 'head_of_fa', 'head of department (finance)', 'head of department finance'], + ['head fa', 'head of fa', 'head_of_fa', 'head of department (finance)', 'head of department finance'], [ + 'view_new_students' => ['read' => true], 'view_invoice' => ['read' => true], 'view_payment' => ['read' => true], 'view_financial_reports' => ['create' => true, 'read' => true, 'update' => true], diff --git a/app/Database/Migrations/2026-09-07-000100_BackfillHeadFaNewStudentsAccess.php b/app/Database/Migrations/2026-09-07-000100_BackfillHeadFaNewStudentsAccess.php new file mode 100644 index 0000000..9e7beef --- /dev/null +++ b/app/Database/Migrations/2026-09-07-000100_BackfillHeadFaNewStudentsAccess.php @@ -0,0 +1,195 @@ +resolveRoleIds([ + 'head fa', + 'head of fa', + 'head_of_fa', + 'head of department (finance)', + 'head of department finance', + ]); + + if ($roleIds === []) { + return; + } + + $this->grantPermission($roleIds, 'view_new_students', ['read' => true]); + $this->grantNavUrl($roleIds, 'admin/enrollment/new-students'); + cache()->clean(); + } + + public function down(): void + { + } + + /** + * @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))); + $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))); + } + + /** + * @param list $roleIds + * @param array{create?: bool, read?: bool, update?: bool, delete?: bool} $flags + */ + private function grantPermission(array $roleIds, string $permissionName, array $flags): void + { + if (! $this->db->tableExists('permissions') || ! $this->db->tableExists('role_permissions')) { + return; + } + + $permissionId = $this->resolvePermissionId($permissionName); + $now = date('Y-m-d H:i:s'); + + 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 ($existing === null) { + $grant['role_id'] = $roleId; + $grant['permission_id'] = $permissionId; + $grant['created_at'] = $now; + if ($this->db->fieldExists('can_manage', 'role_permissions')) { + $grant['can_manage'] = 0; + } + $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, + ]); + } + } + + 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 $roleIds + */ + private function grantNavUrl(array $roleIds, string $url): void + { + if ( + ! $this->db->tableExists('nav_items') + || ! $this->db->tableExists('role_nav_items') + || ! $this->db->fieldExists('role_id', 'role_nav_items') + ) { + return; + } + + $navIds = $this->resolveNavItemIdsByUrl($url); + if ($navIds === []) { + return; + } + + $now = date('Y-m-d H:i:s'); + foreach ($roleIds 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, + ]); + } + } + } + } + + /** + * @return list + */ + private function resolveNavItemIdsByUrl(string $url): array + { + $targetUrl = $this->normalizePath($url); + $rows = $this->db->table('nav_items') + ->select('id, url') + ->where('url IS NOT NULL', null, false) + ->get() + ->getResultArray(); + + $ids = []; + foreach ($rows as $row) { + if ($this->normalizePath((string) ($row['url'] ?? '')) === $targetUrl) { + $ids[] = (int) $row['id']; + } + } + + return array_values(array_unique(array_filter($ids))); + } + + private function normalizePath(string $path): string + { + return trim(preg_replace('#/+#', '/', $path), '/'); + } +} diff --git a/app/Filters/AuthFilter.php b/app/Filters/AuthFilter.php index 6b941fd..566f3e9 100644 --- a/app/Filters/AuthFilter.php +++ b/app/Filters/AuthFilter.php @@ -85,6 +85,14 @@ class AuthFilter implements FilterInterface return; } + if ($this->isAllowedFamilyCardRequest($request, $roleIds)) { + return; + } + + if ($this->isAllowedPrintRequestSupportRequest($request, $roleIds)) { + return; + } + return $this->deny($request, "You don't have permission to use this feature."); } @@ -322,6 +330,70 @@ class AuthFilter implements FilterInterface return false; } + private function isAllowedFamilyCardRequest(RequestInterface $request, array $roleIds): bool + { + if ($this->normalizePath($request->getUri()->getPath()) !== 'family/card') { + return false; + } + + foreach (['view_new_students', 'view_financial_reports'] as $permissionName) { + if ($this->userHasNamedPermission($roleIds, $permissionName, 'read')) { + return true; + } + } + + return false; + } + + private function isAllowedPrintRequestSupportRequest(RequestInterface $request, array $roleIds): bool + { + $path = $this->normalizePath($request->getUri()->getPath()); + $printRequestPaths = [ + 'print-requests/update/', + 'print-requests/delete/', + 'print-requests/file/', + 'uploads/print_requests/', + ]; + + foreach ($printRequestPaths as $prefix) { + if (str_starts_with($path, $prefix)) { + return $this->hasGrantedNavUrl($roleIds, 'admin/print-requests'); + } + } + + return false; + } + + private function hasGrantedNavUrl(array $roleIds, string $url): 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; + } + + $targetUrl = $this->normalizePath($url); + $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) { + if ($this->normalizePath((string) ($row['url'] ?? '')) === $targetUrl) { + return true; + } + } + + return false; + } + private function normalizePath(string $path): string { $path = trim(preg_replace('#/+#', '/', $path), '/');