package handler import ( "net/http" "strconv" "sql-platform/internal/middleware" "sql-platform/internal/model" "github.com/gin-gonic/gin" ) // GET /api/v1/instances func ListInstances(c *gin.Context) { userID, _, role := middleware.GetCurrentUser(c) var instances []model.Instance query := model.DB.Order("id") // developer only sees instances in their groups if role == "developer" { var groupIDs []int model.DB.Model(&model.UserGroup{}).Where("user_id = ?", userID).Pluck("group_id", &groupIDs) if len(groupIDs) > 0 { var instanceIDs []int model.DB.Model(&model.InstanceGroupMember{}).Where("group_id IN ?", groupIDs).Pluck("instance_id", &instanceIDs) if len(instanceIDs) > 0 { query = query.Where("id IN ?", instanceIDs) } else { c.JSON(http.StatusOK, []model.Instance{}) return } } } if err := query.Find(&instances).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } // mask passwords in response for i := range instances { instances[i].Password = "" } c.JSON(http.StatusOK, instances) } // GET /api/v1/instances/:id func GetInstance(c *gin.Context) { id := c.Param("id") var instance model.Instance if err := model.DB.First(&instance, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } instance.Password = "******" c.JSON(http.StatusOK, instance) } // POST /api/v1/instances func CreateInstance(c *gin.Context) { var instance model.Instance if err := c.ShouldBindJSON(&instance); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } if err := model.DB.Create(&instance).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } // Auto-detect version and cluster_mode after creation go func() { result := testDBConnectionFull(&instance) updates := map[string]interface{}{} if result.Success && result.Version != "" { updates["version"] = result.Version } if result.ClusterMode { updates["cluster_mode"] = true } if len(updates) > 0 { model.DB.Model(&model.Instance{}).Where("id = ?", instance.ID).Updates(updates) } }() instance.Password = "******" c.JSON(http.StatusCreated, instance) } // PUT /api/v1/instances/:id func UpdateInstance(c *gin.Context) { id := c.Param("id") var instance model.Instance if err := model.DB.First(&instance, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } var input map[string]interface{} if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } // if password is masked, don't update it if pw, ok := input["password"].(string); ok && pw == "******" { delete(input, "password") } if err := model.DB.Model(&instance).Updates(input).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } model.DB.First(&instance, id) instance.Password = "******" c.JSON(http.StatusOK, instance) } // DELETE /api/v1/instances/:id func DeleteInstance(c *gin.Context) { id := c.Param("id") if err := model.DB.Delete(&model.Instance{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Instance deleted"}) } // POST /api/v1/instances/:id/test-connection func TestConnection(c *gin.Context) { id := c.Param("id") var instance model.Instance if err := model.DB.First(&instance, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } result := testDBConnectionFull(&instance) // Update version & cluster_mode in DB if detected updates := map[string]interface{}{} if result.Success && result.Version != "" { updates["version"] = result.Version } if result.ClusterMode { updates["cluster_mode"] = true } if len(updates) > 0 { model.DB.Model(&model.Instance{}).Where("id = ?", instance.ID).Updates(updates) } if result.Success { c.JSON(http.StatusOK, gin.H{"success": true, "message": result.Message, "version": result.Version, "cluster_mode": result.ClusterMode}) } else { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": result.Message}) } } // GET /api/v1/instances/stats func InstanceStats(c *gin.Context) { var total int64 model.DB.Model(&model.Instance{}).Count(&total) type typeCount struct { DBType string `json:"db_type"` Count int64 `json:"count"` } var byType []typeCount model.DB.Model(&model.Instance{}).Select("db_type, count(*) as count").Group("db_type").Scan(&byType) c.JSON(http.StatusOK, gin.H{ "total": total, "by_type": byType, }) } // Helper: parse id param to int func parseID(s string) (int, bool) { id, err := strconv.Atoi(s) return id, err == nil } // GET /api/v1/instances/:id/metrics — live metrics for an instance func InstanceMetrics(c *gin.Context) { id := c.Param("id") var instance model.Instance if err := model.DB.First(&instance, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance not found"}) return } switch instance.DBType { case "redis": metrics := getRedisMetrics(&instance) c.JSON(http.StatusOK, metrics) default: // For MySQL/PG: return basic status c.JSON(http.StatusOK, gin.H{ "status": "online", "version": instance.Version, }) } }