Files
vps-tracker/apps/api/src/lib/permissions.ts
T
DenozordecandCursor da818fe104
Docker / build (push) Failing after 19s
feat(topology): добавить схему инфраструктуры на React Flow
Редактор схем с VPS-нодами, связями и персистом в SQLite по space.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 16:52:36 +07:00

132 lines
3.3 KiB
TypeScript

/**
* Portal JWT RBAC helpers (mirrors @authportal/shared hasPermission).
* Format: vps:<section>:<read|write|admin>
*/
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
}