From cb8e15ec4c00a110d3d0f7748bb41fc6e928e496 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:23:34 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20integraci=C3=B3n=20Hostinger=20+=20Clou?= =?UTF-8?q?dflare=20API=20(modelo,=20servicio,=20controlador,=20rutas,=20m?= =?UTF-8?q?igraci=C3=B3n)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/migrate.go | 3 + pkg/models/cloudflare_config.go | 62 +++++++ pkg/models/hostinger_config.go | 58 ++++++ pkg/services/cloudflare_service.go | 214 ++++++++++++++++++++++ pkg/services/hostinger_service.go | 192 +++++++++++++++++++ rest/controllers/cloudflare_controller.go | 146 +++++++++++++++ rest/controllers/hostinger_controller.go | 136 ++++++++++++++ rest/routes/user.go | 18 ++ 8 files changed, 829 insertions(+) create mode 100644 pkg/models/cloudflare_config.go create mode 100644 pkg/models/hostinger_config.go create mode 100644 pkg/services/cloudflare_service.go create mode 100644 pkg/services/hostinger_service.go create mode 100644 rest/controllers/cloudflare_controller.go create mode 100644 rest/controllers/hostinger_controller.go diff --git a/migrations/migrate.go b/migrations/migrate.go index 89e84bb..53238a8 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -50,6 +50,9 @@ func Migrate() { &models.Contrato{}, &models.NotificacionLog{}, &models.SmtpConfig{}, + // Integraciones externas + &models.HostingerConfig{}, + &models.CloudflareConfig{}, ); err != nil { log.Fatalf("Error during main migration: %v", err) } diff --git a/pkg/models/cloudflare_config.go b/pkg/models/cloudflare_config.go new file mode 100644 index 0000000..0cc1db3 --- /dev/null +++ b/pkg/models/cloudflare_config.go @@ -0,0 +1,62 @@ +package models + +import ( + "github.com/sujit-baniya/fiber-boilerplate/app" + "gorm.io/gorm" +) + +// CloudflareConfig almacena el API Token de Cloudflare. +// Solo un registro puede estar activo a la vez. +type CloudflareConfig struct { + gorm.Model + // API Token (recomendado) – se usa como Bearer token + APIToken string `json:"api_token" gorm:"column:api_token;type:text;not null"` + // Account ID principal (opcional, para endpoints de cuenta) + AccountID string `json:"account_id" gorm:"column:account_id;type:text"` + Activo bool `json:"activo" gorm:"column:activo;default:true"` + Nota string `json:"nota" gorm:"column:nota;type:text"` +} + +func (CloudflareConfig) TableName() string { return "cloudflare_config" } + +// GetCloudflareConfig obtiene la configuración activa. +func GetCloudflareConfig() (*CloudflareConfig, error) { + var item CloudflareConfig + if err := app.Http.Database.DB.Where("activo = ?", true).Order("id DESC").First(&item).Error; err != nil { + return nil, err + } + return &item, nil +} + +// SaveCloudflareConfig desactiva la config previa y guarda la nueva. +func SaveCloudflareConfig(s CloudflareConfig) error { + app.Http.Database.DB.Model(&CloudflareConfig{}).Where("activo = ?", true). + Update("activo", false) + s.Activo = true + if s.ID > 0 { + return app.Http.Database.DB.Model(&s).Updates(map[string]interface{}{ + "api_token": s.APIToken, + "account_id": s.AccountID, + "nota": s.Nota, + "activo": true, + }).Error + } + return app.Http.Database.DB.Create(&s).Error +} + +// GetAllCloudflareConfig lista todos los registros con paginación. +func GetAllCloudflareConfig(limit, offset int) ([]CloudflareConfig, int64, error) { + var items []CloudflareConfig + var total int64 + db := app.Http.Database.DB.Model(&CloudflareConfig{}) + db.Count(&total) + if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil { + return nil, 0, err + } + return items, total, nil +} + +// DeleteCloudflareConfig elimina un registro. +func DeleteCloudflareConfig(id uint) error { + return app.Http.Database.DB.Delete(&CloudflareConfig{}, id).Error +} diff --git a/pkg/models/hostinger_config.go b/pkg/models/hostinger_config.go new file mode 100644 index 0000000..0854e65 --- /dev/null +++ b/pkg/models/hostinger_config.go @@ -0,0 +1,58 @@ +package models + +import ( + "github.com/sujit-baniya/fiber-boilerplate/app" + "gorm.io/gorm" +) + +// HostingerConfig almacena el token de acceso a la API de Hostinger. +// Solo un registro puede estar activo a la vez. +type HostingerConfig struct { + gorm.Model + Token string `json:"token" gorm:"column:token;type:text;not null"` // Bearer token de Hostinger + Activo bool `json:"activo" gorm:"column:activo;default:true"` + Nota string `json:"nota" gorm:"column:nota;type:text"` +} + +func (HostingerConfig) TableName() string { return "hostinger_config" } + +// GetHostingerConfig obtiene la configuración activa. +func GetHostingerConfig() (*HostingerConfig, error) { + var item HostingerConfig + if err := app.Http.Database.DB.Where("activo = ?", true).Order("id DESC").First(&item).Error; err != nil { + return nil, err + } + return &item, nil +} + +// SaveHostingerConfig desactiva la config previa y guarda la nueva. +func SaveHostingerConfig(s HostingerConfig) error { + app.Http.Database.DB.Model(&HostingerConfig{}).Where("activo = ?", true). + Update("activo", false) + s.Activo = true + if s.ID > 0 { + return app.Http.Database.DB.Model(&s).Updates(map[string]interface{}{ + "token": s.Token, + "nota": s.Nota, + "activo": true, + }).Error + } + return app.Http.Database.DB.Create(&s).Error +} + +// GetAllHostingerConfig lista todos los registros con paginación. +func GetAllHostingerConfig(limit, offset int) ([]HostingerConfig, int64, error) { + var items []HostingerConfig + var total int64 + db := app.Http.Database.DB.Model(&HostingerConfig{}) + db.Count(&total) + if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil { + return nil, 0, err + } + return items, total, nil +} + +// DeleteHostingerConfig elimina un registro. +func DeleteHostingerConfig(id uint) error { + return app.Http.Database.DB.Delete(&HostingerConfig{}, id).Error +} diff --git a/pkg/services/cloudflare_service.go b/pkg/services/cloudflare_service.go new file mode 100644 index 0000000..05c43af --- /dev/null +++ b/pkg/services/cloudflare_service.go @@ -0,0 +1,214 @@ +package services + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +const cloudflareBaseURL = "https://api.cloudflare.com/client/v4" + +// CloudflareClient es el cliente HTTP para la API de Cloudflare. +type CloudflareClient struct { + token string + accountID string + httpClient *http.Client +} + +// NewCloudflareClient crea un cliente con el API Token. +func NewCloudflareClient(token, accountID string) *CloudflareClient { + return &CloudflareClient{ + token: token, + accountID: accountID, + httpClient: &http.Client{ + Timeout: 20 * time.Second, + }, + } +} + +// get realiza una petición GET autenticada y devuelve el body. +func (c *CloudflareClient) get(path string, dest interface{}) error { + req, err := http.NewRequest(http.MethodGet, cloudflareBaseURL+path, nil) + if err != nil { + return fmt.Errorf("cloudflare: crear request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+c.token) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("cloudflare: ejecutar request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("cloudflare: leer body: %w", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("cloudflare: status %d – %s", resp.StatusCode, string(body)) + } + + // Cloudflare siempre envuelve en {"result":..., "success":true} + var wrapper struct { + Success bool `json:"success"` + Errors []CFError `json:"errors"` + Result json.RawMessage `json:"result"` + } + if err := json.Unmarshal(body, &wrapper); err != nil { + return fmt.Errorf("cloudflare: decodificar envelope: %w", err) + } + if !wrapper.Success { + if len(wrapper.Errors) > 0 { + return fmt.Errorf("cloudflare API error %d: %s", wrapper.Errors[0].Code, wrapper.Errors[0].Message) + } + return fmt.Errorf("cloudflare: respuesta no exitosa") + } + if dest != nil && wrapper.Result != nil { + if err := json.Unmarshal(wrapper.Result, dest); err != nil { + return fmt.Errorf("cloudflare: decodificar result: %w", err) + } + } + return nil +} + +// ─── Tipos de error ────────────────────────────────────────────────────────── + +type CFError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// ─── Tipos de respuesta ────────────────────────────────────────────────────── + +// CFUser información del usuario autenticado. +type CFUser struct { + ID string `json:"id"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Username string `json:"username"` +} + +// CFZone representa una zona (dominio) en Cloudflare. +type CFZone struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Paused bool `json:"paused"` + Type string `json:"type"` + NameServers []string `json:"name_servers"` + OriginalNS []string `json:"original_name_servers"` + ModifiedOn string `json:"modified_on"` + ActivatedOn string `json:"activated_on"` + Plan CFPlan `json:"plan"` +} + +// CFPlan representa el plan de una zona. +type CFPlan struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// CFDNSRecord representa un registro DNS. +type CFDNSRecord struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Content string `json:"content"` + Proxied bool `json:"proxied"` + Proxiable bool `json:"proxiable"` + TTL int `json:"ttl"` + Priority int `json:"priority,omitempty"` + CreatedOn string `json:"created_on"` + ModifiedOn string `json:"modified_on"` +} + +// CFSSLStatus representa el estado SSL de una zona. +type CFSSLStatus struct { + ID string `json:"id"` + Type string `json:"type"` + Status string `json:"status"` + Hosts []string `json:"hosts"` + PrimaryCert string `json:"primary_certificate"` + ExpiresOn string `json:"expires_on"` +} + +// CFFirewallRule representa una regla de firewall (Access Rules). +type CFFirewallRule struct { + ID string `json:"id"` + Notes string `json:"notes"` + AllowedModes []string `json:"allowed_modes"` + Mode string `json:"mode"` + Configuration CFFirewallCfg `json:"configuration"` + Scope CFScope `json:"scope"` + CreatedOn string `json:"created_on"` + ModifiedOn string `json:"modified_on"` +} + +// CFFirewallCfg configuración del target en la regla. +type CFFirewallCfg struct { + Target string `json:"target"` + Value string `json:"value"` +} + +// CFScope scope de la regla (zone o account). +type CFScope struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +// ─── Métodos de la API ─────────────────────────────────────────────────────── + +// GetUser obtiene la información del usuario autenticado. +func (c *CloudflareClient) GetUser() (*CFUser, error) { + var user CFUser + if err := c.get("/user", &user); err != nil { + return nil, err + } + return &user, nil +} + +// GetZones lista todas las zonas de la cuenta. +func (c *CloudflareClient) GetZones() ([]CFZone, error) { + var zones []CFZone + // pagina 1, 50 por página (máximo) + if err := c.get("/zones?per_page=50&page=1", &zones); err != nil { + return nil, err + } + return zones, nil +} + +// GetDNSRecords devuelve los registros DNS de una zona. +func (c *CloudflareClient) GetDNSRecords(zoneID string) ([]CFDNSRecord, error) { + var records []CFDNSRecord + path := fmt.Sprintf("/zones/%s/dns_records?per_page=100&page=1", zoneID) + if err := c.get(path, &records); err != nil { + return nil, err + } + return records, nil +} + +// GetSSLCertificates devuelve los certificate packs de una zona. +func (c *CloudflareClient) GetSSLCertificates(zoneID string) ([]CFSSLStatus, error) { + var certs []CFSSLStatus + path := fmt.Sprintf("/zones/%s/ssl/certificate_packs", zoneID) + if err := c.get(path, &certs); err != nil { + return nil, err + } + return certs, nil +} + +// GetFirewallRules devuelve las reglas de acceso IP de una zona. +func (c *CloudflareClient) GetFirewallRules(zoneID string) ([]CFFirewallRule, error) { + var rules []CFFirewallRule + path := fmt.Sprintf("/zones/%s/firewall/access_rules/rules?per_page=50&page=1", zoneID) + if err := c.get(path, &rules); err != nil { + return nil, err + } + return rules, nil +} diff --git a/pkg/services/hostinger_service.go b/pkg/services/hostinger_service.go new file mode 100644 index 0000000..e53d21d --- /dev/null +++ b/pkg/services/hostinger_service.go @@ -0,0 +1,192 @@ +package services + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +const hostingerBaseURL = "https://developers.hostinger.com/api" + +// HostingerClient es el cliente HTTP para la API de Hostinger. +type HostingerClient struct { + token string + httpClient *http.Client +} + +// NewHostingerClient crea un cliente con el token Bearer. +func NewHostingerClient(token string) *HostingerClient { + return &HostingerClient{ + token: token, + httpClient: &http.Client{ + Timeout: 20 * time.Second, + }, + } +} + +// get realiza una petición GET autenticada y decodifica el cuerpo en dest. +func (c *HostingerClient) get(path string, dest interface{}) error { + req, err := http.NewRequest(http.MethodGet, hostingerBaseURL+path, nil) + if err != nil { + return fmt.Errorf("hostinger: crear request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+c.token) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("hostinger: ejecutar request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("hostinger: leer body: %w", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("hostinger: status %d – %s", resp.StatusCode, string(body)) + } + + if dest != nil { + if err := json.Unmarshal(body, dest); err != nil { + return fmt.Errorf("hostinger: decodificar respuesta: %w", err) + } + } + return nil +} + +// ─── Tipos de respuesta ────────────────────────────────────────────────────── + +// HostingerVPS representa una máquina virtual VPS. +type HostingerVPS struct { + ID int `json:"id"` + Hostname string `json:"hostname"` + State string `json:"state"` + Plan string `json:"plan"` + DataCenter string `json:"data_center"` + CPU int `json:"cpus"` + RAMBytes int64 `json:"ram"` + DiskBytes int64 `json:"disk"` + IPV4 string `json:"ipv4"` + IPV6 string `json:"ipv6"` +} + +// HostingerDomain representa un dominio en el portafolio. +type HostingerDomain struct { + Domain string `json:"domain"` + Status string `json:"status"` + ExpiresAt string `json:"expires_at"` + RegisteredAt string `json:"registered_at"` +} + +// HostingerDNSRecord representa un registro DNS. +type HostingerDNSRecord struct { + ID int `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Content string `json:"content"` + TTL int `json:"ttl"` + Priority int `json:"priority,omitempty"` +} + +// HostingerOrder representa una orden de facturación. +type HostingerOrder struct { + ID int `json:"id"` + Status string `json:"status"` + Total float64 `json:"total"` + Currency string `json:"currency"` + CreatedAt string `json:"created_at"` +} + +// HostingerHosting representa una cuenta de hosting. +type HostingerHosting struct { + ID int `json:"id"` + Domain string `json:"domain"` + Plan string `json:"plan"` + State string `json:"state"` + ExpiresAt string `json:"expires_at"` + DiskUsed int64 `json:"disk_used"` + DiskLimit int64 `json:"disk_limit"` +} + +// ─── Métodos de la API ─────────────────────────────────────────────────────── + +// GetVPSList obtiene la lista de VPS/VMs. +func (c *HostingerClient) GetVPSList() ([]HostingerVPS, error) { + var result struct { + Data []HostingerVPS `json:"data"` + } + if err := c.get("/vps/v1/virtual-machines", &result); err != nil { + // intenta respuesta array directa + var arr []HostingerVPS + if err2 := c.get("/vps/v1/virtual-machines", &arr); err2 != nil { + return nil, err + } + return arr, nil + } + return result.Data, nil +} + +// GetDomains obtiene el portafolio de dominios. +func (c *HostingerClient) GetDomains() ([]HostingerDomain, error) { + var result struct { + Data []HostingerDomain `json:"data"` + } + if err := c.get("/domains/v1/portfolio", &result); err != nil { + var arr []HostingerDomain + if err2 := c.get("/domains/v1/portfolio", &arr); err2 != nil { + return nil, err + } + return arr, nil + } + return result.Data, nil +} + +// GetDNSRecords obtiene los registros DNS de un dominio. +func (c *HostingerClient) GetDNSRecords(domain string) ([]HostingerDNSRecord, error) { + var result struct { + Data []HostingerDNSRecord `json:"data"` + } + path := fmt.Sprintf("/dns/v1/zones/%s/records", domain) + if err := c.get(path, &result); err != nil { + var arr []HostingerDNSRecord + if err2 := c.get(path, &arr); err2 != nil { + return nil, err + } + return arr, nil + } + return result.Data, nil +} + +// GetOrders obtiene las órdenes de facturación. +func (c *HostingerClient) GetOrders() ([]HostingerOrder, error) { + var result struct { + Data []HostingerOrder `json:"data"` + } + if err := c.get("/billing/v1/orders", &result); err != nil { + var arr []HostingerOrder + if err2 := c.get("/billing/v1/orders", &arr); err2 != nil { + return nil, err + } + return arr, nil + } + return result.Data, nil +} + +// GetHostingAccounts obtiene las cuentas de hosting. +func (c *HostingerClient) GetHostingAccounts() ([]HostingerHosting, error) { + var result struct { + Data []HostingerHosting `json:"data"` + } + if err := c.get("/hosting/v1/accounts", &result); err != nil { + var arr []HostingerHosting + if err2 := c.get("/hosting/v1/accounts", &arr); err2 != nil { + return nil, err + } + return arr, nil + } + return result.Data, nil +} diff --git a/rest/controllers/cloudflare_controller.go b/rest/controllers/cloudflare_controller.go new file mode 100644 index 0000000..d09d94d --- /dev/null +++ b/rest/controllers/cloudflare_controller.go @@ -0,0 +1,146 @@ +package controllers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" + "github.com/sujit-baniya/fiber-boilerplate/pkg/services" +) + +// ─── Configuración ──────────────────────────────────────────────────────────── + +// CloudflareConfigPage renderiza la vista de gestión de Cloudflare. +func CloudflareConfigPage(c *fiber.Ctx) error { + cfg, _ := models.GetCloudflareConfig() + return c.Render("cloudflare", fiber.Map{ + "Title": "Cloudflare API", + "Config": cfg, + }) +} + +// 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: b.APIToken, + AccountID: 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 ─────────────────────────────────────────────────────────────────── + +func cloudflareClient() (*services.CloudflareClient, error) { + cfg, err := models.GetCloudflareConfig() + if err != nil { + return nil, err + } + return services.NewCloudflareClient(cfg.APIToken, cfg.AccountID), nil +} + +// ─── 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.StatusBadGateway).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.StatusBadGateway).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.StatusBadGateway).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.StatusBadGateway).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.StatusBadGateway).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(fiber.Map{"data": data, "zone_id": zoneID}) +} diff --git a/rest/controllers/hostinger_controller.go b/rest/controllers/hostinger_controller.go new file mode 100644 index 0000000..6eb6d42 --- /dev/null +++ b/rest/controllers/hostinger_controller.go @@ -0,0 +1,136 @@ +package controllers + +import ( + "github.com/gofiber/fiber/v2" + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" + "github.com/sujit-baniya/fiber-boilerplate/pkg/services" +) + +// ─── Configuración ──────────────────────────────────────────────────────────── + +// HostingerConfigPage renderiza la vista de gestión de Hostinger. +func HostingerConfigPage(c *fiber.Ctx) error { + cfg, _ := models.GetHostingerConfig() + return c.Render("hostinger", fiber.Map{ + "Title": "Hostinger API", + "Config": cfg, + }) +} + +// SaveHostingerConfig guarda o actualiza el token de Hostinger. +func SaveHostingerConfig(c *fiber.Ctx) error { + type body struct { + ID uint `json:"id" form:"id"` + Token string `json:"token" form:"token"` + 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.Token == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "token requerido"}) + } + + cfg := models.HostingerConfig{ + Nota: b.Nota, + Token: b.Token, + } + cfg.ID = b.ID + + if err := models.SaveHostingerConfig(cfg); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(fiber.Map{"message": "Configuración guardada"}) +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +func hostingerClient() (*services.HostingerClient, error) { + cfg, err := models.GetHostingerConfig() + if err != nil { + return nil, err + } + return services.NewHostingerClient(cfg.Token), nil +} + +// ─── Endpoints de datos ─────────────────────────────────────────────────────── + +// GetHostingerVPS devuelve la lista de VPS/VMs. +func GetHostingerVPS(c *fiber.Ctx) error { + client, err := hostingerClient() + if err != nil { + return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ + "error": "No se encontró configuración activa de Hostinger", + }) + } + data, err := client.GetVPSList() + if err != nil { + return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(fiber.Map{"data": data}) +} + +// GetHostingerDomains devuelve el portafolio de dominios. +func GetHostingerDomains(c *fiber.Ctx) error { + client, err := hostingerClient() + if err != nil { + return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ + "error": "No se encontró configuración activa de Hostinger", + }) + } + data, err := client.GetDomains() + if err != nil { + return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(fiber.Map{"data": data}) +} + +// GetHostingerDNS devuelve los registros DNS de un dominio (:domain). +func GetHostingerDNS(c *fiber.Ctx) error { + domain := c.Params("domain") + if domain == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "dominio requerido"}) + } + client, err := hostingerClient() + if err != nil { + return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ + "error": "No se encontró configuración activa de Hostinger", + }) + } + data, err := client.GetDNSRecords(domain) + if err != nil { + return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(fiber.Map{"data": data, "domain": domain}) +} + +// GetHostingerOrders devuelve las órdenes de facturación. +func GetHostingerOrders(c *fiber.Ctx) error { + client, err := hostingerClient() + if err != nil { + return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ + "error": "No se encontró configuración activa de Hostinger", + }) + } + data, err := client.GetOrders() + if err != nil { + return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(fiber.Map{"data": data}) +} + +// GetHostingerHosting devuelve las cuentas de hosting. +func GetHostingerHosting(c *fiber.Ctx) error { + client, err := hostingerClient() + if err != nil { + return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ + "error": "No se encontró configuración activa de Hostinger", + }) + } + data, err := client.GetHostingAccounts() + if err != nil { + return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(fiber.Map{"data": data}) +} diff --git a/rest/routes/user.go b/rest/routes/user.go index 4251ca5..f348cc5 100755 --- a/rest/routes/user.go +++ b/rest/routes/user.go @@ -98,4 +98,22 @@ func UserRoutes(app fiber.Router) { // ─── Módulo de Renovaciones / Contratos ────────────────────────── RenovacionesRoutes(protected) + + // ─── Hostinger API ──────────────────────────────────────────────── + protected.Get("/hostinger", middlewares.MenuMiddleware, controllers.HostingerConfigPage) + protected.Post("/hostinger/config", controllers.SaveHostingerConfig) + protected.Get("/hostinger/vps", controllers.GetHostingerVPS) + protected.Get("/hostinger/domains", controllers.GetHostingerDomains) + protected.Get("/hostinger/dns/:domain", controllers.GetHostingerDNS) + protected.Get("/hostinger/orders", controllers.GetHostingerOrders) + protected.Get("/hostinger/hosting", controllers.GetHostingerHosting) + + // ─── Cloudflare API ─────────────────────────────────────────────── + protected.Get("/cloudflare", middlewares.MenuMiddleware, controllers.CloudflareConfigPage) + protected.Post("/cloudflare/config", controllers.SaveCloudflareConfig) + protected.Get("/cloudflare/user", controllers.GetCloudflareUser) + protected.Get("/cloudflare/zones", controllers.GetCloudflareZones) + protected.Get("/cloudflare/zones/:zone_id/dns", controllers.GetCloudflareDNS) + protected.Get("/cloudflare/zones/:zone_id/ssl", controllers.GetCloudflareSSL) + protected.Get("/cloudflare/zones/:zone_id/firewall", controllers.GetCloudflareFirewall) }