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
+22 -10
View File
@@ -107,13 +107,23 @@ interface AppSidebarProps {
taskBadge?: number
commsBadge?: number
paymentBadge?: number
navPerms?: Record<string, boolean>
}
export function AppSidebar({ member, roleName, taskBadge = 0, commsBadge = 0, paymentBadge = 0 }: AppSidebarProps) {
export function AppSidebar({ member, roleName, taskBadge = 0, commsBadge = 0, paymentBadge = 0, navPerms = {} }: AppSidebarProps) {
const pathname = usePathname()
const space = member.spaces
const initials = (member.display_name || 'U').split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
const isAdmin = member.role === 'admin' || member.role === 'board'
// Feature-management links: admin/board see all; a member with a delegated
// *.manage permission sees just that one (matches the page guards).
const can = (perm: string) => isAdmin || !!navPerms[perm]
const canForms = can('forms.manage')
const canCerts = can('certifications.manage')
const canClasses = can('classes.manage')
const canEquip = can('equipment.manage')
const canDoor = can('door.manage')
const showAdminSection = isAdmin || canForms || canCerts || canClasses || canEquip || canDoor
const [drawerOpen, setDrawerOpen] = useState(false)
const menuButtonRef = useRef<HTMLButtonElement>(null)
const drawerCloseRef = useRef<HTMLButtonElement>(null)
@@ -174,6 +184,8 @@ export function AppSidebar({ member, roleName, taskBadge = 0, commsBadge = 0, pa
</NavSection>
<NavSection id="learn" title="Access & Resources">
{/* Classes/Equipment use exact-match (not isActive) so /classes/manage
and /equipment/manage highlight only the Admin link, not both. */}
<NavLink href="/classes" label="Classes" icon={GraduationCap} active={pathname === '/classes' || pathname.startsWith('/classes?')} onClick={onNav} />
<NavLink href="/equipment" label="Equipment" icon={Hammer} active={pathname === '/equipment' || pathname.startsWith('/equipment?')} onClick={onNav} />
<NavLink href="/doors" label="Doors" icon={DoorClosed} active={isActive('/doors')} onClick={onNav} />
@@ -199,16 +211,16 @@ export function AppSidebar({ member, roleName, taskBadge = 0, commsBadge = 0, pa
<NavLink href="/me" label="My access" icon={BadgeCheck} active={isActive('/me')} onClick={onNav} />
</NavSection>
{isAdmin && (
{showAdminSection && (
<NavSection id="admin" title="Admin">
<NavLink href="/customize" label="Customize" icon={SlidersHorizontal} active={isActive('/customize')} onClick={onNav} />
<NavLink href="/forms" label="Forms & waivers" icon={ClipboardList} active={isActive('/forms')} onClick={onNav} />
<NavLink href="/certifications" label="Certifications" icon={Award} active={isActive('/certifications')} onClick={onNav} />
<NavLink href="/classes/manage" label="Manage classes" icon={GraduationCap} active={isActive('/classes/manage')} onClick={onNav} />
<NavLink href="/equipment/manage" label="Manage equipment" icon={Hammer} active={isActive('/equipment/manage')} onClick={onNav} />
<NavLink href="/door/manage" label="Door access" icon={DoorClosed} active={isActive('/door/manage')} onClick={onNav} />
<NavLink href="/import" label="Import / Sync" icon={Download} active={isActive('/import')} onClick={onNav} />
<NavLink href="/settings" label="Settings" icon={Settings2} active={isActive('/settings')} onClick={onNav} />
{isAdmin && <NavLink href="/customize" label="Customize" icon={SlidersHorizontal} active={isActive('/customize')} onClick={onNav} />}
{canForms && <NavLink href="/forms" label="Forms & waivers" icon={ClipboardList} active={isActive('/forms')} onClick={onNav} />}
{canCerts && <NavLink href="/certifications" label="Certifications" icon={Award} active={isActive('/certifications')} onClick={onNav} />}
{canClasses && <NavLink href="/classes/manage" label="Manage classes" icon={GraduationCap} active={isActive('/classes/manage')} onClick={onNav} />}
{canEquip && <NavLink href="/equipment/manage" label="Manage equipment" icon={Hammer} active={isActive('/equipment/manage')} onClick={onNav} />}
{canDoor && <NavLink href="/door/manage" label="Door access" icon={DoorClosed} active={isActive('/door/manage')} onClick={onNav} />}
{isAdmin && <NavLink href="/import" label="Import / Sync" icon={Download} active={isActive('/import')} onClick={onNav} />}
{isAdmin && <NavLink href="/settings" label="Settings" icon={Settings2} active={isActive('/settings')} onClick={onNav} />}
</NavSection>
)}
</nav>