package middlewares import ( "fmt" "log" "net/url" "strings" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) // 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"}) } // Se busca SIN filtrar por activo para poder distinguir "no existe" de // "está apagado". Estos rechazos ocurren antes del motor del agente, así // que sin registrarlos el síntoma es "el chat de prueba anda pero el // widget no" sin ninguna pista de por qué — que es exactamente el lazo en // el que se puede quedar alguien diagnosticando esto. agente, err := models.GetUmindAgentePorSiteKey(siteKey) if err != nil { log.Printf("[UMIND_WIDGET] site_key desconocida: %q (origen %q)", siteKey, c.Get("Origin")) return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": true, "message": "sitio no encontrado"}) } if !agente.Activo { models.RegistrarEventoUmind(agente.ID, "error", "widget", "El widget fue rechazado porque el agente está inactivo", "Activá el agente para que el widget vuelva a responder. El chat de prueba sí funciona con un agente inactivo, por eso la diferencia.") return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": true, "message": "sitio no encontrado"}) } tenant, err := models.GetUmindTenantByID(agente.TenantID) if err != nil { models.RegistrarEventoUmind(agente.ID, "error", "widget", "El widget no encontró el tenant del agente", err.Error()) return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": true, "message": "sitio no encontrado"}) } if !tenant.Activo { models.RegistrarEventoUmind(agente.ID, "error", "widget", "El widget fue rechazado porque el tenant está inactivo", fmt.Sprintf("Activá el tenant %q para que el widget vuelva a responder.", tenant.Nombre)) 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) { detalle := fmt.Sprintf("Llamada desde %q. Dominios permitidos del tenant: %q.", host, tenant.DominiosPermitidos) if strings.TrimSpace(tenant.DominiosPermitidos) == "" { detalle = fmt.Sprintf("Llamada desde %q, pero el tenant no tiene ningún dominio permitido configurado (la lista vacía no permite nada).", host) } else if host == "" { detalle = "La petición llegó sin Origin ni Referer, así que no se pudo verificar de qué sitio viene." } models.RegistrarEventoUmind(agente.ID, "error", "widget", "El widget fue rechazado por dominio no autorizado", detalle) 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_agente", agente) 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()) }