package handler import ( "net/http" "strconv" "strings" "sql-platform/internal/handler/resp" "sql-platform/internal/middleware" "sql-platform/internal/model" "sql-platform/internal/safe" "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 { resp.InternalErr(c, err) return } // 密码出站遮蔽由 Instance.MarshalJSON 统一收口 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 { resp.NotFound(c, "Instance not found") return } 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 { resp.BadRequest(c, err.Error()) return } // Encrypt password before storing if instance.Password != "" { instance.Password = strings.TrimSpace(instance.Password) encPw, err := encryptPassword(instance.Password) if err == nil { instance.Password = "enc:" + encPw } } if err := model.DB.Create(&instance).Error; err != nil { resp.InternalErr(c, err) return } // Auto-detect version, cluster_mode and status after creation probe := func() { result := testDBConnectionFull(&instance) updates := map[string]interface{}{} if result.Success { updates["status"] = "connected" if result.Version != "" { updates["version"] = result.Version } if instance.DBType == "redis" { updates["cluster_mode"] = result.ClusterMode } } else { updates["status"] = "error" } if len(updates) > 0 { model.DB.Model(&model.Instance{}).Where("id = ?", instance.ID).Updates(updates) } } safe.Go("instance-probe:"+strconv.Itoa(instance.ID), probe) 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 { resp.NotFound(c, "Instance not found") return } var input map[string]interface{} if err := c.ShouldBindJSON(&input); err != nil { resp.BadRequest(c, err.Error()) return } // if password is empty or masked placeholder, don't update it if pw, ok := input["password"].(string); ok { pw = strings.TrimSpace(pw) if pw == "" || pw == "******" { delete(input, "password") } else { // Encrypt new password encPw, err := encryptPassword(pw) if err == nil { input["password"] = "enc:" + encPw } } } if err := model.DB.Model(&instance).Updates(input).Error; err != nil { resp.InternalErr(c, err) return } model.DB.First(&instance, id) 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 { resp.InternalErr(c, err) 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 { resp.NotFound(c, "Instance not found") return } result := testDBConnectionFull(&instance) // Keep the persisted list status in sync with an explicit connection test. updates := map[string]interface{}{} if result.Success { updates["status"] = "connected" } else { updates["status"] = "error" } if result.Success && result.Version != "" { updates["version"] = result.Version } if instance.DBType == "redis" { updates["cluster_mode"] = result.ClusterMode } 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 { resp.NotFound(c, "Instance not found") return } switch instance.DBType { case "redis": metrics := getRedisMetrics(&instance) c.JSON(http.StatusOK, metrics) case "mysql": metrics := getMySQLMetrics(&instance) c.JSON(http.StatusOK, metrics) case "postgresql", "postgres", "pg": metrics := getPostgresMetrics(&instance) c.JSON(http.StatusOK, metrics) default: c.JSON(http.StatusOK, gin.H{ "status": "online", "version": instance.Version, }) } }