1. 后端 CreateInstanceGroup 改用 input struct 接收 readonly_instance_ids/write_instance_ids - 旧代码直接绑定 model.InstanceGroup(不含关联ID字段),导致穿梭框数据被丢弃 - 新代码解析后写入 instance_group_members 表 - 与 UpdateInstanceGroup 保持一致的逻辑 2. 弹框布局修复 - z-50 → z-[100] 避免被顶部栏遮挡 - flex-col + overflow-y-auto 独立滚动区域 - 添加 header bar + X 关闭按钮 - 背景加 backdrop-blur-sm 3. 全局排查确认:其他 handler 均使用 input struct 或 map,无同类问题
411 lines
11 KiB
Go
411 lines
11 KiB
Go
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 input struct {
|
|
Name string `json:"name"`
|
|
Description *string `json:"description"`
|
|
DBType string `json:"db_type"`
|
|
IsActive *bool `json:"is_active"`
|
|
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
|
|
}
|
|
|
|
desc := ""
|
|
if input.Description != nil {
|
|
desc = *input.Description
|
|
}
|
|
isActive := true
|
|
if input.IsActive != nil {
|
|
isActive = *input.IsActive
|
|
}
|
|
dbType := input.DBType
|
|
if dbType == "" {
|
|
dbType = "all"
|
|
}
|
|
|
|
group := model.InstanceGroup{
|
|
Name: input.Name,
|
|
Description: &desc,
|
|
DBType: dbType,
|
|
IsActive: &isActive,
|
|
}
|
|
if err := model.DB.Create(&group).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Create instance members
|
|
for _, iid := range input.ReadonlyInstanceIDs {
|
|
model.DB.Create(&model.InstanceGroupMember{
|
|
GroupID: group.ID, InstanceID: iid, AccessType: "readonly",
|
|
})
|
|
}
|
|
for _, iid := range input.WriteInstanceIDs {
|
|
model.DB.Create(&model.InstanceGroupMember{
|
|
GroupID: group.ID, InstanceID: iid, AccessType: "write",
|
|
})
|
|
}
|
|
|
|
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 = "******"
|
|
}
|
|
|
|
// build instance lookup map
|
|
instMap := map[int]model.Instance{}
|
|
for _, inst := range instances {
|
|
instMap[inst.ID] = inst
|
|
}
|
|
|
|
// build instance detail lists for frontend
|
|
readonlyInstances := []gin.H{}
|
|
for _, m := range readonlyMembers {
|
|
if inst, ok := instMap[m.InstanceID]; ok {
|
|
readonlyInstances = append(readonlyInstances, gin.H{
|
|
"id": inst.ID, "name": inst.Name, "db_type": inst.DBType,
|
|
"host": inst.Host, "port": inst.Port, "status": inst.Status, "env": inst.Env,
|
|
})
|
|
}
|
|
}
|
|
writeInstances := []gin.H{}
|
|
for _, m := range writeMembers {
|
|
if inst, ok := instMap[m.InstanceID]; ok {
|
|
writeInstances = append(writeInstances, gin.H{
|
|
"id": inst.ID, "name": inst.Name, "db_type": inst.DBType,
|
|
"host": inst.Host, "port": inst.Port, "status": inst.Status, "env": inst.Env,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Query users for this group
|
|
var userGroups []model.UserGroup
|
|
model.DB.Where("group_id = ?", gid).Find(&userGroups)
|
|
var groupUsers []gin.H
|
|
for _, ug := range userGroups {
|
|
var u model.User
|
|
if err := model.DB.First(&u, ug.UserID).Error; err == nil {
|
|
groupUsers = append(groupUsers, gin.H{
|
|
"id": u.ID, "username": u.Username, "display_name": u.DisplayName, "role": u.Role,
|
|
})
|
|
}
|
|
}
|
|
if groupUsers == nil {
|
|
groupUsers = []gin.H{}
|
|
}
|
|
|
|
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),
|
|
"readonly_instances": readonlyInstances,
|
|
"write_instances": writeInstances,
|
|
"users": groupUsers,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|