fix(nav): gate Admin manage links by actual permission, not just role

Audit follow-up (UI P1). The page guards authorize feature management via
user_has_permission, but the Admin nav links were shown by role only:
a member with a delegated forms/certifications/classes/equipment/door
*.manage permission had no link (orphaned), and an admin lacking the
permission saw a link that redirected. The layout now resolves those five
permissions and the sidebar shows each link on (isAdmin || permission);
Customize/Import/Settings stay admin-only. Also: aria-label on the
forum delete button; comment the intentional Classes/Equipment
nav-active exact-match.
This commit is contained in:
Moheeb Zara
2026-05-16 22:27:26 -07:00
parent 3efa3cdb46
commit c4cba104d1
3 changed files with 43 additions and 10 deletions
+1
View File
@@ -65,6 +65,7 @@ export function ThreadActions({ threadId, pinned, locked, canModerate, isAuthor
onClick={handleDelete}
className="text-muted-foreground hover:text-red-500 transition p-1.5"
title="Delete"
aria-label="Delete thread"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
+20
View File
@@ -56,6 +56,25 @@ export default async function AppLayout({ children }: { children: React.ReactNod
const roleMap = await getRoleLabelMap(supabase, member.space_id)
const roleName = roleMap[member.role]?.name ?? member.role
// The Admin nav section's feature-management links were previously gated by
// role only, so a member granted a delegated *.manage permission had no nav
// link to it (and the page guards authorize by permission, not role). Resolve
// the actual permissions here so the nav matches what the guards allow.
const isAdminRole = member.role === 'admin' || member.role === 'board'
const NAV_PERM_CODES = [
'forms.manage', 'certifications.manage', 'classes.manage',
'equipment.manage', 'door.manage',
] as const
const navPerms: Record<string, boolean> = {}
if (!isAdminRole) {
const results = await Promise.all(
NAV_PERM_CODES.map(perm =>
supabase.rpc('user_has_permission', { uid: user.id, sid: member.space_id, perm }),
),
)
NAV_PERM_CODES.forEach((perm, i) => { navPerms[perm] = !!results[i].data })
}
return (
<ConfirmProvider>
<div className="flex h-screen bg-background overflow-hidden">
@@ -70,6 +89,7 @@ export default async function AppLayout({ children }: { children: React.ReactNod
roleName={roleName}
taskBadge={taskCount ?? 0}
paymentBadge={paymentCount ?? 0}
navPerms={navPerms}
/>
<main id="main-content" tabIndex={-1} className="flex-1 overflow-y-auto pt-[52px] md:pt-0 flex flex-col">
<div className="flex-1">{children}</div>