508 lines
18 KiB
Go
508 lines
18 KiB
Go
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"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()
|
|
if err := c.Render("hostinger", fiber.Map{
|
|
"Title": "Hostinger API",
|
|
"Config": cfg,
|
|
"user": c.Locals("user"),
|
|
"modules": c.Locals("userModules"),
|
|
}, "layouts/main"); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 y actualiza automáticamente
|
|
// los servidores vinculados (IP, RAM, CPU, disco, estado, vencimiento).
|
|
func GetHostingerVPS(c *fiber.Ctx) error {
|
|
client, err := hostingerClient()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{
|
|
"error": "No se encontró configuración activa de Hostinger",
|
|
})
|
|
}
|
|
data, err := client.GetVPSList()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
// Auto-sync: actualizar servidores vinculados por hostinger_vps_id
|
|
go syncServidoresFromVPSList(client, data)
|
|
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.StatusFailedDependency).JSON(fiber.Map{
|
|
"error": "No se encontró configuración activa de Hostinger",
|
|
})
|
|
}
|
|
data, err := client.GetDomains()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"data": data})
|
|
}
|
|
|
|
// GetHostingerDNS devuelve los registros DNS y los nameservers 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.StatusFailedDependency).JSON(fiber.Map{
|
|
"error": "No se encontró configuración activa de Hostinger",
|
|
})
|
|
}
|
|
records, err := client.GetDNSRecords(domain)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
// 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 y actualiza
|
|
// automáticamente el vencimiento de los servidores vinculados.
|
|
func GetHostingerOrders(c *fiber.Ctx) error {
|
|
client, err := hostingerClient()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{
|
|
"error": "No se encontró configuración activa de Hostinger",
|
|
})
|
|
}
|
|
data, err := client.GetOrders()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
// Auto-sync: actualizar vencimiento de servidores que ya tienen subscription_id vinculado
|
|
go syncVencimientoFromOrders(data)
|
|
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.StatusFailedDependency).JSON(fiber.Map{
|
|
"error": "No se encontró configuración activa de Hostinger",
|
|
})
|
|
}
|
|
data, err := client.GetHostingAccounts()
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.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.
|
|
// Acepta query params date_from y date_to (RFC3339). Por defecto: últimas 24 h.
|
|
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"})
|
|
}
|
|
now := time.Now().UTC()
|
|
dateFrom := c.Query("date_from", now.Add(-24*time.Hour).Format(time.RFC3339))
|
|
dateTo := c.Query("date_to", now.Format(time.RFC3339))
|
|
data, err := client.GetVPSMetrics(id, dateFrom, dateTo)
|
|
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})
|
|
}
|
|
|
|
// ─── Auto-sync background ────────────────────────────────────────────────────
|
|
|
|
// syncVencimientoFromOrders actualiza el campo vencimiento de todos los servidores
|
|
// que tienen hostinger_subscription_id vinculado, usando los datos de billing ya
|
|
// descargados (sin llamadas adicionales a la API).
|
|
func syncVencimientoFromOrders(orders []services.HostingerSubscription) {
|
|
db := app.Http.Database.DB
|
|
|
|
// Indexar suscripciones por ID para búsqueda O(1)
|
|
subByID := make(map[string]services.HostingerSubscription, len(orders))
|
|
for _, sub := range orders {
|
|
subByID[sub.ID] = sub
|
|
}
|
|
|
|
var servidores []models.Servidor
|
|
db.Where("hostinger_subscription_id != '' AND hostinger_subscription_id IS NOT NULL").Find(&servidores)
|
|
|
|
for _, srv := range servidores {
|
|
sub, ok := subByID[srv.HostingerSubscriptionID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
venc := sub.ExpiresAt
|
|
if venc == "" {
|
|
venc = sub.NextBillingAt
|
|
}
|
|
if venc == "" || len(venc) < 10 {
|
|
continue
|
|
}
|
|
db.Model(&models.Servidor{}).Where("id = ?", srv.ID).Update("vencimiento", venc[:10])
|
|
}
|
|
}
|
|
|
|
// syncServidoresFromVPSList actualiza specs y vencimiento de todos los servidores
|
|
// que tienen hostinger_vps_id vinculado, usando la lista de VPS ya descargada.
|
|
// También auto-descubre el vínculo comparando la IP del VPS con ip_servidor.
|
|
func syncServidoresFromVPSList(client *services.HostingerClient, vpsList []services.HostingerVPS) {
|
|
db := app.Http.Database.DB
|
|
|
|
// Cargar todos los servidores
|
|
var todos []models.Servidor
|
|
db.Find(&todos)
|
|
|
|
// Índice por hostinger_vps_id (ya vinculados)
|
|
byVpsID := make(map[int]*models.Servidor, len(todos))
|
|
// Índice por IP (para auto-descubrimiento)
|
|
byIP := make(map[string]*models.Servidor, len(todos))
|
|
for i := range todos {
|
|
if todos[i].HostingerVpsID != nil {
|
|
byVpsID[*todos[i].HostingerVpsID] = &todos[i]
|
|
}
|
|
if todos[i].IpServidor != "" {
|
|
byIP[todos[i].IpServidor] = &todos[i]
|
|
}
|
|
}
|
|
|
|
for _, vps := range vpsList {
|
|
// 1. Buscar por hostinger_vps_id ya guardado
|
|
srv := byVpsID[vps.ID]
|
|
|
|
// 2. Auto-descubrir por IP si no está vinculado
|
|
if srv == nil {
|
|
for _, ip4 := range vps.IPV4 {
|
|
if s, ok := byIP[ip4.Address]; ok {
|
|
srv = s
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if srv == nil {
|
|
continue
|
|
}
|
|
|
|
updates := map[string]interface{}{
|
|
"hostinger_vps_id": vps.ID,
|
|
"hostinger_state": vps.State,
|
|
}
|
|
if len(vps.IPV4) > 0 && vps.IPV4[0].Address != "" {
|
|
updates["ip_servidor"] = vps.IPV4[0].Address
|
|
}
|
|
if vps.RAMBytes > 0 {
|
|
updates["ram"] = fmt.Sprintf("%.0f GB", float64(vps.RAMBytes)/1024.0)
|
|
}
|
|
if vps.CPU > 0 {
|
|
updates["nucleos"] = fmt.Sprintf("%d", vps.CPU)
|
|
}
|
|
if vps.DiskBytes > 0 {
|
|
updates["disco"] = fmt.Sprintf("%d GB", vps.DiskBytes/1024)
|
|
}
|
|
|
|
// Guardar subscription_id del VPS para el puente con facturación
|
|
subID := vps.SubscriptionID
|
|
if subID == "" {
|
|
subID = srv.HostingerSubscriptionID
|
|
}
|
|
if subID != "" {
|
|
updates["hostinger_subscription_id"] = subID
|
|
// Intentar traer fecha de vencimiento desde billing
|
|
if sub, err := client.GetSubscriptionByID(subID); err == nil {
|
|
venc := sub.ExpiresAt
|
|
if venc == "" {
|
|
venc = sub.NextBillingAt
|
|
}
|
|
if venc != "" && len(venc) >= 10 {
|
|
updates["vencimiento"] = venc[:10]
|
|
}
|
|
}
|
|
}
|
|
|
|
db.Model(&models.Servidor{}).Where("id = ?", srv.ID).Updates(updates)
|
|
}
|
|
}
|