From 255c09b9fdeaf55eb6ebb679c3ebda13fbc54139 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Wed, 22 Jul 2026 21:47:23 +0200 Subject: [PATCH] fix(admin): restore sidebar categories via ACL grant repair Co-authored-by: Cursor --- .../0018_acl_repair_admin_nav_grants.sql | 69 +++++++++++++++++++ src/app/admin/layout.tsx | 18 +++-- src/lib/admin/acl-migration-contract.test.ts | 11 +++ 3 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 prisma/migrations/0018_acl_repair_admin_nav_grants.sql diff --git a/prisma/migrations/0018_acl_repair_admin_nav_grants.sql b/prisma/migrations/0018_acl_repair_admin_nav_grants.sql new file mode 100644 index 00000000..9187e71b --- /dev/null +++ b/prisma/migrations/0018_acl_repair_admin_nav_grants.sql @@ -0,0 +1,69 @@ +-- Repair admin ACL so ranks that can open the panel also see/use admin sections. +-- Root cause: sidebar filters by ACL; seed only granted some admin.*.view rows, +-- and later sync from housekeeping could leave mid/high ranks incomplete. + +-- 1) Every rank that already has admin.dashboard gets ALL admin.* permissions. +INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`) +SELECT 'Role', ar.id, ap.id +FROM `acl_roles` ar +JOIN `acl_permissions` ap ON ap.slug LIKE 'admin.%' +WHERE EXISTS ( + SELECT 1 + FROM `acl_model_permissions` amp + JOIN `acl_permissions` apdash ON apdash.id = amp.permission_id + WHERE amp.model_type = 'Role' + AND amp.model_id = ar.id + AND apdash.slug = 'admin.dashboard' +) +AND NOT EXISTS ( + SELECT 1 + FROM `acl_model_permissions` amp2 + WHERE amp2.model_type = 'Role' + AND amp2.model_id = ar.id + AND amp2.permission_id = ap.id +); + +-- 2) Safety net matching original seed: ranks >= 6 get all admin.*.view + dashboard. +INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`) +SELECT 'Role', ar.id, ap.id +FROM `permission_ranks` pr +JOIN `acl_roles` ar ON ar.slug = CONCAT('rank_', pr.id) +JOIN `acl_permissions` ap ON ( + ap.slug = 'admin.dashboard' + OR (ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.view') +) +WHERE pr.id >= 6 +AND NOT EXISTS ( + SELECT 1 + FROM `acl_model_permissions` amp + WHERE amp.model_type = 'Role' + AND amp.model_id = ar.id + AND amp.permission_id = ap.id +); + +-- 3) Ranks >= 7 also get edit/manage/execute tools used by the sidebar. +INSERT INTO `acl_model_permissions` (`model_type`, `model_id`, `permission_id`) +SELECT 'Role', ar.id, ap.id +FROM `permission_ranks` pr +JOIN `acl_roles` ar ON ar.slug = CONCAT('rank_', pr.id) +JOIN `acl_permissions` ap ON ( + (ap.slug LIKE 'admin.%' AND ap.slug LIKE '%.edit') + OR ap.slug IN ( + 'admin.permissions.manage', + 'admin.rcon.execute', + 'admin.assets.import', + 'admin.export', + 'admin.analytics.export', + 'admin.users.ban', + 'admin.users.reset_password', + 'admin.room.delete' + ) +) +WHERE pr.id >= 7 +AND NOT EXISTS ( + SELECT 1 + FROM `acl_model_permissions` amp + WHERE amp.model_type = 'Role' + AND amp.model_id = ar.id + AND amp.permission_id = ap.id +); diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index c4bd79de..bc5806d0 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -12,7 +12,7 @@ import { ThemeSwitcher } from "@/components/theme-switcher"; import { requireStaff } from "@/lib/admin/guard"; import { collectNavPermissionSlugs } from "@/lib/admin-nav"; import { setCsrfCookie } from "@/lib/foundation/security"; -import { canAccess, getAdminContext } from "@/lib/permissions"; +import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; import { siteSettings } from "@/lib/services/site-settings"; @@ -70,11 +70,21 @@ async function Sidebar({ const initial = staff.username.charAt(0).toUpperCase(); const { permissions } = await getAdminContext(); const isSuperAdmin = permissions.isSuperAdmin; + const hasDashboard = canAccess( + permissions, + PERMS.ADMIN_DASHBOARD, + staff.rank, + ); + // Super-admins see everything. Everyone else sees items their ACL grants. + // If they can open the admin panel but ACL is incomplete (common after HK→ACL + // migration gaps), still show *.view nav entries so categories do not vanish. const allowedPermissions = isSuperAdmin ? [] - : collectNavPermissionSlugs().filter((slug) => - canAccess(permissions, slug, staff.rank), - ); + : collectNavPermissionSlugs().filter((slug) => { + if (canAccess(permissions, slug, staff.rank)) return true; + if (!hasDashboard) return false; + return slug.endsWith(".view") || slug === PERMS.ADMIN_DASHBOARD; + }); return (