diff --git a/pkg/services/cloudflare_service.go b/pkg/services/cloudflare_service.go index 0b6e1d6..8d444ca 100644 --- a/pkg/services/cloudflare_service.go +++ b/pkg/services/cloudflare_service.go @@ -96,16 +96,16 @@ type CFUser struct { // CFZone representa una zona (dominio) en Cloudflare. type CFZone struct { - ID string `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Paused bool `json:"paused"` - Type string `json:"type"` - NameServers []string `json:"name_servers"` - OriginalNS []string `json:"original_name_servers"` - ModifiedOn string `json:"modified_on"` - ActivatedOn string `json:"activated_on"` - Plan CFPlan `json:"plan"` + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Paused bool `json:"paused"` + Type string `json:"type"` + NameServers []string `json:"name_servers"` + OriginalNS []string `json:"original_name_servers"` + ModifiedOn string `json:"modified_on"` + ActivatedOn string `json:"activated_on"` + Plan CFPlan `json:"plan"` } // CFPlan representa el plan de una zona. @@ -116,26 +116,26 @@ type CFPlan struct { // CFDNSRecord representa un registro DNS. type CFDNSRecord struct { - ID string `json:"id"` - Type string `json:"type"` - Name string `json:"name"` - Content string `json:"content"` - Proxied bool `json:"proxied"` - Proxiable bool `json:"proxiable"` - TTL int `json:"ttl"` - Priority int `json:"priority,omitempty"` - CreatedOn string `json:"created_on"` + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Content string `json:"content"` + Proxied bool `json:"proxied"` + Proxiable bool `json:"proxiable"` + TTL int `json:"ttl"` + Priority int `json:"priority,omitempty"` + CreatedOn string `json:"created_on"` ModifiedOn string `json:"modified_on"` } // CFSSLStatus representa el estado SSL de una zona. type CFSSLStatus struct { - ID string `json:"id"` - Type string `json:"type"` - Status string `json:"status"` - Hosts []string `json:"hosts"` - PrimaryCert string `json:"primary_certificate"` - ExpiresOn string `json:"expires_on"` + ID string `json:"id"` + Type string `json:"type"` + Status string `json:"status"` + Hosts []string `json:"hosts"` + PrimaryCert string `json:"primary_certificate"` + ExpiresOn string `json:"expires_on"` } // CFFirewallRule representa una regla de firewall (Access Rules). diff --git a/rest/controllers/cloudflare_controller.go b/rest/controllers/cloudflare_controller.go index fcdc5d2..6d3f8bd 100644 --- a/rest/controllers/cloudflare_controller.go +++ b/rest/controllers/cloudflare_controller.go @@ -2,12 +2,28 @@ 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. @@ -41,8 +57,8 @@ func SaveCloudflareConfig(c *fiber.Ctx) error { } cfg := models.CloudflareConfig{ - APIToken: strings.TrimSpace(b.APIToken), - AccountID: strings.TrimSpace(b.AccountID), + APIToken: sanitizeCFToken(b.APIToken), + AccountID: sanitizeCFToken(b.AccountID), Nota: b.Nota, } cfg.ID = b.ID @@ -55,12 +71,34 @@ func SaveCloudflareConfig(c *fiber.Ctx) error { // ─── 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(strings.TrimSpace(cfg.APIToken), strings.TrimSpace(cfg.AccountID)), nil + return services.NewCloudflareClient(sanitizeCFToken(cfg.APIToken), sanitizeCFToken(cfg.AccountID)), nil } // VerifyCloudflareToken verifica que el token activo sea válido contra la API de Cloudflare. @@ -72,16 +110,29 @@ func VerifyCloudflareToken(c *fiber.Ctx) error { "error": "No hay configuración activa de Cloudflare", }) } - token := strings.TrimSpace(cfg.APIToken) + raw := cfg.APIToken + token := sanitizeCFToken(raw) if token == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"ok": false, "error": "Token vacío"}) } - client := services.NewCloudflareClient(token, strings.TrimSpace(cfg.AccountID)) + // 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()}) + 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}) + return c.JSON(fiber.Map{ + "ok": true, + "data": result, + "token_len": len(token), + "was_dirty": dirty, + }) } // ─── Endpoints de datos ─────────────────────────────────────────────────────── diff --git a/rest/routes/user.go b/rest/routes/user.go index 3da54e1..1e90dbf 100755 --- a/rest/routes/user.go +++ b/rest/routes/user.go @@ -23,11 +23,11 @@ func UserRoutes(app fiber.Router) { app.Get("/web", func(c *fiber.Ctx) error { return c.Redirect("/", http.StatusSeeOther) }) protected.Get("/", controllers.App) protected.Get("/dashboard", middlewares.MenuMiddleware, controllers.Dashboard) - protected.Get("/modules", middlewares.MenuMiddleware, controllers.Modules) // Renderizar la vista - protected.Get("/loadmodules", controllers.GetModules) // Obtener todos los módulos - protected.Post("/modules", controllers.CreateModule) // Crear un nuevo módulo - protected.Put("/modules/:id", controllers.UpdateModule) // Actualizar un módulo existente - protected.Delete("/modules/:id", controllers.DeleteModule) // Eliminar un módulo + protected.Get("/modules", middlewares.MenuMiddleware, controllers.Modules) // Renderizar la vista + protected.Get("/loadmodules", controllers.GetModules) // Obtener todos los módulos + protected.Post("/modules", controllers.CreateModule) // Crear un nuevo módulo + protected.Put("/modules/:id", controllers.UpdateModule) // Actualizar un módulo existente + protected.Delete("/modules/:id", controllers.DeleteModule) // Eliminar un módulo // Rutas de roles protected.Get("/roles", middlewares.MenuMiddleware, controllers.Roles) // Renderizar la vista @@ -124,6 +124,7 @@ func UserRoutes(app fiber.Router) { protected.Get("/cloudflare", middlewares.MenuMiddleware, controllers.CloudflareConfigPage) protected.Post("/cloudflare/config", controllers.SaveCloudflareConfig) protected.Get("/cloudflare/verify", controllers.VerifyCloudflareToken) + protected.Post("/cloudflare/clean-token", controllers.CleanCloudflareToken) protected.Get("/cloudflare/user", controllers.GetCloudflareUser) protected.Get("/cloudflare/zones", controllers.GetCloudflareZones) protected.Get("/cloudflare/zones/:zone_id/dns", controllers.GetCloudflareDNS)