/** * Portal JWT RBAC helpers (mirrors @authportal/shared hasPermission). * Format: vps:
: */ 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/dashboard'), permission: 'vps:dashboard:read', }, { methods: ['GET'], match: (p) => p === '/api/vps' || p.startsWith('/api/vps/') || p.startsWith('/api/projects') || p.startsWith('/api/topology') || p.startsWith('/api/data'), permission: 'vps:vps:read', }, { methods: ['POST', 'PUT', 'PATCH', 'DELETE'], match: (p) => p.startsWith('/api/vps') || p.startsWith('/api/projects') || p.startsWith('/api/topology'), permission: 'vps:vps:write', }, { methods: ['GET'], match: (p) => p.startsWith('/api/providers') || p.startsWith('/api/provider-accounts'), permission: 'vps:accounts:read', }, { methods: ['POST', 'PUT', 'PATCH', 'DELETE'], match: (p) => p.startsWith('/api/providers') || p.startsWith('/api/provider-accounts'), permission: 'vps:accounts:write', }, { methods: ['GET'], match: (p) => p.startsWith('/api/payments') || p.startsWith('/api/balance-ledger') || p.startsWith('/api/rates'), permission: 'vps:payments:read', }, { methods: ['POST', 'PUT', 'PATCH', 'DELETE'], match: (p) => p.startsWith('/api/payments') || p.startsWith('/api/balance-ledger'), permission: 'vps:payments:write', }, { methods: ['GET', 'POST'], match: (p) => p.startsWith('/api/sync'), permission: 'vps:sync:write', }, { methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], match: (p) => p.startsWith('/api/spaces'), permission: 'vps:dashboard:read', }, { methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], match: (p) => p.startsWith('/api/settings') || p.startsWith('/api/backup') || p.startsWith('/api/audit') || p.startsWith('/api/migrate') || p.startsWith('/api/notifications') || p.startsWith('/api/app-switcher'), permission: 'vps: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 vps user for unmatched /api/* if (pathname.startsWith('/api/')) return 'vps:dashboard:read' return null }