package handler import ( "net/http" "strconv" "sql-platform/internal/model" "github.com/gin-gonic/gin" ) // GET /api/v1/instance-groups // (already in ghost.go as stub, replace with real implementation) // GET /api/v1/instance-groups/:id func GetInstanceGroup(c *gin.Context) { id := c.Param("id") var group model.InstanceGroup if err := model.DB.First(&group, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance group not found"}) return } c.JSON(http.StatusOK, enrichGroup(group)) } // POST /api/v1/instance-groups func CreateInstanceGroup(c *gin.Context) { var group model.InstanceGroup if err := c.ShouldBindJSON(&group); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } if err := model.DB.Create(&group).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } c.JSON(http.StatusCreated, enrichGroup(group)) } // PUT /api/v1/instance-groups/:id func UpdateInstanceGroup(c *gin.Context) { id := c.Param("id") gid, _ := strconv.Atoi(id) var group model.InstanceGroup if err := model.DB.First(&group, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Instance group not found"}) return } var input struct { Name *string `json:"name"` Description *string `json:"description"` DBType *string `json:"db_type"` Permissions *string `json:"permissions"` IsActive *bool `json:"is_active"` WorkflowID *int `json:"workflow_id"` AccessType *string `json:"access_type"` AllowedDatabases *string `json:"allowed_databases"` ReadonlyInstanceIDs *[]int `json:"readonly_instance_ids"` WriteInstanceIDs *[]int `json:"write_instance_ids"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } updates := map[string]interface{}{} if input.Name != nil { updates["name"] = *input.Name } if input.Description != nil { updates["description"] = *input.Description } if input.DBType != nil { updates["db_type"] = *input.DBType } if input.Permissions != nil { updates["permissions"] = *input.Permissions } if input.IsActive != nil { updates["is_active"] = *input.IsActive } if input.WorkflowID != nil { updates["workflow_id"] = *input.WorkflowID } if input.AccessType != nil { updates["access_type"] = *input.AccessType } if input.AllowedDatabases != nil { updates["allowed_databases"] = *input.AllowedDatabases } if len(updates) > 0 { if err := model.DB.Model(&group).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } } // Update instance members if provided if input.ReadonlyInstanceIDs != nil || input.WriteInstanceIDs != nil { // Clear all existing members for this group model.DB.Where("group_id = ?", gid).Delete(&model.InstanceGroupMember{}) // Add readonly instances if input.ReadonlyInstanceIDs != nil { for _, iid := range *input.ReadonlyInstanceIDs { member := model.InstanceGroupMember{ GroupID: gid, InstanceID: iid, AccessType: "readonly", } model.DB.Create(&member) } } // Add write instances if input.WriteInstanceIDs != nil { for _, iid := range *input.WriteInstanceIDs { member := model.InstanceGroupMember{ GroupID: gid, InstanceID: iid, AccessType: "write", } model.DB.Create(&member) } } } model.DB.First(&group, id) c.JSON(http.StatusOK, enrichGroup(group)) } // DELETE /api/v1/instance-groups/:id func DeleteInstanceGroup(c *gin.Context) { id := c.Param("id") if err := model.DB.Delete(&model.InstanceGroup{}, id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } // also clean up members model.DB.Where("group_id = ?", id).Delete(&model.InstanceGroupMember{}) c.JSON(http.StatusOK, gin.H{"message": "Instance group deleted"}) } // POST /api/v1/instance-groups/:id/instances func AssignGroupInstances(c *gin.Context) { groupID, _ := strconv.Atoi(c.Param("id")) var input struct { InstanceIDs []int `json:"instance_ids"` AccessType string `json:"access_type"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } if input.AccessType == "" { input.AccessType = "readonly" } for _, iid := range input.InstanceIDs { member := model.InstanceGroupMember{ GroupID: groupID, InstanceID: iid, AccessType: input.AccessType, } model.DB.Where("group_id = ? AND instance_id = ? AND access_type = ?", groupID, iid, input.AccessType).FirstOrCreate(&member) } c.JSON(http.StatusOK, gin.H{"message": "Instances assigned"}) } // DELETE /api/v1/instance-groups/:id/instances/:instanceId func RemoveGroupInstance(c *gin.Context) { groupID := c.Param("id") instanceID := c.Param("instanceId") accessType := c.Query("access_type") query := model.DB.Where("group_id = ? AND instance_id = ?", groupID, instanceID) if accessType != "" { query = query.Where("access_type = ?", accessType) } query.Delete(&model.InstanceGroupMember{}) c.JSON(http.StatusOK, gin.H{"message": "Instance removed from group"}) } // GET /api/v1/instance-groups/:id/users func ListGroupUsers(c *gin.Context) { groupID := c.Param("id") var userGroups []model.UserGroup model.DB.Where("group_id = ?", groupID).Find(&userGroups) c.JSON(http.StatusOK, userGroups) } // POST /api/v1/instance-groups/:id/users func AssignGroupUsers(c *gin.Context) { groupID, _ := strconv.Atoi(c.Param("id")) var input struct { UserIDs []int `json:"user_ids"` Permissions []string `json:"permissions"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } // delete existing model.DB.Where("group_id = ?", groupID).Delete(&model.UserGroup{}) perms := "" if len(input.Permissions) > 0 { // store as JSON array string import_json_str := "[" for i, p := range input.Permissions { if i > 0 { import_json_str += "," } import_json_str += `"` + p + `"` } import_json_str += "]" perms = import_json_str } for _, uid := range input.UserIDs { ug := model.UserGroup{UserID: uid, GroupID: groupID} if perms != "" { ug.Permissions = &perms } model.DB.Create(&ug) } c.JSON(http.StatusOK, gin.H{"message": "Users assigned"}) } // PUT /api/v1/instance-groups/users/:userId/groups func UpdateUserGroups(c *gin.Context) { userID := c.Param("userId") var input struct { GroupIDs []int `json:"group_ids"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } uid, _ := strconv.Atoi(userID) model.DB.Where("user_id = ?", uid).Delete(&model.UserGroup{}) for _, gid := range input.GroupIDs { model.DB.Create(&model.UserGroup{UserID: uid, GroupID: gid}) } c.JSON(http.StatusOK, gin.H{"message": "User groups updated"}) } // enrichGroup adds computed counts to InstanceGroup response func enrichGroup(group model.InstanceGroup) gin.H { gid := group.ID var readonlyCount, writeCount, userCount int64 var readonlyMembers, writeMembers []model.InstanceGroupMember model.DB.Where("group_id = ? AND access_type IN ?", gid, []string{"readonly", "readwrite"}).Find(&readonlyMembers) readonlyCount = int64(len(readonlyMembers)) model.DB.Where("group_id = ? AND access_type IN ?", gid, []string{"write", "readwrite"}).Find(&writeMembers) writeCount = int64(len(writeMembers)) model.DB.Model(&model.UserGroup{}).Where("group_id = ?", gid).Count(&userCount) // collect instance details allInstanceIDs := []int{} for _, m := range readonlyMembers { allInstanceIDs = append(allInstanceIDs, m.InstanceID) } for _, m := range writeMembers { allInstanceIDs = append(allInstanceIDs, m.InstanceID) } // unique seen := map[int]bool{} uniqueIDs := []int{} for _, id := range allInstanceIDs { if !seen[id] { seen[id] = true uniqueIDs = append(uniqueIDs, id) } } var instances []model.Instance if len(uniqueIDs) > 0 { model.DB.Where("id IN ?", uniqueIDs).Find(&instances) } for i := range instances { instances[i].Password = "******" } return gin.H{ "id": group.ID, "name": group.Name, "description": group.Description, "db_type": group.DBType, "permissions": group.Permissions, "is_active": group.IsActive, "created_at": group.CreatedAt, "updated_at": group.UpdatedAt, "workflow_id": group.WorkflowID, "access_type": group.AccessType, "allowed_databases": group.AllowedDatabases, "readonly_instance_count": readonlyCount, "write_instance_count": writeCount, "user_count": userCount, "readonly_instance_ids": collectInstanceIDs(readonlyMembers), "write_instance_ids": collectInstanceIDs(writeMembers), } } func collectInstanceIDs(members []model.InstanceGroupMember) []int { ids := []int{} seen := map[int]bool{} for _, m := range members { if !seen[m.InstanceID] { seen[m.InstanceID] = true ids = append(ids, m.InstanceID) } } return ids }