70 lines
3.3 KiB
TypeScript
70 lines
3.3 KiB
TypeScript
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<Set<Permission>> => {
|
||
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<boolean> => {
|
||
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<boolean> {
|
||
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<NavRole | null> => {
|
||
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";
|
||
});
|