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.
67 lines
2.0 KiB
Go
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())
|
|
}
|