diff --git a/pkg/services/hostinger_service.go b/pkg/services/hostinger_service.go
index f627dd6..8ccd669 100644
--- a/pkg/services/hostinger_service.go
+++ b/pkg/services/hostinger_service.go
@@ -1,6 +1,7 @@
package services
import (
+ "bytes"
"context"
"encoding/json"
"fmt"
@@ -54,6 +55,37 @@ func (c *HostingerClient) getRaw(ctx context.Context, path string) ([]byte, erro
return body, nil
}
+// writeRaw envía una petición HTTP autenticada con método y body opcionales.
+func (c *HostingerClient) writeRaw(ctx context.Context, method, path string, payload interface{}) ([]byte, error) {
+ var reqBody io.Reader
+ if payload != nil {
+ b, err := json.Marshal(payload)
+ if err != nil {
+ return nil, fmt.Errorf("hostinger: serializar body: %w", err)
+ }
+ reqBody = bytes.NewReader(b)
+ }
+ req, err := http.NewRequestWithContext(ctx, method, hostingerBaseURL+path, reqBody)
+ 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)
@@ -115,6 +147,19 @@ type HostingerNameServers struct {
NS4 string `json:"ns4"`
}
+// HostingerDNSZoneInput es un grupo de registros para PUT /dns/v1/zones/{domain}.
+type HostingerDNSZoneInput struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ TTL int `json:"ttl,omitempty"`
+ Records []HostingerDNSZoneValue `json:"records"`
+}
+
+// HostingerDNSZoneValue es el valor de contenido de un registro DNS.
+type HostingerDNSZoneValue struct {
+ Content string `json:"content"`
+}
+
// HostingerSubscription representa una suscripción de facturación.
type HostingerSubscription struct {
ID string `json:"id"`
@@ -263,6 +308,122 @@ func (c *HostingerClient) GetOrders() ([]HostingerSubscription, error) {
return arr, nil
}
+// ─── DNS: escritura ─────────────────────────────────────────────────────────
+
+// UpdateDNSZone agrega/actualiza registros en la zona DNS.
+// overwrite=true reemplaza registros del mismo tipo/nombre; false los agrega.
+func (c *HostingerClient) UpdateDNSZone(domain string, overwrite bool, zone []HostingerDNSZoneInput) error {
+ path := fmt.Sprintf("/dns/v1/zones/%s", domain)
+ _, err := c.writeRaw(context.Background(), http.MethodPut, path,
+ map[string]interface{}{"overwrite": overwrite, "zone": zone})
+ return err
+}
+
+// ResetDNS restaura la zona DNS de un dominio a sus valores por defecto.
+func (c *HostingerClient) ResetDNS(domain string) error {
+ path := fmt.Sprintf("/dns/v1/zones/%s/reset", domain)
+ _, err := c.writeRaw(context.Background(), http.MethodPost, path, map[string]bool{"sync": true})
+ return err
+}
+
+// ─── Domains: escritura ──────────────────────────────────────────────────────
+
+// UpdateNameservers actualiza los nameservers de un dominio.
+func (c *HostingerClient) UpdateNameservers(domain, ns1, ns2, ns3, ns4 string) error {
+ path := fmt.Sprintf("/domains/v1/portfolio/%s/nameservers", domain)
+ _, err := c.writeRaw(context.Background(), http.MethodPut, path,
+ map[string]string{"ns1": ns1, "ns2": ns2, "ns3": ns3, "ns4": ns4})
+ return err
+}
+
+// SetDomainLock activa (PUT) o desactiva (DELETE) el bloqueo del dominio.
+func (c *HostingerClient) SetDomainLock(domain string, enable bool) error {
+ path := fmt.Sprintf("/domains/v1/portfolio/%s/domain-lock", domain)
+ method := http.MethodPut
+ if !enable {
+ method = http.MethodDelete
+ }
+ _, err := c.writeRaw(context.Background(), method, path, nil)
+ return err
+}
+
+// SetPrivacyProtection activa (PUT) o desactiva (DELETE) la protección WHOIS.
+func (c *HostingerClient) SetPrivacyProtection(domain string, enable bool) error {
+ path := fmt.Sprintf("/domains/v1/portfolio/%s/privacy-protection", domain)
+ method := http.MethodPut
+ if !enable {
+ method = http.MethodDelete
+ }
+ _, err := c.writeRaw(context.Background(), method, path, nil)
+ return err
+}
+
+// ─── VPS: acciones ───────────────────────────────────────────────────────────
+
+// VPSAction ejecuta una acción en una VM: start | stop | restart.
+func (c *HostingerClient) VPSAction(vmID int, action string) error {
+ path := fmt.Sprintf("/vps/v1/virtual-machines/%d/%s", vmID, action)
+ _, err := c.writeRaw(context.Background(), http.MethodPost, path, nil)
+ return err
+}
+
+// SetVPSRootPassword cambia la contraseña root de una VM.
+func (c *HostingerClient) SetVPSRootPassword(vmID int, password string) error {
+ path := fmt.Sprintf("/vps/v1/virtual-machines/%d/root-password", vmID)
+ _, err := c.writeRaw(context.Background(), http.MethodPut, path, map[string]string{"password": password})
+ return err
+}
+
+// SetVPSHostname cambia el hostname de una VM.
+func (c *HostingerClient) SetVPSHostname(vmID int, hostname string) error {
+ path := fmt.Sprintf("/vps/v1/virtual-machines/%d/hostname", vmID)
+ _, err := c.writeRaw(context.Background(), http.MethodPut, path, map[string]string{"hostname": hostname})
+ return err
+}
+
+// GetVPSMetrics obtiene las métricas de uso de una VM.
+func (c *HostingerClient) GetVPSMetrics(vmID int) (json.RawMessage, error) {
+ path := fmt.Sprintf("/vps/v1/virtual-machines/%d/metrics", vmID)
+ body, err := c.getRaw(context.Background(), path)
+ if err != nil {
+ return nil, err
+ }
+ return json.RawMessage(body), nil
+}
+
+// GetVPSBackups obtiene los backups de una VM.
+func (c *HostingerClient) GetVPSBackups(vmID int) (json.RawMessage, error) {
+ path := fmt.Sprintf("/vps/v1/virtual-machines/%d/backups", vmID)
+ body, err := c.getRaw(context.Background(), path)
+ if err != nil {
+ return nil, err
+ }
+ return json.RawMessage(body), nil
+}
+
+// ─── Billing: acciones ───────────────────────────────────────────────────────
+
+// ToggleAutoRenewal activa (true) o desactiva la autorenovación de una suscripción.
+func (c *HostingerClient) ToggleAutoRenewal(subscriptionID string, enable bool) (*HostingerSubscription, error) {
+ var path, method string
+ if enable {
+ path = fmt.Sprintf("/billing/v1/subscriptions/%s/auto-renewal/enable", subscriptionID)
+ method = http.MethodPatch
+ } else {
+ path = fmt.Sprintf("/billing/v1/subscriptions/%s/auto-renewal/disable", subscriptionID)
+ method = http.MethodDelete
+ }
+ body, err := c.writeRaw(context.Background(), method, path, nil)
+ if err != nil {
+ return nil, err
+ }
+ var sub HostingerSubscription
+ if err := json.Unmarshal(body, &sub); err != nil {
+ return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta: %w", err)
+ }
+ return &sub, nil
+}
+
// GetHostingAccounts obtiene las cuentas de hosting.
// Endpoint: GET /hosting/v1/websites
func (c *HostingerClient) GetHostingAccounts() ([]HostingerHosting, error) {
diff --git a/resources/views/hostinger.html b/resources/views/hostinger.html
index ff1ac2c..26bcb3d 100644
--- a/resources/views/hostinger.html
+++ b/resources/views/hostinger.html
@@ -78,6 +78,38 @@
class="mt-3 w-full text-center text-xs py-1.5 rounded border border-[#8eb02f] text-[#6d8c24] hover:bg-green-50 transition">
Ver DNS del hostname
+
+
+
+
+
+
+
+
+
+
+
@@ -109,10 +141,31 @@
|
|
-
+
+
+
+
+
+
+
|
@@ -166,6 +219,7 @@
Precio |
Período |
Vence / Próximo cobro |
+ Auto-renovar |
@@ -180,6 +234,16 @@
|
|
|
+
+
+
+ |
@@ -189,6 +253,106 @@
+
+
+
+
+
Cambiar contraseña root
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Cambiar Hostname
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Editar Nameservers —
+
+
+
+
+
+
@@ -232,11 +396,46 @@
+
+
+
+
Agregar registro
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
@@ -293,9 +492,19 @@ document.addEventListener('alpine:init', () => {
dnsRecords: [],
dnsDomain: '',
dnsNameServers: null,
+ dnsAddForm: { name: '', type: 'A', content: '', ttl: 14400 },
dnsModal: false,
configModal: false,
cfgForm: { id: 0, token: '', nota: '' },
+ // VPS actions
+ vpsActionLoading: {},
+ vpsPasswordModal: false,
+ vpsPasswordForm: { id: 0, password: '' },
+ vpsHostnameModal: false,
+ vpsHostnameForm: { id: 0, hostname: '' },
+ // Nameservers
+ nsModal: false,
+ nsForm: { domain: '', ns1: '', ns2: '', ns3: '', ns4: '' },
toast: { show: false, msg: '', type: 'ok' },
async init() {
@@ -376,6 +585,121 @@ document.addEventListener('alpine:init', () => {
showToast(msg, type = 'ok') {
this.toast = { show: true, msg, type };
setTimeout(() => this.toast.show = false, 3500);
+ },
+
+ async vpsAction(id, action) {
+ this.vpsActionLoading = { ...this.vpsActionLoading, [id]: action };
+ try {
+ await axios.post(`/app/hostinger/vps/${id}/${action}`);
+ this.showToast(`${action.charAt(0).toUpperCase()+action.slice(1)} enviado`);
+ setTimeout(() => this.loadTab(), 1500);
+ } catch (e) {
+ this.showToast(e.response?.data?.error || `Error: ${action}`, 'error');
+ }
+ const updated = { ...this.vpsActionLoading };
+ delete updated[id];
+ this.vpsActionLoading = updated;
+ },
+
+ async setRootPassword() {
+ this.loading = true;
+ try {
+ await axios.put(`/app/hostinger/vps/${this.vpsPasswordForm.id}/root-password`,
+ { password: this.vpsPasswordForm.password });
+ this.showToast('Contraseña cambiada correctamente');
+ this.vpsPasswordModal = false;
+ this.vpsPasswordForm = { id: 0, password: '' };
+ } catch (e) {
+ this.showToast(e.response?.data?.error || 'Error al cambiar contraseña', 'error');
+ }
+ this.loading = false;
+ },
+
+ async setHostname() {
+ this.loading = true;
+ try {
+ await axios.put(`/app/hostinger/vps/${this.vpsHostnameForm.id}/hostname`,
+ { hostname: this.vpsHostnameForm.hostname });
+ this.showToast('Hostname actualizado');
+ this.vpsHostnameModal = false;
+ this.vpsHostnameForm = { id: 0, hostname: '' };
+ await this.loadTab();
+ } catch (e) {
+ this.showToast(e.response?.data?.error || 'Error al cambiar hostname', 'error');
+ }
+ this.loading = false;
+ },
+
+ async updateNameservers() {
+ this.loading = true;
+ try {
+ await axios.put(`/app/hostinger/domains/${encodeURIComponent(this.nsForm.domain)}/nameservers`,
+ { ns1: this.nsForm.ns1, ns2: this.nsForm.ns2, ns3: this.nsForm.ns3, ns4: this.nsForm.ns4 });
+ this.showToast('Nameservers actualizados');
+ this.nsModal = false;
+ await this.loadTab();
+ } catch (e) {
+ this.showToast(e.response?.data?.error || 'Error al actualizar nameservers', 'error');
+ }
+ this.loading = false;
+ },
+
+ async toggleDomainLock(domain, enable) {
+ try {
+ if (enable) await axios.put(`/app/hostinger/domains/${encodeURIComponent(domain)}/domain-lock`);
+ else await axios.delete(`/app/hostinger/domains/${encodeURIComponent(domain)}/domain-lock`);
+ this.showToast(enable ? 'Dominio bloqueado' : 'Dominio desbloqueado');
+ await this.loadTab();
+ } catch (e) { this.showToast(e.response?.data?.error || 'Error', 'error'); }
+ },
+
+ async togglePrivacy(domain, enable) {
+ try {
+ if (enable) await axios.put(`/app/hostinger/domains/${encodeURIComponent(domain)}/privacy`);
+ else await axios.delete(`/app/hostinger/domains/${encodeURIComponent(domain)}/privacy`);
+ this.showToast(enable ? 'Privacy activada' : 'Privacy desactivada');
+ await this.loadTab();
+ } catch (e) { this.showToast(e.response?.data?.error || 'Error', 'error'); }
+ },
+
+ async toggleAutoRenewal(id, enable) {
+ try {
+ if (enable) await axios.patch(`/app/hostinger/billing/${id}/auto-renewal/enable`);
+ else await axios.delete(`/app/hostinger/billing/${id}/auto-renewal/disable`);
+ this.showToast(enable ? 'Autorenovación activada' : 'Autorenovación desactivada');
+ await this.loadTab();
+ } catch (e) { this.showToast(e.response?.data?.error || 'Error', 'error'); }
+ },
+
+ async addDNSRecord() {
+ this.loading = true;
+ try {
+ await axios.put(`/app/hostinger/dns/${encodeURIComponent(this.dnsDomain)}`, {
+ overwrite: false,
+ zone: [{ name: this.dnsAddForm.name, type: this.dnsAddForm.type,
+ ttl: this.dnsAddForm.ttl,
+ records: [{ content: this.dnsAddForm.content }] }]
+ });
+ this.showToast('Registro DNS agregado');
+ this.dnsAddForm = { name: '', type: 'A', content: '', ttl: 14400 };
+ await this.loadDNSForDomain(this.dnsDomain);
+ } catch (e) {
+ this.showToast(e.response?.data?.error || 'Error al agregar registro', 'error');
+ }
+ this.loading = false;
+ },
+
+ async resetDNS() {
+ if (!confirm(`¿Resetear la zona DNS de ${this.dnsDomain}? Se restaurarán los valores por defecto.`)) return;
+ this.loading = true;
+ try {
+ await axios.post(`/app/hostinger/dns/${encodeURIComponent(this.dnsDomain)}/reset`);
+ this.showToast('Zona DNS reseteada');
+ await this.loadDNSForDomain(this.dnsDomain);
+ } catch (e) {
+ this.showToast(e.response?.data?.error || 'Error al resetear DNS', 'error');
+ }
+ this.loading = false;
}
}));
});
diff --git a/rest/controllers/hostinger_controller.go b/rest/controllers/hostinger_controller.go
index 9c8d120..c856357 100644
--- a/rest/controllers/hostinger_controller.go
+++ b/rest/controllers/hostinger_controller.go
@@ -141,3 +141,240 @@ func GetHostingerHosting(c *fiber.Ctx) error {
}
return c.JSON(fiber.Map{"data": data})
}
+
+// ─── DNS: escritura ──────────────────────────────────────────────────────────
+
+// UpdateHostingerDNS agrega o actualiza registros DNS.
+func UpdateHostingerDNS(c *fiber.Ctx) error {
+ domain := c.Params("domain")
+ if domain == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "dominio requerido"})
+ }
+ var body struct {
+ Overwrite bool `json:"overwrite"`
+ Zone []services.HostingerDNSZoneInput `json:"zone"`
+ }
+ if err := c.BodyParser(&body); err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
+ }
+ client, err := hostingerClient()
+ if err != nil {
+ return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
+ }
+ if err := client.UpdateDNSZone(domain, body.Overwrite, body.Zone); err != nil {
+ return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"message": "DNS actualizado"})
+}
+
+// ResetHostingerDNS resetea la zona DNS de un dominio.
+func ResetHostingerDNS(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.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
+ }
+ if err := client.ResetDNS(domain); err != nil {
+ return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"message": "Zona DNS reseteada"})
+}
+
+// ─── Domains: escritura ──────────────────────────────────────────────────────
+
+// UpdateHostingerNameservers actualiza los nameservers de un dominio.
+func UpdateHostingerNameservers(c *fiber.Ctx) error {
+ domain := c.Params("domain")
+ var body struct {
+ NS1 string `json:"ns1"`
+ NS2 string `json:"ns2"`
+ NS3 string `json:"ns3"`
+ NS4 string `json:"ns4"`
+ }
+ if err := c.BodyParser(&body); err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
+ }
+ client, err := hostingerClient()
+ if err != nil {
+ return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
+ }
+ if err := client.UpdateNameservers(domain, body.NS1, body.NS2, body.NS3, body.NS4); err != nil {
+ return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"message": "Nameservers actualizados"})
+}
+
+// EnableHostingerDomainLock activa el bloqueo del dominio.
+func EnableHostingerDomainLock(c *fiber.Ctx) error {
+ return hostingerDomainToggle(c, "domain-lock", true)
+}
+
+// DisableHostingerDomainLock desactiva el bloqueo del dominio.
+func DisableHostingerDomainLock(c *fiber.Ctx) error {
+ return hostingerDomainToggle(c, "domain-lock", false)
+}
+
+// EnableHostingerPrivacy activa la protección WHOIS.
+func EnableHostingerPrivacy(c *fiber.Ctx) error {
+ return hostingerDomainToggle(c, "privacy", true)
+}
+
+// DisableHostingerPrivacy desactiva la protección WHOIS.
+func DisableHostingerPrivacy(c *fiber.Ctx) error {
+ return hostingerDomainToggle(c, "privacy", false)
+}
+
+func hostingerDomainToggle(c *fiber.Ctx, feature string, enable bool) error {
+ domain := c.Params("domain")
+ client, err := hostingerClient()
+ if err != nil {
+ return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
+ }
+ var e error
+ switch feature {
+ case "domain-lock":
+ e = client.SetDomainLock(domain, enable)
+ case "privacy":
+ e = client.SetPrivacyProtection(domain, enable)
+ }
+ if e != nil {
+ return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": e.Error()})
+ }
+ return c.JSON(fiber.Map{"message": "Operación realizada"})
+}
+
+// ─── VPS: acciones ───────────────────────────────────────────────────────────
+
+// StartHostingerVPS arranca una VM.
+func StartHostingerVPS(c *fiber.Ctx) error { return vpsAction(c, "start") }
+
+// StopHostingerVPS apaga una VM.
+func StopHostingerVPS(c *fiber.Ctx) error { return vpsAction(c, "stop") }
+
+// RestartHostingerVPS reinicia una VM.
+func RestartHostingerVPS(c *fiber.Ctx) error { return vpsAction(c, "restart") }
+
+func vpsAction(c *fiber.Ctx, action string) error {
+ id, err := c.ParamsInt("id")
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
+ }
+ client, errC := hostingerClient()
+ if errC != nil {
+ return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
+ }
+ if err := client.VPSAction(id, action); err != nil {
+ return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"message": action + " enviado"})
+}
+
+// SetHostingerVPSRootPassword cambia la contraseña root de una VM.
+func SetHostingerVPSRootPassword(c *fiber.Ctx) error {
+ id, err := c.ParamsInt("id")
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
+ }
+ var body struct {
+ Password string `json:"password"`
+ }
+ if err := c.BodyParser(&body); err != nil || body.Password == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "password requerido"})
+ }
+ client, errC := hostingerClient()
+ if errC != nil {
+ return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
+ }
+ if err := client.SetVPSRootPassword(id, body.Password); err != nil {
+ return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"message": "Contraseña actualizada"})
+}
+
+// SetHostingerVPSHostname cambia el hostname de una VM.
+func SetHostingerVPSHostname(c *fiber.Ctx) error {
+ id, err := c.ParamsInt("id")
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
+ }
+ var body struct {
+ Hostname string `json:"hostname"`
+ }
+ if err := c.BodyParser(&body); err != nil || body.Hostname == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "hostname requerido"})
+ }
+ client, errC := hostingerClient()
+ if errC != nil {
+ return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
+ }
+ if err := client.SetVPSHostname(id, body.Hostname); err != nil {
+ return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"message": "Hostname actualizado"})
+}
+
+// GetHostingerVPSMetrics retorna las métricas de una VM.
+func GetHostingerVPSMetrics(c *fiber.Ctx) error {
+ id, err := c.ParamsInt("id")
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
+ }
+ client, errC := hostingerClient()
+ if errC != nil {
+ return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
+ }
+ data, err := client.GetVPSMetrics(id)
+ if err != nil {
+ return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(data)
+}
+
+// GetHostingerVPSBackups retorna los backups de una VM.
+func GetHostingerVPSBackups(c *fiber.Ctx) error {
+ id, err := c.ParamsInt("id")
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
+ }
+ client, errC := hostingerClient()
+ if errC != nil {
+ return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
+ }
+ data, err := client.GetVPSBackups(id)
+ if err != nil {
+ return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(data)
+}
+
+// ─── Billing: acciones ───────────────────────────────────────────────────────
+
+// EnableHostingerAutoRenewal activa la autorenovación de una suscripción.
+func EnableHostingerAutoRenewal(c *fiber.Ctx) error {
+ return hostingerAutoRenewalToggle(c, true)
+}
+
+// DisableHostingerAutoRenewal desactiva la autorenovación de una suscripción.
+func DisableHostingerAutoRenewal(c *fiber.Ctx) error {
+ return hostingerAutoRenewalToggle(c, false)
+}
+
+func hostingerAutoRenewalToggle(c *fiber.Ctx, enable bool) error {
+ subID := c.Params("id")
+ if subID == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id requerido"})
+ }
+ client, err := hostingerClient()
+ if err != nil {
+ return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
+ }
+ sub, err := client.ToggleAutoRenewal(subID, enable)
+ if err != nil {
+ return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
+ }
+ return c.JSON(fiber.Map{"data": sub})
+}
+
diff --git a/rest/routes/user.go b/rest/routes/user.go
index 6f0658f..83e219e 100755
--- a/rest/routes/user.go
+++ b/rest/routes/user.go
@@ -117,8 +117,27 @@ func UserRoutes(app fiber.Router) {
protected.Get("/hostinger/vps", controllers.GetHostingerVPS)
protected.Get("/hostinger/domains", controllers.GetHostingerDomains)
protected.Get("/hostinger/dns/:domain", controllers.GetHostingerDNS)
+ protected.Put("/hostinger/dns/:domain", controllers.UpdateHostingerDNS)
+ protected.Post("/hostinger/dns/:domain/reset", controllers.ResetHostingerDNS)
protected.Get("/hostinger/orders", controllers.GetHostingerOrders)
protected.Get("/hostinger/hosting", controllers.GetHostingerHosting)
+ // Domains: escritura
+ protected.Put("/hostinger/domains/:domain/nameservers", controllers.UpdateHostingerNameservers)
+ protected.Put("/hostinger/domains/:domain/domain-lock", controllers.EnableHostingerDomainLock)
+ protected.Delete("/hostinger/domains/:domain/domain-lock", controllers.DisableHostingerDomainLock)
+ protected.Put("/hostinger/domains/:domain/privacy", controllers.EnableHostingerPrivacy)
+ protected.Delete("/hostinger/domains/:domain/privacy", controllers.DisableHostingerPrivacy)
+ // VPS: acciones
+ protected.Post("/hostinger/vps/:id/start", controllers.StartHostingerVPS)
+ protected.Post("/hostinger/vps/:id/stop", controllers.StopHostingerVPS)
+ protected.Post("/hostinger/vps/:id/restart", controllers.RestartHostingerVPS)
+ protected.Put("/hostinger/vps/:id/root-password", controllers.SetHostingerVPSRootPassword)
+ protected.Put("/hostinger/vps/:id/hostname", controllers.SetHostingerVPSHostname)
+ protected.Get("/hostinger/vps/:id/metrics", controllers.GetHostingerVPSMetrics)
+ protected.Get("/hostinger/vps/:id/backups", controllers.GetHostingerVPSBackups)
+ // Billing: autorenovación
+ protected.Patch("/hostinger/billing/:id/auto-renewal/enable", controllers.EnableHostingerAutoRenewal)
+ protected.Delete("/hostinger/billing/:id/auto-renewal/disable", controllers.DisableHostingerAutoRenewal)
// ─── Cloudflare API ───────────────────────────────────────────────
protected.Get("/cloudflare", middlewares.MenuMiddleware, controllers.CloudflareConfigPage)