feat(umind): el cliente administra sus agentes desde el portal
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
08265510ea
commit
5b78f6677c
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/rest/middlewares"
|
||||
)
|
||||
|
||||
// UmindIndex renderiza el panel de administración de uMind.
|
||||
@@ -50,6 +51,12 @@ func GetUmindTenants(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
items = tenantsVisibles(c, items)
|
||||
if _, esStaff := middlewares.UmindScopeDe(c); !esStaff {
|
||||
// Para el cliente la paginación global no tiene sentido (ve pocos
|
||||
// tenants); el total pasa a ser el de lo que realmente puede ver.
|
||||
total = int64(len(items))
|
||||
}
|
||||
return c.JSON(fiber.Map{
|
||||
"items": items,
|
||||
"total": total,
|
||||
@@ -109,6 +116,9 @@ func UpdateUmindTenantHandler(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if err := accesoTenant(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
var req umindTenantReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
@@ -132,6 +142,9 @@ func DeleteUmindTenantHandler(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if err := accesoTenant(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := models.DeleteUmindTenant(uint(id)); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
@@ -148,6 +161,12 @@ func GetUmindAgentesHandler(c *fiber.Ctx) error {
|
||||
if err != nil || tenantID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
|
||||
}
|
||||
if err := accesoTenant(c, uint(tenantID)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := accesoTenant(c, uint(tenantID)); err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := models.GetUmindAgentesByTenant(uint(tenantID))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
@@ -176,9 +195,15 @@ func CreateUmindAgenteHandler(c *fiber.Ctx) error {
|
||||
if strings.TrimSpace(req.Nombre) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "nombre es requerido"})
|
||||
}
|
||||
if err := accesoTenant(c, req.TenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := models.GetUmindTenantByID(req.TenantID); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant no encontrado"})
|
||||
}
|
||||
if err := verificarCupoAgentes(c, req.TenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
agente := &models.UmindAgente{
|
||||
TenantID: req.TenantID,
|
||||
@@ -200,6 +225,9 @@ func UpdateUmindAgenteHandler(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if _, err := accesoAgente(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
var req umindAgenteReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
@@ -223,6 +251,9 @@ func DeleteUmindAgenteHandler(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if _, err := accesoAgente(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := models.DeleteUmindAgente(uint(id)); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
@@ -236,6 +267,9 @@ func GetUmindDocumentosHandler(c *fiber.Ctx) error {
|
||||
if err != nil || agenteID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
||||
}
|
||||
if _, err := accesoAgente(c, uint(agenteID)); err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := models.GetUmindDocumentosByAgente(uint(agenteID))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
@@ -258,6 +292,9 @@ func CreateUmindDocumentoHandler(c *fiber.Ctx) error {
|
||||
if req.AgenteID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
||||
}
|
||||
if _, err := accesoAgente(c, req.AgenteID); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(req.URL) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "url requerida"})
|
||||
}
|
||||
@@ -285,6 +322,9 @@ func DeleteUmindDocumentoHandler(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if err := accesoDocumento(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := models.DeleteUmindDocumento(uint(id)); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
@@ -298,6 +338,9 @@ func GetUmindSesionesHandler(c *fiber.Ctx) error {
|
||||
if err != nil || agenteID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
||||
}
|
||||
if _, err := accesoAgente(c, uint(agenteID)); err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := models.GetUmindSesiones(uint(agenteID), 50)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
@@ -310,6 +353,9 @@ func GetUmindHistorialHandler(c *fiber.Ctx) error {
|
||||
if err != nil || agenteID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
||||
}
|
||||
if _, err := accesoAgente(c, uint(agenteID)); err != nil {
|
||||
return err
|
||||
}
|
||||
sessionID := c.Query("session_id")
|
||||
if sessionID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "session_id requerido"})
|
||||
@@ -330,6 +376,9 @@ func GetUmindHerramientasHandler(c *fiber.Ctx) error {
|
||||
if err != nil || agenteID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
||||
}
|
||||
if _, err := accesoAgente(c, uint(agenteID)); err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := models.GetUmindHerramientasByAgente(uint(agenteID))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
@@ -365,6 +414,9 @@ func CreateUmindHerramientaHandler(c *fiber.Ctx) error {
|
||||
if req.AgenteID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
||||
}
|
||||
if _, err := accesoAgente(c, req.AgenteID); err != nil {
|
||||
return err
|
||||
}
|
||||
if !umindNombreToolRegex.MatchString(req.Nombre) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "el nombre debe ser minúsculas/números/guion_bajo, empezar con letra (3-64 caracteres)"})
|
||||
}
|
||||
@@ -400,6 +452,9 @@ func UpdateUmindHerramientaHandler(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if err := accesoHerramienta(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
var req umindHerramientaReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
@@ -442,6 +497,9 @@ func DeleteUmindHerramientaHandler(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if err := accesoHerramienta(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := models.DeleteUmindHerramienta(uint(id)); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
@@ -455,6 +513,9 @@ func GetUmindCanalesHandler(c *fiber.Ctx) error {
|
||||
if err != nil || agenteID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
||||
}
|
||||
if _, err := accesoAgente(c, uint(agenteID)); err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := models.GetUmindCanalesByAgente(uint(agenteID))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
@@ -493,6 +554,9 @@ func CreateUmindCanalHandler(c *fiber.Ctx) error {
|
||||
if req.AgenteID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
||||
}
|
||||
if _, err := accesoAgente(c, req.AgenteID); err != nil {
|
||||
return err
|
||||
}
|
||||
switch req.Tipo {
|
||||
case "telegram":
|
||||
if strings.TrimSpace(req.Credenciales["bot_token"]) == "" {
|
||||
@@ -534,6 +598,9 @@ func UpdateUmindCanalHandler(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if err := accesoCanal(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
var req umindCanalReq
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
@@ -558,6 +625,9 @@ func DeleteUmindCanalHandler(c *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if err := accesoCanal(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := models.DeleteUmindCanal(uint(id)); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
@@ -581,9 +651,9 @@ func UmindChatPruebaHandler(c *fiber.Ctx) error {
|
||||
if strings.TrimSpace(req.Mensaje) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "mensaje requerido"})
|
||||
}
|
||||
agente, err := models.GetUmindAgenteByID(req.AgenteID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "agente no encontrado"})
|
||||
agente, errAcceso := accesoAgente(c, req.AgenteID)
|
||||
if errAcceso != nil {
|
||||
return errAcceso
|
||||
}
|
||||
sessionID := strings.TrimSpace(req.SessionID)
|
||||
if sessionID == "" {
|
||||
@@ -606,6 +676,9 @@ func GetUmindEventosHandler(c *fiber.Ctx) error {
|
||||
if err != nil || agenteID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
|
||||
}
|
||||
if _, err := accesoAgente(c, uint(agenteID)); err != nil {
|
||||
return err
|
||||
}
|
||||
items, err := models.GetUmindEventosByAgente(uint(agenteID), 100)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
@@ -623,6 +696,9 @@ func GetUmindUsoHandler(c *fiber.Ctx) error {
|
||||
if err != nil || tenantID == 0 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
|
||||
}
|
||||
if err := accesoTenant(c, uint(tenantID)); err != nil {
|
||||
return err
|
||||
}
|
||||
desde, hasta := rangoFechas(c.Query("desde"), c.Query("hasta"))
|
||||
|
||||
resumen, err := models.GetResumenUso(uint(tenantID), desde, hasta)
|
||||
|
||||
Reference in New Issue
Block a user