539 lines
18 KiB
Go
539 lines
18 KiB
Go
package controllers
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"unicode"
|
||
|
||
"github.com/gofiber/fiber/v2"
|
||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||
)
|
||
|
||
// sanitizeCFToken limpia el token antes de usarlo como Bearer en Cloudflare.
|
||
// Cubre los casos más comunes de pegado incorrecto desde el dashboard:
|
||
// - BOM UTF-8
|
||
// - Prefijo "Bearer " copiado accidentalmente
|
||
// - Comillas simples/dobles alrededor del token
|
||
// - Espacios y caracteres de control (tabulaciones, saltos de línea)
|
||
// - Caracteres fuera del rango ASCII imprimible (U+0021–U+007E)
|
||
func sanitizeCFToken(s string) string {
|
||
// 1. BOM UTF-8
|
||
s = strings.TrimPrefix(s, "\xef\xbb\xbf")
|
||
// 2. Espacios al inicio/fin (cubre paste con espacio visible)
|
||
s = strings.TrimSpace(s)
|
||
// 3. Prefijo "Bearer " copiado accidentalmente (case-insensitive)
|
||
if after, ok := strings.CutPrefix(strings.ToLower(s), "bearer "); ok {
|
||
s = s[len(s)-len(after):] // preservar casing original del token
|
||
}
|
||
s = strings.TrimSpace(s)
|
||
// 4. Comillas envolventes (simples o dobles)
|
||
if len(s) >= 2 {
|
||
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '\'' && s[len(s)-1] == '\'') {
|
||
s = s[1 : len(s)-1]
|
||
}
|
||
}
|
||
// 5. Solo ASCII imprimible (U+0021–U+007E), descarta control chars y unicode
|
||
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 las credenciales de Cloudflare.
|
||
func SaveCloudflareConfig(c *fiber.Ctx) error {
|
||
type body struct {
|
||
ID uint `json:"id" form:"id"`
|
||
AuthType string `json:"auth_type" form:"auth_type"` // "token" o "global_key"
|
||
APIToken string `json:"api_token" form:"api_token"`
|
||
Email string `json:"email" form:"email"`
|
||
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"})
|
||
}
|
||
if b.AuthType == "" {
|
||
b.AuthType = "token"
|
||
}
|
||
if b.AuthType == "global_key" && b.Email == "" {
|
||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "email requerido para auth_type=global_key"})
|
||
}
|
||
|
||
cfg := models.CloudflareConfig{
|
||
AuthType: b.AuthType,
|
||
APIToken: sanitizeCFToken(b.APIToken),
|
||
Email: strings.TrimSpace(b.Email),
|
||
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)
|
||
cfg.Email = strings.TrimSpace(cfg.Email)
|
||
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
|
||
}
|
||
if cfg.AuthType == "global_key" {
|
||
return services.NewCloudflareClientGlobalKey(
|
||
strings.TrimSpace(cfg.Email),
|
||
sanitizeCFToken(cfg.APIToken),
|
||
sanitizeCFToken(cfg.AccountID),
|
||
), nil
|
||
}
|
||
return services.NewCloudflareClient(sanitizeCFToken(cfg.APIToken), sanitizeCFToken(cfg.AccountID)), nil
|
||
}
|
||
|
||
// VerifyCloudflareToken verifica las credenciales 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 := raw != token
|
||
runes := []rune(token)
|
||
preview := token
|
||
if len(runes) > 14 {
|
||
preview = string(runes[:6]) + "…" + string(runes[len(runes)-6:])
|
||
}
|
||
|
||
isGlobal := cfg.AuthType == "global_key"
|
||
|
||
// Diagnóstico de formato según tipo
|
||
var tokenDiag fiber.Map
|
||
if isGlobal {
|
||
hintFormat := ""
|
||
if len(token) != 37 {
|
||
hintFormat = fmt.Sprintf(
|
||
"Longitud %d ≠ 37. La Global API Key tiene exactamente 37 caracteres hexadecimales.",
|
||
len(token),
|
||
)
|
||
}
|
||
tokenDiag = fiber.Map{
|
||
"auth_type": "global_key",
|
||
"length": len(token),
|
||
"expected_length": "37 caracteres (Global API Key)",
|
||
"preview": preview,
|
||
"was_sanitized": dirty,
|
||
"email": cfg.Email,
|
||
"hint_format": hintFormat,
|
||
}
|
||
} else {
|
||
hintFormat := ""
|
||
if len(token) != 40 {
|
||
hintFormat = fmt.Sprintf(
|
||
"Longitud %d ≠ 40. Posible token incompleto, con prefijo extra, o tipo incorrecto (¿estás usando Global API Key en vez de API Token?)",
|
||
len(token),
|
||
)
|
||
}
|
||
tokenDiag = fiber.Map{
|
||
"auth_type": "token",
|
||
"length": len(token),
|
||
"expected_length": "40 caracteres (API Token estándar)",
|
||
"preview": preview,
|
||
"was_sanitized": dirty,
|
||
"raw_length": len([]rune(raw)),
|
||
"hint_format": hintFormat,
|
||
}
|
||
}
|
||
|
||
var client *services.CloudflareClient
|
||
if isGlobal {
|
||
client = services.NewCloudflareClientGlobalKey(
|
||
strings.TrimSpace(cfg.Email),
|
||
token,
|
||
sanitizeCFToken(cfg.AccountID),
|
||
)
|
||
} else {
|
||
client = services.NewCloudflareClient(token, sanitizeCFToken(cfg.AccountID))
|
||
}
|
||
|
||
// ── Para Global API Key: no existe /user/tokens/verify, verificamos con /user ──
|
||
if isGlobal {
|
||
user, err := client.GetUser()
|
||
if err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||
"ok": false,
|
||
"step": "get_user",
|
||
"error": err.Error(),
|
||
"token": tokenDiag,
|
||
"hint": "Verifica que el email y la Global API Key sean correctos. La Global API Key se encuentra en dash.cloudflare.com → Mi Perfil → API Tokens → Global API Key → View.",
|
||
})
|
||
}
|
||
|
||
zones, zonesErr := client.GetZones()
|
||
zonesOK := zonesErr == nil
|
||
zonesErrMsg := ""
|
||
if zonesErr != nil {
|
||
zonesErrMsg = zonesErr.Error()
|
||
}
|
||
|
||
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()
|
||
}
|
||
}
|
||
|
||
return c.JSON(fiber.Map{
|
||
"ok": true,
|
||
"auth_type": "global_key",
|
||
"user_email": user.Email,
|
||
"user_id": user.ID,
|
||
"token": tokenDiag,
|
||
"permissions_test": fiber.Map{
|
||
"zone_read": fiber.Map{"ok": zonesOK, "error": zonesErrMsg},
|
||
"dns_read": fiber.Map{"ok": dnsOK, "error": dnsErrMsg, "zone_tested": dnsZoneID},
|
||
},
|
||
})
|
||
}
|
||
|
||
// ── API Token normal ──
|
||
verify, err := client.VerifyToken()
|
||
if err != nil {
|
||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||
"ok": false,
|
||
"step": "verify_token",
|
||
"error": err.Error(),
|
||
"token": tokenDiag,
|
||
"hint": "Ve a dash.cloudflare.com → Mi Perfil → API Tokens → crea un token nuevo con permisos Zone:Read y DNS:Read/Write",
|
||
})
|
||
}
|
||
|
||
// 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).
|
||
// Intenta primero certificate_packs?status=all; si falla (plan Free o sin ACM)
|
||
// devuelve el estado de Universal SSL como fallback.
|
||
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",
|
||
})
|
||
}
|
||
|
||
// 1. Intentar certificate_packs?status=all
|
||
packs, packsErr := client.GetSSLCertificates(zoneID)
|
||
if packsErr == nil {
|
||
return c.JSON(fiber.Map{"data": packs, "zone_id": zoneID, "source": "certificate_packs"})
|
||
}
|
||
|
||
// 2. Fallback: Universal SSL settings (disponible en todos los planes)
|
||
universal, uErr := client.GetUniversalSSLSettings(zoneID)
|
||
if uErr != nil {
|
||
// Devolver ambos errores para diagnóstico
|
||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||
"error": packsErr.Error(),
|
||
"universal_error": uErr.Error(),
|
||
"hint": "El endpoint ssl/certificate_packs requiere permiso 'SSL and Certificates:Read'. Si usas API Token, asegúrate de que tenga ese permiso.",
|
||
})
|
||
}
|
||
return c.JSON(fiber.Map{
|
||
"data": nil,
|
||
"zone_id": zoneID,
|
||
"source": "universal_ssl",
|
||
"universal_ssl": fiber.Map{
|
||
"enabled": universal.Enabled,
|
||
"note": "Plan actual no expone certificate_packs. Universal SSL: " + func() string { if universal.Enabled { return "ACTIVO" }; return "INACTIVO" }(),
|
||
},
|
||
"packs_error": packsErr.Error(),
|
||
})
|
||
}
|
||
|
||
// 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"})
|
||
}
|