Files
soft_usite/rest/controllers/saas_api_controller.go
T
2026-05-16 21:41:44 -05:00

358 lines
11 KiB
Go

package controllers
import (
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/helpers"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
// ─── Panel: lista de configuraciones ─────────────────────────────────────────
// SaasApiIndex renderiza la vista del panel de integraciones SaaS.
func SaasApiIndex(c *fiber.Ctx) error {
data := fiber.Map{
"user": c.Locals("user").(map[string]interface{}),
"modules": c.Locals("userModules"),
}
if err := c.Render("saas_api", data, "layouts/main"); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return nil
}
// GetSaasApiConfigs devuelve la lista paginada en JSON.
func GetSaasApiConfigs(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.GetAllSaasApiConfigs(limit, offset)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
saasOpts, _ := models.GetAllSaasProductosSelect()
return c.JSON(fiber.Map{
"items": items,
"saas": saasOpts,
"total": total,
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
"page": page,
"limit": limit,
})
}
// CreateSaasApiConfig crea una nueva configuración de integración.
func CreateSaasApiConfig(c *fiber.Ctx) error {
type Req struct {
SaasID uint `json:"saas_id"`
Nombre string `json:"nombre"`
Pasarela string `json:"pasarela"`
EndpointURL string `json:"endpoint_url"`
Metodo string `json:"metodo"`
ApiKeyHeader string `json:"api_key_header"`
ApiKeyValue string `json:"api_key_value"`
PayloadTemplate string `json:"payload_template"`
TimeoutSeg int `json:"timeout_seg"`
Activo bool `json:"activo"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
}
if req.SaasID == 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "saas_id es requerido"})
}
if strings.TrimSpace(req.Nombre) == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "nombre es requerido"})
}
if strings.TrimSpace(req.EndpointURL) == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "endpoint_url es requerido"})
}
metodo := strings.ToUpper(strings.TrimSpace(req.Metodo))
if metodo == "" {
metodo = "POST"
}
pasarela := strings.ToLower(strings.TrimSpace(req.Pasarela))
if pasarela == "" {
pasarela = "ambas"
}
timeout := req.TimeoutSeg
if timeout <= 0 {
timeout = 10
}
item := models.SaasApiConfig{
SaasID: req.SaasID,
Nombre: req.Nombre,
Pasarela: pasarela,
EndpointURL: req.EndpointURL,
Metodo: metodo,
ApiKeyHeader: req.ApiKeyHeader,
ApiKeyValue: req.ApiKeyValue,
PayloadTemplate: req.PayloadTemplate,
TimeoutSeg: timeout,
Activo: req.Activo,
WebhookToken: helpers.RandomString(32),
}
if err := models.CreateSaasApiConfig(&item); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(item)
}
// UpdateSaasApiConfig actualiza una configuración existente.
func UpdateSaasApiConfig(c *fiber.Ctx) error {
idParam := c.Params("id")
id64, err := strconv.ParseUint(idParam, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
}
item, err := models.GetSaasApiConfigByID(uint(id64))
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "no encontrado"})
}
type Req struct {
SaasID uint `json:"saas_id"`
Nombre string `json:"nombre"`
Pasarela string `json:"pasarela"`
EndpointURL string `json:"endpoint_url"`
Metodo string `json:"metodo"`
ApiKeyHeader string `json:"api_key_header"`
ApiKeyValue string `json:"api_key_value"`
PayloadTemplate string `json:"payload_template"`
TimeoutSeg int `json:"timeout_seg"`
Activo bool `json:"activo"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
}
if req.SaasID != 0 {
item.SaasID = req.SaasID
}
if strings.TrimSpace(req.Nombre) != "" {
item.Nombre = req.Nombre
}
pasarela := strings.ToLower(strings.TrimSpace(req.Pasarela))
if pasarela != "" {
item.Pasarela = pasarela
}
if strings.TrimSpace(req.EndpointURL) != "" {
item.EndpointURL = req.EndpointURL
}
metodo := strings.ToUpper(strings.TrimSpace(req.Metodo))
if metodo != "" {
item.Metodo = metodo
}
item.ApiKeyHeader = req.ApiKeyHeader
// Solo actualizar api_key_value si se envió un valor (para no borrar el secreto con "")
if req.ApiKeyValue != "" {
item.ApiKeyValue = req.ApiKeyValue
}
item.PayloadTemplate = req.PayloadTemplate
if req.TimeoutSeg > 0 {
item.TimeoutSeg = req.TimeoutSeg
}
item.Activo = req.Activo
if err := models.UpdateSaasApiConfig(item); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(item)
}
// DeleteSaasApiConfig elimina una configuración.
func DeleteSaasApiConfig(c *fiber.Ctx) error {
idParam := c.Params("id")
id64, err := strconv.ParseUint(idParam, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
}
if err := models.DeleteSaasApiConfig(uint(id64)); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
// ProbaSaasApiConfig envía una petición de prueba al endpoint configurado.
// Body JSON opcional: { "payload": "..." } — si no se envía usa el payload_template tal cual.
func ProbaSaasApiConfig(c *fiber.Ctx) error {
idParam := c.Params("id")
id64, err := strconv.ParseUint(idParam, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
}
cfg, err := models.GetSaasApiConfigByID(uint(id64))
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "no encontrado"})
}
type Req struct {
Payload string `json:"payload"`
}
var req Req
_ = c.BodyParser(&req)
body := strings.TrimSpace(req.Payload)
if body == "" {
body = strings.TrimSpace(cfg.PayloadTemplate)
}
timeout := cfg.TimeoutSeg
if timeout <= 0 {
timeout = 10
}
client := &http.Client{Timeout: time.Duration(timeout) * time.Second}
var httpReq *http.Request
metodo := strings.ToUpper(cfg.Metodo)
if metodo == "" {
metodo = "POST"
}
if body != "" && metodo != "GET" {
httpReq, err = http.NewRequest(metodo, cfg.EndpointURL, strings.NewReader(body))
if err != nil {
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": "URL inválida: " + err.Error()})
}
httpReq.Header.Set("Content-Type", "application/json")
} else {
httpReq, err = http.NewRequest(metodo, cfg.EndpointURL, nil)
if err != nil {
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": "URL inválida: " + err.Error()})
}
}
if cfg.ApiKeyHeader != "" && cfg.ApiKeyValue != "" {
httpReq.Header.Set(cfg.ApiKeyHeader, cfg.ApiKeyValue)
}
httpReq.Header.Set("User-Agent", "u-site-tester/1.0")
start := time.Now()
resp, err := client.Do(httpReq)
latency := time.Since(start).Milliseconds()
if err != nil {
return c.JSON(fiber.Map{
"ok": false,
"error": err.Error(),
"latency_ms": latency,
})
}
defer resp.Body.Close()
respBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) // máx 64 KB
return c.JSON(fiber.Map{
"ok": resp.StatusCode >= 200 && resp.StatusCode < 300,
"http_status": resp.StatusCode,
"body": string(respBytes),
"latency_ms": latency,
})
}
// ─── Panel: logs de despacho ──────────────────────────────────────────────────
// SaasDispatchLogIndex renderiza la vista de logs de despacho.
func SaasDispatchLogIndex(c *fiber.Ctx) error {
data := fiber.Map{
"user": c.Locals("user").(map[string]interface{}),
"modules": c.Locals("userModules"),
}
if err := c.Render("saas_dispatch_logs", data, "layouts/main"); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return nil
}
// GetSaasDispatchLogs devuelve los logs de despacho paginados.
func GetSaasDispatchLogs(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
if page < 1 {
page = 1
}
limit := 30
offset := (page - 1) * limit
items, total, err := models.GetSaasDispatchLogs(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,
"limit": limit,
})
}
// ─── Webhook entrante por token ───────────────────────────────────────────────
// SaasWebhookInHandler recibe llamadas entrantes del SaaS externo identificadas
// por el WebhookToken único de la integración. No requiere autenticación.
// Ruta pública: POST /webhooks/saas-in/:token
func SaasWebhookInHandler(c *fiber.Ctx) error {
token := strings.TrimSpace(c.Params("token"))
if token == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "token requerido"})
}
cfg, err := models.GetSaasApiConfigByWebhookToken(token)
if err != nil {
// Responder 200 para evitar reintentos agresivos de sistemas externos
log.Printf("[SaasWebhookIn] Token desconocido: %s", token)
return c.Status(fiber.StatusOK).JSON(fiber.Map{"ok": false, "msg": "token no reconocido"})
}
// Capturar headers relevantes como JSON
headersMap := map[string]string{}
c.Request().Header.VisitAll(func(k, v []byte) {
key := string(k)
// Solo guardar headers informativos, excluir cookies/auth
if key != "Cookie" && key != "Authorization" {
headersMap[key] = string(v)
}
})
headersJSON, _ := json.Marshal(headersMap)
rawBody := string(c.Body())
entry := models.SaasWebhookInLog{
SaasApiConfigID: cfg.ID,
Token: token,
Metodo: c.Method(),
IP: c.IP(),
Body: rawBody,
Headers: string(headersJSON),
}
_ = models.SaveSaasWebhookInLog(&entry)
log.Printf("[SaasWebhookIn] Integración '%s' (ID=%d) recibió callback desde IP=%s method=%s",
cfg.Nombre, cfg.ID, c.IP(), c.Method())
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"ok": true,
"integracion": cfg.Nombre,
"saas_api_id": cfg.ID,
"message": fmt.Sprintf("Webhook recibido para integración: %s", cfg.Nombre),
})
}