From 5b78f6677c1d0fcb9e991283177ae74dc5d702e9 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:51:12 -0500 Subject: [PATCH] feat(umind): el cliente administra sus agentes desde el portal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Acá es donde uMind deja de ser una herramienta interna: el cliente entra a /portal/studio con su sesión de portal y gestiona lo suyo. - UmindScopePortal/UmindScopeStaff es el ÚNICO punto donde se decide el alcance. El del cliente sale de GetClienteIDsForPortalUser, el mismo que ya autoriza el resto del portal. nil = staff sin restricción, slice vacío = no ve nada; una ruta sin scope también cae en "no ve nada" para que olvidarse el middleware falle visible y no abra todo. - Un solo set de handlers montado bajo /app/umind y /portal/umind (RegistrarRutasUmind). Duplicarlos sería duplicar las chances de olvidar un chequeo. - Guarda de acceso en TODOS los handlers, incluidos los sub-recursos que llegan por :id (documento, tool, canal, conexión): hay que cargarlos para saber de quién son, si no un cliente podría borrar el canal de otro adivinando el id. Responden 404, no 403: un 403 confirmaría que el recurso existe. - Cierra un bug preexistente: las lecturas GET /app/umind/* no tenían SoloAdmin ni pasaban por MenuMiddleware, así que cualquier usuario de staff podía leer los tenants de todos los clientes. - Límite de agentes por plan (409 con mensaje claro). Un tenant sin plan no tiene límite: cortarles de golpe sería peor que dejarlos como estaban. - /umind/ai-configs reemplaza con alcance a /app/api/ai-config/select, que devolvía TODAS las configs del sistema. - El SPA deduce por la URL si es staff o cliente (base del router, prefijo de API y URL de login) y oculta lo que es solo de staff. - Test de aislamiento entre clientes: 7 casos, incluido que un scope vacío no se confunda con staff. Co-Authored-By: Claude Sonnet 5 --- orchestrator/index.html | 2 +- orchestrator/src/components/Sidebar.vue | 20 +-- orchestrator/src/lib/api.js | 18 ++- orchestrator/src/lib/contexto.js | 21 +++ orchestrator/src/router.js | 3 +- orchestrator/src/views/AgenteDetail.vue | 43 +++--- orchestrator/src/views/Home.vue | 14 +- orchestrator/src/views/TenantAgentes.vue | 15 +- public/orchestrator/assets/index-BfxLbsxs.js | 26 ---- public/orchestrator/assets/index-DPluLgCZ.js | 26 ++++ public/orchestrator/index.html | 4 +- rest/controllers/umind_admin_controller.go | 82 ++++++++++- rest/controllers/umind_oauth_controller.go | 10 +- rest/controllers/umind_scope_helpers.go | 140 +++++++++++++++++++ rest/middlewares/umind_scope.go | 80 +++++++++++ rest/middlewares/umind_scope_test.go | 63 +++++++++ rest/routes/portal.go | 13 ++ rest/routes/umind.go | 66 +++++++++ rest/routes/user.go | 36 ++--- 19 files changed, 575 insertions(+), 107 deletions(-) create mode 100644 orchestrator/src/lib/contexto.js delete mode 100644 public/orchestrator/assets/index-BfxLbsxs.js create mode 100644 public/orchestrator/assets/index-DPluLgCZ.js create mode 100644 rest/controllers/umind_scope_helpers.go create mode 100644 rest/middlewares/umind_scope.go create mode 100644 rest/middlewares/umind_scope_test.go create mode 100644 rest/routes/umind.go diff --git a/orchestrator/index.html b/orchestrator/index.html index 266cd59..25ed500 100644 --- a/orchestrator/index.html +++ b/orchestrator/index.html @@ -3,7 +3,7 @@ - uMind — Orquestador + uMind Studio
diff --git a/orchestrator/src/components/Sidebar.vue b/orchestrator/src/components/Sidebar.vue index 4a6c233..9a5b70b 100644 --- a/orchestrator/src/components/Sidebar.vue +++ b/orchestrator/src/components/Sidebar.vue @@ -2,6 +2,7 @@ import { computed, onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import { api } from '../lib/api.js' +import { apiUmind, contexto } from '../lib/contexto.js' const route = useRoute() const router = useRouter() @@ -27,7 +28,7 @@ async function cargar() { loading.value = true error.value = '' try { - const t = await api.get('/app/umind/tenants') + const t = await api.get(apiUmind('/umind/tenants')) tenants.value = t.items || [] } catch (e) { error.value = e.message @@ -39,6 +40,7 @@ async function cargar() { // Clientes y planes solo los necesita el staff para asignarlos; si el endpoint // no está disponible (portal del cliente) el formulario sigue funcionando. async function cargarAsignables() { + if (contexto.esPortal) return // endpoints de staff: el cliente no los alcanza try { const [c, p] = await Promise.all([ api.get('/app/api/clientes/select'), @@ -80,11 +82,11 @@ async function guardar() { } try { if (editing.value) { - await api.put(`/app/umind/tenants/${editing.value.ID}`, payload) + await api.put(apiUmind(`/umind/tenants/${editing.value.ID}`), payload) showForm.value = false await cargar() } else { - const r = await api.post('/app/umind/tenants', payload) + const r = await api.post(apiUmind('/umind/tenants'), payload) showForm.value = false await cargar() router.push(`/tenants/${r.id}`) @@ -96,7 +98,7 @@ async function guardar() { async function eliminar(t) { if (!confirm(`¿Eliminar el tenant "${t.nombre}"? Esto borra también sus agentes. No se puede deshacer.`)) return - await api.del(`/app/umind/tenants/${t.ID}`) + await api.del(apiUmind(`/umind/tenants/${t.ID}`)) if (tenantActivoId.value === String(t.ID)) router.push('/') await cargar() } @@ -112,11 +114,11 @@ onMounted(() => {