mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 19:43:18 +00:00
- Add IAM v2 auth with role_id, is_owner, status checks, and API key without tenant_admin bypass - Enforce CRUD permissions and project/warehouse scope across API routes - Rewrite usuarios.vue with ribbon, tree, user/role management, and permission matrix - Add dynamic menu, route guards, usePermissions/useScope composables - Apply role templates on create; filter project docs by category; hide cost tab without permission - Add iam_test unit tests; update bootstrap and SaaS tenant admin to use role_id Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
116 lines
3.9 KiB
TypeScript
116 lines
3.9 KiB
TypeScript
import type { Context } from "hono";
|
|
import type { AuthUser } from "./auth.ts";
|
|
import type { Db } from "./db.ts";
|
|
import { respondApiError, routeLabel, type RpcEnvelope } from "./http_errors.ts";
|
|
import { hasPermission } from "./permissions.ts";
|
|
import { projectDocPerm } from "./iam_catalog.ts";
|
|
import { assertProjectAccess, getUserScope, type UserScope } from "./user_scope.ts";
|
|
|
|
export async function denyUnlessProjectScope(
|
|
c: Context,
|
|
projectId: number | null | undefined,
|
|
): Promise<boolean> {
|
|
if (projectId == null || !Number.isFinite(projectId)) return true;
|
|
const user = c.get("user") as AuthUser;
|
|
if (user.realm === "platform" || user.is_owner) return true;
|
|
const scope = await getUserScope(user);
|
|
if (assertProjectAccess(scope, projectId)) return true;
|
|
respondApiError(c, "FORBIDDEN", "Obra fuera de su alcance", {
|
|
route: routeLabel(c),
|
|
project_id: projectId,
|
|
});
|
|
return false;
|
|
}
|
|
|
|
export function filterProjectsByScope<T extends { id: number }>(
|
|
items: T[],
|
|
scope: UserScope,
|
|
): T[] {
|
|
if (scope.allProjects) return items;
|
|
const allowed = new Set(scope.projectIds);
|
|
return items.filter((p) => allowed.has(Number(p.id)));
|
|
}
|
|
|
|
export async function filterProjectListEnvelope(
|
|
c: Context,
|
|
env: RpcEnvelope<{ projects?: Array<{ id: number }> }>,
|
|
): Promise<RpcEnvelope<{ projects?: Array<{ id: number }> }>> {
|
|
const user = c.get("user") as AuthUser;
|
|
if (!env.ok || !env.data?.projects || user.realm === "platform" || user.is_owner) return env;
|
|
const scope = await getUserScope(user);
|
|
return {
|
|
...env,
|
|
data: {
|
|
...env.data,
|
|
projects: filterProjectsByScope(env.data.projects, scope),
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function projectDocCategory(
|
|
db: Db,
|
|
typeCode: string,
|
|
): Promise<string> {
|
|
const row = await db.prepare(
|
|
"SELECT category FROM project_document_types WHERE code = ?",
|
|
).get(typeCode) as { category: string } | undefined;
|
|
return row?.category ?? "otro";
|
|
}
|
|
|
|
export async function denyUnlessProjectDocView(
|
|
c: Context,
|
|
db: Db,
|
|
typeCode: string,
|
|
): Promise<boolean> {
|
|
const user = c.get("user") as AuthUser;
|
|
if (user.realm === "platform" || user.is_owner) return true;
|
|
const category = await projectDocCategory(db, typeCode);
|
|
if (await hasPermission(user, projectDocPerm(category, "view"))) return true;
|
|
respondApiError(c, "FORBIDDEN", "Sin permiso para este tipo de documento de obra", {
|
|
route: routeLabel(c),
|
|
type_code: typeCode,
|
|
category,
|
|
});
|
|
return false;
|
|
}
|
|
|
|
export async function denyUnlessProjectDocCreate(
|
|
c: Context,
|
|
db: Db,
|
|
typeCode: string,
|
|
): Promise<boolean> {
|
|
const user = c.get("user") as AuthUser;
|
|
if (user.realm === "platform" || user.is_owner) return true;
|
|
const category = await projectDocCategory(db, typeCode);
|
|
if (await hasPermission(user, projectDocPerm(category, "create"))) return true;
|
|
respondApiError(c, "FORBIDDEN", "Sin permiso para subir este tipo de documento", {
|
|
route: routeLabel(c),
|
|
type_code: typeCode,
|
|
category,
|
|
});
|
|
return false;
|
|
}
|
|
|
|
export async function filterProjectDocumentsForUser(
|
|
user: AuthUser,
|
|
db: Db,
|
|
documents: Array<{ type_code?: string; type?: string }>,
|
|
): Promise<typeof documents> {
|
|
if (user.realm === "platform" || user.is_owner) return documents;
|
|
const out: typeof documents = [];
|
|
for (const doc of documents) {
|
|
const typeCode = String(doc.type_code ?? doc.type ?? "");
|
|
const category = await projectDocCategory(db, typeCode);
|
|
if (await hasPermission(user, projectDocPerm(category, "view"))) out.push(doc);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export async function denyUnlessWarehouseCentral(c: Context): Promise<boolean> {
|
|
const user = c.get("user") as AuthUser;
|
|
if (user.realm === "platform" || user.is_owner) return true;
|
|
const scope = await getUserScope(user);
|
|
if (scope.warehouseCentral) return true;
|
|
respondApiError(c, "FORBIDDEN", "Sin acceso al almacén central", { route: routeLabel(c) });
|
|
return false;
|
|
}
|