Files
soft_usite/pkg/services/hostinger_service.go
T
2026-05-14 19:56:44 -05:00

238 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"context"
"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: 10 * time.Second,
},
}
}
// getRaw realiza una petición GET autenticada y devuelve el body crudo.
// El contexto permite cancelar la llamada si el caller abandona la petición.
func (c *HostingerClient) getRaw(ctx context.Context, path string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, hostingerBaseURL+path, nil)
if err != nil {
return nil, 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 nil, fmt.Errorf("hostinger: ejecutar request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("hostinger: leer body: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("hostinger: status %d %s", resp.StatusCode, string(body))
}
return body, nil
}
// get realiza una petición GET autenticada y decodifica el cuerpo en dest.
func (c *HostingerClient) get(path string, dest interface{}) error {
body, err := c.getRaw(context.Background(), path)
if err != nil {
return err
}
if dest != nil {
if err := json.Unmarshal(body, dest); err != nil {
return fmt.Errorf("hostinger: decodificar respuesta: %w", err)
}
}
return nil
}
// ─── Tipos de respuesta ──────────────────────────────────────────────────────
// HostingerIPAddress representa una dirección IP de un VPS (IPv4 o IPv6).
type HostingerIPAddress struct {
ID int `json:"id"`
Address string `json:"address"`
PTR string `json:"ptr"`
}
// 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:"memory"`
DiskBytes int64 `json:"disk"`
IPV4 []HostingerIPAddress `json:"ipv4"`
IPV6 []HostingerIPAddress `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"`
}
// HostingerSubscription representa una suscripción de facturación.
type HostingerSubscription struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
BillingPeriod int `json:"billing_period"`
BillingPeriodUnit string `json:"billing_period_unit"`
CurrencyCode string `json:"currency_code"`
TotalPrice int `json:"total_price"`
RenewalPrice int `json:"renewal_price"`
IsAutoRenewed bool `json:"is_auto_renewed"`
CreatedAt string `json:"created_at"`
ExpiresAt string `json:"expires_at"`
NextBillingAt string `json:"next_billing_at"`
}
// HostingerHosting representa una cuenta de hosting.
type HostingerHosting struct {
Domain string `json:"domain"`
VhostType string `json:"vhost_type"`
IsEnabled bool `json:"is_enabled"`
Username string `json:"username"`
ClientID int `json:"client_id"`
OrderID int `json:"order_id"`
CreatedAt string `json:"created_at"`
RootDirectory string `json:"root_directory"`
ParentDomain string `json:"parent_domain"`
}
// ─── Métodos de la API ───────────────────────────────────────────────────────
// GetVPSList obtiene la lista de VPS/VMs.
func (c *HostingerClient) GetVPSList() ([]HostingerVPS, error) {
body, err := c.getRaw(context.Background(), "/vps/v1/virtual-machines")
if err != nil {
return nil, err
}
var wrapped struct {
Data []HostingerVPS `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err == nil && wrapped.Data != nil {
return wrapped.Data, nil
}
var arr []HostingerVPS
if err := json.Unmarshal(body, &arr); err != nil {
return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta VPS: %w", err)
}
return arr, nil
}
// GetDomains obtiene el portafolio de dominios.
func (c *HostingerClient) GetDomains() ([]HostingerDomain, error) {
body, err := c.getRaw(context.Background(), "/domains/v1/portfolio")
if err != nil {
return nil, err
}
var wrapped struct {
Data []HostingerDomain `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err == nil && wrapped.Data != nil {
return wrapped.Data, nil
}
var arr []HostingerDomain
if err := json.Unmarshal(body, &arr); err != nil {
return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta dominios: %w", err)
}
return arr, nil
}
// hostingerDNSZone es la estructura interna de la respuesta de zona DNS.
type hostingerDNSZone struct {
Records []HostingerDNSRecord `json:"records"`
}
// GetDNSRecords obtiene los registros DNS de un dominio.
// Endpoint: GET /dns/v1/zones/{domain} — devuelve un array de zonas con records embebidos.
func (c *HostingerClient) GetDNSRecords(domain string) ([]HostingerDNSRecord, error) {
path := fmt.Sprintf("/dns/v1/zones/%s", domain)
var zones []hostingerDNSZone
if err := c.get(path, &zones); err != nil {
return nil, err
}
if len(zones) > 0 {
return zones[0].Records, nil
}
return []HostingerDNSRecord{}, nil
}
// GetOrders obtiene las suscripciones de facturación.
func (c *HostingerClient) GetOrders() ([]HostingerSubscription, error) {
body, err := c.getRaw(context.Background(), "/billing/v1/subscriptions")
if err != nil {
return nil, err
}
var wrapped struct {
Data []HostingerSubscription `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err == nil && wrapped.Data != nil {
return wrapped.Data, nil
}
var arr []HostingerSubscription
if err := json.Unmarshal(body, &arr); err != nil {
return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta subscriptions: %w", err)
}
return arr, nil
}
// GetHostingAccounts obtiene las cuentas de hosting.
// Endpoint: GET /hosting/v1/websites
func (c *HostingerClient) GetHostingAccounts() ([]HostingerHosting, error) {
body, err := c.getRaw(context.Background(), "/hosting/v1/websites")
if err != nil {
return nil, err
}
var wrapped struct {
Data []HostingerHosting `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err == nil && wrapped.Data != nil {
return wrapped.Data, nil
}
var arr []HostingerHosting
if err := json.Unmarshal(body, &arr); err != nil {
return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta hosting: %w", err)
}
return arr, nil
}