perf(ux): request-cached permission checks (dedupe RPC fan-out)

New React cache()-wrapped hasPermission(uid,sid,perm): identical checks
resolve once per server render, so the (app) layout's nav-permission
resolution and a page's checks for the same perm (door.manage,
forms.manage) no longer double-round-trip user_has_permission. Pure
dedupe -- semantics unchanged, permission logic stays in SQL/RLS.
Adopted in the layout + members page.
This commit is contained in:
Moheeb Zara
2026-05-17 01:36:54 -07:00
parent ca498d5163
commit b8440e9c9e
3 changed files with 27 additions and 19 deletions
+3 -4
View File
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { AppSidebar } from '@/components/app-sidebar'
import { CommandPalette } from '@/components/command-palette'
import { hasPermission } from '@/lib/permissions-cache'
import { ConfirmProvider } from '@/components/ui/confirm'
import { getRoleLabelMap } from '@/lib/role-labels'
@@ -69,11 +70,9 @@ export default async function AppLayout({ children }: { children: React.ReactNod
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.map(perm => hasPermission(user.id, member.space_id, perm)),
)
NAV_PERM_CODES.forEach((perm, i) => { navPerms[perm] = !!results[i].data })
NAV_PERM_CODES.forEach((perm, i) => { navPerms[perm] = results[i] })
}
return (
+8 -15
View File
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { PageHeader, PageTitle } from '@/components/ui/page-title'
import { hasPermission } from '@/lib/permissions-cache'
import { MembersClient } from './members-client'
import { InvitesPanel } from '../customize/panels/invites-panel'
@@ -90,21 +91,13 @@ export default async function MembersPage() {
// certifications.grant (the Instructor capability) is independent of
// admin/board, so check it explicitly to decide whether to show the
// per-member certifications panel.
const { data: canGrantCerts } = await supabase.rpc('user_has_permission', {
uid: user.id,
sid: self.space_id,
perm: 'certifications.grant',
})
const { data: canManageCards } = await supabase.rpc('user_has_permission', {
uid: user.id,
sid: self.space_id,
perm: 'door.manage',
})
const { data: canViewForms } = await supabase.rpc('user_has_permission', {
uid: user.id,
sid: self.space_id,
perm: 'forms.manage',
})
// Request-cached: door.manage / forms.manage here dedupe with the layout's
// nav-permission resolution for the same render.
const [canGrantCerts, canManageCards, canViewForms] = await Promise.all([
hasPermission(user.id, self.space_id, 'certifications.grant'),
hasPermission(user.id, self.space_id, 'door.manage'),
hasPermission(user.id, self.space_id, 'forms.manage'),
])
return (
<MembersClient