Es la base del cobro por uso: hasta ahora no había ninguna medición de consumo en todo el repo. - UmindUso registra cada evento facturable con el costo YA calculado al precio vigente del plan. Congelarlo evita que subir un precio revalúe consumo pasado, que haría indefendible una factura ante un reclamo. - callAI devuelve los tokens que reportó el proveedor (campo usage, igual en todos los OpenAI-compatibles; input+output en Anthropic). Se mide cada ronda de tool-calling, no solo la última: todas gastan tokens. - ExtraerTextoOCR y TranscribirAudioSelfHosted reciben agenteID; 0 = no medir, que es lo que pasan los botones "Probar" del panel de staff. - Aviso al superar el tope del plan, una vez por mes y sin cortar el servicio. El flag de "ya avisé" es en memoria a propósito. - GET /app/umind/uso con filtros de fecha: resumen por tipo + detalle. - Test del cálculo de costo por tipo, incluida fracción de 1k tokens y tenant sin plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
549 lines
18 KiB
Go
549 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"})
|
||
}
|