import { cache } from "react"; import { prisma } from "@/lib/db/prisma"; import { Permission } from "@/generated/prisma/enums"; import type { NavRole } from "@/lib/navigation/nav-config"; /** * Foundation for Phase 3+ role gating — not exercised by any UI yet. * Deliberately re-checks the database rather than trusting only the JWT: * permission changes (e.g. an owner demoting a barber) must take effect * without waiting for the token to expire. See * docs/implementation-plan.md §11 ("Permission enforcement happens AGAIN * at the server-action/service layer — never trust the route group alone"). * * `cache()`-wrapped (same request-dedup pattern as getActingStaff/ * getPrimaryBusiness) — every (owner) page checks 1-2 permissions across * its layout and the page itself, which without this repeated the same * StaffProfile+Role+RolePermission join 2-3× per request (see the Phase 7 * performance pass). */ export const getStaffPermissions = cache(async (userId: string): Promise> => { const staffProfile = await prisma.staffProfile.findUnique({ where: { userId }, include: { role: { include: { permissions: true } } }, }); if (!staffProfile) return new Set(); return new Set(staffProfile.role.permissions.map((rolePermission) => rolePermission.permission)); }); /** * Phase 9 — the platform-level Super Administrator (see * scripts/ensure-super-admin.ts). Deliberately re-checks the database * rather than trusting the JWT, same rationale as getStaffPermissions. */ export const isSuperAdmin = cache(async (userId: string): Promise => { const user = await prisma.user.findUnique({ where: { id: userId }, select: { isSuperAdmin: true } }); return user?.isSuperAdmin ?? false; }); /** * A Super Administrator has every permission an Owner has — OWNER_PERMISSIONS * (src/lib/roles/default-permissions.ts) already IS the full Permission enum, * so short-circuiting true here rather than granting a StaffProfile/Role is * exactly equivalent, without giving the Super Admin a StaffProfile row at * all (see docs/phase-9-summary.md, "Super Administrator" — this is what * keeps them out of the Team list, barber picker, and appointment FKs * structurally rather than via a filter). */ export async function hasPermission(userId: string, permission: Permission): Promise { if (await isSuperAdmin(userId)) return true; const permissions = await getStaffPermissions(userId); return permissions.has(permission); } /** * The single role classification behind the merged nav (see * lib/navigation/nav-config.ts) — "staff" vs "owner" is just whether the * account's Role grants MANAGE_STAFF, and "superadmin" short-circuits both * (a Super Administrator has no StaffProfile at all, see isSuperAdmin). * Returns null for a signed-in user who is neither staff nor a super * admin — callers should redirect those away, same as today. */ export const getNavRole = cache(async (userId: string): Promise => { if (await isSuperAdmin(userId)) return "superadmin"; const staffProfile = await prisma.staffProfile.findUnique({ where: { userId }, select: { id: true } }); if (!staffProfile) return null; const permissions = await getStaffPermissions(userId); return permissions.has(Permission.MANAGE_STAFF) ? "owner" : "staff"; });