feat: integración Hostinger + Cloudflare API (modelo, servicio, controlador, rutas, migración)
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user