package controllers import ( "strings" "unicode" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/services" ) // sanitizeCFToken elimina espacios, saltos de línea, BOM y cualquier caracter // no imprimible que pueda romper la autenticación Bearer de Cloudflare. func sanitizeCFToken(s string) string { // Eliminar BOM UTF-8 si existe s = strings.TrimPrefix(s, "\xef\xbb\xbf") // Filtrar solo rómanos imprimibles (printable ASCII, excluye control chars) var b strings.Builder for _, r := range s { if r > 32 && r < 127 && unicode.IsPrint(r) { b.WriteRune(r) } } return b.String() } // ─── Configuración ──────────────────────────────────────────────────────────── // CloudflareConfigPage renderiza la vista de gestión de Cloudflare. func CloudflareConfigPage(c *fiber.Ctx) error { cfg, _ := models.GetCloudflareConfig() if err := c.Render("cloudflare", fiber.Map{ "Title": "Cloudflare API", "Config": cfg, "user": c.Locals("user"), "modules": c.Locals("userModules"), }, "layouts/main"); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return nil } // SaveCloudflareConfig guarda o actualiza el API token de Cloudflare. func SaveCloudflareConfig(c *fiber.Ctx) error { type body struct { ID uint `json:"id" form:"id"` APIToken string `json:"api_token" form:"api_token"` AccountID string `json:"account_id" form:"account_id"` Nota string `json:"nota" form:"nota"` } var b body if err := c.BodyParser(&b); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) } if b.APIToken == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "api_token requerido"}) } cfg := models.CloudflareConfig{ APIToken: sanitizeCFToken(b.APIToken), AccountID: sanitizeCFToken(b.AccountID), Nota: b.Nota, } cfg.ID = b.ID if err := models.SaveCloudflareConfig(cfg); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"message": "Configuración guardada"}) } // ─── Helper ─────────────────────────────────────────────────────────────────── // CleanCloudflareToken re-guarda el token activo aplicando saneamiento. // Útil cuando el token fue guardado antes del fix de caracteres invisibles. func CleanCloudflareToken(c *fiber.Ctx) error { cfg, err := models.GetCloudflareConfig() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Sin configuración activa"}) } before := len([]rune(cfg.APIToken)) cfg.APIToken = sanitizeCFToken(cfg.APIToken) cfg.AccountID = sanitizeCFToken(cfg.AccountID) after := len([]rune(cfg.APIToken)) if err := models.SaveCloudflareConfig(*cfg); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "message": "Token re-guardado limpio", "chars_before": before, "chars_after": after, "chars_removed": before - after, }) } func cloudflareClient() (*services.CloudflareClient, error) { cfg, err := models.GetCloudflareConfig() if err != nil { return nil, err } return services.NewCloudflareClient(sanitizeCFToken(cfg.APIToken), sanitizeCFToken(cfg.AccountID)), nil } // VerifyCloudflareToken verifica que el token activo sea válido contra la API de Cloudflare. func VerifyCloudflareToken(c *fiber.Ctx) error { cfg, err := models.GetCloudflareConfig() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "ok": false, "error": "No hay configuración activa de Cloudflare", }) } raw := cfg.APIToken token := sanitizeCFToken(raw) if token == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"ok": false, "error": "Token vacío"}) } // Si la longitud cambia después de sanear, había caracteres basura dirty := len([]rune(raw)) != len([]rune(token)) client := services.NewCloudflareClient(token, sanitizeCFToken(cfg.AccountID)) result, err := client.VerifyToken() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "ok": false, "error": err.Error(), "token_len": len(token), "was_dirty": dirty, }) } return c.JSON(fiber.Map{ "ok": true, "data": result, "token_len": len(token), "was_dirty": dirty, }) } // ─── Endpoints de datos ─────────────────────────────────────────────────────── // GetCloudflareUser devuelve la información del usuario autenticado en Cloudflare. func GetCloudflareUser(c *fiber.Ctx) error { client, err := cloudflareClient() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "error": "No se encontró configuración activa de Cloudflare", }) } data, err := client.GetUser() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"data": data}) } // GetCloudflareZones lista todas las zonas (dominios) de la cuenta. func GetCloudflareZones(c *fiber.Ctx) error { client, err := cloudflareClient() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "error": "No se encontró configuración activa de Cloudflare", }) } data, err := client.GetZones() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"data": data}) } // GetCloudflareDNS devuelve los registros DNS de una zona (:zone_id). func GetCloudflareDNS(c *fiber.Ctx) error { zoneID := c.Params("zone_id") if zoneID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "zone_id requerido"}) } client, err := cloudflareClient() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "error": "No se encontró configuración activa de Cloudflare", }) } data, err := client.GetDNSRecords(zoneID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"data": data, "zone_id": zoneID}) } // GetCloudflareSSL devuelve los certificados SSL de una zona (:zone_id). func GetCloudflareSSL(c *fiber.Ctx) error { zoneID := c.Params("zone_id") if zoneID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "zone_id requerido"}) } client, err := cloudflareClient() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "error": "No se encontró configuración activa de Cloudflare", }) } data, err := client.GetSSLCertificates(zoneID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"data": data, "zone_id": zoneID}) } // GetCloudflareFirewall devuelve las reglas de firewall de una zona (:zone_id). func GetCloudflareFirewall(c *fiber.Ctx) error { zoneID := c.Params("zone_id") if zoneID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "zone_id requerido"}) } client, err := cloudflareClient() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "error": "No se encontró configuración activa de Cloudflare", }) } data, err := client.GetFirewallRules(zoneID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"data": data, "zone_id": zoneID}) } // ─── DNS CRUD ───────────────────────────────────────────────────────────────── // CreateCloudflareDNS crea un nuevo registro DNS en la zona (:zone_id). func CreateCloudflareDNS(c *fiber.Ctx) error { zoneID := c.Params("zone_id") if zoneID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "zone_id requerido"}) } var input services.CFDNSRecordInput if err := c.BodyParser(&input); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) } if input.Type == "" || input.Name == "" || input.Content == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type, name y content son requeridos"}) } if input.TTL == 0 { input.TTL = 1 // automático } client, err := cloudflareClient() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "error": "No se encontró configuración activa de Cloudflare", }) } record, err := client.CreateDNSRecord(zoneID, input) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.Status(fiber.StatusCreated).JSON(fiber.Map{"data": record}) } // UpdateCloudflareDNS actualiza (PUT completo) un registro DNS (:zone_id/:record_id). func UpdateCloudflareDNS(c *fiber.Ctx) error { zoneID := c.Params("zone_id") recordID := c.Params("record_id") if zoneID == "" || recordID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "zone_id y record_id requeridos"}) } var input services.CFDNSRecordInput if err := c.BodyParser(&input); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) } if input.Type == "" || input.Name == "" || input.Content == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type, name y content son requeridos"}) } if input.TTL == 0 { input.TTL = 1 } client, err := cloudflareClient() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "error": "No se encontró configuración activa de Cloudflare", }) } record, err := client.UpdateDNSRecord(zoneID, recordID, input) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"data": record}) } // PatchCloudflareDNS actualiza parcialmente un registro DNS (:zone_id/:record_id). func PatchCloudflareDNS(c *fiber.Ctx) error { zoneID := c.Params("zone_id") recordID := c.Params("record_id") if zoneID == "" || recordID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "zone_id y record_id requeridos"}) } var input services.CFDNSRecordInput if err := c.BodyParser(&input); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) } client, err := cloudflareClient() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "error": "No se encontró configuración activa de Cloudflare", }) } record, err := client.PatchDNSRecord(zoneID, recordID, input) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"data": record}) } // DeleteCloudflareDNS elimina un registro DNS (:zone_id/:record_id). func DeleteCloudflareDNS(c *fiber.Ctx) error { zoneID := c.Params("zone_id") recordID := c.Params("record_id") if zoneID == "" || recordID == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "zone_id y record_id requeridos"}) } client, err := cloudflareClient() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "error": "No se encontró configuración activa de Cloudflare", }) } if err := client.DeleteDNSRecord(zoneID, recordID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"message": "Registro DNS eliminado"}) }