1
0
Fork 0
polaris-task-force/src/utils/access-control/hasPermission.ts

162 lines
4.9 KiB
TypeScript

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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> => {
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<boolean> => {
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;
};
}