import type { Context, Next } from "hono"; import type { AuthUser } from "./auth.ts"; import { tenantScope } from "./auth.ts"; import { withIamTenant } from "./iam_db.ts"; import { respondApiError, routeLabel } from "./http_errors.ts"; const permissionCache = new Map; at: number }>(); const CACHE_TTL_MS = 60_000; export async function userPermissions( tenantId: number | null, roleCode: string, ): Promise> { const key = `${tenantId ?? 0}:${roleCode}`; const hit = permissionCache.get(key); if (hit && Date.now() - hit.at < CACHE_TTL_MS) return hit.perms; if (roleCode === "tenant_admin") { const all = new Set([ "view_expenses", "manage_expenses", "view_warehouse", "manage_warehouse", "close_warehouse", "manage_cost_settings", "manage_users", "manage_budget", "manage_payroll", "manage_workers", "manage_projects", "manage_companies", "manage_documents", "manage_settings", "view_reports", ]); permissionCache.set(key, { perms: all, at: Date.now() }); return all; } const rows = await withIamTenant(tenantId, async (db) => await db.prepare( "SELECT permission_code FROM role_permissions WHERE role_code = ?", ).all(roleCode) as { permission_code: string }[], ); const perms = new Set(rows.map((r) => r.permission_code)); permissionCache.set(key, { perms, at: Date.now() }); return perms; } export async function hasPermission( user: AuthUser, code: string, ): Promise { if (user.role === "tenant_admin") return true; const role = user.role === "user" ? "user" : user.role; const perms = await userPermissions(user.tenant_id, role); return perms.has(code); } export function requirePermission(code: string) { return async (c: Context, next: Next) => { const user = c.get("user") as AuthUser; if (user.realm === "platform") { await next(); return; } if (!await hasPermission(user, code)) { return respondApiError( c, "FORBIDDEN", `Permiso requerido: ${code}`, { route: routeLabel(c), permission: code, role: user.role }, ); } await next(); }; } export function requireAnyPermission(...codes: string[]) { return async (c: Context, next: Next) => { const user = c.get("user") as AuthUser; if (user.realm === "platform") { await next(); return; } for (const code of codes) { if (await hasPermission(user, code)) { await next(); return; } } return respondApiError( c, "FORBIDDEN", `Se requiere alguno de: ${codes.join(", ")}`, { route: routeLabel(c), permissions: codes }, ); }; } export async function callIamFn( tenantId: number | null, fn: string, payload: Record = {}, ): Promise { const row = await withIamTenant(tenantId, async (db) => await db.prepare(`SELECT ${fn}($1::jsonb) AS result`).get(payload) as { result: unknown }, ); const raw = row?.result; if (raw && typeof raw === "object" && "ok" in (raw as object)) { return raw as T; } return null; }