panels-origin/api/user_scope.ts
Cursor Agent aed2f4531b
Implement client IAM v2: roles per tenant, CRUD matrix, scope, and panel UI
- 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>
2026-09-08 18:16:59 +00:00

77 lines
2.4 KiB
TypeScript

import type { AuthUser } from "./auth.ts";
import { withIamTenant } from "./iam_db.ts";
export type UserScope = {
allProjects: boolean;
projectIds: number[];
warehouseCentral: boolean;
warehouseProjects: boolean;
};
const scopeCache = new Map<number, { scope: UserScope; at: number }>();
const SCOPE_TTL_MS = 60_000;
export function invalidateUserScopeCache(userId?: number) {
if (userId != null) scopeCache.delete(userId);
else scopeCache.clear();
}
export async function loadUserScope(user: AuthUser): Promise<UserScope> {
if (user.is_owner || user.realm === "platform") {
return {
allProjects: true,
projectIds: [],
warehouseCentral: true,
warehouseProjects: true,
};
}
const hit = scopeCache.get(user.id);
if (hit && Date.now() - hit.at < SCOPE_TTL_MS) return hit.scope;
const row = await withIamTenant(user.tenant_id, async (db) => {
const u = await db.prepare(
`SELECT scope_all_projects, warehouse_central, warehouse_projects FROM users WHERE id = ?`,
).get(user.id) as {
scope_all_projects: boolean;
warehouse_central: boolean;
warehouse_projects: boolean;
} | undefined;
if (!u) return null;
const projects = await db.prepare(
`SELECT project_id FROM user_projects WHERE user_id = ? ORDER BY project_id`,
).all(user.id) as { project_id: number }[];
return {
allProjects: Boolean(u.scope_all_projects),
projectIds: projects.map((p) => Number(p.project_id)),
warehouseCentral: Boolean(u.warehouse_central),
warehouseProjects: Boolean(u.warehouse_projects),
};
});
const scope = row ?? {
allProjects: false,
projectIds: [],
warehouseCentral: false,
warehouseProjects: false,
};
scopeCache.set(user.id, { scope, at: Date.now() });
return scope;
}
export function allowedProjectIds(scope: UserScope): number[] | null {
if (scope.allProjects) return null;
return scope.projectIds;
}
export function assertProjectAccess(scope: UserScope, projectId: number | null | undefined): boolean {
if (projectId == null || !Number.isFinite(projectId)) return false;
if (scope.allProjects) return true;
return scope.projectIds.includes(Number(projectId));
}
export async function getUserScope(user: AuthUser): Promise<UserScope> {
return await loadUserScope(user);
}
export function isOwnerAccountRow(row: { is_owner?: boolean; role_code?: string }): boolean {
return Boolean(row.is_owner) || row.role_code === "tenant_admin";
}