Files
NextChair/src/lib/auth/permissions.ts
T
Stefan Lazic 3300045d9e Staff / Owner
Merged staff and owner settings
2026-08-05 16:35:52 +01:00

70 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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";
});