+
diff --git a/pkg/models/umind.go b/pkg/models/umind.go
index 837f0f9..a28c558 100644
--- a/pkg/models/umind.go
+++ b/pkg/models/umind.go
@@ -11,29 +11,25 @@ import (
"gorm.io/gorm"
)
-// UmindTenant representa un sitio/cliente que tiene el widget de uMind
-// instalado. SiteKey es pública (va en el
-
+
+
diff --git a/rest/controllers/api/umind_canal_webhook_controller.go b/rest/controllers/api/umind_canal_webhook_controller.go
index 19f3a41..2f1eb49 100644
--- a/rest/controllers/api/umind_canal_webhook_controller.go
+++ b/rest/controllers/api/umind_canal_webhook_controller.go
@@ -24,7 +24,7 @@ type umindTgUpdate struct {
Message *umindTgMessage `json:"message"`
}
-// UmindTelegramWebhook recibe updates del bot de Telegram de un tenant.
+// UmindTelegramWebhook recibe updates del bot de Telegram de un agente.
// Ruta: POST /webhooks/umind-telegram/:webhook_secret
// El webhook_secret es un identificador nuestro (no el bot token real) —
// ver el comentario en models.UmindCanal.
diff --git a/rest/controllers/api/umind_widget_controller.go b/rest/controllers/api/umind_widget_controller.go
index 37606cb..d44ee81 100644
--- a/rest/controllers/api/umind_widget_controller.go
+++ b/rest/controllers/api/umind_widget_controller.go
@@ -10,6 +10,14 @@ import (
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
+func agenteDeContexto(c *fiber.Ctx) (*models.UmindAgente, error) {
+ agente, ok := c.Locals("umind_agente").(*models.UmindAgente)
+ if !ok || agente == nil {
+ return nil, fiber.NewError(fiber.StatusUnauthorized, "no autenticado")
+ }
+ return agente, nil
+}
+
func tenantDeContexto(c *fiber.Ctx) (*models.UmindTenant, error) {
tenant, ok := c.Locals("umind_tenant").(*models.UmindTenant)
if !ok || tenant == nil {
@@ -25,18 +33,24 @@ func nuevaSessionID() string {
}
// UmindWidgetInit devuelve una sesión nueva y el mensaje de bienvenida del
-// tenant, sin gastar una llamada al LLM solo para saludar.
+// agente, sin gastar una llamada al LLM solo para saludar. El nombre que se
+// muestra es el del negocio (tenant dueño del agente), no la etiqueta
+// interna del agente.
// Ruta: GET /widget/:site_key/init
func UmindWidgetInit(c *fiber.Ctx) error {
+ agente, err := agenteDeContexto(c)
+ if err != nil {
+ return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": true, "message": err.Error()})
+ }
tenant, err := tenantDeContexto(c)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": true, "message": err.Error()})
}
- bienvenida := strings.TrimSpace(tenant.MensajeBienvenida)
+ bienvenida := strings.TrimSpace(agente.MensajeBienvenida)
if bienvenida == "" {
bienvenida = "¡Hola! ¿En qué puedo ayudarte?"
}
- color := strings.TrimSpace(tenant.Color)
+ color := strings.TrimSpace(agente.Color)
if color == "" {
color = "#8eb02f"
}
@@ -51,7 +65,7 @@ func UmindWidgetInit(c *fiber.Ctx) error {
// UmindWidgetMensaje procesa un mensaje del visitante y devuelve la respuesta del agente.
// Ruta: POST /widget/:site_key/mensaje
func UmindWidgetMensaje(c *fiber.Ctx) error {
- tenant, err := tenantDeContexto(c)
+ agente, err := agenteDeContexto(c)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": true, "message": err.Error()})
}
@@ -75,7 +89,7 @@ func UmindWidgetMensaje(c *fiber.Ctx) error {
req.Mensaje = req.Mensaje[:4000]
}
- respuesta, err := services.ProcessWidgetMessage(tenant, sessionID, strings.TrimSpace(req.Mensaje))
+ respuesta, err := services.ProcessWidgetMessage(agente, sessionID, strings.TrimSpace(req.Mensaje))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": true, "message": err.Error()})
}
diff --git a/rest/controllers/umind_admin_controller.go b/rest/controllers/umind_admin_controller.go
index d72431b..bc7fd24 100644
--- a/rest/controllers/umind_admin_controller.go
+++ b/rest/controllers/umind_admin_controller.go
@@ -21,7 +21,22 @@ func UmindIndex(c *fiber.Ctx) error {
}, "layouts/main")
}
+var umindColorHexRegex = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
+
+// umindColor valida el hex del color de marca del widget; si viene vacío o
+// inválido cae al verde por defecto en vez de guardar basura (el valor se
+// aplica tal cual como CSS custom property en el widget embebido).
+func umindColor(color string) string {
+ color = strings.TrimSpace(color)
+ if umindColorHexRegex.MatchString(color) {
+ return color
+ }
+ return "#8eb02f"
+}
+
// ─── Tenants ─────────────────────────────────────────────────────────────────
+// Un tenant es el negocio/sitio dueño de los dominios permitidos — la config
+// de IA, tono, tools, etc. viven en sus UmindAgente (ver más abajo).
func GetUmindTenants(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
@@ -45,26 +60,9 @@ func GetUmindTenants(c *fiber.Ctx) error {
type umindTenantReq struct {
Nombre string `json:"nombre"`
DominiosPermitidos []string `json:"dominios_permitidos"`
- AiConfigID *uint `json:"ai_config_id"`
- Tono string `json:"tono"`
- MensajeBienvenida string `json:"mensaje_bienvenida"`
- Color string `json:"color"`
Activo bool `json:"activo"`
}
-var umindColorHexRegex = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
-
-// colorTenant valida el hex del color de marca del widget; si viene vacío o
-// inválido cae al verde por defecto en vez de guardar basura (el valor se
-// aplica tal cual como CSS custom property en el widget embebido).
-func colorTenant(color string) string {
- color = strings.TrimSpace(color)
- if umindColorHexRegex.MatchString(color) {
- return color
- }
- return "#8eb02f"
-}
-
func (r umindTenantReq) dominiosLimpios() []string {
var out []string
for _, d := range r.DominiosPermitidos {
@@ -92,17 +90,13 @@ func CreateUmindTenantHandler(c *fiber.Ctx) error {
tenant := &models.UmindTenant{
Nombre: strings.TrimSpace(req.Nombre),
DominiosPermitidos: strings.Join(dominios, ","),
- AiConfigID: req.AiConfigID,
- Tono: req.Tono,
- MensajeBienvenida: req.MensajeBienvenida,
- Color: colorTenant(req.Color),
Activo: true,
CreadoPorID: extraerUserID(c),
}
if err := models.CreateUmindTenant(tenant); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
- return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "id": tenant.ID, "site_key": tenant.SiteKey})
+ return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "id": tenant.ID})
}
func UpdateUmindTenantHandler(c *fiber.Ctx) error {
@@ -118,10 +112,6 @@ func UpdateUmindTenantHandler(c *fiber.Ctx) error {
updates := map[string]interface{}{
"nombre": strings.TrimSpace(req.Nombre),
"dominios_permitidos": strings.Join(dominios, ","),
- "ai_config_id": req.AiConfigID,
- "tono": req.Tono,
- "mensaje_bienvenida": req.MensajeBienvenida,
- "color": colorTenant(req.Color),
"activo": req.Activo,
}
if err := models.UpdateUmindTenant(uint(id), updates); err != nil {
@@ -141,14 +131,105 @@ func DeleteUmindTenantHandler(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"ok": true})
}
-// ─── Documentos / ingesta ────────────────────────────────────────────────────
+// ─── Agentes ─────────────────────────────────────────────────────────────────
+// Un tenant puede tener varios agentes independientes (ej. "Ventas",
+// "Soporte"), cada uno con su propia config de IA, base de conocimiento,
+// tools, canales y conexión de correo.
-func GetUmindDocumentosHandler(c *fiber.Ctx) error {
+func GetUmindAgentesHandler(c *fiber.Ctx) error {
tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
if err != nil || tenantID == 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
}
- items, err := models.GetUmindDocumentosByTenant(uint(tenantID))
+ items, err := models.GetUmindAgentesByTenant(uint(tenantID))
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"items": items})
+}
+
+type umindAgenteReq struct {
+ TenantID uint `json:"tenant_id"`
+ Nombre string `json:"nombre"`
+ AiConfigID *uint `json:"ai_config_id"`
+ Tono string `json:"tono"`
+ MensajeBienvenida string `json:"mensaje_bienvenida"`
+ Color string `json:"color"`
+ Activo bool `json:"activo"`
+}
+
+func CreateUmindAgenteHandler(c *fiber.Ctx) error {
+ var req umindAgenteReq
+ if err := c.BodyParser(&req); err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
+ }
+ if req.TenantID == 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
+ }
+ if strings.TrimSpace(req.Nombre) == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "nombre es requerido"})
+ }
+ if _, err := models.GetUmindTenantByID(req.TenantID); err != nil {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant no encontrado"})
+ }
+
+ agente := &models.UmindAgente{
+ TenantID: req.TenantID,
+ Nombre: strings.TrimSpace(req.Nombre),
+ AiConfigID: req.AiConfigID,
+ Tono: req.Tono,
+ MensajeBienvenida: req.MensajeBienvenida,
+ Color: umindColor(req.Color),
+ Activo: true,
+ }
+ if err := models.CreateUmindAgente(agente); err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "id": agente.ID, "site_key": agente.SiteKey})
+}
+
+func UpdateUmindAgenteHandler(c *fiber.Ctx) error {
+ id, err := strconv.ParseUint(c.Params("id"), 10, 64)
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
+ }
+ var req umindAgenteReq
+ if err := c.BodyParser(&req); err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
+ }
+ updates := map[string]interface{}{
+ "nombre": strings.TrimSpace(req.Nombre),
+ "ai_config_id": req.AiConfigID,
+ "tono": req.Tono,
+ "mensaje_bienvenida": req.MensajeBienvenida,
+ "color": umindColor(req.Color),
+ "activo": req.Activo,
+ }
+ if err := models.UpdateUmindAgente(uint(id), updates); err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"ok": true})
+}
+
+func DeleteUmindAgenteHandler(c *fiber.Ctx) error {
+ id, err := strconv.ParseUint(c.Params("id"), 10, 64)
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
+ }
+ if err := models.DeleteUmindAgente(uint(id)); err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"ok": true})
+}
+
+// ─── Documentos / ingesta ────────────────────────────────────────────────────
+
+func GetUmindDocumentosHandler(c *fiber.Ctx) error {
+ agenteID, err := strconv.ParseUint(c.Query("agente_id"), 10, 64)
+ if err != nil || agenteID == 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
+ }
+ items, err := models.GetUmindDocumentosByAgente(uint(agenteID))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
@@ -160,25 +241,25 @@ func GetUmindDocumentosHandler(c *fiber.Ctx) error {
// segundos/minutos según cuántas páginas tenga el sitio.
func CreateUmindDocumentoHandler(c *fiber.Ctx) error {
var req struct {
- TenantID uint `json:"tenant_id"`
+ AgenteID uint `json:"agente_id"`
URL string `json:"url"`
MaxPaginas int `json:"max_paginas"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
}
- if req.TenantID == 0 {
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
+ if req.AgenteID == 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
}
if strings.TrimSpace(req.URL) == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "url requerida"})
}
- if _, err := models.GetUmindTenantByID(req.TenantID); err != nil {
- return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant no encontrado"})
+ if _, err := models.GetUmindAgenteByID(req.AgenteID); err != nil {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "agente no encontrado"})
}
doc := &models.UmindDocumento{
- TenantID: req.TenantID,
+ AgenteID: req.AgenteID,
Tipo: "url",
Origen: strings.TrimSpace(req.URL),
Estado: "procesando",
@@ -187,7 +268,7 @@ func CreateUmindDocumentoHandler(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
- go services.IngestarTenant(req.TenantID, doc.ID, doc.Origen, req.MaxPaginas)
+ go services.IngestarAgente(req.AgenteID, doc.ID, doc.Origen, req.MaxPaginas)
return c.Status(fiber.StatusAccepted).JSON(fiber.Map{"ok": true, "id": doc.ID, "estado": "procesando"})
}
@@ -206,11 +287,11 @@ func DeleteUmindDocumentoHandler(c *fiber.Ctx) error {
// ─── Conversaciones ───────────────────────────────────────────────────────────
func GetUmindSesionesHandler(c *fiber.Ctx) error {
- tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
- if err != nil || tenantID == 0 {
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
+ agenteID, err := strconv.ParseUint(c.Query("agente_id"), 10, 64)
+ if err != nil || agenteID == 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
}
- items, err := models.GetUmindSesiones(uint(tenantID), 50)
+ items, err := models.GetUmindSesiones(uint(agenteID), 50)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
@@ -218,15 +299,15 @@ func GetUmindSesionesHandler(c *fiber.Ctx) error {
}
func GetUmindHistorialHandler(c *fiber.Ctx) error {
- tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
- if err != nil || tenantID == 0 {
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
+ agenteID, err := strconv.ParseUint(c.Query("agente_id"), 10, 64)
+ if err != nil || agenteID == 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
}
sessionID := c.Query("session_id")
if sessionID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "session_id requerido"})
}
- items, err := models.GetUmindHistorial(uint(tenantID), sessionID, 200)
+ items, err := models.GetUmindHistorial(uint(agenteID), sessionID, 200)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
@@ -238,11 +319,11 @@ func GetUmindHistorialHandler(c *fiber.Ctx) error {
var umindNombreToolRegex = regexp.MustCompile(`^[a-z][a-z0-9_]{2,63}$`)
func GetUmindHerramientasHandler(c *fiber.Ctx) error {
- tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
- if err != nil || tenantID == 0 {
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
+ agenteID, err := strconv.ParseUint(c.Query("agente_id"), 10, 64)
+ if err != nil || agenteID == 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
}
- items, err := models.GetUmindHerramientasByTenant(uint(tenantID))
+ items, err := models.GetUmindHerramientasByAgente(uint(agenteID))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
@@ -250,7 +331,7 @@ func GetUmindHerramientasHandler(c *fiber.Ctx) error {
out := make([]fiber.Map, len(items))
for i, h := range items {
out[i] = fiber.Map{
- "ID": h.ID, "tenant_id": h.TenantID, "nombre": h.Nombre, "descripcion": h.Descripcion,
+ "ID": h.ID, "agente_id": h.AgenteID, "nombre": h.Nombre, "descripcion": h.Descripcion,
"parametros_json": h.ParametrosJSON, "url": h.URL, "auth_header_nombre": h.AuthHeaderNombre,
"auth_configurado": h.AuthHeaderValorEnc != "", "activa": h.Activa,
}
@@ -259,7 +340,7 @@ func GetUmindHerramientasHandler(c *fiber.Ctx) error {
}
type umindHerramientaReq struct {
- TenantID uint `json:"tenant_id"`
+ AgenteID uint `json:"agente_id"`
Nombre string `json:"nombre"`
Descripcion string `json:"descripcion"`
Parametros []models.UmindHerramientaParametro `json:"parametros"`
@@ -274,8 +355,8 @@ func CreateUmindHerramientaHandler(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
}
- if req.TenantID == 0 {
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
+ if req.AgenteID == 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
}
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)"})
@@ -297,7 +378,7 @@ func CreateUmindHerramientaHandler(c *fiber.Ctx) error {
}
h := &models.UmindHerramienta{
- TenantID: req.TenantID, Nombre: req.Nombre, Descripcion: strings.TrimSpace(req.Descripcion),
+ AgenteID: req.AgenteID, Nombre: req.Nombre, Descripcion: strings.TrimSpace(req.Descripcion),
ParametrosJSON: parametrosJSON, URL: strings.TrimSpace(req.URL),
AuthHeaderNombre: strings.TrimSpace(req.AuthHeaderNombre), AuthHeaderValorEnc: authEnc, Activa: true,
}
@@ -363,11 +444,11 @@ func DeleteUmindHerramientaHandler(c *fiber.Ctx) error {
// ─── Canales (Telegram / WhatsApp) ─────────────────────────────────────────
func GetUmindCanalesHandler(c *fiber.Ctx) error {
- tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
- if err != nil || tenantID == 0 {
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
+ agenteID, err := strconv.ParseUint(c.Query("agente_id"), 10, 64)
+ if err != nil || agenteID == 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
}
- items, err := models.GetUmindCanalesByTenant(uint(tenantID))
+ items, err := models.GetUmindCanalesByAgente(uint(agenteID))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
@@ -380,7 +461,7 @@ func GetUmindCanalesHandler(c *fiber.Ctx) error {
webhookURL = fmt.Sprintf("%s/webhooks/umind-whatsapp/%s", app.Http.Server.Url, canal.WebhookSecret)
}
out[i] = fiber.Map{
- "ID": canal.ID, "tenant_id": canal.TenantID, "tipo": canal.Tipo, "activo": canal.Activo,
+ "ID": canal.ID, "agente_id": canal.AgenteID, "tipo": canal.Tipo, "activo": canal.Activo,
"webhook_url": webhookURL, "ultimo_error": canal.UltimoError,
}
}
@@ -388,7 +469,7 @@ func GetUmindCanalesHandler(c *fiber.Ctx) error {
}
type umindCanalReq struct {
- TenantID uint `json:"tenant_id"`
+ AgenteID uint `json:"agente_id"`
Tipo string `json:"tipo"`
Credenciales map[string]string `json:"credenciales"`
Activo bool `json:"activo"`
@@ -399,8 +480,8 @@ func CreateUmindCanalHandler(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
}
- if req.TenantID == 0 {
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
+ if req.AgenteID == 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
}
switch req.Tipo {
case "telegram":
@@ -421,7 +502,7 @@ func CreateUmindCanalHandler(c *fiber.Ctx) error {
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
- canal := &models.UmindCanal{TenantID: req.TenantID, Tipo: req.Tipo, Activo: true, CredencialesEnc: credencialesEnc}
+ canal := &models.UmindCanal{AgenteID: req.AgenteID, Tipo: req.Tipo, Activo: true, CredencialesEnc: credencialesEnc}
if err := models.CreateUmindCanal(canal); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
@@ -472,12 +553,12 @@ func DeleteUmindCanalHandler(c *fiber.Ctx) error {
// ─── Chat de prueba ─────────────────────────────────────────────────────────
-// UmindChatPruebaHandler deja que el staff pruebe el agente de un tenant
-// directo desde el panel, sin pasar por site_key/dominio (ya está gateado
-// por la sesión con la que se llega acá).
+// UmindChatPruebaHandler deja que el staff pruebe un agente puntual directo
+// desde el panel, sin pasar por site_key/dominio (ya está gateado por la
+// sesión con la que se llega acá).
func UmindChatPruebaHandler(c *fiber.Ctx) error {
var req struct {
- TenantID uint `json:"tenant_id"`
+ AgenteID uint `json:"agente_id"`
SessionID string `json:"session_id"`
Mensaje string `json:"mensaje"`
}
@@ -487,15 +568,15 @@ func UmindChatPruebaHandler(c *fiber.Ctx) error {
if strings.TrimSpace(req.Mensaje) == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "mensaje requerido"})
}
- tenant, err := models.GetUmindTenantByID(req.TenantID)
+ agente, err := models.GetUmindAgenteByID(req.AgenteID)
if err != nil {
- return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant no encontrado"})
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "agente no encontrado"})
}
sessionID := strings.TrimSpace(req.SessionID)
if sessionID == "" {
sessionID = "staff-preview:" + strconv.FormatUint(uint64(extraerUserID(c)), 10)
}
- respuesta, err := services.ProcessWidgetMessage(tenant, sessionID, strings.TrimSpace(req.Mensaje))
+ respuesta, err := services.ProcessWidgetMessage(agente, sessionID, strings.TrimSpace(req.Mensaje))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
diff --git a/rest/controllers/umind_oauth_controller.go b/rest/controllers/umind_oauth_controller.go
index bb1f45a..e5f4e01 100644
--- a/rest/controllers/umind_oauth_controller.go
+++ b/rest/controllers/umind_oauth_controller.go
@@ -11,20 +11,20 @@ import (
)
// GetUmindConexionesHandler lista las cuentas de correo conectadas de un
-// tenant, sin exponer los tokens (ni cifrados ni en claro).
+// agente, sin exponer los tokens (ni cifrados ni en claro).
func GetUmindConexionesHandler(c *fiber.Ctx) error {
- tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
- if err != nil || tenantID == 0 {
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
+ agenteID, err := strconv.ParseUint(c.Query("agente_id"), 10, 64)
+ if err != nil || agenteID == 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
}
- items, err := models.GetUmindConexionesByTenant(uint(tenantID))
+ items, err := models.GetUmindConexionesByAgente(uint(agenteID))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
out := make([]fiber.Map, len(items))
for i, cx := range items {
out[i] = fiber.Map{
- "ID": cx.ID, "tenant_id": cx.TenantID, "proveedor": cx.Proveedor,
+ "ID": cx.ID, "agente_id": cx.AgenteID, "proveedor": cx.Proveedor,
"email": cx.Email, "activo": cx.Activo, "expira_en": cx.ExpiraEn,
}
}
@@ -32,18 +32,18 @@ func GetUmindConexionesHandler(c *fiber.Ctx) error {
}
// UmindConectarHandler redirige al staff a la pantalla de consentimiento de
-// Google/Microsoft. Ruta: GET /app/umind/conexiones/conectar?tenant_id=&proveedor=
+// Google/Microsoft. Ruta: GET /app/umind/conexiones/conectar?agente_id=&proveedor=
func UmindConectarHandler(c *fiber.Ctx) error {
- tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
- if err != nil || tenantID == 0 {
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
+ agenteID, err := strconv.ParseUint(c.Query("agente_id"), 10, 64)
+ if err != nil || agenteID == 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agente_id requerido"})
}
proveedor := c.Query("proveedor")
- if _, err := models.GetUmindTenantByID(uint(tenantID)); err != nil {
- return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant no encontrado"})
+ if _, err := models.GetUmindAgenteByID(uint(agenteID)); err != nil {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "agente no encontrado"})
}
- url, err := services.IniciarConexionOAuth(proveedor, uint(tenantID))
+ url, err := services.IniciarConexionOAuth(proveedor, uint(agenteID))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
@@ -69,7 +69,11 @@ func UmindOAuthCallbackHandler(c *fiber.Ctx) error {
log.Printf("[UMIND_OAUTH] error completando conexión (%s): %v", proveedor, err)
return c.Redirect("/orchestrator/?oauth_error=1", fiber.StatusFound)
}
- return c.Redirect(fmt.Sprintf("/orchestrator/tenants/%d?tab=conexiones", conexion.TenantID), fiber.StatusFound)
+ agente, err := models.GetUmindAgenteByID(conexion.AgenteID)
+ if err != nil {
+ return c.Redirect("/orchestrator/?oauth_error=1", fiber.StatusFound)
+ }
+ return c.Redirect(fmt.Sprintf("/orchestrator/tenants/%d/agentes/%d?tab=conexiones", agente.TenantID, agente.ID), fiber.StatusFound)
}
func DeleteUmindConexionHandler(c *fiber.Ctx) error {
diff --git a/rest/middlewares/auth_umind_widget.go b/rest/middlewares/auth_umind_widget.go
index 6613264..81f7aa9 100644
--- a/rest/middlewares/auth_umind_widget.go
+++ b/rest/middlewares/auth_umind_widget.go
@@ -8,20 +8,25 @@ import (
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
-// AuthUmindWidget resuelve el tenant a partir de la site_key en la URL y
-// valida que la petición venga de uno de sus dominios permitidos (Origin, o
-// Referer si el navegador no manda Origin). La site_key no es un secreto —
-// cualquiera puede verla en el HTML público del sitio — la protección real es
-// el chequeo de dominio, igual que una site key de reCAPTCHA/Analytics.
+// AuthUmindWidget resuelve el agente a partir de la site_key en la URL y
+// valida que la petición venga de uno de los dominios permitidos del tenant
+// dueño de ese agente (Origin, o Referer si el navegador no manda Origin).
+// La site_key no es un secreto — cualquiera puede verla en el HTML público
+// del sitio — la protección real es el chequeo de dominio, igual que una
+// site key de reCAPTCHA/Analytics.
func AuthUmindWidget(c *fiber.Ctx) error {
siteKey := c.Params("site_key")
if siteKey == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": true, "message": "site_key requerida"})
}
- tenant, err := models.GetUmindTenantBySiteKey(siteKey)
+ agente, err := models.GetUmindAgenteBySiteKey(siteKey)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": true, "message": "sitio no encontrado"})
}
+ tenant, err := models.GetUmindTenantByID(agente.TenantID)
+ if err != nil || !tenant.Activo {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": true, "message": "sitio no encontrado"})
+ }
origen := c.Get("Origin")
if origen == "" {
@@ -36,6 +41,7 @@ func AuthUmindWidget(c *fiber.Ctx) error {
c.Set("Access-Control-Allow-Origin", origen)
c.Set("Vary", "Origin")
}
+ c.Locals("umind_agente", agente)
c.Locals("umind_tenant", tenant)
return c.Next()
}
diff --git a/rest/routes/user.go b/rest/routes/user.go
index 472dbbf..5cd638b 100755
--- a/rest/routes/user.go
+++ b/rest/routes/user.go
@@ -360,6 +360,10 @@ func UserRoutes(app fiber.Router) {
protected.Post("/umind/tenants", middlewares.SoloAdmin, controllers.CreateUmindTenantHandler)
protected.Put("/umind/tenants/:id", middlewares.SoloAdmin, controllers.UpdateUmindTenantHandler)
protected.Delete("/umind/tenants/:id", middlewares.SoloAdmin, controllers.DeleteUmindTenantHandler)
+ protected.Get("/umind/agentes", controllers.GetUmindAgentesHandler)
+ protected.Post("/umind/agentes", middlewares.SoloAdmin, controllers.CreateUmindAgenteHandler)
+ protected.Put("/umind/agentes/:id", middlewares.SoloAdmin, controllers.UpdateUmindAgenteHandler)
+ protected.Delete("/umind/agentes/:id", middlewares.SoloAdmin, controllers.DeleteUmindAgenteHandler)
protected.Get("/umind/documentos", controllers.GetUmindDocumentosHandler)
protected.Post("/umind/documentos", middlewares.SoloAdmin, controllers.CreateUmindDocumentoHandler)
protected.Delete("/umind/documentos/:id", middlewares.SoloAdmin, controllers.DeleteUmindDocumentoHandler)