Files
SingBox-Gopanel/internal/handler/admin_server_api.go
CN-JS-HuiBai b3435e5ef8
Some checks failed
build / build (api, amd64, linux) (push) Has been cancelled
build / build (api, arm64, linux) (push) Has been cancelled
build / build (api.exe, amd64, windows) (push) Has been cancelled
基本功能已初步完善
2026-04-17 20:41:47 +08:00

866 lines
22 KiB
Go

package handler
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
"time"
"xboard-go/internal/database"
"xboard-go/internal/model"
"xboard-go/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func AdminServerGroupsFetch(c *gin.Context) {
var groups []model.ServerGroup
if err := database.DB.Order("id DESC").Find(&groups).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch server groups")
return
}
var servers []model.Server
if err := database.DB.Find(&servers).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch servers")
return
}
type usageCount struct {
GroupID *int
Total int64
}
var userCounts []usageCount
if err := database.DB.Model(&model.User{}).
Select("group_id, COUNT(*) AS total").
Group("group_id").
Scan(&userCounts).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch group usage")
return
}
userCountMap := make(map[int]int64, len(userCounts))
for _, item := range userCounts {
if item.GroupID == nil {
continue
}
userCountMap[*item.GroupID] = item.Total
}
serverCountMap := make(map[int]int)
for _, server := range servers {
for _, groupID := range decodeIntSlice(server.GroupIDs) {
serverCountMap[groupID] += 1
}
}
result := make([]gin.H, 0, len(groups))
for _, group := range groups {
result = append(result, gin.H{
"id": group.ID,
"name": group.Name,
"created_at": group.CreatedAt,
"updated_at": group.UpdatedAt,
"user_count": userCountMap[group.ID],
"server_count": serverCountMap[group.ID],
})
}
Success(c, result)
}
func AdminServerGroupSave(c *gin.Context) {
var payload struct {
ID *int `json:"id"`
Name string `json:"name"`
}
if err := c.ShouldBindJSON(&payload); err != nil {
Fail(c, http.StatusBadRequest, "invalid request body")
return
}
name := strings.TrimSpace(payload.Name)
if name == "" {
Fail(c, http.StatusUnprocessableEntity, "group name is required")
return
}
now := time.Now().Unix()
if payload.ID != nil && *payload.ID > 0 {
if err := database.DB.Model(&model.ServerGroup{}).
Where("id = ?", *payload.ID).
Updates(map[string]any{
"name": name,
"updated_at": now,
}).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to save server group")
return
}
Success(c, true)
return
}
group := model.ServerGroup{
Name: name,
CreatedAt: now,
UpdatedAt: now,
}
if err := database.DB.Create(&group).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to create server group")
return
}
Success(c, true)
}
func AdminServerGroupDrop(c *gin.Context) {
var payload struct {
ID int `json:"id"`
}
if err := c.ShouldBindJSON(&payload); err != nil || payload.ID <= 0 {
Fail(c, http.StatusBadRequest, "group id is required")
return
}
var group model.ServerGroup
if err := database.DB.Where("id = ?", payload.ID).First(&group).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
Fail(c, http.StatusBadRequest, "server group does not exist")
return
}
Fail(c, http.StatusInternalServerError, "failed to load server group")
return
}
var servers []model.Server
if err := database.DB.Find(&servers).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to inspect server usage")
return
}
for _, server := range servers {
if intSliceContains(decodeIntSlice(server.GroupIDs), payload.ID) {
Fail(c, http.StatusBadRequest, "server group is still used by nodes")
return
}
}
var planUsage int64
if err := database.DB.Model(&model.Plan{}).Where("group_id = ?", payload.ID).Count(&planUsage).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to inspect plan usage")
return
}
if planUsage > 0 {
Fail(c, http.StatusBadRequest, "server group is still used by plans")
return
}
var userUsage int64
if err := database.DB.Model(&model.User{}).Where("group_id = ?", payload.ID).Count(&userUsage).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to inspect user usage")
return
}
if userUsage > 0 {
Fail(c, http.StatusBadRequest, "server group is still used by users")
return
}
if err := database.DB.Delete(&group).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to delete server group")
return
}
Success(c, true)
}
func AdminServerRoutesFetch(c *gin.Context) {
var routes []model.ServerRoute
if err := database.DB.Order("id DESC").Find(&routes).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch server routes")
return
}
result := make([]gin.H, 0, len(routes))
for _, route := range routes {
result = append(result, gin.H{
"id": route.ID,
"remarks": stringValue(route.Remarks),
"match": decodeStringSlice(route.Match),
"action": stringValue(route.Action),
"action_value": stringValue(route.ActionValue),
"created_at": route.CreatedAt,
"updated_at": route.UpdatedAt,
})
}
Success(c, result)
}
func AdminServerRouteSave(c *gin.Context) {
var payload struct {
ID *int `json:"id"`
Remarks string `json:"remarks"`
Match []any `json:"match"`
Action string `json:"action"`
ActionValue any `json:"action_value"`
}
if err := c.ShouldBindJSON(&payload); err != nil {
Fail(c, http.StatusBadRequest, "invalid request body")
return
}
remarks := strings.TrimSpace(payload.Remarks)
if remarks == "" {
Fail(c, http.StatusUnprocessableEntity, "remarks is required")
return
}
match := filterNonEmptyStrings(payload.Match)
if len(match) == 0 {
Fail(c, http.StatusUnprocessableEntity, "match is required")
return
}
action := strings.TrimSpace(payload.Action)
if !isAllowedRouteAction(action) {
Fail(c, http.StatusUnprocessableEntity, "invalid route action")
return
}
matchJSON, err := marshalJSON(match, false)
if err != nil {
Fail(c, http.StatusBadRequest, "invalid route match")
return
}
now := time.Now().Unix()
values := map[string]any{
"remarks": remarks,
"match": matchJSON,
"action": action,
"action_value": nullableString(payload.ActionValue),
"updated_at": now,
}
if payload.ID != nil && *payload.ID > 0 {
if err := database.DB.Model(&model.ServerRoute{}).Where("id = ?", *payload.ID).Updates(values).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to save server route")
return
}
Success(c, true)
return
}
values["created_at"] = now
if err := database.DB.Model(&model.ServerRoute{}).Create(values).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to create server route")
return
}
Success(c, true)
}
func AdminServerRouteDrop(c *gin.Context) {
var payload struct {
ID int `json:"id"`
}
if err := c.ShouldBindJSON(&payload); err != nil || payload.ID <= 0 {
Fail(c, http.StatusBadRequest, "route id is required")
return
}
result := database.DB.Where("id = ?", payload.ID).Delete(&model.ServerRoute{})
if result.Error != nil {
Fail(c, http.StatusInternalServerError, "failed to delete server route")
return
}
if result.RowsAffected == 0 {
Fail(c, http.StatusBadRequest, "server route does not exist")
return
}
Success(c, true)
}
func AdminServerManageGetNodes(c *gin.Context) {
var servers []model.Server
if err := database.DB.Order("sort ASC, id ASC").Find(&servers).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch nodes")
return
}
var groups []model.ServerGroup
if err := database.DB.Find(&groups).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to fetch node groups")
return
}
groupMap := make(map[int]model.ServerGroup, len(groups))
for _, group := range groups {
groupMap[group.ID] = group
}
serverMap := make(map[int]model.Server, len(servers))
for _, server := range servers {
serverMap[server.ID] = server
}
result := make([]gin.H, 0, len(servers))
for _, server := range servers {
result = append(result, serializeAdminServer(server, groupMap, serverMap))
}
Success(c, result)
}
func AdminServerManageSort(c *gin.Context) {
var payload []struct {
ID int `json:"id"`
Order int `json:"order"`
}
if err := c.ShouldBindJSON(&payload); err != nil {
Fail(c, http.StatusBadRequest, "invalid request body")
return
}
if err := database.DB.Transaction(func(tx *gorm.DB) error {
for _, item := range payload {
if item.ID <= 0 {
continue
}
if err := tx.Model(&model.Server{}).Where("id = ?", item.ID).Update("sort", item.Order).Error; err != nil {
return err
}
}
return nil
}); err != nil {
Fail(c, http.StatusInternalServerError, "failed to sort nodes")
return
}
Success(c, true)
}
func AdminServerManageSave(c *gin.Context) {
payload := map[string]any{}
if err := c.ShouldBindJSON(&payload); err != nil {
Fail(c, http.StatusBadRequest, "invalid request body")
return
}
values, id, err := normalizeServerPayload(payload)
if err != nil {
Fail(c, http.StatusBadRequest, err.Error())
return
}
if id > 0 {
result := database.DB.Model(&model.Server{}).Where("id = ?", id).Updates(values)
if result.Error != nil {
Fail(c, http.StatusInternalServerError, "failed to save node")
return
}
if result.RowsAffected == 0 {
Fail(c, http.StatusBadRequest, "node does not exist")
return
}
Success(c, true)
return
}
if err := database.DB.Model(&model.Server{}).Create(values).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to create node")
return
}
Success(c, true)
}
func AdminServerManageUpdate(c *gin.Context) {
var payload struct {
ID int `json:"id"`
Show *int `json:"show"`
}
if err := c.ShouldBindJSON(&payload); err != nil || payload.ID <= 0 {
Fail(c, http.StatusBadRequest, "invalid request body")
return
}
updates := map[string]any{}
if payload.Show != nil {
updates["show"] = *payload.Show != 0
}
if len(updates) == 0 {
Fail(c, http.StatusBadRequest, "no updates were provided")
return
}
result := database.DB.Model(&model.Server{}).Where("id = ?", payload.ID).Updates(updates)
if result.Error != nil {
Fail(c, http.StatusInternalServerError, "failed to update node")
return
}
if result.RowsAffected == 0 {
Fail(c, http.StatusBadRequest, "node does not exist")
return
}
Success(c, true)
}
func AdminServerManageDrop(c *gin.Context) {
var payload struct {
ID int `json:"id"`
}
if err := c.ShouldBindJSON(&payload); err != nil || payload.ID <= 0 {
Fail(c, http.StatusBadRequest, "node id is required")
return
}
result := database.DB.Where("id = ?", payload.ID).Delete(&model.Server{})
if result.Error != nil {
Fail(c, http.StatusInternalServerError, "failed to delete node")
return
}
if result.RowsAffected == 0 {
Fail(c, http.StatusBadRequest, "node does not exist")
return
}
Success(c, true)
}
func AdminServerManageCopy(c *gin.Context) {
var payload struct {
ID int `json:"id"`
}
if err := c.ShouldBindJSON(&payload); err != nil || payload.ID <= 0 {
Fail(c, http.StatusBadRequest, "node id is required")
return
}
var server model.Server
if err := database.DB.Where("id = ?", payload.ID).First(&server).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
Fail(c, http.StatusBadRequest, "node does not exist")
return
}
Fail(c, http.StatusInternalServerError, "failed to load node")
return
}
now := time.Now()
server.ID = 0
server.Code = nil
server.Show = false
server.U = 0
server.D = 0
server.CreatedAt = &now
server.UpdatedAt = &now
if err := database.DB.Create(&server).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to copy node")
return
}
Success(c, true)
}
func AdminServerManageBatchDelete(c *gin.Context) {
var payload struct {
IDs []int `json:"ids"`
}
if err := c.ShouldBindJSON(&payload); err != nil {
Fail(c, http.StatusBadRequest, "invalid request body")
return
}
ids := sanitizePositiveIDs(payload.IDs)
if len(ids) == 0 {
Fail(c, http.StatusBadRequest, "ids is required")
return
}
if err := database.DB.Where("id IN ?", ids).Delete(&model.Server{}).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to batch delete nodes")
return
}
Success(c, true)
}
func AdminServerManageResetTraffic(c *gin.Context) {
var payload struct {
ID int `json:"id"`
}
if err := c.ShouldBindJSON(&payload); err != nil || payload.ID <= 0 {
Fail(c, http.StatusBadRequest, "node id is required")
return
}
result := database.DB.Model(&model.Server{}).Where("id = ?", payload.ID).Updates(map[string]any{
"u": 0,
"d": 0,
})
if result.Error != nil {
Fail(c, http.StatusInternalServerError, "failed to reset node traffic")
return
}
if result.RowsAffected == 0 {
Fail(c, http.StatusBadRequest, "node does not exist")
return
}
Success(c, true)
}
func AdminServerManageBatchResetTraffic(c *gin.Context) {
var payload struct {
IDs []int `json:"ids"`
}
if err := c.ShouldBindJSON(&payload); err != nil {
Fail(c, http.StatusBadRequest, "invalid request body")
return
}
ids := sanitizePositiveIDs(payload.IDs)
if len(ids) == 0 {
Fail(c, http.StatusBadRequest, "ids is required")
return
}
if err := database.DB.Model(&model.Server{}).Where("id IN ?", ids).Updates(map[string]any{
"u": 0,
"d": 0,
}).Error; err != nil {
Fail(c, http.StatusInternalServerError, "failed to batch reset node traffic")
return
}
Success(c, true)
}
func serializeAdminServer(server model.Server, groups map[int]model.ServerGroup, servers map[int]model.Server) gin.H {
groupIDs := decodeIntSlice(server.GroupIDs)
groupList := make([]gin.H, 0, len(groupIDs))
for _, groupID := range groupIDs {
group, ok := groups[groupID]
if !ok {
continue
}
groupList = append(groupList, gin.H{
"id": group.ID,
"name": group.Name,
})
}
var parent any
if server.ParentID != nil {
if parentServer, ok := servers[*server.ParentID]; ok {
parent = gin.H{
"id": parentServer.ID,
"code": stringValue(parentServer.Code),
"type": parentServer.Type,
"name": parentServer.Name,
"host": parentServer.Host,
"port": parentServer.Port,
"server_port": parentServer.ServerPort,
}
}
}
lastCheckAt, _ := database.CacheGetJSON[int64](nodeLastCheckKey(&server))
lastPushAt, _ := database.CacheGetJSON[int64](nodeLastPushKey(&server))
online, _ := database.CacheGetJSON[int](nodeOnlineKey(&server))
loadStatus, _ := database.CacheGetJSON[map[string]any](nodeLoadStatusKey(&server))
metrics, _ := database.CacheGetJSON[map[string]any](nodeMetricsKey(&server))
isOnline := 0
now := time.Now().Unix()
if lastCheckAt > 0 && now-lastCheckAt <= 300 {
isOnline = 1
}
if lastPushAt > 0 && now-lastPushAt <= 300 {
isOnline = 2
}
availableStatus := 0
if isOnline == 1 {
availableStatus = 1
} else if isOnline == 2 {
availableStatus = 2
}
hasChildren := false
for _, s := range servers {
if s.ParentID != nil && *s.ParentID == server.ID {
hasChildren = true
break
}
}
return gin.H{
"id": server.ID,
"type": server.Type,
"code": stringValue(server.Code),
"parent_id": intValue(server.ParentID),
"has_children": hasChildren,
"group_ids": groupIDs,
"route_ids": decodeIntSlice(server.RouteIDs),
"name": server.Name,
"rate": server.Rate,
"transfer_enable": int64Value(server.TransferEnable),
"u": server.U,
"d": server.D,
"tags": decodeStringSlice(server.Tags),
"host": server.Host,
"port": server.Port,
"server_port": server.ServerPort,
"protocol_settings": decodeJSON(server.ProtocolSettings),
"custom_outbounds": decodeJSON(server.CustomOutbounds),
"custom_routes": decodeJSON(server.CustomRoutes),
"cert_config": decodeJSON(server.CertConfig),
"show": server.Show,
"sort": intValue(server.Sort),
"rate_time_enable": server.RateTimeEnable,
"rate_time_ranges": decodeJSON(server.RateTimeRanges),
"created_at": unixTimeValue(server.CreatedAt),
"updated_at": unixTimeValue(server.UpdatedAt),
"groups": groupList,
"parent": parent,
"last_check_at": lastCheckAt,
"last_push_at": lastPushAt,
"online": online,
"is_online": isOnline,
"available_status": availableStatus,
"load_status": loadStatus,
"metrics": metrics,
}
}
func normalizeServerPayload(payload map[string]any) (map[string]any, int, error) {
id := intFromAny(payload["id"])
serverType := service.NormalizeServerType(stringFromAny(payload["type"]))
if serverType == "" {
return nil, 0, errors.New("node type is required")
}
if !service.IsValidServerType(serverType) {
return nil, 0, errors.New("invalid node type")
}
name := strings.TrimSpace(stringFromAny(payload["name"]))
if name == "" {
return nil, 0, errors.New("node name is required")
}
host := strings.TrimSpace(stringFromAny(payload["host"]))
if host == "" {
return nil, 0, errors.New("node host is required")
}
port := strings.TrimSpace(stringFromAny(payload["port"]))
if port == "" {
return nil, 0, errors.New("node port is required")
}
serverPort := intFromAny(payload["server_port"])
if serverPort <= 0 {
return nil, 0, errors.New("server_port is required")
}
rate, ok := float32FromAny(payload["rate"])
if !ok {
return nil, 0, errors.New("rate is required")
}
groupIDs, err := marshalJSON(payload["group_ids"], true)
if err != nil {
return nil, 0, errors.New("invalid group_ids")
}
routeIDs, err := marshalJSON(payload["route_ids"], true)
if err != nil {
return nil, 0, errors.New("invalid route_ids")
}
tags, err := marshalJSON(payload["tags"], true)
if err != nil {
return nil, 0, errors.New("invalid tags")
}
protocolSettings, err := marshalJSON(payload["protocol_settings"], false)
if err != nil {
return nil, 0, errors.New("invalid protocol_settings")
}
rateTimeRanges, err := marshalJSON(payload["rate_time_ranges"], false)
if err != nil {
return nil, 0, errors.New("invalid rate_time_ranges")
}
customOutbounds, err := marshalJSON(payload["custom_outbounds"], false)
if err != nil {
return nil, 0, errors.New("invalid custom_outbounds")
}
customRoutes, err := marshalJSON(payload["custom_routes"], false)
if err != nil {
return nil, 0, errors.New("invalid custom_routes")
}
certConfig, err := marshalJSON(payload["cert_config"], false)
if err != nil {
return nil, 0, errors.New("invalid cert_config")
}
values := map[string]any{
"type": serverType,
"code": nullableString(payload["code"]),
"parent_id": nullableInt(payload["parent_id"]),
"group_ids": groupIDs,
"route_ids": routeIDs,
"name": name,
"rate": rate,
"transfer_enable": nullableInt64(payload["transfer_enable"]),
"tags": tags,
"host": host,
"port": port,
"server_port": serverPort,
"protocol_settings": protocolSettings,
"custom_outbounds": customOutbounds,
"custom_routes": customRoutes,
"cert_config": certConfig,
"show": boolFromAny(payload["show"]),
"sort": nullableInt(payload["sort"]),
"rate_time_enable": boolFromAny(payload["rate_time_enable"]),
"rate_time_ranges": rateTimeRanges,
}
return values, id, nil
}
func decodeJSON(raw *string) any {
if raw == nil || strings.TrimSpace(*raw) == "" {
return nil
}
var value any
if err := json.Unmarshal([]byte(*raw), &value); err != nil {
return *raw
}
return value
}
func decodeIntSlice(raw *string) []int {
if raw == nil || strings.TrimSpace(*raw) == "" {
return []int{}
}
var value []any
if err := json.Unmarshal([]byte(*raw), &value); err == nil {
result := make([]int, 0, len(value))
for _, item := range value {
if parsed := intFromAny(item); parsed > 0 {
result = append(result, parsed)
}
}
return result
}
parts := strings.Split(*raw, ",")
result := make([]int, 0, len(parts))
for _, part := range parts {
if parsed, err := strconv.Atoi(strings.TrimSpace(part)); err == nil && parsed > 0 {
result = append(result, parsed)
}
}
return result
}
func decodeStringSlice(raw *string) []string {
if raw == nil || strings.TrimSpace(*raw) == "" {
return []string{}
}
var value []any
if err := json.Unmarshal([]byte(*raw), &value); err == nil {
return filterNonEmptyStrings(value)
}
return filterNonEmptyStrings(strings.Split(*raw, ","))
}
func float32FromAny(value any) (float32, bool) {
switch typed := value.(type) {
case float32:
return typed, true
case float64:
return float32(typed), true
case int:
return float32(typed), true
case int64:
return float32(typed), true
case json.Number:
parsed, err := typed.Float64()
return float32(parsed), err == nil
case string:
parsed, err := strconv.ParseFloat(strings.TrimSpace(typed), 32)
return float32(parsed), err == nil
default:
return 0, false
}
}
func nullableInt(value any) any {
parsed := intFromAny(value)
if parsed <= 0 {
return nil
}
return parsed
}
func nullableInt64(value any) any {
parsed := intFromAny(value)
if parsed <= 0 {
return nil
}
return int64(parsed)
}
func nullableString(value any) any {
text := strings.TrimSpace(stringFromAny(value))
if text == "" {
return nil
}
return text
}
func filterNonEmptyStrings(values any) []string {
result := make([]string, 0)
switch typed := values.(type) {
case []string:
for _, item := range typed {
item = strings.TrimSpace(item)
if item != "" {
result = append(result, item)
}
}
case []any:
for _, item := range typed {
text := strings.TrimSpace(stringFromAny(item))
if text != "" {
result = append(result, text)
}
}
}
return result
}
func intSliceContains(values []int, target int) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
func isAllowedRouteAction(action string) bool {
switch action {
case "block", "direct", "dns", "proxy":
return true
default:
return false
}
}