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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user