up
This commit is contained in:
@@ -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).
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user