mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 17:43:19 +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>
110 lines
3.8 KiB
Vue
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>
|