fix(hostinger): DNS aplanado + nameservers, facturacion precios formateados, subscriptions endpoint
This commit is contained in:
@@ -99,14 +99,20 @@ type HostingerDomain struct {
|
|||||||
RegisteredAt string `json:"registered_at"`
|
RegisteredAt string `json:"registered_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostingerDNSRecord representa un registro DNS.
|
// HostingerDNSRecord representa un registro DNS aplanado para la vista.
|
||||||
type HostingerDNSRecord struct {
|
type HostingerDNSRecord struct {
|
||||||
ID int `json:"id"`
|
Type string `json:"type"`
|
||||||
Type string `json:"type"`
|
Name string `json:"name"`
|
||||||
Name string `json:"name"`
|
Content string `json:"content"`
|
||||||
Content string `json:"content"`
|
TTL int `json:"ttl"`
|
||||||
TTL int `json:"ttl"`
|
}
|
||||||
Priority int `json:"priority,omitempty"`
|
|
||||||
|
// HostingerNameServers contiene los nameservers de un dominio.
|
||||||
|
type HostingerNameServers struct {
|
||||||
|
NS1 string `json:"ns1"`
|
||||||
|
NS2 string `json:"ns2"`
|
||||||
|
NS3 string `json:"ns3"`
|
||||||
|
NS4 string `json:"ns4"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostingerSubscription representa una suscripción de facturación.
|
// HostingerSubscription representa una suscripción de facturación.
|
||||||
@@ -178,23 +184,64 @@ func (c *HostingerClient) GetDomains() ([]HostingerDomain, error) {
|
|||||||
return arr, nil
|
return arr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// hostingerDNSZone es la estructura interna de la respuesta de zona DNS.
|
// hostingerDNSContent es el valor individual de un registro DNS.
|
||||||
type hostingerDNSZone struct {
|
type hostingerDNSContent struct {
|
||||||
Records []HostingerDNSRecord `json:"records"`
|
Content string `json:"content"`
|
||||||
|
IsDisabled bool `json:"is_disabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDNSRecords obtiene los registros DNS de un dominio.
|
// hostingerDNSEntry es un grupo de registros del mismo tipo/nombre desde la API.
|
||||||
// Endpoint: GET /dns/v1/zones/{domain} — devuelve un array de zonas con records embebidos.
|
type hostingerDNSEntry struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Records []hostingerDNSContent `json:"records"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDNSRecords obtiene los registros DNS de un dominio y los aplana para la vista.
|
||||||
|
// Endpoint: GET /dns/v1/zones/{domain} — array de {name, ttl, type, records:[{content}]}
|
||||||
func (c *HostingerClient) GetDNSRecords(domain string) ([]HostingerDNSRecord, error) {
|
func (c *HostingerClient) GetDNSRecords(domain string) ([]HostingerDNSRecord, error) {
|
||||||
path := fmt.Sprintf("/dns/v1/zones/%s", domain)
|
path := fmt.Sprintf("/dns/v1/zones/%s", domain)
|
||||||
var zones []hostingerDNSZone
|
body, err := c.getRaw(context.Background(), path)
|
||||||
if err := c.get(path, &zones); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(zones) > 0 {
|
var entries []hostingerDNSEntry
|
||||||
return zones[0].Records, nil
|
if err := json.Unmarshal(body, &entries); err != nil {
|
||||||
|
return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta DNS: %w", err)
|
||||||
}
|
}
|
||||||
return []HostingerDNSRecord{}, nil
|
var result []HostingerDNSRecord
|
||||||
|
for _, e := range entries {
|
||||||
|
for _, r := range e.Records {
|
||||||
|
result = append(result, HostingerDNSRecord{
|
||||||
|
Type: e.Type,
|
||||||
|
Name: e.Name,
|
||||||
|
Content: r.Content,
|
||||||
|
TTL: e.TTL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
result = []HostingerDNSRecord{}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomainNameServers obtiene los nameservers del dominio desde el portafolio.
|
||||||
|
// Endpoint: GET /domains/v1/portfolio/{domain}
|
||||||
|
func (c *HostingerClient) GetDomainNameServers(domain string) (*HostingerNameServers, error) {
|
||||||
|
path := fmt.Sprintf("/domains/v1/portfolio/%s", domain)
|
||||||
|
body, err := c.getRaw(context.Background(), path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
NameServers HostingerNameServers `json:"name_servers"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &resp); err != nil {
|
||||||
|
return nil, fmt.Errorf("hostinger: no se pudo interpretar nameservers: %w", err)
|
||||||
|
}
|
||||||
|
return &resp.NameServers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrders obtiene las suscripciones de facturación.
|
// GetOrders obtiene las suscripciones de facturación.
|
||||||
|
|||||||
@@ -164,8 +164,8 @@
|
|||||||
<th class="py-2 px-3">Nombre</th>
|
<th class="py-2 px-3">Nombre</th>
|
||||||
<th class="py-2 px-3">Estado</th>
|
<th class="py-2 px-3">Estado</th>
|
||||||
<th class="py-2 px-3">Precio</th>
|
<th class="py-2 px-3">Precio</th>
|
||||||
<th class="py-2 px-3">Moneda</th>
|
<th class="py-2 px-3">Período</th>
|
||||||
<th class="py-2 px-3">Vence</th>
|
<th class="py-2 px-3">Vence / Próximo cobro</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -174,12 +174,12 @@
|
|||||||
<td class="py-2 px-3 text-xs" x-text="o.name || '—'"></td>
|
<td class="py-2 px-3 text-xs" x-text="o.name || '—'"></td>
|
||||||
<td class="py-2 px-3">
|
<td class="py-2 px-3">
|
||||||
<span class="px-2 py-0.5 rounded-full text-xs"
|
<span class="px-2 py-0.5 rounded-full text-xs"
|
||||||
:class="o.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'"
|
:class="o.status === 'active' ? 'bg-green-100 text-green-700' : o.status === 'in_trial' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-500'"
|
||||||
x-text="o.status"></span>
|
x-text="o.status"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-2 px-3 text-xs font-semibold" x-text="o.total_price || '—'"></td>
|
<td class="py-2 px-3 text-xs font-semibold font-mono" x-text="o.total_price ? o.currency_code + '\u00a0' + new Intl.NumberFormat('es-CO').format(o.total_price / 100) : '—'"></td>
|
||||||
<td class="py-2 px-3 text-xs" x-text="o.currency_code || '—'"></td>
|
<td class="py-2 px-3 text-xs text-gray-500" x-text="o.billing_period + '\u00a0' + (o.billing_period_unit === 'year' ? (o.billing_period === 1 ? 'año' : 'años') : (o.billing_period === 1 ? 'mes' : 'meses'))"></td>
|
||||||
<td class="py-2 px-3 text-xs text-gray-500" x-text="o.expires_at || '—'"></td>
|
<td class="py-2 px-3 text-xs text-gray-500" x-text="o.expires_at || o.next_billing_at || '—'"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -202,6 +202,15 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto p-5">
|
<div class="overflow-y-auto p-5">
|
||||||
|
<!-- Nameservers -->
|
||||||
|
<div x-show="dnsNameServers" class="mb-4 p-3 bg-gray-50 rounded-lg border">
|
||||||
|
<p class="text-xs font-semibold text-gray-500 mb-2">Nameservers</p>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<template x-for="(ns, i) in [dnsNameServers?.ns1, dnsNameServers?.ns2, dnsNameServers?.ns3, dnsNameServers?.ns4].filter(Boolean)" :key="i">
|
||||||
|
<span class="px-2 py-1 bg-white border rounded text-xs font-mono text-gray-700" x-text="ns"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div x-show="dnsRecords.length === 0" class="text-sm text-gray-400 text-center py-8">Sin registros DNS encontrados.</div>
|
<div x-show="dnsRecords.length === 0" class="text-sm text-gray-400 text-center py-8">Sin registros DNS encontrados.</div>
|
||||||
<table class="table-auto w-full text-xs" x-show="dnsRecords.length > 0">
|
<table class="table-auto w-full text-xs" x-show="dnsRecords.length > 0">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -213,7 +222,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template x-for="r in dnsRecords" :key="r.id">
|
<template x-for="(r, rIdx) in dnsRecords" :key="rIdx">
|
||||||
<tr class="border-b hover:bg-gray-50">
|
<tr class="border-b hover:bg-gray-50">
|
||||||
<td class="py-1.5 px-2"><span class="px-1.5 py-0.5 bg-blue-100 text-blue-700 rounded text-xs font-mono" x-text="r.type"></span></td>
|
<td class="py-1.5 px-2"><span class="px-1.5 py-0.5 bg-blue-100 text-blue-700 rounded text-xs font-mono" x-text="r.type"></span></td>
|
||||||
<td class="py-1.5 px-2 font-mono" x-text="r.name"></td>
|
<td class="py-1.5 px-2 font-mono" x-text="r.name"></td>
|
||||||
@@ -283,6 +292,7 @@ document.addEventListener('alpine:init', () => {
|
|||||||
orders: [],
|
orders: [],
|
||||||
dnsRecords: [],
|
dnsRecords: [],
|
||||||
dnsDomain: '',
|
dnsDomain: '',
|
||||||
|
dnsNameServers: null,
|
||||||
dnsModal: false,
|
dnsModal: false,
|
||||||
configModal: false,
|
configModal: false,
|
||||||
cfgForm: { id: 0, token: '', nota: '' },
|
cfgForm: { id: 0, token: '', nota: '' },
|
||||||
@@ -329,9 +339,11 @@ document.addEventListener('alpine:init', () => {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.dnsDomain = domain;
|
this.dnsDomain = domain;
|
||||||
this.dnsRecords = [];
|
this.dnsRecords = [];
|
||||||
|
this.dnsNameServers = null;
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('/app/hostinger/dns/' + encodeURIComponent(domain));
|
const res = await axios.get('/app/hostinger/dns/' + encodeURIComponent(domain));
|
||||||
this.dnsRecords = res.data.data || [];
|
this.dnsRecords = res.data.data || [];
|
||||||
|
this.dnsNameServers = res.data.name_servers || null;
|
||||||
this.dnsModal = true;
|
this.dnsModal = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.showToast(e.response?.data?.error || 'Error al cargar DNS', 'error');
|
this.showToast(e.response?.data?.error || 'Error al cargar DNS', 'error');
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func GetHostingerDomains(c *fiber.Ctx) error {
|
|||||||
return c.JSON(fiber.Map{"data": data})
|
return c.JSON(fiber.Map{"data": data})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHostingerDNS devuelve los registros DNS de un dominio (:domain).
|
// GetHostingerDNS devuelve los registros DNS y los nameservers de un dominio (:domain).
|
||||||
func GetHostingerDNS(c *fiber.Ctx) error {
|
func GetHostingerDNS(c *fiber.Ctx) error {
|
||||||
domain := c.Params("domain")
|
domain := c.Params("domain")
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
@@ -103,11 +103,13 @@ func GetHostingerDNS(c *fiber.Ctx) error {
|
|||||||
"error": "No se encontró configuración activa de Hostinger",
|
"error": "No se encontró configuración activa de Hostinger",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
data, err := client.GetDNSRecords(domain)
|
records, err := client.GetDNSRecords(domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
|
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
|
||||||
}
|
}
|
||||||
return c.JSON(fiber.Map{"data": data, "domain": domain})
|
// Nameservers son opcionales: si falla (ej. VPS hostnames no están en portafolio) se ignora.
|
||||||
|
ns, _ := client.GetDomainNameServers(domain)
|
||||||
|
return c.JSON(fiber.Map{"data": records, "name_servers": ns, "domain": domain})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHostingerOrders devuelve las órdenes de facturación.
|
// GetHostingerOrders devuelve las órdenes de facturación.
|
||||||
|
|||||||
Reference in New Issue
Block a user