mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-24 13:36:24 +00:00
c77608bc47
* fix(nodes): make node API tokens write-only * fix(nodes): keep token optional on edit for write-only API tokens NodeView no longer returns apiToken, so the edit form must consume hasApiToken and not require re-entering the token. Relaxes the form validation on edit, adds a keep-current placeholder, and adds the i18n key to all 13 locales.
187 lines
7.7 KiB
Go
187 lines
7.7 KiB
Go
package service
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
|
)
|
|
|
|
// NodeView is the browser/API read contract for nodes. Credentials are
|
|
// write-only: responses expose only whether a node has a token configured.
|
|
type NodeView struct {
|
|
Id int `json:"id" example:"1"`
|
|
Name string `json:"name" example:"edge-1"`
|
|
Remark string `json:"remark" example:"Primary edge"`
|
|
Scheme string `json:"scheme" example:"https"`
|
|
Address string `json:"address" example:"node.example.com"`
|
|
Port int `json:"port" example:"2053"`
|
|
BasePath string `json:"basePath" example:"/"`
|
|
HasApiToken bool `json:"hasApiToken" example:"true"`
|
|
Enable bool `json:"enable" example:"true"`
|
|
AllowPrivateAddress bool `json:"allowPrivateAddress" example:"false"`
|
|
TlsVerifyMode string `json:"tlsVerifyMode" example:"verify"`
|
|
PinnedCertSha256 string `json:"pinnedCertSha256" example:""`
|
|
InboundSyncMode string `json:"inboundSyncMode" example:"all"`
|
|
InboundTags []string `json:"inboundTags" example:"[\"in-443-tcp\"]"`
|
|
OutboundTag string `json:"outboundTag" example:"direct"`
|
|
Guid string `json:"guid" example:"node-guid"`
|
|
Status string `json:"status" example:"online"`
|
|
LastHeartbeat int64 `json:"lastHeartbeat" example:"1700000000"`
|
|
LatencyMs int `json:"latencyMs" example:"42"`
|
|
XrayVersion string `json:"xrayVersion" example:"25.10.31"`
|
|
PanelVersion string `json:"panelVersion" example:"v3.x.x"`
|
|
CpuPct float64 `json:"cpuPct" example:"12.5"`
|
|
MemPct float64 `json:"memPct" example:"45.2"`
|
|
UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
|
|
NetUp uint64 `json:"netUp" example:"2097152"`
|
|
NetDown uint64 `json:"netDown" example:"1048576"`
|
|
LastError string `json:"lastError" example:""`
|
|
XrayState string `json:"xrayState" example:"running"`
|
|
XrayError string `json:"xrayError" example:""`
|
|
ConfigDirty bool `json:"configDirty" example:"false"`
|
|
ConfigDirtyAt int64 `json:"configDirtyAt" example:"0"`
|
|
InboundCount int `json:"inboundCount" example:"3"`
|
|
ClientCount int `json:"clientCount" example:"25"`
|
|
OnlineCount int `json:"onlineCount" example:"5"`
|
|
ActiveCount int `json:"activeCount" example:"20"`
|
|
DisabledCount int `json:"disabledCount" example:"2"`
|
|
DepletedCount int `json:"depletedCount" example:"1"`
|
|
ParentGuid string `json:"parentGuid,omitempty" example:""`
|
|
Transitive bool `json:"transitive,omitempty" example:"false"`
|
|
CreatedAt int64 `json:"createdAt" example:"1700000000"`
|
|
UpdatedAt int64 `json:"updatedAt" example:"1700003600"`
|
|
}
|
|
|
|
func toNodeView(n *model.Node) *NodeView {
|
|
if n == nil {
|
|
return nil
|
|
}
|
|
return &NodeView{
|
|
Id: n.Id,
|
|
Name: n.Name,
|
|
Remark: n.Remark,
|
|
Scheme: n.Scheme,
|
|
Address: n.Address,
|
|
Port: n.Port,
|
|
BasePath: n.BasePath,
|
|
HasApiToken: n.ApiToken != "",
|
|
Enable: n.Enable,
|
|
AllowPrivateAddress: n.AllowPrivateAddress,
|
|
TlsVerifyMode: n.TlsVerifyMode,
|
|
PinnedCertSha256: n.PinnedCertSha256,
|
|
InboundSyncMode: n.InboundSyncMode,
|
|
InboundTags: n.InboundTags,
|
|
OutboundTag: n.OutboundTag,
|
|
Guid: n.Guid,
|
|
Status: n.Status,
|
|
LastHeartbeat: n.LastHeartbeat,
|
|
LatencyMs: n.LatencyMs,
|
|
XrayVersion: n.XrayVersion,
|
|
PanelVersion: n.PanelVersion,
|
|
CpuPct: n.CpuPct,
|
|
MemPct: n.MemPct,
|
|
UptimeSecs: n.UptimeSecs,
|
|
NetUp: n.NetUp,
|
|
NetDown: n.NetDown,
|
|
LastError: n.LastError,
|
|
XrayState: n.XrayState,
|
|
XrayError: n.XrayError,
|
|
ConfigDirty: n.ConfigDirty,
|
|
ConfigDirtyAt: n.ConfigDirtyAt,
|
|
InboundCount: n.InboundCount,
|
|
ClientCount: n.ClientCount,
|
|
OnlineCount: n.OnlineCount,
|
|
ActiveCount: n.ActiveCount,
|
|
DisabledCount: n.DisabledCount,
|
|
DepletedCount: n.DepletedCount,
|
|
ParentGuid: n.ParentGuid,
|
|
Transitive: n.Transitive,
|
|
CreatedAt: n.CreatedAt,
|
|
UpdatedAt: n.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func toNodeViews(nodes []*model.Node) []*NodeView {
|
|
views := make([]*NodeView, 0, len(nodes))
|
|
for _, node := range nodes {
|
|
views = append(views, toNodeView(node))
|
|
}
|
|
return views
|
|
}
|
|
|
|
// NodeMutationRequest is the node write/probe contract. ApiToken is accepted
|
|
// only as input. On update, nil means keep the stored token; replacement and
|
|
// clearing are explicit and mutually exclusive.
|
|
type NodeMutationRequest struct {
|
|
Id int `json:"id" form:"id"`
|
|
Name string `json:"name" form:"name" validate:"required"`
|
|
Remark string `json:"remark" form:"remark"`
|
|
Scheme string `json:"scheme" form:"scheme" validate:"omitempty,oneof=http https"`
|
|
Address string `json:"address" form:"address" validate:"required"`
|
|
Port int `json:"port" form:"port" validate:"gte=1,lte=65535"`
|
|
BasePath string `json:"basePath" form:"basePath"`
|
|
ApiToken *string `json:"apiToken,omitempty" form:"apiToken"`
|
|
ClearApiToken bool `json:"clearApiToken,omitempty" form:"clearApiToken"`
|
|
Enable bool `json:"enable" form:"enable"`
|
|
AllowPrivateAddress bool `json:"allowPrivateAddress" form:"allowPrivateAddress"`
|
|
TlsVerifyMode string `json:"tlsVerifyMode" form:"tlsVerifyMode" validate:"omitempty,oneof=verify skip pin mtls"`
|
|
PinnedCertSha256 string `json:"pinnedCertSha256" form:"pinnedCertSha256"`
|
|
InboundSyncMode string `json:"inboundSyncMode" form:"inboundSyncMode" validate:"omitempty,oneof=all selected"`
|
|
InboundTags []string `json:"inboundTags" form:"inboundTags"`
|
|
OutboundTag string `json:"outboundTag" form:"outboundTag"`
|
|
}
|
|
|
|
func (r *NodeMutationRequest) validateCredentials(create bool) error {
|
|
if r == nil {
|
|
return common.NewError("node request is required")
|
|
}
|
|
if r.ApiToken != nil && r.ClearApiToken {
|
|
return common.NewError("apiToken and clearApiToken are mutually exclusive")
|
|
}
|
|
if r.ApiToken != nil {
|
|
*r.ApiToken = strings.TrimSpace(*r.ApiToken)
|
|
if *r.ApiToken == "" {
|
|
if create {
|
|
return common.NewError("apiToken is required unless mtls is enabled")
|
|
}
|
|
r.ApiToken = nil
|
|
}
|
|
}
|
|
if create {
|
|
if r.ClearApiToken {
|
|
return common.NewError("credentials cannot be cleared while creating a node")
|
|
}
|
|
if r.ApiToken == nil && r.TlsVerifyMode != "mtls" {
|
|
return common.NewError("apiToken is required unless mtls is enabled")
|
|
}
|
|
}
|
|
if r.ClearApiToken && r.Enable && r.TlsVerifyMode != "mtls" {
|
|
return common.NewError("disable the node or enable mtls before clearing its apiToken")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *NodeMutationRequest) toNode() *model.Node {
|
|
n := &model.Node{
|
|
Id: r.Id,
|
|
Name: r.Name,
|
|
Remark: r.Remark,
|
|
Scheme: r.Scheme,
|
|
Address: r.Address,
|
|
Port: r.Port,
|
|
BasePath: r.BasePath,
|
|
Enable: r.Enable,
|
|
AllowPrivateAddress: r.AllowPrivateAddress,
|
|
TlsVerifyMode: r.TlsVerifyMode,
|
|
PinnedCertSha256: r.PinnedCertSha256,
|
|
InboundSyncMode: r.InboundSyncMode,
|
|
InboundTags: r.InboundTags,
|
|
OutboundTag: r.OutboundTag,
|
|
}
|
|
if r.ApiToken != nil {
|
|
n.ApiToken = *r.ApiToken
|
|
}
|
|
return n
|
|
}
|