import type { Payload, PayloadRequest } from "payload"; import { loadUserPermissions } from "@/utils/access-control/loadUserPermissions"; import type { Permission } from "@/permissions"; /** * Minimal user shape for permission checks. * Accepts the full Payload User or a stripped-down { id } from server actions. */ interface UserLike { id: number | string; roleDocs?: unknown; } /** * Check whether a user has a specific permission. * * Resolution order: * 1. No user → false. * 2. User has a role with `isSuperuser: true` → true (bypasses all checks). * 3. User has a role whose `permissions` array includes the given permission → true. * 4. Otherwise → false. * * Results are cached per-user for 30s (see `loadUserPermissions`). * * @example * const canCreate = await hasPermission(payload, user, "users:create"); */ export async function hasPermission( payload: Payload, user: UserLike | null | undefined, permission: Permission, ): Promise { if (!user) return false; const { permissions, isSuperuser } = await loadUserPermissions(payload, user); if (isSuperuser) return true; return permissions.has(permission); } /** * Check whether a user has a superuser role (bypasses all permission checks). * * Use this for the legacy `isDeveloper` replacement where the check was * "can do anything" rather than a specific permission. */ export async function isSuperuser( payload: Payload, user: UserLike | null | undefined, ): Promise { if (!user) return false; const { isSuperuser: su } = await loadUserPermissions(payload, user); return su; } /** * Check whether a user has ANY of the given permissions. */ export async function hasAnyPermission( payload: Payload, user: UserLike | null | undefined, ...permissions: Permission[] ): Promise { if (!user) return false; const { permissions: userPerms, isSuperuser: su } = await loadUserPermissions(payload, user); if (su) return true; return permissions.some((p) => userPerms.has(p)); } /** * Check whether a user has ALL of the given permissions. */ export async function hasAllPermissions( payload: Payload, user: UserLike | null | undefined, ...permissions: Permission[] ): Promise { if (!user) return false; const { permissions: userPerms, isSuperuser: su } = await loadUserPermissions(payload, user); if (su) return true; return permissions.every((p) => userPerms.has(p)); } /** * Factory that creates a Payload collection access function requiring a specific * permission. Drop-in replacement for `isDeveloper` / `isAdmin` in collection * `access` blocks. * * @example * access: { * create: requirePermission("users:create"), * update: requirePermission("users:update"), * } */ export function requirePermission(permission: Permission) { return async ({ req }: { req: PayloadRequest }): Promise => { return hasPermission(req.payload, req.user, permission); }; } /** * Factory that creates a Payload collection access function requiring ANY of * the given permissions. * * @example * access: { * update: requireAnyPermission("tickets:update", "tickets:staff"), * } */ export function requireAnyPermission(...permissions: Permission[]) { return async ({ req }: { req: PayloadRequest }): Promise => { return hasAnyPermission(req.payload, req.user, ...permissions); }; } /** * Factory that creates a Payload collection access function requiring campaign ownership. * * A user can access a campaign if: * 1. They own the campaign (owner field matches their user ID), OR * 2. They have the "campaigns:manage" permission * * @example * access: { * update: requireCampaignOwnership("campaigns:update"), * delete: requireCampaignOwnership("campaigns:delete"), * } */ export function requireCampaignOwnership(permission: Permission) { return async ({ req, id }: { req: PayloadRequest; id?: any }) => { if (!req.user) return false; // Check permission first (grants access to all campaigns for managers) const { permissions: userPerms, isSuperuser: su } = await loadUserPermissions(req.payload, req.user); if (su || userPerms.has(permission)) return true; // If no user permissions for manage, check ownership let campaign; try { campaign = await req.payload.findByID({ collection: "campaigns", id: id, depth: 1, overrideAccess: true, }); } catch (error) { // If campaign lookup fails (e.g. invalid ID), deny access return false; } if (!campaign) return false; // User owns the campaign if owner field matches their ID // campaign.owner can be number (ID) or User object const campaignOwnerId = campaign.owner ? (typeof campaign.owner === "object" ? campaign.owner.id : campaign.owner) : null; const userId = Number(req.user.id); if (campaignOwnerId && campaignOwnerId === userId) return true; // User does not own this campaign and lacks manage permission return false; }; }