panels-origin/web-panel/components/IamDetail.vue
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

110 lines
3.8 KiB
Vue

<template>
<div v-if="detail.kind === 'iam-user' && detail.iamUser" class="iam-detail">
<div class="section-title">Usuario</div>
<p><strong>{{ detail.iamUser.display_name }}</strong></p>
<p class="muted">{{ detail.iamUser.username }}</p>
<p>Rol: {{ detail.iamUser.role_label || detail.iamUser.role_code }}</p>
<p>Estado: {{ detail.iamUser.status }}</p>
<p v-if="detail.iamUser.is_owner" class="owner-badge">Cuenta propietaria (solo SaaS)</p>
<template v-if="!detail.iamUser.is_owner">
<div class="section-title mt-3">Alcance</div>
<p v-if="detail.iamUser.scope_all_projects">Todas las obras</p>
<p v-else>Obras asignadas: {{ (detail.iamUser.project_ids ?? []).length }}</p>
<p>Central: {{ detail.iamUser.warehouse_central ? "Sí" : "No" }}</p>
<p>Obras: {{ detail.iamUser.warehouse_projects !== false ? "Sí" : "No" }}</p>
</template>
</div>
<div v-else-if="detail.kind === 'iam-role' && detail.iamRole" class="iam-detail">
<div class="section-title">Matriz del rol</div>
<p><strong>{{ detail.iamRole.label }}</strong> ({{ detail.iamRole.code }})</p>
<Message v-if="detail.iamRole.is_system" severity="info" :closable="false">Rol de sistema: no editable</Message>
<div v-else class="perm-grid mt-3">
<div v-for="perm in groupedPermissions" :key="perm.code" class="perm-item">
<Checkbox
v-model="localPerms"
:input-id="perm.code"
:value="perm.code"
:disabled="!canEdit"
/>
<label :for="perm.code">{{ perm.label }}</label>
</div>
</div>
<Button
v-if="canEdit"
label="Guardar permisos"
icon="pi pi-save"
class="mt-3 w-full"
size="small"
@click="savePerms"
/>
</div>
</template>
<script setup lang="ts">
import { displayErrorMessage } from "~/composables/api-response";
type PermRow = { code: string; label: string; module?: string; group?: string };
const props = defineProps<{
detail: {
kind: string;
iamUser?: Record<string, unknown>;
iamRole?: Record<string, unknown> & { id?: number; is_system?: boolean };
};
}>();
const { api } = useApi();
const toast = useToast();
const perms = usePermissions();
const catalog = ref<PermRow[]>([]);
const localPerms = ref<string[]>([]);
const canEdit = computed(() =>
perms.can("users.update") && props.detail.kind === "iam-role" && !props.detail.iamRole?.is_system,
);
const groupedPermissions = computed(() => catalog.value);
async function loadCatalog() {
const r = await api<{ permissions?: PermRow[] }>("/v1/iam/permissions");
catalog.value = r.permissions ?? [];
}
async function loadRolePerms(roleId: number) {
const r = await api<{ permissions?: string[] }>(`/v1/iam/roles/${roleId}/permissions`);
localPerms.value = r.permissions ?? [];
}
async function savePerms() {
const roleId = props.detail.iamRole?.id;
if (!roleId) return;
try {
await api(`/v1/iam/roles/${roleId}/permissions`, {
method: "PUT",
json: { permissions: localPerms.value },
});
toast.add({ severity: "success", summary: "Permisos guardados", life: 2500 });
} catch (e) {
toast.add({ severity: "error", summary: displayErrorMessage((e as { data?: unknown }).data ?? e), life: 4000 });
}
}
watch(
() => props.detail,
async (d) => {
if (!catalog.value.length) await loadCatalog();
if (d.kind === "iam-role" && d.iamRole?.id) await loadRolePerms(Number(d.iamRole.id));
},
{ immediate: true, deep: true },
);
</script>
<style scoped>
.iam-detail { font-size: 13px; }
.section-title { font-weight: 600; margin-bottom: 4px; }
.muted { color: var(--text-color-secondary); }
.owner-badge { color: var(--orange-500); font-weight: 600; }
.perm-grid { display: grid; grid-template-columns: 1fr; gap: 6px; max-height: 50vh; overflow: auto; }
.perm-item { display: flex; align-items: flex-start; gap: 8px; }
</style>