/** * Portal JWT RBAC helpers (mirrors @authportal/shared hasPermission). * Format: cfdm:
: */ export type AuthUser = { id: string; email: string; name: string; apps: string[]; permissions: string[]; isAdmin?: boolean; }; export function hasPermission( granted: readonly string[], required: string, ): boolean { if (granted.includes(required)) return true; const parts = required.split(":"); if (parts.length !== 3) return false; const [app, section, action] = parts; if (action === "read") { return ( granted.includes(`${app}:${section}:write`) || granted.includes(`${app}:${section}:admin`) ); } if (action === "write") { return granted.includes(`${app}:${section}:admin`); } return false; } type Rule = { methods: string[]; match: (path: string) => boolean; permission: string; }; const RULES: Rule[] = [ { methods: ["GET"], match: (p) => p.startsWith("/api/v1/domains") || p.startsWith("/api/v1/domain-monitors") || p === "/api/v1/domain-monitors", permission: "cfdm:domains:read", }, { methods: ["POST", "PUT", "PATCH", "DELETE"], match: (p) => p.startsWith("/api/v1/domains") || p.startsWith("/api/v1/domain-monitors"), permission: "cfdm:domains:write", }, { methods: ["GET"], match: (p) => p.startsWith("/api/v1/dns") || p.startsWith("/api/v1/subdomains"), permission: "cfdm:dns:read", }, { methods: ["POST", "PUT", "PATCH", "DELETE"], match: (p) => p.startsWith("/api/v1/dns") || p.startsWith("/api/v1/subdomains"), permission: "cfdm:dns:write", }, { methods: ["GET"], match: (p) => p.startsWith("/api/v1/certificates"), permission: "cfdm:certificates:read", }, { methods: ["POST", "PUT", "PATCH", "DELETE"], match: (p) => p.startsWith("/api/v1/certificates"), permission: "cfdm:certificates:write", }, { methods: ["GET"], match: (p) => p.startsWith("/api/v1/groups") || p.startsWith("/api/v1/service-groups"), permission: "cfdm:groups:read", }, { methods: ["POST", "PUT", "PATCH", "DELETE"], match: (p) => p.startsWith("/api/v1/groups") || p.startsWith("/api/v1/service-groups"), permission: "cfdm:groups:write", }, { methods: ["GET"], match: (p) => p.startsWith("/api/v1/services") || p.startsWith("/api/v1/service-bindings") || p.startsWith("/api/v1/health-checks") || p === "/api/v1/ops-summary", permission: "cfdm:services:read", }, { methods: ["POST", "PUT", "PATCH", "DELETE"], match: (p) => p.startsWith("/api/v1/services") || p.startsWith("/api/v1/service-bindings") || p.startsWith("/api/v1/health-checks"), permission: "cfdm:services:write", }, { methods: ["GET", "POST"], match: (p) => p.startsWith("/api/v1/sync"), permission: "cfdm:domains:write", }, { methods: ["GET", "POST", "PUT", "PATCH", "DELETE"], match: (p) => p.startsWith("/api/v1/settings") || p.startsWith("/api/v1/notifications") || p.startsWith("/api/v1/health-check"), permission: "cfdm:settings:admin", }, { methods: ["GET"], match: (p) => p.startsWith("/api/v1/audit"), permission: "cfdm:settings:admin", }, ]; /** Resolve required permission for method+path, or null if public / unknown. */ export function permissionForRequest( method: string, path: string, ): string | null { const m = method.toUpperCase(); const pathname = path.split("?")[0] ?? path; for (const rule of RULES) { if (!rule.methods.includes(m)) continue; if (rule.match(pathname)) return rule.permission; } // Default: any authenticated cfdm user for unmatched /api/v1/* if (pathname.startsWith("/api/v1/")) return "cfdm:domains:read"; return null; }