This commit is contained in:
Lizandro Guarnizo
2026-05-11 21:54:00 -05:00
parent c362735cac
commit 11b0265b04
3 changed files with 89 additions and 37 deletions
+25 -25
View File
@@ -96,16 +96,16 @@ type CFUser struct {
// CFZone representa una zona (dominio) en Cloudflare. // CFZone representa una zona (dominio) en Cloudflare.
type CFZone struct { type CFZone struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Status string `json:"status"` Status string `json:"status"`
Paused bool `json:"paused"` Paused bool `json:"paused"`
Type string `json:"type"` Type string `json:"type"`
NameServers []string `json:"name_servers"` NameServers []string `json:"name_servers"`
OriginalNS []string `json:"original_name_servers"` OriginalNS []string `json:"original_name_servers"`
ModifiedOn string `json:"modified_on"` ModifiedOn string `json:"modified_on"`
ActivatedOn string `json:"activated_on"` ActivatedOn string `json:"activated_on"`
Plan CFPlan `json:"plan"` Plan CFPlan `json:"plan"`
} }
// CFPlan representa el plan de una zona. // CFPlan representa el plan de una zona.
@@ -116,26 +116,26 @@ type CFPlan struct {
// CFDNSRecord representa un registro DNS. // CFDNSRecord representa un registro DNS.
type CFDNSRecord struct { type CFDNSRecord struct {
ID string `json:"id"` ID string `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
Content string `json:"content"` Content string `json:"content"`
Proxied bool `json:"proxied"` Proxied bool `json:"proxied"`
Proxiable bool `json:"proxiable"` Proxiable bool `json:"proxiable"`
TTL int `json:"ttl"` TTL int `json:"ttl"`
Priority int `json:"priority,omitempty"` Priority int `json:"priority,omitempty"`
CreatedOn string `json:"created_on"` CreatedOn string `json:"created_on"`
ModifiedOn string `json:"modified_on"` ModifiedOn string `json:"modified_on"`
} }
// CFSSLStatus representa el estado SSL de una zona. // CFSSLStatus representa el estado SSL de una zona.
type CFSSLStatus struct { type CFSSLStatus struct {
ID string `json:"id"` ID string `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Status string `json:"status"` Status string `json:"status"`
Hosts []string `json:"hosts"` Hosts []string `json:"hosts"`
PrimaryCert string `json:"primary_certificate"` PrimaryCert string `json:"primary_certificate"`
ExpiresOn string `json:"expires_on"` ExpiresOn string `json:"expires_on"`
} }
// CFFirewallRule representa una regla de firewall (Access Rules). // CFFirewallRule representa una regla de firewall (Access Rules).
+58 -7
View File
@@ -2,12 +2,28 @@ package controllers
import ( import (
"strings" "strings"
"unicode"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/models"
"github.com/sujit-baniya/fiber-boilerplate/pkg/services" "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 ──────────────────────────────────────────────────────────── // ─── Configuración ────────────────────────────────────────────────────────────
// CloudflareConfigPage renderiza la vista de gestión de Cloudflare. // CloudflareConfigPage renderiza la vista de gestión de Cloudflare.
@@ -41,8 +57,8 @@ func SaveCloudflareConfig(c *fiber.Ctx) error {
} }
cfg := models.CloudflareConfig{ cfg := models.CloudflareConfig{
APIToken: strings.TrimSpace(b.APIToken), APIToken: sanitizeCFToken(b.APIToken),
AccountID: strings.TrimSpace(b.AccountID), AccountID: sanitizeCFToken(b.AccountID),
Nota: b.Nota, Nota: b.Nota,
} }
cfg.ID = b.ID cfg.ID = b.ID
@@ -55,12 +71,34 @@ func SaveCloudflareConfig(c *fiber.Ctx) error {
// ─── Helper ─────────────────────────────────────────────────────────────────── // ─── 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) { func cloudflareClient() (*services.CloudflareClient, error) {
cfg, err := models.GetCloudflareConfig() cfg, err := models.GetCloudflareConfig()
if err != nil { if err != nil {
return nil, err 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. // 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", "error": "No hay configuración activa de Cloudflare",
}) })
} }
token := strings.TrimSpace(cfg.APIToken) raw := cfg.APIToken
token := sanitizeCFToken(raw)
if token == "" { if token == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"ok": false, "error": "Token vacío"}) 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() result, err := client.VerifyToken()
if err != nil { 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 ─────────────────────────────────────────────────────── // ─── Endpoints de datos ───────────────────────────────────────────────────────
+6 -5
View File
@@ -23,11 +23,11 @@ func UserRoutes(app fiber.Router) {
app.Get("/web", func(c *fiber.Ctx) error { return c.Redirect("/", http.StatusSeeOther) }) app.Get("/web", func(c *fiber.Ctx) error { return c.Redirect("/", http.StatusSeeOther) })
protected.Get("/", controllers.App) protected.Get("/", controllers.App)
protected.Get("/dashboard", middlewares.MenuMiddleware, controllers.Dashboard) protected.Get("/dashboard", middlewares.MenuMiddleware, controllers.Dashboard)
protected.Get("/modules", middlewares.MenuMiddleware, controllers.Modules) // Renderizar la vista protected.Get("/modules", middlewares.MenuMiddleware, controllers.Modules) // Renderizar la vista
protected.Get("/loadmodules", controllers.GetModules) // Obtener todos los módulos protected.Get("/loadmodules", controllers.GetModules) // Obtener todos los módulos
protected.Post("/modules", controllers.CreateModule) // Crear un nuevo módulo protected.Post("/modules", controllers.CreateModule) // Crear un nuevo módulo
protected.Put("/modules/:id", controllers.UpdateModule) // Actualizar un módulo existente protected.Put("/modules/:id", controllers.UpdateModule) // Actualizar un módulo existente
protected.Delete("/modules/:id", controllers.DeleteModule) // Eliminar un módulo protected.Delete("/modules/:id", controllers.DeleteModule) // Eliminar un módulo
// Rutas de roles // Rutas de roles
protected.Get("/roles", middlewares.MenuMiddleware, controllers.Roles) // Renderizar la vista 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.Get("/cloudflare", middlewares.MenuMiddleware, controllers.CloudflareConfigPage)
protected.Post("/cloudflare/config", controllers.SaveCloudflareConfig) protected.Post("/cloudflare/config", controllers.SaveCloudflareConfig)
protected.Get("/cloudflare/verify", controllers.VerifyCloudflareToken) protected.Get("/cloudflare/verify", controllers.VerifyCloudflareToken)
protected.Post("/cloudflare/clean-token", controllers.CleanCloudflareToken)
protected.Get("/cloudflare/user", controllers.GetCloudflareUser) protected.Get("/cloudflare/user", controllers.GetCloudflareUser)
protected.Get("/cloudflare/zones", controllers.GetCloudflareZones) protected.Get("/cloudflare/zones", controllers.GetCloudflareZones)
protected.Get("/cloudflare/zones/:zone_id/dns", controllers.GetCloudflareDNS) protected.Get("/cloudflare/zones/:zone_id/dns", controllers.GetCloudflareDNS)