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) } }