Files
soft_usite/rest/controllers/cloudflare_controller.go
T
2026-05-11 22:13:43 -05:00

385 lines
13 KiB
Go

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 el token y devuelve sus permisos reales.
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"})
}
dirty := len([]rune(raw)) != len([]rune(token))
runes := []rune(token)
preview := ""
if len(runes) >= 8 {
preview = string(runes[:4]) + "…" + string(runes[len(runes)-4:])
}
client := services.NewCloudflareClient(token, sanitizeCFToken(cfg.AccountID))
// 1. Verificar token
verify, err := client.VerifyToken()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"ok": false,
"step": "verify_token",
"error": err.Error(),
"hint": "Token inválido. Crea uno nuevo en dash.cloudflare.com → Mi Perfil → API Tokens",
"token_len": len(token),
"token_prev": preview,
"was_dirty": dirty,
})
}
// 2. Obtener políticas reales del token
detail, detailErr := client.GetTokenDetail(verify.ID)
detailErrMsg := ""
if detailErr != nil {
detailErrMsg = detailErr.Error()
}
// 3. Probar Zone:Zone:Read
zones, zonesErr := client.GetZones()
zonesOK := zonesErr == nil
zonesErrMsg := ""
if zonesErr != nil {
zonesErrMsg = zonesErr.Error()
}
// 4. Probar Zone:DNS:Read en la primera zona
dnsOK := false
dnsErrMsg := ""
dnsZoneID := ""
if zonesOK && len(zones) > 0 {
dnsZoneID = zones[0].ID
_, dnsErr := client.GetDNSRecords(dnsZoneID)
dnsOK = dnsErr == nil
if dnsErr != nil {
dnsErrMsg = dnsErr.Error()
}
}
hint := "Token válido con todos los permisos necesarios"
if !zonesOK {
hint = "Falta permiso Zone:Zone:Read"
} else if !dnsOK {
hint = "Falta permiso Zone:DNS:Read — o el token tiene scope de zonas específicas y no incluye la zona consultada. En Cloudflare: edita el token → Zone Resources → cambia a 'All zones'."
}
return c.JSON(fiber.Map{
"ok": true,
"token_status": verify.Status,
"token_id": verify.ID,
"token_len": len(token),
"token_prev": preview,
"was_dirty": dirty,
"detail_error": detailErrMsg,
"policies": func() interface{} { if detail != nil { return detail.Policies }; return nil }(),
"permissions_test": fiber.Map{
"zone_read": fiber.Map{"ok": zonesOK, "error": zonesErrMsg},
"dns_read": fiber.Map{"ok": dnsOK, "error": dnsErrMsg, "zone_tested": dnsZoneID},
},
"hint": hint,
})
}
// ─── 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"})
}