Files
soft_usite/rest/middlewares/auth_umind_widget.go
T
Lizandro GD 5ee5880dc2 Agrega uMind: chat con IA embebible por tenant (F1 — widget web + RAG)
Multi-tenant dentro de soft_usite, reutilizando la infraestructura ya
existente (AiConfig, motor de function-calling del agente de Telegram)
en vez de un servicio nuevo aparte:

- UmindTenant: sitio/cliente con dominios permitidos, config de IA para
  el chat y personalidad/tono.
- Ingesta: crawler simple (mismo dominio, N páginas) + chunking +
  embeddings (config global con módulo "umind_embeddings", pensada
  para OpenAI ya que Claude no ofrece embeddings) guardados como JSON,
  con búsqueda por similitud coseno en memoria (sin pgvector todavía).
- Agente acotado: única herramienta buscar_conocimiento, sin acceso a
  nada interno — si no encuentra la respuesta, lo dice en vez de
  inventar.
- Widget público (/widget/umind.js + /widget/:site_key/*), autenticado
  por site_key + validación de dominio (Origin/Referer), no por
  secreto, ya que la key viaja en el HTML público del sitio instalado.
- Panel /app/umind: tenants, estado de ingesta, historial de
  conversaciones por sesión.
2026-08-07 16:18:16 +00:00

67 lines
2.0 KiB
Go

package middlewares
import (
"net/url"
"strings"
"github.com/gofiber/fiber/v2"
"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.
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)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": true, "message": "sitio no encontrado"})
}
origen := c.Get("Origin")
if origen == "" {
origen = c.Get("Referer")
}
host := hostDeOrigen(origen)
if !tenant.DominioPermitido(host) {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": true, "message": "dominio no autorizado para este sitio"})
}
if origen != "" {
c.Set("Access-Control-Allow-Origin", origen)
c.Set("Vary", "Origin")
}
c.Locals("umind_tenant", tenant)
return c.Next()
}
// UmindWidgetCORS responde el preflight OPTIONS que el navegador manda antes
// del POST real (el widget corre en un dominio distinto al de la API).
func UmindWidgetCORS(c *fiber.Ctx) error {
origen := c.Get("Origin")
if origen != "" {
c.Set("Access-Control-Allow-Origin", origen)
c.Set("Vary", "Origin")
}
c.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
c.Set("Access-Control-Allow-Headers", "Content-Type")
c.Set("Access-Control-Max-Age", "600")
return c.SendStatus(fiber.StatusNoContent)
}
func hostDeOrigen(origenURL string) string {
if origenURL == "" {
return ""
}
u, err := url.Parse(origenURL)
if err != nil {
return ""
}
return strings.ToLower(u.Hostname())
}