panels-origin/api/iam_http.ts
Cursor Agent ab89727522
feat: gastos, almacén central/obra y control presupuestal
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>
2026-09-04 04:04:17 +00:00

65 lines
2.9 KiB
TypeScript

import type { Hono } from "hono";
import type { AuthUser } from "./auth.ts";
import { requireAuth, tenantScope } from "./auth.ts";
import { callCoreFn } from "./rpc.ts";
import { respondRpc, routeLabel } from "./http_errors.ts";
import { requirePermission, callIamFn } from "./permissions.ts";
type App = Hono<{ Variables: { user: AuthUser } }>;
export function registerIamRoutes(app: App) {
app.get("/v1/iam/users", requireAuth, requirePermission("manage_users"), async (c) => {
const tid = tenantScope(c.get("user"));
const row = await callIamFn(tid, "iam.fn_user_list", { tenant_id: tid });
if (!row) {
return respondRpc(c, { ok: false, code: "INTERNAL", layer: "api", message: "No se pudo consultar usuarios IAM", data: null, errors: null });
}
return respondRpc(c, row as Parameters<typeof respondRpc>[1]);
});
app.get("/v1/iam/permissions", requireAuth, requirePermission("manage_users"), async (c) => {
const tid = tenantScope(c.get("user"));
const row = await callIamFn(tid, "iam.fn_permission_list", {});
if (!row) {
return respondRpc(c, { ok: false, code: "INTERNAL", layer: "api", message: "No se pudo consultar permisos IAM", data: null, errors: null });
}
return respondRpc(c, row as Parameters<typeof respondRpc>[1]);
});
app.get("/v1/iam/roles/:role/permissions", requireAuth, requirePermission("manage_users"), async (c) => {
const tid = tenantScope(c.get("user"));
const row = await callIamFn(tid, "iam.fn_role_permissions_get", { role_code: c.req.param("role") });
if (!row) {
return respondRpc(c, { ok: false, code: "INTERNAL", layer: "api", message: "No se pudo consultar permisos del rol", data: null, errors: null });
}
return respondRpc(c, row as Parameters<typeof respondRpc>[1]);
});
app.put("/v1/iam/roles/:role/permissions", requireAuth, requirePermission("manage_users"), async (c) => {
const body = await c.req.json<{ permissions?: string[] }>();
const tid = tenantScope(c.get("user"));
const row = await callIamFn(tid, "iam.fn_role_permissions_save", {
role_code: c.req.param("role"),
permissions: body.permissions ?? [],
});
if (!row) {
return respondRpc(c, { ok: false, code: "INTERNAL", layer: "api", message: "No se pudo guardar permisos del rol", data: null, errors: null });
}
return respondRpc(c, row as Parameters<typeof respondRpc>[1]);
});
app.get("/v1/me/permissions", requireAuth, async (c) => {
const user = c.get("user");
const { userPermissions } = await import("./permissions.ts");
const role = user.role === "tenant_admin" ? "tenant_admin" : "user";
const perms = await userPermissions(user.tenant_id, role);
return respondRpc(c, {
ok: true,
code: "OK",
layer: "api",
message: `Permisos del usuario ${user.username}`,
data: { permissions: [...perms], role: user.role },
errors: null,
});
});
}