866 lines
22 KiB
Go
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,
|
|
"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 := "offline"
|
|
if isOnline == 1 {
|
|
availableStatus = "online_no_push"
|
|
} else if isOnline == 2 {
|
|
availableStatus = "online"
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|