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>
86 lines
2.9 KiB
Go
86 lines
2.9 KiB
Go
package models
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// UmindCanal es un canal de mensajería adicional (Telegram, WhatsApp) que
|
|
// alimenta al mismo agente del tenant que ya atiende el widget web. Los
|
|
// secretos reales (bot token, access token de WhatsApp, etc.) viven cifrados
|
|
// en CredencialesEnc (ver pkg/services/umind_secrets.go) — el modelo solo
|
|
// persiste el string ya cifrado, no conoce la clave.
|
|
//
|
|
// WebhookSecret es un identificador público generado por nosotros, distinto
|
|
// del secreto real del proveedor, usado SOLO para enrutar el webhook
|
|
// entrante al canal correcto (va en la URL que se registra en
|
|
// Telegram/Meta). Evita que el token real del proveedor termine en logs de
|
|
// acceso o de un proxy intermedio.
|
|
type UmindCanal struct {
|
|
gorm.Model
|
|
TenantID uint `json:"tenant_id" gorm:"column:tenant_id;index;not null"`
|
|
Tipo string `json:"tipo" gorm:"column:tipo;size:20;not null"` // telegram | whatsapp
|
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
|
WebhookSecret string `json:"webhook_secret" gorm:"column:webhook_secret;uniqueIndex;size:40;not null"`
|
|
CredencialesEnc string `json:"-" gorm:"column:credenciales_enc;type:text"`
|
|
UltimoError string `json:"ultimo_error" gorm:"column:ultimo_error;type:text"`
|
|
}
|
|
|
|
func (UmindCanal) TableName() string { return "umind_canales" }
|
|
|
|
func GenerarWebhookSecret() (string, error) {
|
|
b := make([]byte, 16)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("no se pudo generar el webhook_secret: %w", err)
|
|
}
|
|
return "umc_" + hex.EncodeToString(b), nil
|
|
}
|
|
|
|
func CreateUmindCanal(c *UmindCanal) error {
|
|
if c.WebhookSecret == "" {
|
|
secret, err := GenerarWebhookSecret()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.WebhookSecret = secret
|
|
}
|
|
return app.Http.Database.DB.Create(c).Error
|
|
}
|
|
|
|
func GetUmindCanalesByTenant(tenantID uint) ([]UmindCanal, error) {
|
|
var items []UmindCanal
|
|
err := app.Http.Database.DB.Where("tenant_id = ?", tenantID).Order("id DESC").Find(&items).Error
|
|
return items, err
|
|
}
|
|
|
|
func GetUmindCanalByID(id uint) (*UmindCanal, error) {
|
|
var c UmindCanal
|
|
if err := app.Http.Database.DB.First(&c, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
// GetUmindCanalByWebhookSecret resuelve el canal a partir del identificador
|
|
// público que viene en la URL del webhook. Solo matchea si está activo.
|
|
func GetUmindCanalByWebhookSecret(tipo, webhookSecret string) (*UmindCanal, error) {
|
|
var c UmindCanal
|
|
err := app.Http.Database.DB.Where("tipo = ? AND webhook_secret = ? AND activo = ?", tipo, webhookSecret, true).First(&c).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
func UpdateUmindCanal(id uint, updates map[string]interface{}) error {
|
|
return app.Http.Database.DB.Model(&UmindCanal{}).Where("id = ?", id).Updates(updates).Error
|
|
}
|
|
|
|
func DeleteUmindCanal(id uint) error {
|
|
return app.Http.Database.DB.Delete(&UmindCanal{}, id).Error
|
|
}
|