mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 12:03:17 +00:00
Implementa el módulo integral de costos según el plan acordado: - Esquema Liquibase: expense_entries, almacén (central + obra), IVA, tenant_cost_settings, permisos IAM y RLS - Funciones RPC core: gastos, almacén, control presupuestal, sync nómina, cierre de almacén al concluir proyecto - API HTTP: expenses_http, warehouse_http, cost_control_http, iam_http - Middleware requirePermission con matriz IAM - UI web-panel: /gastos, /almacen, /control-presupuesto, /usuarios - Componentes BudgetItemPicker, CostSemaphore y semáforos CSS - Smoke tests extendidos para gastos/almacén/control Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
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<string, { perms: Set<string>; at: number }>();
|
|
const CACHE_TTL_MS = 60_000;
|
|
|
|
export async function userPermissions(
|
|
tenantId: number | null,
|
|
roleCode: string,
|
|
): Promise<Set<string>> {
|
|
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<boolean> {
|
|
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<T = unknown>(
|
|
tenantId: number | null,
|
|
fn: string,
|
|
payload: Record<string, unknown> = {},
|
|
): Promise<T | null> {
|
|
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;
|
|
}
|