package controllers import ( "fmt" "math" "net" "strconv" "strings" "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" ) // RENDER func Servidor(c *fiber.Ctx) error { data := fiber.Map{ "user": c.Locals("user").(map[string]interface{}), "modules": c.Locals("userModules"), } if err := c.Render("servidor", data, "layouts/main"); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "message": "Error rendering template", "error": err.Error(), }) } return nil } // ServidorDashboard renders the server dashboard view func ServidorDashboard(c *fiber.Ctx) error { data := fiber.Map{ "user": c.Locals("user").(map[string]interface{}), "modules": c.Locals("userModules"), } if err := c.Render("servidor_dashboard", data, "layouts/main"); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "message": "Error rendering template", "error": err.Error(), }) } return nil } func GetServidor(c *fiber.Ctx) error { pageStr := c.Query("page", "1") page, err := strconv.Atoi(pageStr) if err != nil || page < 1 { page = 1 } searchQuery := c.Query("search", "") limitStr := c.Query("limit", "10") limit, _ := strconv.Atoi(limitStr) offset := (page - 1) * limit records, total, err := models.GetAllServidores(limit, offset, searchQuery) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "message": "Error retrieving records", "error": err.Error(), }) } // Build connection count map per server type conxCount struct { ServidorID uint `gorm:"column:servidor_id"` Count int64 `gorm:"column:count"` } var counts []conxCount serverIDs := make([]uint, len(records)) for i, s := range records { serverIDs[i] = s.ID } if len(serverIDs) > 0 { app.Http.Database.DB.Table("conx_db"). Select("servidor_id, count(*) as count"). Where("servidor_id IN ? AND deleted_at IS NULL", serverIDs). Group("servidor_id"). Scan(&counts) } countMap := make(map[uint]int64) for _, cc := range counts { countMap[cc.ServidorID] = cc.Count } // Attach conx_count to each record type servidorWithCount struct { models.Servidor ConxCount int64 `json:"conx_count"` } enriched := make([]servidorWithCount, len(records)) for i, s := range records { enriched[i] = servidorWithCount{Servidor: s, ConxCount: countMap[s.ID]} } totalPages := int(math.Ceil(float64(total) / float64(limit))) return c.JSON(fiber.Map{ "registros": enriched, "total": total, "totalPages": totalPages, "page": page, "limit": limit, }) } func GetServidorSelect(c *fiber.Ctx) error { records, err := models.GetAllServidoresSelect() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "message": "Error retrieving records", "error": err.Error(), }) } return c.JSON(fiber.Map{ "registros": records, }) } func CreateServidor(c *fiber.Ctx) error { var m models.Servidor if err := c.BodyParser(&m); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "message": "Error parsing request body", "error": err.Error(), }) } if err := models.CreateServidor(m); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "message": "Error creating servidor", "error": err.Error(), }) } return c.Status(fiber.StatusCreated).JSON(m) } func UpdateServidor(c *fiber.Ctx) error { uid, err := strconv.ParseUint(c.Params("id"), 10, 32) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "message": "ID inválido", "error": err.Error(), }) } var m models.Servidor if err := c.BodyParser(&m); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "message": "Error al parsear el cuerpo de la solicitud", "error": err.Error(), }) } m.ID = uint(uid) // Cargar el registro actual para detectar cambios que deben propagarse a Hostinger var actual models.Servidor app.Http.Database.DB.First(&actual, uid) nombreAnterior := actual.Nombre // Usar map para Updates y así no perder campos con valor cero (incluido hostinger_vps_id=nil) updateMap := map[string]interface{}{ "nombre": m.Nombre, "ip_servidor": m.IpServidor, "so": m.So, "vencimiento": m.Vencimiento, "ram": m.Ram, "nucleos": m.Nucleos, "disco": m.Disco, "ultimo_ping": m.UltimoPing, "prov_servidor_id": m.ProvServidorID, "tipo_servidor_id": m.TipoServidorID, "hostinger_vps_id": m.HostingerVpsID, "hostinger_subscription_id": m.HostingerSubscriptionID, } if err := app.Http.Database.DB.Model(&models.Servidor{}).Where("id = ?", uid).Updates(updateMap).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "message": "Error al actualizar el servidor", "error": err.Error(), }) } // Sincronizar hostname en Hostinger si está vinculado y el nombre cambió if actual.HostingerVpsID != nil && m.Nombre != "" && m.Nombre != nombreAnterior { if client, hErr := hostingerClient(); hErr == nil { _ = client.SetVPSHostname(*actual.HostingerVpsID, m.Nombre) } } return c.Status(fiber.StatusOK).JSON(m) } // SyncServidorFromHostinger sincroniza los datos del VPS de Hostinger hacia el servidor local. // Actualiza: ip_servidor, ram, nucleos, disco, hostinger_state, vencimiento (si hay suscripción). // La verdad de las specs físicas siempre viene de Hostinger. func SyncServidorFromHostinger(c *fiber.Ctx) error { id := c.Params("id") db := app.Http.Database.DB var servidor models.Servidor if err := db.First(&servidor, id).Error; err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "servidor no encontrado"}) } if servidor.HostingerVpsID == nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "el servidor no tiene hostinger_vps_id configurado"}) } client, err := hostingerClient() if err != nil { return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"}) } vps, err := client.GetVPSByID(*servidor.HostingerVpsID) if err != nil { return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()}) } if vps.ID == 0 { return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": "Hostinger no devolvio datos para ese VPS ID"}) } updates := map[string]interface{}{ "hostinger_state": vps.State, } // IP principal (primer IPv4) if len(vps.IPV4) > 0 && vps.IPV4[0].Address != "" { updates["ip_servidor"] = vps.IPV4[0].Address } // RAM: Hostinger devuelve en MB → convertir a GB if vps.RAMBytes > 0 { gbRam := float64(vps.RAMBytes) / 1024.0 updates["ram"] = fmt.Sprintf("%.0f GB", gbRam) } // CPU cores if vps.CPU > 0 { updates["nucleos"] = fmt.Sprintf("%d", vps.CPU) } // Disco: Hostinger devuelve en MB (igual que memory) → convertir a GB if vps.DiskBytes > 0 { gbDisk := vps.DiskBytes / 1024 updates["disco"] = fmt.Sprintf("%d GB", gbDisk) } // Vencimiento: resolución en cascada // 1) subscription_id que viene directo del VPS (el más confiable) // 2) subscription_id ya guardado en el servidor local // 3) fallback fuzzy por nombre subID := vps.SubscriptionID if subID == "" && servidor.HostingerSubscriptionID != "" { subID = servidor.HostingerSubscriptionID } if subID != "" { // Lookup directo — sin ambigüedad if sub, sErr := client.GetSubscriptionByID(subID); sErr == nil { // Para suscripciones activas con auto-renovación, expires_at viene vacío; // en ese caso usar next_billing_at como fecha de vencimiento. venc := sub.ExpiresAt if venc == "" { venc = sub.NextBillingAt } if venc != "" && len(venc) >= 10 { updates["vencimiento"] = venc[:10] } updates["hostinger_subscription_id"] = subID } } else { // Fallback: buscar por nombre si aún no tenemos subscription_id if subs, sErr := client.GetOrders(); sErr == nil { for _, sub := range subs { venc := sub.ExpiresAt if venc == "" { venc = sub.NextBillingAt } if venc != "" && len(venc) >= 10 && strings.Contains(strings.ToLower(sub.Name), strings.ToLower(vps.Hostname)) { updates["vencimiento"] = venc[:10] updates["hostinger_subscription_id"] = sub.ID break } } } } if err := db.Model(&servidor).Updates(updates).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo guardar", "detalle": err.Error()}) } db.First(&servidor, id) return c.JSON(fiber.Map{"ok": true, "servidor": servidor, "campos_actualizados": updates}) } func DeleteServidor(c *fiber.Ctx) error { uid, err := strconv.ParseUint(c.Params("id"), 10, 32) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "message": "ID inválido", "error": err.Error(), }) } var m models.Servidor m.ID = uint(uid) if err := models.DeleteServidor(m); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "message": "Error al eliminar el servidor", "error": err.Error(), }) } return c.Status(fiber.StatusOK).JSON(fiber.Map{ "message": "Servidor eliminado exitosamente", }) } // GetServidorDashboard retorna un servidor con todas sus conexiones de BD func GetServidorDashboard(c *fiber.Ctx) error { servidorID := c.Params("id") uid, err := strconv.ParseUint(servidorID, 10, 32) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "ID inválido", }) } // Obtener servidor var servidor models.Servidor db := app.Http.Database.DB if err := db.Preload("ProvServidor").Preload("TipoServidor").First(&servidor, uid).Error; err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ "error": "Servidor no encontrado", }) } // Obtener conexiones de BD asociadas var conexiones []models.ConxDb if err := db.Preload("Servidor").Preload("TipoDb").Where("servidor_id = ?", uid).Find(&conexiones).Error; err != nil { conexiones = []models.ConxDb{} } return c.JSON(fiber.Map{ "servidor": servidor, "conexiones": conexiones, }) } // PingConexion tests the connection to a database and returns the response time func PingConexion(c *fiber.Ctx) error { conexionID := c.Params("id") uid, err := strconv.ParseUint(conexionID, 10, 32) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "exitoso": false, "error": "ID inválido", }) } var conexion models.ConxDb db := app.Http.Database.DB if err := db.Preload("TipoDb").Preload("Servidor").First(&conexion, uid).Error; err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ "exitoso": false, "error": "Conexión no encontrada", }) } host := conexion.HostEfectivo() puerto := conexion.Puerto // Primero intentar conexión real a la BD (más fiable que solo TCP) inicio := time.Now() testErr := services.TestDBConnection(conexion) if testErr == nil { return c.JSON(fiber.Map{ "exitoso": true, "tiempo": time.Since(inicio).Milliseconds(), "host": host, "puerto": puerto, "tipo": conexion.TipoDb.Nombre, "error": "", }) } // Si la BD no conecta, probar al menos TCP (puerto abierto) tcpAddr := net.JoinHostPort(host, puerto) inicioTCP := time.Now() conn, errTCP := net.DialTimeout("tcp", tcpAddr, 3*time.Second) if errTCP == nil { conn.Close() return c.JSON(fiber.Map{ "exitoso": false, "tiempo": time.Since(inicioTCP).Milliseconds(), "host": host, "puerto": puerto, "tipo": conexion.TipoDb.Nombre, // Puerto abierto pero la BD rechaza la conexión "error": fmt.Sprintf("Puerto %s abierto pero la BD no responde: %s", puerto, testErr.Error()), }) } return c.JSON(fiber.Map{ "exitoso": false, "tiempo": int64(0), "host": host, "puerto": puerto, "tipo": conexion.TipoDb.Nombre, "error": fmt.Sprintf("No se puede alcanzar %s:%s — verifica que el host sea accesible desde este servidor. Tip: si el DB solo escucha en localhost del servidor, configura el campo 'Host' de la conexión.", host, puerto), }) }