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:
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
|
|||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
import { AppSidebar } from '@/components/app-sidebar'
|
import { AppSidebar } from '@/components/app-sidebar'
|
||||||
import { CommandPalette } from '@/components/command-palette'
|
import { CommandPalette } from '@/components/command-palette'
|
||||||
|
import { hasPermission } from '@/lib/permissions-cache'
|
||||||
import { ConfirmProvider } from '@/components/ui/confirm'
|
import { ConfirmProvider } from '@/components/ui/confirm'
|
||||||
import { getRoleLabelMap } from '@/lib/role-labels'
|
import { getRoleLabelMap } from '@/lib/role-labels'
|
||||||
|
|
||||||
@@ -69,11 +70,9 @@ export default async function AppLayout({ children }: { children: React.ReactNod
|
|||||||
const navPerms: Record<string, boolean> = {}
|
const navPerms: Record<string, boolean> = {}
|
||||||
if (!isAdminRole) {
|
if (!isAdminRole) {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
NAV_PERM_CODES.map(perm =>
|
NAV_PERM_CODES.map(perm => hasPermission(user.id, member.space_id, 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 })
|
NAV_PERM_CODES.forEach((perm, i) => { navPerms[perm] = results[i] })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createClient } from '@/lib/supabase/server'
|
import { createClient } from '@/lib/supabase/server'
|
||||||
import { PageHeader, PageTitle } from '@/components/ui/page-title'
|
import { PageHeader, PageTitle } from '@/components/ui/page-title'
|
||||||
|
import { hasPermission } from '@/lib/permissions-cache'
|
||||||
import { MembersClient } from './members-client'
|
import { MembersClient } from './members-client'
|
||||||
import { InvitesPanel } from '../customize/panels/invites-panel'
|
import { InvitesPanel } from '../customize/panels/invites-panel'
|
||||||
|
|
||||||
@@ -90,21 +91,13 @@ export default async function MembersPage() {
|
|||||||
// certifications.grant (the Instructor capability) is independent of
|
// certifications.grant (the Instructor capability) is independent of
|
||||||
// admin/board, so check it explicitly to decide whether to show the
|
// admin/board, so check it explicitly to decide whether to show the
|
||||||
// per-member certifications panel.
|
// per-member certifications panel.
|
||||||
const { data: canGrantCerts } = await supabase.rpc('user_has_permission', {
|
// Request-cached: door.manage / forms.manage here dedupe with the layout's
|
||||||
uid: user.id,
|
// nav-permission resolution for the same render.
|
||||||
sid: self.space_id,
|
const [canGrantCerts, canManageCards, canViewForms] = await Promise.all([
|
||||||
perm: 'certifications.grant',
|
hasPermission(user.id, self.space_id, 'certifications.grant'),
|
||||||
})
|
hasPermission(user.id, self.space_id, 'door.manage'),
|
||||||
const { data: canManageCards } = await supabase.rpc('user_has_permission', {
|
hasPermission(user.id, self.space_id, 'forms.manage'),
|
||||||
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',
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MembersClient
|
<MembersClient
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { cache } from 'react'
|
||||||
|
import { createClient } from '@/lib/supabase/server'
|
||||||
|
|
||||||
|
// Request-scoped memoized permission check. The same (uid, sid, perm) is
|
||||||
|
// resolved at most once per server render, so the layout and the page it
|
||||||
|
// renders no longer double-round-trip the `user_has_permission` RPC for
|
||||||
|
// overlapping permissions (e.g. door.manage / forms.manage). Semantics are
|
||||||
|
// identical to calling the RPC directly -- this only dedupes, it does NOT
|
||||||
|
// reimplement permission logic (that stays in the SQL function + RLS).
|
||||||
|
export const hasPermission = cache(
|
||||||
|
async (uid: string, sid: string, perm: string): Promise<boolean> => {
|
||||||
|
const supabase = await createClient()
|
||||||
|
const { data } = await supabase.rpc('user_has_permission', { uid, sid, perm })
|
||||||
|
return !!data
|
||||||
|
},
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user