import type { Access, AccessArgs, AccessResult, Payload, PayloadRequest } from "payload"; import type { Permission } from "@/permissions"; import { canAccessAdminPanel as canAccessAdminPanelByPermissions, hasPermission, isAdminPanelRequest, requireAdminPageAccess, } from "@/utils/access-control/hasPermission"; import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification"; import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification"; /** * Division-scoped access control. * * Division membership is established by the two qualification helpers: * - Intelligence: `hasIntelligenceQualification` (intel-domain RBAC permissions * or the "intelligence" profile qualification) * - Logistics: `hasLogisticsQualification` (logistics-domain RBAC permissions * or a "logistics" profile qualification) * * Explicit RBAC permissions always still work on their own - these helpers are * a union (permission OR qualification OR superuser), never a replacement. */ /** * Collections whose admin panel is delegated to a division. Division members * get full admin-panel access to exactly these collections (and nothing else, * because `requireAdminPageAccess` still gates every other collection's * admin-panel read behind `admin::manage` permissions). * * NOTE: `assets`, `resources`, and `vehicles` are intentionally NOT here. * Admin-panel access to those collections is gated solely by the * `admin::manage` "Manage Admin Page" permission (via * `requireAdminPageAccess`), not by division membership. This prevents * division members (e.g. an intelligence-officer role that also holds the * logistics qualification) from viewing those backend pages without the * explicit manage permission. */ const DIVISION_ADMIN_QUALIFICATIONS: Record< string, (payload: Payload, user: { id: number | string }) => Promise > = { technologies: hasIntelligenceQualification, }; /** * Access factory: allows users holding the explicit permission OR members of * the Intelligence division. */ export function requireIntelligencePermission(permission: Permission) { return async ({ req }: { req: PayloadRequest }): Promise => { if (await hasPermission(req.payload, req.user, permission)) return true; if (!req.user) return false; return hasIntelligenceQualification(req.payload, req.user); }; } /** * Access factory: allows users holding the explicit permission OR members of * the Logistics division. */ export function requireLogisticsPermission(permission: Permission) { return async ({ req }: { req: PayloadRequest }): Promise => { if (await hasPermission(req.payload, req.user, permission)) return true; if (!req.user) return false; return hasLogisticsQualification(req.payload, req.user); }; } /** * Field-level access factory for approval-gating fields (approval status, * is-live flags, ...). Only "super users" may write them: superuser roles or * holders of the `system:admin-access` permission (the Admin tier). * * Used as both `create` and `update` field access: on create the field is * stripped from non-superuser payloads so the schema default (e.g. * "in_progress") applies; on update attempts to change the value are ignored. */ export function requireApprovalPermission() { return async ({ req }: { req: PayloadRequest }): Promise => hasPermission(req.payload, req.user, "system:admin-access"); } /** * Field-level access factory for approval-status select fields. * * Anyone with the collection's update permission may set the status to * "In Progress" or "Ready for Review". Only superusers (holders of * `system:admin-access`) may set it to final states ("Approved", * "Rejected", "Revision Requested"). */ export function approvalStatusFieldAccess(collectionSlug: string) { const fn = async ({ req, data }: { req: PayloadRequest; data?: Record }): Promise => { const value = data?.approvalStatus as string | undefined; const isSuperuser = await hasPermission(req.payload, req.user, "system:admin-access"); if (isSuperuser) return true; const allowedValues = ["in_progress", "ready_for_review"]; if (!value || allowedValues.includes(value)) { return await hasPermission(req.payload, req.user, `${collectionSlug}:update` as Permission); } return false; }; return { create: fn, update: fn, }; } /** * Same contract as `requireAdminPageAccess`, with one addition: for the four * division-scoped collections an admin-panel request also passes when the user * qualifies for the owning division. Non-admin (REST/API) requests keep the * original read access behavior unchanged. */ export function scopedAdminPageAccess( collectionSlug: string, readAccess?: Access | boolean, ): Access { const base = requireAdminPageAccess(collectionSlug, readAccess); const divisionCheck = DIVISION_ADMIN_QUALIFICATIONS[collectionSlug]; if (!divisionCheck) return base; return async (args: AccessArgs): Promise => { const { req } = args; if (req.user && isAdminPanelRequest(req)) { try { if (await divisionCheck(req.payload, req.user)) return true; } catch { // Qualification lookup failed; fall through to the permission-based // decision instead of failing the request outright. } } return base(args); }; } /** * Division-aware admin panel gate: permission-based access (superuser or any * `admin::manage` permission) OR membership of a division that owns at * least one admin panel collection. */ export async function canAccessAdminPanel( payload: Payload, user: { id: number | string; roleDocs?: unknown } | null | undefined, ): Promise { if (!user) return false; if (await canAccessAdminPanelByPermissions(payload, user)) return true; for (const divisionCheck of Object.values(DIVISION_ADMIN_QUALIFICATIONS)) { if (await divisionCheck(payload, user)) return true; } return false; }