SPA nueva en /orchestrator (Vue 3 + Vite, servida por el mismo binario Go bajo /orchestrator para que la cookie de sesión funcione sin tocar CORS), reemplaza al panel Alpine.js como punto de entrada del menú. Backend, todo aditivo sobre el motor de uMind ya existente: - UmindHerramienta: tools custom por tenant que llaman un webhook HTTP, integradas al loop de function-calling existente. Cliente HTTP con guardas SSRF (bloqueo de IPs privadas/loopback/link-local resuelto en el momento de conectar, no antes, para cerrar la ventana de DNS rebinding) que no existían en el proyecto. - UmindCanal: Telegram y WhatsApp Business Cloud API como canales adicionales del mismo agente que ya atiende el widget web, ambos reusando ProcessWidgetMessage. WhatsApp valida X-Hub-Signature-256. Credenciales cifradas en reposo con el mismo AES-GCM+APP_KEY que ya usa el proyecto para la contraseña SMTP (primer uso para secretos de uMind). - Se conecta middlewares.Limit() (rate limiter que existía pero no se usaba en ningún lado) al widget público y a los webhooks nuevos. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
488 lines
19 KiB
Go
488 lines
19 KiB
Go
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
|
)
|
|
|
|
// UmindIndex renderiza el panel de administración de uMind.
|
|
func UmindIndex(c *fiber.Ctx) error {
|
|
return c.Render("umind", fiber.Map{
|
|
"user": c.Locals("user"),
|
|
"modules": c.Locals("userModules"),
|
|
}, "layouts/main")
|
|
}
|
|
|
|
// ─── Tenants ─────────────────────────────────────────────────────────────────
|
|
|
|
func GetUmindTenants(c *fiber.Ctx) error {
|
|
page, _ := strconv.Atoi(c.Query("page", "1"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
limit := 20
|
|
offset := (page - 1) * limit
|
|
items, total, err := models.GetAllUmindTenants(limit, offset)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{
|
|
"items": items,
|
|
"total": total,
|
|
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
|
|
"page": page,
|
|
})
|
|
}
|
|
|
|
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"`
|
|
Activo bool `json:"activo"`
|
|
}
|
|
|
|
func (r umindTenantReq) dominiosLimpios() []string {
|
|
var out []string
|
|
for _, d := range r.DominiosPermitidos {
|
|
d = strings.ToLower(strings.TrimSpace(d))
|
|
if d != "" {
|
|
out = append(out, d)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func CreateUmindTenantHandler(c *fiber.Ctx) error {
|
|
var req umindTenantReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
if strings.TrimSpace(req.Nombre) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "nombre es requerido"})
|
|
}
|
|
dominios := req.dominiosLimpios()
|
|
if len(dominios) == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agrega al menos un dominio permitido"})
|
|
}
|
|
|
|
tenant := &models.UmindTenant{
|
|
Nombre: strings.TrimSpace(req.Nombre),
|
|
DominiosPermitidos: strings.Join(dominios, ","),
|
|
AiConfigID: req.AiConfigID,
|
|
Tono: req.Tono,
|
|
MensajeBienvenida: req.MensajeBienvenida,
|
|
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})
|
|
}
|
|
|
|
func UpdateUmindTenantHandler(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 umindTenantReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
dominios := req.dominiosLimpios()
|
|
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,
|
|
"activo": req.Activo,
|
|
}
|
|
if err := models.UpdateUmindTenant(uint(id), updates); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func DeleteUmindTenantHandler(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.DeleteUmindTenant(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 {
|
|
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))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"items": items})
|
|
}
|
|
|
|
// CreateUmindDocumentoHandler registra una nueva fuente (por ahora solo URL a
|
|
// crawlear) y dispara la ingesta en segundo plano — puede tardar varios
|
|
// 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"`
|
|
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 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"})
|
|
}
|
|
|
|
doc := &models.UmindDocumento{
|
|
TenantID: req.TenantID,
|
|
Tipo: "url",
|
|
Origen: strings.TrimSpace(req.URL),
|
|
Estado: "procesando",
|
|
}
|
|
if err := models.CreateUmindDocumento(doc); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
go services.IngestarTenant(req.TenantID, doc.ID, doc.Origen, req.MaxPaginas)
|
|
|
|
return c.Status(fiber.StatusAccepted).JSON(fiber.Map{"ok": true, "id": doc.ID, "estado": "procesando"})
|
|
}
|
|
|
|
func DeleteUmindDocumentoHandler(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.DeleteUmindDocumento(uint(id)); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
// ─── 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"})
|
|
}
|
|
items, err := models.GetUmindSesiones(uint(tenantID), 50)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"items": items})
|
|
}
|
|
|
|
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"})
|
|
}
|
|
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)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"items": items})
|
|
}
|
|
|
|
// ─── Tools custom (webhooks) ───────────────────────────────────────────────
|
|
|
|
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"})
|
|
}
|
|
items, err := models.GetUmindHerramientasByTenant(uint(tenantID))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
// No se devuelve el secreto cifrado ni el descifrado — solo si hay uno configurado.
|
|
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,
|
|
"parametros_json": h.ParametrosJSON, "url": h.URL, "auth_header_nombre": h.AuthHeaderNombre,
|
|
"auth_configurado": h.AuthHeaderValorEnc != "", "activa": h.Activa,
|
|
}
|
|
}
|
|
return c.JSON(fiber.Map{"items": out})
|
|
}
|
|
|
|
type umindHerramientaReq struct {
|
|
TenantID uint `json:"tenant_id"`
|
|
Nombre string `json:"nombre"`
|
|
Descripcion string `json:"descripcion"`
|
|
Parametros []models.UmindHerramientaParametro `json:"parametros"`
|
|
URL string `json:"url"`
|
|
AuthHeaderNombre string `json:"auth_header_nombre"`
|
|
AuthHeaderValor *string `json:"auth_header_valor"` // nil = no tocar (en updates)
|
|
Activa bool `json:"activa"`
|
|
}
|
|
|
|
func CreateUmindHerramientaHandler(c *fiber.Ctx) error {
|
|
var req umindHerramientaReq
|
|
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 !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)"})
|
|
}
|
|
if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(req.URL)), "https://") {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "la URL debe ser https"})
|
|
}
|
|
|
|
parametrosJSON, err := models.ParametrosToJSON(req.Parametros)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "parametros inválidos"})
|
|
}
|
|
var authEnc string
|
|
if req.AuthHeaderValor != nil && *req.AuthHeaderValor != "" {
|
|
authEnc, err = services.CifrarSecretoUmind(*req.AuthHeaderValor)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
}
|
|
|
|
h := &models.UmindHerramienta{
|
|
TenantID: req.TenantID, Nombre: req.Nombre, Descripcion: strings.TrimSpace(req.Descripcion),
|
|
ParametrosJSON: parametrosJSON, URL: strings.TrimSpace(req.URL),
|
|
AuthHeaderNombre: strings.TrimSpace(req.AuthHeaderNombre), AuthHeaderValorEnc: authEnc, Activa: true,
|
|
}
|
|
if err := models.CreateUmindHerramienta(h); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "id": h.ID})
|
|
}
|
|
|
|
func UpdateUmindHerramientaHandler(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 umindHerramientaReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
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)"})
|
|
}
|
|
if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(req.URL)), "https://") {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "la URL debe ser https"})
|
|
}
|
|
parametrosJSON, err := models.ParametrosToJSON(req.Parametros)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "parametros inválidos"})
|
|
}
|
|
|
|
updates := map[string]interface{}{
|
|
"nombre": req.Nombre, "descripcion": strings.TrimSpace(req.Descripcion),
|
|
"parametros_json": parametrosJSON, "url": strings.TrimSpace(req.URL),
|
|
"auth_header_nombre": strings.TrimSpace(req.AuthHeaderNombre), "activa": req.Activa,
|
|
}
|
|
if req.AuthHeaderValor != nil {
|
|
if *req.AuthHeaderValor == "" {
|
|
updates["auth_header_valor_enc"] = ""
|
|
} else {
|
|
enc, err := services.CifrarSecretoUmind(*req.AuthHeaderValor)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
updates["auth_header_valor_enc"] = enc
|
|
}
|
|
}
|
|
if err := models.UpdateUmindHerramienta(uint(id), updates); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func DeleteUmindHerramientaHandler(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.DeleteUmindHerramienta(uint(id)); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
// ─── 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"})
|
|
}
|
|
items, err := models.GetUmindCanalesByTenant(uint(tenantID))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
out := make([]fiber.Map, len(items))
|
|
for i, canal := range items {
|
|
webhookURL := ""
|
|
if canal.Tipo == "telegram" {
|
|
webhookURL = fmt.Sprintf("%s/webhooks/umind-telegram/%s", app.Http.Server.Url, canal.WebhookSecret)
|
|
} else if canal.Tipo == "whatsapp" {
|
|
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,
|
|
"webhook_url": webhookURL, "ultimo_error": canal.UltimoError,
|
|
}
|
|
}
|
|
return c.JSON(fiber.Map{"items": out})
|
|
}
|
|
|
|
type umindCanalReq struct {
|
|
TenantID uint `json:"tenant_id"`
|
|
Tipo string `json:"tipo"`
|
|
Credenciales map[string]string `json:"credenciales"`
|
|
Activo bool `json:"activo"`
|
|
}
|
|
|
|
func CreateUmindCanalHandler(c *fiber.Ctx) error {
|
|
var req umindCanalReq
|
|
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"})
|
|
}
|
|
switch req.Tipo {
|
|
case "telegram":
|
|
if strings.TrimSpace(req.Credenciales["bot_token"]) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bot_token requerido"})
|
|
}
|
|
case "whatsapp":
|
|
for _, k := range []string{"phone_number_id", "access_token", "app_secret", "verify_token"} {
|
|
if strings.TrimSpace(req.Credenciales[k]) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": k + " requerido"})
|
|
}
|
|
}
|
|
default:
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tipo debe ser telegram o whatsapp"})
|
|
}
|
|
|
|
credencialesEnc, err := services.CifrarCredencialesCanal(req.Credenciales)
|
|
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}
|
|
if err := models.CreateUmindCanal(canal); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
if req.Tipo == "telegram" {
|
|
webhookURL := fmt.Sprintf("%s/webhooks/umind-telegram/%s", app.Http.Server.Url, canal.WebhookSecret)
|
|
if err := services.RegistrarWebhookTelegram(req.Credenciales["bot_token"], webhookURL); err != nil {
|
|
models.UpdateUmindCanal(canal.ID, map[string]interface{}{"ultimo_error": err.Error()})
|
|
}
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "id": canal.ID, "webhook_secret": canal.WebhookSecret})
|
|
}
|
|
|
|
func UpdateUmindCanalHandler(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 umindCanalReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
updates := map[string]interface{}{"activo": req.Activo}
|
|
if len(req.Credenciales) > 0 {
|
|
enc, err := services.CifrarCredencialesCanal(req.Credenciales)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
updates["credenciales_enc"] = enc
|
|
updates["ultimo_error"] = ""
|
|
}
|
|
if err := models.UpdateUmindCanal(uint(id), updates); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func DeleteUmindCanalHandler(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.DeleteUmindCanal(uint(id)); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
// ─── 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á).
|
|
func UmindChatPruebaHandler(c *fiber.Ctx) error {
|
|
var req struct {
|
|
TenantID uint `json:"tenant_id"`
|
|
SessionID string `json:"session_id"`
|
|
Mensaje string `json:"mensaje"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
if strings.TrimSpace(req.Mensaje) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "mensaje requerido"})
|
|
}
|
|
tenant, err := models.GetUmindTenantByID(req.TenantID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant 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))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"session_id": sessionID, "respuesta": respuesta})
|
|
}
|