mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-31 15:37:14 +00:00
feat(api-docs): generate response examples from Go structs; fix SS2022 PSK regen (#4996)
Stop hand-writing OpenAPI response examples, which kept drifting from the real payloads (clients/traffic missing fields, inbounds/list exposing userId which is json:"-", the fictional inbound-443 tag instead of the real in-<port>-<transport> form).
tools/openapigen now emits frontend/src/generated/examples.ts: a per-struct example instance built from type defaults, validate oneof/min bounds, and example: struct tags, with nested-ref expansion and a cycle guard. build-openapi.mjs composes the {success,obj} envelope from it for any endpoint annotated with responseSchema (+ responseSchemaArray for lists); the hand-written response is dropped for those. Service DTOs InboundOption/ApiTokenView/ProbeResultUI are added to the walker.
#4996: client password regeneration now produces a valid Shadowsocks 2022 PSK (correct base64 length per cipher) when an SS2022 inbound is attached, in both the single and bulk client forms; backend surfaces ssMethod on /inbounds/options so the UI can pick the right length.
Also: Swagger UI persists the Authorization token across reloads (persistAuthorization).
This commit is contained in:
@@ -37,6 +37,23 @@ jobs:
|
|||||||
go list ./... | grep -v '/frontend/node_modules/' > /tmp/go-packages.txt
|
go list ./... | grep -v '/frontend/node_modules/' > /tmp/go-packages.txt
|
||||||
go test $(cat /tmp/go-packages.txt)
|
go test $(cat /tmp/go-packages.txt)
|
||||||
|
|
||||||
|
codegen:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version-file: .nvmrc
|
||||||
|
- name: Regenerate schemas, examples and OpenAPI
|
||||||
|
run: npm run gen
|
||||||
|
working-directory: frontend
|
||||||
|
- name: Fail if generated files are stale (run 'npm run gen' and commit)
|
||||||
|
run: git diff --exit-code -- frontend/src/generated frontend/public/openapi.json
|
||||||
|
|
||||||
govulncheck:
|
govulncheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
+28
-28
@@ -41,13 +41,13 @@ type User struct {
|
|||||||
|
|
||||||
// Inbound represents an Xray inbound configuration with traffic statistics and settings.
|
// Inbound represents an Xray inbound configuration with traffic statistics and settings.
|
||||||
type Inbound struct {
|
type Inbound struct {
|
||||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier
|
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement" example:"1"` // Unique identifier
|
||||||
UserId int `json:"-"` // Associated user ID
|
UserId int `json:"-"` // Associated user ID
|
||||||
Up int64 `json:"up" form:"up"` // Upload traffic in bytes
|
Up int64 `json:"up" form:"up"` // Upload traffic in bytes
|
||||||
Down int64 `json:"down" form:"down"` // Download traffic in bytes
|
Down int64 `json:"down" form:"down"` // Download traffic in bytes
|
||||||
Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
|
Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
|
||||||
Remark string `json:"remark" form:"remark"` // Human-readable remark
|
Remark string `json:"remark" form:"remark" example:"VLESS-443"` // Human-readable remark
|
||||||
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled
|
Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1" example:"true"` // Whether the inbound is enabled
|
||||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
|
||||||
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2" validate:"omitempty,oneof=never hourly daily weekly monthly"` // Traffic reset schedule
|
TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2" validate:"omitempty,oneof=never hourly daily weekly monthly"` // Traffic reset schedule
|
||||||
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
|
LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
|
||||||
@@ -55,11 +55,11 @@ type Inbound struct {
|
|||||||
|
|
||||||
// Xray configuration fields
|
// Xray configuration fields
|
||||||
Listen string `json:"listen" form:"listen"`
|
Listen string `json:"listen" form:"listen"`
|
||||||
Port int `json:"port" form:"port" validate:"gte=0,lte=65535"`
|
Port int `json:"port" form:"port" validate:"gte=0,lte=65535" example:"443"`
|
||||||
Protocol Protocol `json:"protocol" form:"protocol" validate:"required,oneof=vmess vless trojan shadowsocks wireguard hysteria http mixed tunnel tun"`
|
Protocol Protocol `json:"protocol" form:"protocol" validate:"required,oneof=vmess vless trojan shadowsocks wireguard hysteria http mixed tunnel tun" example:"vless"`
|
||||||
Settings string `json:"settings" form:"settings"`
|
Settings string `json:"settings" form:"settings"`
|
||||||
StreamSettings string `json:"streamSettings" form:"streamSettings"`
|
StreamSettings string `json:"streamSettings" form:"streamSettings"`
|
||||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
Tag string `json:"tag" form:"tag" gorm:"unique" example:"in-443-tcp"`
|
||||||
Sniffing string `json:"sniffing" form:"sniffing"`
|
Sniffing string `json:"sniffing" form:"sniffing"`
|
||||||
NodeID *int `json:"nodeId,omitempty" form:"nodeId" gorm:"index"`
|
NodeID *int `json:"nodeId,omitempty" form:"nodeId" gorm:"index"`
|
||||||
|
|
||||||
@@ -378,15 +378,15 @@ type Setting struct {
|
|||||||
// endpoint over HTTP using the per-node ApiToken to populate the runtime
|
// endpoint over HTTP using the per-node ApiToken to populate the runtime
|
||||||
// status fields below.
|
// status fields below.
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement" example:"1"`
|
||||||
Name string `json:"name" form:"name" gorm:"uniqueIndex" validate:"required"`
|
Name string `json:"name" form:"name" gorm:"uniqueIndex" validate:"required" example:"de-fra-1"`
|
||||||
Remark string `json:"remark" form:"remark"`
|
Remark string `json:"remark" form:"remark"`
|
||||||
Scheme string `json:"scheme" form:"scheme" validate:"omitempty,oneof=http https"`
|
Scheme string `json:"scheme" form:"scheme" validate:"omitempty,oneof=http https" example:"https"`
|
||||||
Address string `json:"address" form:"address" validate:"required"`
|
Address string `json:"address" form:"address" validate:"required" example:"node1.example.com"`
|
||||||
Port int `json:"port" form:"port" validate:"gte=1,lte=65535"`
|
Port int `json:"port" form:"port" validate:"gte=1,lte=65535" example:"2053"`
|
||||||
BasePath string `json:"basePath" form:"basePath"`
|
BasePath string `json:"basePath" form:"basePath" example:"/"`
|
||||||
ApiToken string `json:"apiToken" form:"apiToken" validate:"required"`
|
ApiToken string `json:"apiToken" form:"apiToken" validate:"required" example:"abcdef0123456789"`
|
||||||
Enable bool `json:"enable" form:"enable" gorm:"default:true"`
|
Enable bool `json:"enable" form:"enable" gorm:"default:true" example:"true"`
|
||||||
AllowPrivateAddress bool `json:"allowPrivateAddress" form:"allowPrivateAddress" gorm:"default:false"`
|
AllowPrivateAddress bool `json:"allowPrivateAddress" form:"allowPrivateAddress" gorm:"default:false"`
|
||||||
TlsVerifyMode string `json:"tlsVerifyMode" form:"tlsVerifyMode" gorm:"column:tls_verify_mode;default:verify" validate:"omitempty,oneof=verify skip pin"`
|
TlsVerifyMode string `json:"tlsVerifyMode" form:"tlsVerifyMode" gorm:"column:tls_verify_mode;default:verify" validate:"omitempty,oneof=verify skip pin"`
|
||||||
PinnedCertSha256 string `json:"pinnedCertSha256" form:"pinnedCertSha256" gorm:"column:pinned_cert_sha256"`
|
PinnedCertSha256 string `json:"pinnedCertSha256" form:"pinnedCertSha256" gorm:"column:pinned_cert_sha256"`
|
||||||
@@ -401,23 +401,23 @@ type Node struct {
|
|||||||
// Heartbeat-updated fields. UpdatedAt advances on every probe even when
|
// Heartbeat-updated fields. UpdatedAt advances on every probe even when
|
||||||
// the row is otherwise unchanged so the UI's "last seen" tooltip is
|
// the row is otherwise unchanged so the UI's "last seen" tooltip is
|
||||||
// truthful without us having to read LastHeartbeat separately.
|
// truthful without us having to read LastHeartbeat separately.
|
||||||
Status string `json:"status" gorm:"default:unknown"` // online|offline|unknown
|
Status string `json:"status" gorm:"default:unknown" example:"online"` // online|offline|unknown
|
||||||
LastHeartbeat int64 `json:"lastHeartbeat"` // unix seconds, 0 = never
|
LastHeartbeat int64 `json:"lastHeartbeat" example:"1700000000"` // unix seconds, 0 = never
|
||||||
LatencyMs int `json:"latencyMs"`
|
LatencyMs int `json:"latencyMs" example:"42"`
|
||||||
XrayVersion string `json:"xrayVersion"`
|
XrayVersion string `json:"xrayVersion" example:"25.10.31"`
|
||||||
PanelVersion string `json:"panelVersion" gorm:"column:panel_version"`
|
PanelVersion string `json:"panelVersion" gorm:"column:panel_version" example:"v3.x.x"`
|
||||||
CpuPct float64 `json:"cpuPct"`
|
CpuPct float64 `json:"cpuPct" example:"23.5"`
|
||||||
MemPct float64 `json:"memPct"`
|
MemPct float64 `json:"memPct" example:"45.1"`
|
||||||
UptimeSecs uint64 `json:"uptimeSecs"`
|
UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
|
||||||
LastError string `json:"lastError"`
|
LastError string `json:"lastError"`
|
||||||
|
|
||||||
ConfigDirty bool `json:"configDirty" gorm:"default:false"`
|
ConfigDirty bool `json:"configDirty" gorm:"default:false"`
|
||||||
ConfigDirtyAt int64 `json:"configDirtyAt"`
|
ConfigDirtyAt int64 `json:"configDirtyAt"`
|
||||||
|
|
||||||
InboundCount int `json:"inboundCount" gorm:"-"`
|
InboundCount int `json:"inboundCount" gorm:"-" example:"5"`
|
||||||
ClientCount int `json:"clientCount" gorm:"-"`
|
ClientCount int `json:"clientCount" gorm:"-" example:"27"`
|
||||||
OnlineCount int `json:"onlineCount" gorm:"-"`
|
OnlineCount int `json:"onlineCount" gorm:"-" example:"3"`
|
||||||
DepletedCount int `json:"depletedCount" gorm:"-"`
|
DepletedCount int `json:"depletedCount" gorm:"-" example:"1"`
|
||||||
|
|
||||||
// ParentGuid + Transitive are set only when a node is surfaced as part of a
|
// ParentGuid + Transitive are set only when a node is surfaced as part of a
|
||||||
// node tree (#4983): direct nodes carry the master panel's own GUID, a
|
// node tree (#4983): direct nodes carry the master panel's own GUID, a
|
||||||
@@ -426,8 +426,8 @@ type Node struct {
|
|||||||
ParentGuid string `json:"parentGuid,omitempty" gorm:"-"`
|
ParentGuid string `json:"parentGuid,omitempty" gorm:"-"`
|
||||||
Transitive bool `json:"transitive,omitempty" gorm:"-"`
|
Transitive bool `json:"transitive,omitempty" gorm:"-"`
|
||||||
|
|
||||||
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
|
CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli" example:"1700000000"`
|
||||||
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
|
UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli" example:"1700000000"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeSummary is the read-only identity of a node as published one hop up: the
|
// NodeSummary is the read-only identity of a node as published one hop up: the
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
|
"gen": "npm run gen:zod && npm run gen:api",
|
||||||
"gen:api": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/build-openapi.mjs",
|
"gen:api": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/build-openapi.mjs",
|
||||||
"gen:zod": "cd .. && go run ./tools/openapigen"
|
"gen:zod": "cd .. && go run ./tools/openapigen"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -304,38 +304,41 @@
|
|||||||
"success": true,
|
"success": true,
|
||||||
"obj": [
|
"obj": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"clientStats": [
|
||||||
"userId": 1,
|
{
|
||||||
"up": 0,
|
"down": 2097152,
|
||||||
|
"email": "user1",
|
||||||
|
"enable": true,
|
||||||
|
"expiryTime": 1735689600000,
|
||||||
|
"id": 14825,
|
||||||
|
"inboundId": 1,
|
||||||
|
"lastOnline": 1735680000000,
|
||||||
|
"reset": 0,
|
||||||
|
"subId": "i7tvdpeffi0hvvf1",
|
||||||
|
"total": 10737418240,
|
||||||
|
"up": 1048576,
|
||||||
|
"uuid": "e18c9a96-71bf-48d4-933f-8b9a46d4290c"
|
||||||
|
}
|
||||||
|
],
|
||||||
"down": 0,
|
"down": 0,
|
||||||
"total": 0,
|
|
||||||
"remark": "VLESS-443",
|
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"expiryTime": 0,
|
"expiryTime": 0,
|
||||||
|
"fallbackParent": null,
|
||||||
|
"id": 1,
|
||||||
|
"lastTrafficResetTime": 0,
|
||||||
"listen": "",
|
"listen": "",
|
||||||
|
"nodeId": null,
|
||||||
|
"originNodeGuid": "",
|
||||||
"port": 443,
|
"port": 443,
|
||||||
"protocol": "vless",
|
"protocol": "vless",
|
||||||
"settings": {
|
"remark": "VLESS-443",
|
||||||
"clients": [],
|
"settings": null,
|
||||||
"decryption": "none"
|
"sniffing": null,
|
||||||
},
|
"streamSettings": null,
|
||||||
"streamSettings": {
|
"tag": "in-443-tcp",
|
||||||
"network": "tcp",
|
"total": 0,
|
||||||
"security": "reality",
|
"trafficReset": "never",
|
||||||
"realitySettings": {
|
"up": 0
|
||||||
"show": false,
|
|
||||||
"dest": "..."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tag": "inbound-443",
|
|
||||||
"sniffing": {
|
|
||||||
"enabled": true,
|
|
||||||
"destOverride": [
|
|
||||||
"http",
|
|
||||||
"tls"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"clientStats": []
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -374,7 +377,6 @@
|
|||||||
"obj": [
|
"obj": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"userId": 1,
|
|
||||||
"remark": "VLESS-443",
|
"remark": "VLESS-443",
|
||||||
"settings": {
|
"settings": {
|
||||||
"clients": [
|
"clients": [
|
||||||
@@ -400,7 +402,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Inbounds"
|
"Inbounds"
|
||||||
],
|
],
|
||||||
"summary": "Lightweight picker projection of the authenticated user’s inbounds. Returns only id, remark, protocol, port, and a server-computed tlsFlowCapable flag (true for VLESS / port-fallback on TCP with tls or reality). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.",
|
"summary": "Lightweight picker projection of the authenticated user’s inbounds. Returns id, remark, tag, protocol, port, a server-computed tlsFlowCapable flag (true for VLESS / port-fallback on TCP with tls or reality), and ssMethod (the Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the client UI to generate a valid Shadowsocks 2022 PSK). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.",
|
||||||
"operationId": "get_panel_api_inbounds_options",
|
"operationId": "get_panel_api_inbounds_options",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@@ -424,9 +426,11 @@
|
|||||||
"obj": [
|
"obj": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"remark": "VLESS-443",
|
|
||||||
"protocol": "vless",
|
|
||||||
"port": 443,
|
"port": 443,
|
||||||
|
"protocol": "vless",
|
||||||
|
"remark": "VLESS-443",
|
||||||
|
"ssMethod": "",
|
||||||
|
"tag": "in-443-tcp",
|
||||||
"tlsFlowCapable": true
|
"tlsFlowCapable": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -3828,8 +3832,8 @@
|
|||||||
"success": true,
|
"success": true,
|
||||||
"obj": {
|
"obj": {
|
||||||
"a1b2-...": [
|
"a1b2-...": [
|
||||||
"inbound-443",
|
"in-443-tcp",
|
||||||
"inbound-8443"
|
"in-8443-tcp"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3914,11 +3918,18 @@
|
|||||||
"example": {
|
"example": {
|
||||||
"success": true,
|
"success": true,
|
||||||
"obj": {
|
"obj": {
|
||||||
"email": "user1",
|
|
||||||
"up": 1048576,
|
|
||||||
"down": 2097152,
|
"down": 2097152,
|
||||||
|
"email": "user1",
|
||||||
|
"enable": true,
|
||||||
|
"expiryTime": 1735689600000,
|
||||||
|
"id": 14825,
|
||||||
|
"inboundId": 1,
|
||||||
|
"lastOnline": 1735680000000,
|
||||||
|
"reset": 0,
|
||||||
|
"subId": "i7tvdpeffi0hvvf1",
|
||||||
"total": 10737418240,
|
"total": 10737418240,
|
||||||
"expiryTime": 1735689600000
|
"up": 1048576,
|
||||||
|
"uuid": "e18c9a96-71bf-48d4-933f-8b9a46d4290c"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4050,31 +4061,38 @@
|
|||||||
"success": true,
|
"success": true,
|
||||||
"obj": [
|
"obj": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
|
||||||
"name": "de-fra-1",
|
|
||||||
"remark": "",
|
|
||||||
"scheme": "https",
|
|
||||||
"address": "node1.example.com",
|
"address": "node1.example.com",
|
||||||
"port": 2053,
|
|
||||||
"basePath": "/",
|
|
||||||
"apiToken": "abcdef...",
|
|
||||||
"enable": true,
|
|
||||||
"allowPrivateAddress": false,
|
"allowPrivateAddress": false,
|
||||||
"status": "online",
|
"apiToken": "abcdef0123456789",
|
||||||
|
"basePath": "/",
|
||||||
|
"clientCount": 27,
|
||||||
|
"configDirty": false,
|
||||||
|
"configDirtyAt": 0,
|
||||||
|
"cpuPct": 23.5,
|
||||||
|
"createdAt": 1700000000,
|
||||||
|
"depletedCount": 1,
|
||||||
|
"enable": true,
|
||||||
|
"guid": "",
|
||||||
|
"id": 1,
|
||||||
|
"inboundCount": 5,
|
||||||
|
"lastError": "",
|
||||||
"lastHeartbeat": 1700000000,
|
"lastHeartbeat": 1700000000,
|
||||||
"latencyMs": 42,
|
"latencyMs": 42,
|
||||||
"xrayVersion": "25.x.x",
|
|
||||||
"panelVersion": "v3.x.x",
|
|
||||||
"cpuPct": 23.5,
|
|
||||||
"memPct": 45.1,
|
"memPct": 45.1,
|
||||||
"uptimeSecs": 86400,
|
"name": "de-fra-1",
|
||||||
"lastError": "",
|
|
||||||
"inboundCount": 5,
|
|
||||||
"clientCount": 27,
|
|
||||||
"onlineCount": 3,
|
"onlineCount": 3,
|
||||||
"depletedCount": 1,
|
"panelVersion": "v3.x.x",
|
||||||
"createdAt": 1700000000,
|
"parentGuid": "",
|
||||||
"updatedAt": 1700000000
|
"pinnedCertSha256": "",
|
||||||
|
"port": 2053,
|
||||||
|
"remark": "",
|
||||||
|
"scheme": "https",
|
||||||
|
"status": "online",
|
||||||
|
"tlsVerifyMode": "verify",
|
||||||
|
"transitive": false,
|
||||||
|
"updatedAt": 1700000000,
|
||||||
|
"uptimeSecs": 86400,
|
||||||
|
"xrayVersion": "25.10.31"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4425,14 +4443,14 @@
|
|||||||
"example": {
|
"example": {
|
||||||
"success": true,
|
"success": true,
|
||||||
"obj": {
|
"obj": {
|
||||||
"status": "online",
|
|
||||||
"latencyMs": 42,
|
|
||||||
"xrayVersion": "25.x.x",
|
|
||||||
"panelVersion": "v3.x.x",
|
|
||||||
"cpuPct": 12.5,
|
"cpuPct": 12.5,
|
||||||
|
"error": "",
|
||||||
|
"latencyMs": 42,
|
||||||
"memPct": 45.2,
|
"memPct": 45.2,
|
||||||
|
"panelVersion": "v3.x.x",
|
||||||
|
"status": "online",
|
||||||
"uptimeSecs": 86400,
|
"uptimeSecs": 86400,
|
||||||
"error": ""
|
"xrayVersion": "25.10.31"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5262,11 +5280,11 @@
|
|||||||
"example": {
|
"example": {
|
||||||
"success": true,
|
"success": true,
|
||||||
"obj": {
|
"obj": {
|
||||||
|
"createdAt": 1736000000,
|
||||||
|
"enabled": true,
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"name": "central-panel-a",
|
"name": "central-panel-a",
|
||||||
"token": "new-token-string",
|
"token": "new-token-string"
|
||||||
"enabled": true,
|
|
||||||
"createdAt": 1736000000
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5435,7 +5453,7 @@
|
|||||||
"success": true,
|
"success": true,
|
||||||
"obj": {
|
"obj": {
|
||||||
"xraySetting": "{...raw xray config...}",
|
"xraySetting": "{...raw xray config...}",
|
||||||
"inboundTags": "[\"inbound-443\"]",
|
"inboundTags": "[\"in-443-tcp\"]",
|
||||||
"clientReverseTags": "[]",
|
"clientReverseTags": "[]",
|
||||||
"outboundTestUrl": "https://www.google.com/generate_204"
|
"outboundTestUrl": "https://www.google.com/generate_204"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { join, dirname } from 'node:path';
|
|||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
import { sections } from '../src/pages/api-docs/endpoints.ts';
|
import { sections } from '../src/pages/api-docs/endpoints.ts';
|
||||||
|
import { EXAMPLES } from '../src/generated/examples.ts';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const outPath = join(__dirname, '..', 'public', 'openapi.json');
|
const outPath = join(__dirname, '..', 'public', 'openapi.json');
|
||||||
@@ -128,7 +129,14 @@ function buildOperation(ep, tag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const responses = {};
|
const responses = {};
|
||||||
const successExample = tryParseJson(ep.response);
|
let successExample = tryParseJson(ep.response);
|
||||||
|
if (successExample === undefined && ep.responseSchema) {
|
||||||
|
const obj = EXAMPLES[ep.responseSchema];
|
||||||
|
if (obj === undefined) {
|
||||||
|
throw new Error(`${ep.method} ${ep.path}: responseSchema "${ep.responseSchema}" has no generated example`);
|
||||||
|
}
|
||||||
|
successExample = { success: true, obj: ep.responseSchemaArray ? [obj] : obj };
|
||||||
|
}
|
||||||
responses['200'] = {
|
responses['200'] = {
|
||||||
description: 'Successful response',
|
description: 'Successful response',
|
||||||
content: {
|
content: {
|
||||||
|
|||||||
@@ -0,0 +1,396 @@
|
|||||||
|
// Code generated by tools/openapigen. DO NOT EDIT.
|
||||||
|
export const EXAMPLES: Record<string, unknown> = {
|
||||||
|
"AllSetting": {
|
||||||
|
"datepicker": "",
|
||||||
|
"expireDiff": 0,
|
||||||
|
"externalTrafficInformEnable": false,
|
||||||
|
"externalTrafficInformURI": "",
|
||||||
|
"ldapAutoCreate": false,
|
||||||
|
"ldapAutoDelete": false,
|
||||||
|
"ldapBaseDN": "",
|
||||||
|
"ldapBindDN": "",
|
||||||
|
"ldapDefaultExpiryDays": 0,
|
||||||
|
"ldapDefaultLimitIP": 0,
|
||||||
|
"ldapDefaultTotalGB": 0,
|
||||||
|
"ldapEnable": false,
|
||||||
|
"ldapFlagField": "",
|
||||||
|
"ldapHost": "",
|
||||||
|
"ldapInboundTags": "",
|
||||||
|
"ldapInvertFlag": false,
|
||||||
|
"ldapPassword": "",
|
||||||
|
"ldapPort": 0,
|
||||||
|
"ldapSyncCron": "",
|
||||||
|
"ldapTruthyValues": "",
|
||||||
|
"ldapUseTLS": false,
|
||||||
|
"ldapUserAttr": "",
|
||||||
|
"ldapUserFilter": "",
|
||||||
|
"ldapVlessField": "",
|
||||||
|
"pageSize": 0,
|
||||||
|
"panelProxy": "",
|
||||||
|
"remarkModel": "",
|
||||||
|
"restartXrayOnClientDisable": false,
|
||||||
|
"sessionMaxAge": 1,
|
||||||
|
"subAnnounce": "",
|
||||||
|
"subCertFile": "",
|
||||||
|
"subClashEnable": false,
|
||||||
|
"subClashEnableRouting": false,
|
||||||
|
"subClashPath": "",
|
||||||
|
"subClashRules": "",
|
||||||
|
"subClashURI": "",
|
||||||
|
"subDomain": "",
|
||||||
|
"subEmailInRemark": false,
|
||||||
|
"subEnable": false,
|
||||||
|
"subEnableRouting": false,
|
||||||
|
"subEncrypt": false,
|
||||||
|
"subJsonEnable": false,
|
||||||
|
"subJsonFinalMask": "",
|
||||||
|
"subJsonMux": "",
|
||||||
|
"subJsonPath": "",
|
||||||
|
"subJsonRules": "",
|
||||||
|
"subJsonURI": "",
|
||||||
|
"subKeyFile": "",
|
||||||
|
"subListen": "",
|
||||||
|
"subPath": "",
|
||||||
|
"subPort": 1,
|
||||||
|
"subProfileUrl": "",
|
||||||
|
"subRoutingRules": "",
|
||||||
|
"subShowInfo": false,
|
||||||
|
"subSupportUrl": "",
|
||||||
|
"subTitle": "",
|
||||||
|
"subURI": "",
|
||||||
|
"subUpdates": 0,
|
||||||
|
"tgBotAPIServer": "",
|
||||||
|
"tgBotBackup": false,
|
||||||
|
"tgBotChatId": "",
|
||||||
|
"tgBotEnable": false,
|
||||||
|
"tgBotLoginNotify": false,
|
||||||
|
"tgBotProxy": "",
|
||||||
|
"tgBotToken": "",
|
||||||
|
"tgCpu": 0,
|
||||||
|
"tgLang": "",
|
||||||
|
"tgRunTime": "",
|
||||||
|
"timeLocation": "",
|
||||||
|
"trafficDiff": 0,
|
||||||
|
"trustedProxyCIDRs": "",
|
||||||
|
"twoFactorEnable": false,
|
||||||
|
"twoFactorToken": "",
|
||||||
|
"webBasePath": "",
|
||||||
|
"webCertFile": "",
|
||||||
|
"webDomain": "",
|
||||||
|
"webKeyFile": "",
|
||||||
|
"webListen": "",
|
||||||
|
"webPort": 1
|
||||||
|
},
|
||||||
|
"AllSettingView": {
|
||||||
|
"datepicker": "",
|
||||||
|
"expireDiff": 0,
|
||||||
|
"externalTrafficInformEnable": false,
|
||||||
|
"externalTrafficInformURI": "",
|
||||||
|
"hasApiToken": false,
|
||||||
|
"hasLdapPassword": false,
|
||||||
|
"hasNordSecret": false,
|
||||||
|
"hasTgBotToken": false,
|
||||||
|
"hasTwoFactorToken": false,
|
||||||
|
"hasWarpSecret": false,
|
||||||
|
"ldapAutoCreate": false,
|
||||||
|
"ldapAutoDelete": false,
|
||||||
|
"ldapBaseDN": "",
|
||||||
|
"ldapBindDN": "",
|
||||||
|
"ldapDefaultExpiryDays": 0,
|
||||||
|
"ldapDefaultLimitIP": 0,
|
||||||
|
"ldapDefaultTotalGB": 0,
|
||||||
|
"ldapEnable": false,
|
||||||
|
"ldapFlagField": "",
|
||||||
|
"ldapHost": "",
|
||||||
|
"ldapInboundTags": "",
|
||||||
|
"ldapInvertFlag": false,
|
||||||
|
"ldapPassword": "",
|
||||||
|
"ldapPort": 0,
|
||||||
|
"ldapSyncCron": "",
|
||||||
|
"ldapTruthyValues": "",
|
||||||
|
"ldapUseTLS": false,
|
||||||
|
"ldapUserAttr": "",
|
||||||
|
"ldapUserFilter": "",
|
||||||
|
"ldapVlessField": "",
|
||||||
|
"pageSize": 0,
|
||||||
|
"panelProxy": "",
|
||||||
|
"remarkModel": "",
|
||||||
|
"restartXrayOnClientDisable": false,
|
||||||
|
"sessionMaxAge": 1,
|
||||||
|
"subAnnounce": "",
|
||||||
|
"subCertFile": "",
|
||||||
|
"subClashEnable": false,
|
||||||
|
"subClashEnableRouting": false,
|
||||||
|
"subClashPath": "",
|
||||||
|
"subClashRules": "",
|
||||||
|
"subClashURI": "",
|
||||||
|
"subDomain": "",
|
||||||
|
"subEmailInRemark": false,
|
||||||
|
"subEnable": false,
|
||||||
|
"subEnableRouting": false,
|
||||||
|
"subEncrypt": false,
|
||||||
|
"subJsonEnable": false,
|
||||||
|
"subJsonFinalMask": "",
|
||||||
|
"subJsonMux": "",
|
||||||
|
"subJsonPath": "",
|
||||||
|
"subJsonRules": "",
|
||||||
|
"subJsonURI": "",
|
||||||
|
"subKeyFile": "",
|
||||||
|
"subListen": "",
|
||||||
|
"subPath": "",
|
||||||
|
"subPort": 1,
|
||||||
|
"subProfileUrl": "",
|
||||||
|
"subRoutingRules": "",
|
||||||
|
"subShowInfo": false,
|
||||||
|
"subSupportUrl": "",
|
||||||
|
"subTitle": "",
|
||||||
|
"subURI": "",
|
||||||
|
"subUpdates": 0,
|
||||||
|
"tgBotAPIServer": "",
|
||||||
|
"tgBotBackup": false,
|
||||||
|
"tgBotChatId": "",
|
||||||
|
"tgBotEnable": false,
|
||||||
|
"tgBotLoginNotify": false,
|
||||||
|
"tgBotProxy": "",
|
||||||
|
"tgBotToken": "",
|
||||||
|
"tgCpu": 0,
|
||||||
|
"tgLang": "",
|
||||||
|
"tgRunTime": "",
|
||||||
|
"timeLocation": "",
|
||||||
|
"trafficDiff": 0,
|
||||||
|
"trustedProxyCIDRs": "",
|
||||||
|
"twoFactorEnable": false,
|
||||||
|
"twoFactorToken": "",
|
||||||
|
"webBasePath": "",
|
||||||
|
"webCertFile": "",
|
||||||
|
"webDomain": "",
|
||||||
|
"webKeyFile": "",
|
||||||
|
"webListen": "",
|
||||||
|
"webPort": 1
|
||||||
|
},
|
||||||
|
"ApiToken": {
|
||||||
|
"createdAt": 0,
|
||||||
|
"enabled": false,
|
||||||
|
"id": 0,
|
||||||
|
"name": "",
|
||||||
|
"token": ""
|
||||||
|
},
|
||||||
|
"ApiTokenView": {
|
||||||
|
"createdAt": 1736000000,
|
||||||
|
"enabled": true,
|
||||||
|
"id": 2,
|
||||||
|
"name": "central-panel-a",
|
||||||
|
"token": "new-token-string"
|
||||||
|
},
|
||||||
|
"Client": {
|
||||||
|
"auth": "",
|
||||||
|
"comment": "",
|
||||||
|
"created_at": 0,
|
||||||
|
"email": "",
|
||||||
|
"enable": false,
|
||||||
|
"expiryTime": 0,
|
||||||
|
"flow": "",
|
||||||
|
"group": "",
|
||||||
|
"id": "",
|
||||||
|
"limitIp": 0,
|
||||||
|
"password": "",
|
||||||
|
"reset": 0,
|
||||||
|
"reverse": null,
|
||||||
|
"security": "",
|
||||||
|
"subId": "",
|
||||||
|
"tgId": 0,
|
||||||
|
"totalGB": 0,
|
||||||
|
"updated_at": 0
|
||||||
|
},
|
||||||
|
"ClientInbound": {
|
||||||
|
"clientId": 0,
|
||||||
|
"createdAt": 0,
|
||||||
|
"flowOverride": "",
|
||||||
|
"inboundId": 0
|
||||||
|
},
|
||||||
|
"ClientRecord": {
|
||||||
|
"auth": "",
|
||||||
|
"comment": "",
|
||||||
|
"createdAt": 0,
|
||||||
|
"email": "",
|
||||||
|
"enable": false,
|
||||||
|
"expiryTime": 0,
|
||||||
|
"flow": "",
|
||||||
|
"group": "",
|
||||||
|
"id": 0,
|
||||||
|
"limitIp": 0,
|
||||||
|
"password": "",
|
||||||
|
"reset": 0,
|
||||||
|
"reverse": null,
|
||||||
|
"security": "",
|
||||||
|
"subId": "",
|
||||||
|
"tgId": 0,
|
||||||
|
"totalGB": 0,
|
||||||
|
"updatedAt": 0,
|
||||||
|
"uuid": ""
|
||||||
|
},
|
||||||
|
"ClientReverse": {
|
||||||
|
"tag": ""
|
||||||
|
},
|
||||||
|
"ClientTraffic": {
|
||||||
|
"down": 2097152,
|
||||||
|
"email": "user1",
|
||||||
|
"enable": true,
|
||||||
|
"expiryTime": 1735689600000,
|
||||||
|
"id": 14825,
|
||||||
|
"inboundId": 1,
|
||||||
|
"lastOnline": 1735680000000,
|
||||||
|
"reset": 0,
|
||||||
|
"subId": "i7tvdpeffi0hvvf1",
|
||||||
|
"total": 10737418240,
|
||||||
|
"up": 1048576,
|
||||||
|
"uuid": "e18c9a96-71bf-48d4-933f-8b9a46d4290c"
|
||||||
|
},
|
||||||
|
"CustomGeoResource": {
|
||||||
|
"alias": "",
|
||||||
|
"createdAt": 0,
|
||||||
|
"id": 0,
|
||||||
|
"lastModified": "",
|
||||||
|
"lastUpdatedAt": 0,
|
||||||
|
"localPath": "",
|
||||||
|
"type": "",
|
||||||
|
"updatedAt": 0,
|
||||||
|
"url": ""
|
||||||
|
},
|
||||||
|
"FallbackParentInfo": {
|
||||||
|
"masterId": 0,
|
||||||
|
"path": ""
|
||||||
|
},
|
||||||
|
"HistoryOfSeeders": {
|
||||||
|
"id": 0,
|
||||||
|
"seederName": ""
|
||||||
|
},
|
||||||
|
"Inbound": {
|
||||||
|
"clientStats": [
|
||||||
|
{
|
||||||
|
"down": 2097152,
|
||||||
|
"email": "user1",
|
||||||
|
"enable": true,
|
||||||
|
"expiryTime": 1735689600000,
|
||||||
|
"id": 14825,
|
||||||
|
"inboundId": 1,
|
||||||
|
"lastOnline": 1735680000000,
|
||||||
|
"reset": 0,
|
||||||
|
"subId": "i7tvdpeffi0hvvf1",
|
||||||
|
"total": 10737418240,
|
||||||
|
"up": 1048576,
|
||||||
|
"uuid": "e18c9a96-71bf-48d4-933f-8b9a46d4290c"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"down": 0,
|
||||||
|
"enable": true,
|
||||||
|
"expiryTime": 0,
|
||||||
|
"fallbackParent": null,
|
||||||
|
"id": 1,
|
||||||
|
"lastTrafficResetTime": 0,
|
||||||
|
"listen": "",
|
||||||
|
"nodeId": null,
|
||||||
|
"originNodeGuid": "",
|
||||||
|
"port": 443,
|
||||||
|
"protocol": "vless",
|
||||||
|
"remark": "VLESS-443",
|
||||||
|
"settings": null,
|
||||||
|
"sniffing": null,
|
||||||
|
"streamSettings": null,
|
||||||
|
"tag": "in-443-tcp",
|
||||||
|
"total": 0,
|
||||||
|
"trafficReset": "never",
|
||||||
|
"up": 0
|
||||||
|
},
|
||||||
|
"InboundClientIps": {
|
||||||
|
"clientEmail": "",
|
||||||
|
"id": 0,
|
||||||
|
"ips": null
|
||||||
|
},
|
||||||
|
"InboundFallback": {
|
||||||
|
"alpn": "",
|
||||||
|
"childId": 0,
|
||||||
|
"dest": "",
|
||||||
|
"id": 0,
|
||||||
|
"masterId": 0,
|
||||||
|
"name": "",
|
||||||
|
"path": "",
|
||||||
|
"sortOrder": 0,
|
||||||
|
"xver": 0
|
||||||
|
},
|
||||||
|
"InboundOption": {
|
||||||
|
"id": 1,
|
||||||
|
"port": 443,
|
||||||
|
"protocol": "vless",
|
||||||
|
"remark": "VLESS-443",
|
||||||
|
"ssMethod": "",
|
||||||
|
"tag": "in-443-tcp",
|
||||||
|
"tlsFlowCapable": true
|
||||||
|
},
|
||||||
|
"Msg": {
|
||||||
|
"msg": "",
|
||||||
|
"obj": null,
|
||||||
|
"success": false
|
||||||
|
},
|
||||||
|
"Node": {
|
||||||
|
"address": "node1.example.com",
|
||||||
|
"allowPrivateAddress": false,
|
||||||
|
"apiToken": "abcdef0123456789",
|
||||||
|
"basePath": "/",
|
||||||
|
"clientCount": 27,
|
||||||
|
"configDirty": false,
|
||||||
|
"configDirtyAt": 0,
|
||||||
|
"cpuPct": 23.5,
|
||||||
|
"createdAt": 1700000000,
|
||||||
|
"depletedCount": 1,
|
||||||
|
"enable": true,
|
||||||
|
"guid": "",
|
||||||
|
"id": 1,
|
||||||
|
"inboundCount": 5,
|
||||||
|
"lastError": "",
|
||||||
|
"lastHeartbeat": 1700000000,
|
||||||
|
"latencyMs": 42,
|
||||||
|
"memPct": 45.1,
|
||||||
|
"name": "de-fra-1",
|
||||||
|
"onlineCount": 3,
|
||||||
|
"panelVersion": "v3.x.x",
|
||||||
|
"parentGuid": "",
|
||||||
|
"pinnedCertSha256": "",
|
||||||
|
"port": 2053,
|
||||||
|
"remark": "",
|
||||||
|
"scheme": "https",
|
||||||
|
"status": "online",
|
||||||
|
"tlsVerifyMode": "verify",
|
||||||
|
"transitive": false,
|
||||||
|
"updatedAt": 1700000000,
|
||||||
|
"uptimeSecs": 86400,
|
||||||
|
"xrayVersion": "25.10.31"
|
||||||
|
},
|
||||||
|
"OutboundTraffics": {
|
||||||
|
"down": 0,
|
||||||
|
"id": 0,
|
||||||
|
"tag": "",
|
||||||
|
"total": 0,
|
||||||
|
"up": 0
|
||||||
|
},
|
||||||
|
"ProbeResultUI": {
|
||||||
|
"cpuPct": 12.5,
|
||||||
|
"error": "",
|
||||||
|
"latencyMs": 42,
|
||||||
|
"memPct": 45.2,
|
||||||
|
"panelVersion": "v3.x.x",
|
||||||
|
"status": "online",
|
||||||
|
"uptimeSecs": 86400,
|
||||||
|
"xrayVersion": "25.10.31"
|
||||||
|
},
|
||||||
|
"Setting": {
|
||||||
|
"id": 0,
|
||||||
|
"key": "",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
"User": {
|
||||||
|
"id": 0,
|
||||||
|
"password": "",
|
||||||
|
"username": ""
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
// Code generated by tools/openapigen. DO NOT EDIT.
|
// Code generated by tools/openapigen. DO NOT EDIT.
|
||||||
|
export type LoginStatus = number;
|
||||||
|
export type ProcessState = string;
|
||||||
export type Protocol = string;
|
export type Protocol = string;
|
||||||
|
export type SubLinkProvider = unknown;
|
||||||
|
export type transportBits = number;
|
||||||
|
|
||||||
export interface AllSetting {
|
export interface AllSetting {
|
||||||
datepicker: string;
|
datepicker: string;
|
||||||
@@ -179,6 +183,14 @@ export interface ApiToken {
|
|||||||
token: string;
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiTokenView {
|
||||||
|
createdAt: number;
|
||||||
|
enabled: boolean;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Client {
|
export interface Client {
|
||||||
auth?: string;
|
auth?: string;
|
||||||
comment: string;
|
comment: string;
|
||||||
@@ -280,6 +292,7 @@ export interface Inbound {
|
|||||||
lastTrafficResetTime: number;
|
lastTrafficResetTime: number;
|
||||||
listen: string;
|
listen: string;
|
||||||
nodeId?: number | null;
|
nodeId?: number | null;
|
||||||
|
originNodeGuid?: string;
|
||||||
port: number;
|
port: number;
|
||||||
protocol: Protocol;
|
protocol: Protocol;
|
||||||
remark: string;
|
remark: string;
|
||||||
@@ -310,6 +323,16 @@ export interface InboundFallback {
|
|||||||
xver: number;
|
xver: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InboundOption {
|
||||||
|
id: number;
|
||||||
|
port: number;
|
||||||
|
protocol: string;
|
||||||
|
remark: string;
|
||||||
|
ssMethod: string;
|
||||||
|
tag: string;
|
||||||
|
tlsFlowCapable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Msg {
|
export interface Msg {
|
||||||
msg: string;
|
msg: string;
|
||||||
obj: unknown;
|
obj: unknown;
|
||||||
@@ -328,6 +351,7 @@ export interface Node {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
depletedCount: number;
|
depletedCount: number;
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
|
guid: string;
|
||||||
id: number;
|
id: number;
|
||||||
inboundCount: number;
|
inboundCount: number;
|
||||||
lastError: string;
|
lastError: string;
|
||||||
@@ -337,12 +361,14 @@ export interface Node {
|
|||||||
name: string;
|
name: string;
|
||||||
onlineCount: number;
|
onlineCount: number;
|
||||||
panelVersion: string;
|
panelVersion: string;
|
||||||
|
parentGuid?: string;
|
||||||
pinnedCertSha256: string;
|
pinnedCertSha256: string;
|
||||||
port: number;
|
port: number;
|
||||||
remark: string;
|
remark: string;
|
||||||
scheme: string;
|
scheme: string;
|
||||||
status: string;
|
status: string;
|
||||||
tlsVerifyMode: string;
|
tlsVerifyMode: string;
|
||||||
|
transitive?: boolean;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
uptimeSecs: number;
|
uptimeSecs: number;
|
||||||
xrayVersion: string;
|
xrayVersion: string;
|
||||||
@@ -356,6 +382,17 @@ export interface OutboundTraffics {
|
|||||||
up: number;
|
up: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProbeResultUI {
|
||||||
|
cpuPct: number;
|
||||||
|
error: string;
|
||||||
|
latencyMs: number;
|
||||||
|
memPct: number;
|
||||||
|
panelVersion: string;
|
||||||
|
status: string;
|
||||||
|
uptimeSecs: number;
|
||||||
|
xrayVersion: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Setting {
|
export interface Setting {
|
||||||
id: number;
|
id: number;
|
||||||
key: string;
|
key: string;
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
// Code generated by tools/openapigen. DO NOT EDIT.
|
// Code generated by tools/openapigen. DO NOT EDIT.
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
export const LoginStatusSchema = z.number().int();
|
||||||
|
export type LoginStatus = z.infer<typeof LoginStatusSchema>;
|
||||||
|
|
||||||
|
export const ProcessStateSchema = z.string();
|
||||||
|
export type ProcessState = z.infer<typeof ProcessStateSchema>;
|
||||||
|
|
||||||
export const ProtocolSchema = z.string();
|
export const ProtocolSchema = z.string();
|
||||||
export type Protocol = z.infer<typeof ProtocolSchema>;
|
export type Protocol = z.infer<typeof ProtocolSchema>;
|
||||||
|
|
||||||
|
export const SubLinkProviderSchema = z.unknown();
|
||||||
|
export type SubLinkProvider = z.infer<typeof SubLinkProviderSchema>;
|
||||||
|
|
||||||
|
export const transportBitsSchema = z.number().int();
|
||||||
|
export type transportBits = z.infer<typeof transportBitsSchema>;
|
||||||
|
|
||||||
export const AllSettingSchema = z.object({
|
export const AllSettingSchema = z.object({
|
||||||
datepicker: z.string(),
|
datepicker: z.string(),
|
||||||
expireDiff: z.number().int().min(0),
|
expireDiff: z.number().int().min(0),
|
||||||
@@ -184,6 +196,15 @@ export const ApiTokenSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type ApiToken = z.infer<typeof ApiTokenSchema>;
|
export type ApiToken = z.infer<typeof ApiTokenSchema>;
|
||||||
|
|
||||||
|
export const ApiTokenViewSchema = z.object({
|
||||||
|
createdAt: z.number().int(),
|
||||||
|
enabled: z.boolean(),
|
||||||
|
id: z.number().int(),
|
||||||
|
name: z.string(),
|
||||||
|
token: z.string().optional(),
|
||||||
|
});
|
||||||
|
export type ApiTokenView = z.infer<typeof ApiTokenViewSchema>;
|
||||||
|
|
||||||
export const ClientSchema = z.object({
|
export const ClientSchema = z.object({
|
||||||
auth: z.string().optional(),
|
auth: z.string().optional(),
|
||||||
comment: z.string(),
|
comment: z.string(),
|
||||||
@@ -293,6 +314,7 @@ export const InboundSchema = z.object({
|
|||||||
lastTrafficResetTime: z.number().int(),
|
lastTrafficResetTime: z.number().int(),
|
||||||
listen: z.string(),
|
listen: z.string(),
|
||||||
nodeId: z.number().int().nullable().optional(),
|
nodeId: z.number().int().nullable().optional(),
|
||||||
|
originNodeGuid: z.string().optional(),
|
||||||
port: z.number().int().min(0).max(65535),
|
port: z.number().int().min(0).max(65535),
|
||||||
protocol: z.enum(['vmess', 'vless', 'trojan', 'shadowsocks', 'wireguard', 'hysteria', 'http', 'mixed', 'tunnel', 'tun']),
|
protocol: z.enum(['vmess', 'vless', 'trojan', 'shadowsocks', 'wireguard', 'hysteria', 'http', 'mixed', 'tunnel', 'tun']),
|
||||||
remark: z.string(),
|
remark: z.string(),
|
||||||
@@ -326,6 +348,17 @@ export const InboundFallbackSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type InboundFallback = z.infer<typeof InboundFallbackSchema>;
|
export type InboundFallback = z.infer<typeof InboundFallbackSchema>;
|
||||||
|
|
||||||
|
export const InboundOptionSchema = z.object({
|
||||||
|
id: z.number().int(),
|
||||||
|
port: z.number().int(),
|
||||||
|
protocol: z.string(),
|
||||||
|
remark: z.string(),
|
||||||
|
ssMethod: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
tlsFlowCapable: z.boolean(),
|
||||||
|
});
|
||||||
|
export type InboundOption = z.infer<typeof InboundOptionSchema>;
|
||||||
|
|
||||||
export const MsgSchema = z.object({
|
export const MsgSchema = z.object({
|
||||||
msg: z.string(),
|
msg: z.string(),
|
||||||
obj: z.unknown(),
|
obj: z.unknown(),
|
||||||
@@ -345,6 +378,7 @@ export const NodeSchema = z.object({
|
|||||||
createdAt: z.number().int(),
|
createdAt: z.number().int(),
|
||||||
depletedCount: z.number().int(),
|
depletedCount: z.number().int(),
|
||||||
enable: z.boolean(),
|
enable: z.boolean(),
|
||||||
|
guid: z.string(),
|
||||||
id: z.number().int(),
|
id: z.number().int(),
|
||||||
inboundCount: z.number().int(),
|
inboundCount: z.number().int(),
|
||||||
lastError: z.string(),
|
lastError: z.string(),
|
||||||
@@ -354,12 +388,14 @@ export const NodeSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
onlineCount: z.number().int(),
|
onlineCount: z.number().int(),
|
||||||
panelVersion: z.string(),
|
panelVersion: z.string(),
|
||||||
|
parentGuid: z.string().optional(),
|
||||||
pinnedCertSha256: z.string(),
|
pinnedCertSha256: z.string(),
|
||||||
port: z.number().int().min(1).max(65535),
|
port: z.number().int().min(1).max(65535),
|
||||||
remark: z.string(),
|
remark: z.string(),
|
||||||
scheme: z.enum(['http', 'https']),
|
scheme: z.enum(['http', 'https']),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
tlsVerifyMode: z.enum(['verify', 'skip', 'pin']),
|
tlsVerifyMode: z.enum(['verify', 'skip', 'pin']),
|
||||||
|
transitive: z.boolean().optional(),
|
||||||
updatedAt: z.number().int(),
|
updatedAt: z.number().int(),
|
||||||
uptimeSecs: z.number().int(),
|
uptimeSecs: z.number().int(),
|
||||||
xrayVersion: z.string(),
|
xrayVersion: z.string(),
|
||||||
@@ -375,6 +411,18 @@ export const OutboundTrafficsSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type OutboundTraffics = z.infer<typeof OutboundTrafficsSchema>;
|
export type OutboundTraffics = z.infer<typeof OutboundTrafficsSchema>;
|
||||||
|
|
||||||
|
export const ProbeResultUISchema = z.object({
|
||||||
|
cpuPct: z.number(),
|
||||||
|
error: z.string(),
|
||||||
|
latencyMs: z.number().int(),
|
||||||
|
memPct: z.number(),
|
||||||
|
panelVersion: z.string(),
|
||||||
|
status: z.string(),
|
||||||
|
uptimeSecs: z.number().int(),
|
||||||
|
xrayVersion: z.string(),
|
||||||
|
});
|
||||||
|
export type ProbeResultUI = z.infer<typeof ProbeResultUISchema>;
|
||||||
|
|
||||||
export const SettingSchema = z.object({
|
export const SettingSchema = z.object({
|
||||||
id: z.number().int(),
|
id: z.number().int(),
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export default function ApiDocsPage() {
|
|||||||
docExpansion="list"
|
docExpansion="list"
|
||||||
deepLinking={false}
|
deepLinking={false}
|
||||||
tryItOutEnabled
|
tryItOutEnabled
|
||||||
|
persistAuthorization
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export interface Endpoint {
|
|||||||
response?: string;
|
response?: string;
|
||||||
errorResponse?: string;
|
errorResponse?: string;
|
||||||
errorStatus?: number;
|
errorStatus?: number;
|
||||||
|
responseSchema?: string;
|
||||||
|
responseSchemaArray?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubscriptionHeader {
|
export interface SubscriptionHeader {
|
||||||
@@ -107,22 +109,22 @@ export const sections: readonly Section[] = [
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/panel/api/inbounds/list',
|
path: '/panel/api/inbounds/list',
|
||||||
summary: 'List every inbound owned by the authenticated user, including each inbound’s clientStats traffic counters. settings, streamSettings, and sniffing are returned as nested JSON objects (no escaped strings); legacy callers that send them back as JSON-encoded strings are still accepted on write.',
|
summary: 'List every inbound owned by the authenticated user, including each inbound’s clientStats traffic counters. settings, streamSettings, and sniffing are returned as nested JSON objects (no escaped strings); legacy callers that send them back as JSON-encoded strings are still accepted on write.',
|
||||||
response:
|
responseSchema: 'Inbound',
|
||||||
'{\n "success": true,\n "obj": [\n {\n "id": 1,\n "userId": 1,\n "up": 0,\n "down": 0,\n "total": 0,\n "remark": "VLESS-443",\n "enable": true,\n "expiryTime": 0,\n "listen": "",\n "port": 443,\n "protocol": "vless",\n "settings": {\n "clients": [],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": { "show": false, "dest": "..." }\n },\n "tag": "inbound-443",\n "sniffing": {\n "enabled": true,\n "destOverride": ["http", "tls"]\n },\n "clientStats": []\n }\n ]\n}',
|
responseSchemaArray: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/panel/api/inbounds/list/slim',
|
path: '/panel/api/inbounds/list/slim',
|
||||||
summary: 'Same shape as /list but with settings.clients[] stripped down to {email, enable, comment} and ClientStats not enriched with UUID/SubId. Use this for list pages; fetch /get/:id when you need the full per-client payload (uuid, password, flow, ...).',
|
summary: 'Same shape as /list but with settings.clients[] stripped down to {email, enable, comment} and ClientStats not enriched with UUID/SubId. Use this for list pages; fetch /get/:id when you need the full per-client payload (uuid, password, flow, ...).',
|
||||||
response:
|
response:
|
||||||
'{\n "success": true,\n "obj": [\n {\n "id": 1,\n "userId": 1,\n "remark": "VLESS-443",\n "settings": {\n "clients": [\n { "email": "alice", "enable": true }\n ],\n "decryption": "none"\n },\n "clientStats": []\n }\n ]\n}',
|
'{\n "success": true,\n "obj": [\n {\n "id": 1,\n "remark": "VLESS-443",\n "settings": {\n "clients": [\n { "email": "alice", "enable": true }\n ],\n "decryption": "none"\n },\n "clientStats": []\n }\n ]\n}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/panel/api/inbounds/options',
|
path: '/panel/api/inbounds/options',
|
||||||
summary: 'Lightweight picker projection of the authenticated user’s inbounds. Returns only id, remark, protocol, port, and a server-computed tlsFlowCapable flag (true for VLESS / port-fallback on TCP with tls or reality). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.',
|
summary: 'Lightweight picker projection of the authenticated user’s inbounds. Returns id, remark, tag, protocol, port, a server-computed tlsFlowCapable flag (true for VLESS / port-fallback on TCP with tls or reality), and ssMethod (the Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the client UI to generate a valid Shadowsocks 2022 PSK). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.',
|
||||||
response:
|
responseSchema: 'InboundOption',
|
||||||
'{\n "success": true,\n "obj": [\n {\n "id": 1,\n "remark": "VLESS-443",\n "protocol": "vless",\n "port": 443,\n "tlsFlowCapable": true\n }\n ]\n}',
|
responseSchemaArray: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -696,7 +698,7 @@ export const sections: readonly Section[] = [
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/panel/api/clients/activeInbounds',
|
path: '/panel/api/clients/activeInbounds',
|
||||||
summary: 'Inbound tags that carried traffic within the heartbeat window, grouped by the hosting node\'s panelGuid. Pairs with onlinesByGuid so the inbounds page only marks a multi-inbound client online on the inbounds it actually used. Nodes that do not report per-inbound activity are absent.',
|
summary: 'Inbound tags that carried traffic within the heartbeat window, grouped by the hosting node\'s panelGuid. Pairs with onlinesByGuid so the inbounds page only marks a multi-inbound client online on the inbounds it actually used. Nodes that do not report per-inbound activity are absent.',
|
||||||
response: '{\n "success": true,\n "obj": {\n "a1b2-...": ["inbound-443", "inbound-8443"]\n }\n}',
|
response: '{\n "success": true,\n "obj": {\n "a1b2-...": ["in-443-tcp", "in-8443-tcp"]\n }\n}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -711,7 +713,7 @@ export const sections: readonly Section[] = [
|
|||||||
params: [
|
params: [
|
||||||
{ name: 'email', in: 'path', type: 'string', desc: 'Client email (unique across the panel).' },
|
{ name: 'email', in: 'path', type: 'string', desc: 'Client email (unique across the panel).' },
|
||||||
],
|
],
|
||||||
response: '{\n "success": true,\n "obj": {\n "email": "user1",\n "up": 1048576,\n "down": 2097152,\n "total": 10737418240,\n "expiryTime": 1735689600000\n }\n}',
|
responseSchema: 'ClientTraffic',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -748,7 +750,8 @@ export const sections: readonly Section[] = [
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/panel/api/nodes/list',
|
path: '/panel/api/nodes/list',
|
||||||
summary: 'List every configured node with its connection details, health, and last heartbeat patch.',
|
summary: 'List every configured node with its connection details, health, and last heartbeat patch.',
|
||||||
response: '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "name": "de-fra-1",\n "remark": "",\n "scheme": "https",\n "address": "node1.example.com",\n "port": 2053,\n "basePath": "/",\n "apiToken": "abcdef...",\n "enable": true,\n "allowPrivateAddress": false,\n "status": "online",\n "lastHeartbeat": 1700000000,\n "latencyMs": 42,\n "xrayVersion": "25.x.x",\n "panelVersion": "v3.x.x",\n "cpuPct": 23.5,\n "memPct": 45.1,\n "uptimeSecs": 86400,\n "lastError": "",\n "inboundCount": 5,\n "clientCount": 27,\n "onlineCount": 3,\n "depletedCount": 1,\n "createdAt": 1700000000,\n "updatedAt": 1700000000\n }\n ]\n}',
|
responseSchema: 'Node',
|
||||||
|
responseSchemaArray: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -805,7 +808,7 @@ export const sections: readonly Section[] = [
|
|||||||
path: '/panel/api/nodes/test',
|
path: '/panel/api/nodes/test',
|
||||||
summary: 'Probe a node without saving it. Uses the body as connection details and returns the same heartbeat snapshot a registered node would have.',
|
summary: 'Probe a node without saving it. Uses the body as connection details and returns the same heartbeat snapshot a registered node would have.',
|
||||||
body: '{\n "scheme": "https",\n "address": "node1.example.com",\n "port": 2053,\n "basePath": "/",\n "apiToken": "abcdef..."\n}',
|
body: '{\n "scheme": "https",\n "address": "node1.example.com",\n "port": 2053,\n "basePath": "/",\n "apiToken": "abcdef..."\n}',
|
||||||
response: '{\n "success": true,\n "obj": {\n "status": "online",\n "latencyMs": 42,\n "xrayVersion": "25.x.x",\n "panelVersion": "v3.x.x",\n "cpuPct": 12.5,\n "memPct": 45.2,\n "uptimeSecs": 86400,\n "error": ""\n }\n}',
|
responseSchema: 'ProbeResultUI',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -978,7 +981,7 @@ export const sections: readonly Section[] = [
|
|||||||
{ name: 'name', in: 'body', type: 'string', desc: 'Human-readable label, e.g. "central-panel-a".' },
|
{ name: 'name', in: 'body', type: 'string', desc: 'Human-readable label, e.g. "central-panel-a".' },
|
||||||
],
|
],
|
||||||
body: '{\n "name": "central-panel-a"\n}',
|
body: '{\n "name": "central-panel-a"\n}',
|
||||||
response: '{\n "success": true,\n "obj": {\n "id": 2,\n "name": "central-panel-a",\n "token": "new-token-string",\n "enabled": true,\n "createdAt": 1736000000\n }\n}',
|
responseSchema: 'ApiTokenView',
|
||||||
errorResponse: '{\n "success": false,\n "msg": "a token with that name already exists"\n}',
|
errorResponse: '{\n "success": false,\n "msg": "a token with that name already exists"\n}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1014,7 +1017,7 @@ export const sections: readonly Section[] = [
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/panel/xray/',
|
path: '/panel/xray/',
|
||||||
summary: 'Return the Xray config template (JSON string), available inbound tags, client reverse tags, and the configured outbound test URL in one response.',
|
summary: 'Return the Xray config template (JSON string), available inbound tags, client reverse tags, and the configured outbound test URL in one response.',
|
||||||
response: '{\n "success": true,\n "obj": {\n "xraySetting": "{...raw xray config...}",\n "inboundTags": "[\\"inbound-443\\"]",\n "clientReverseTags": "[]",\n "outboundTestUrl": "https://www.google.com/generate_204"\n }\n}',
|
response: '{\n "success": true,\n "obj": {\n "xraySetting": "{...raw xray config...}",\n "inboundTags": "[\\"in-443-tcp\\"]",\n "clientReverseTags": "[]",\n "outboundTestUrl": "https://www.google.com/generate_204"\n }\n}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|||||||
@@ -89,6 +89,15 @@ export default function ClientBulkAddModal({
|
|||||||
[form.inboundIds, flowCapableIds],
|
[form.inboundIds, flowCapableIds],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ss2022Method = useMemo(() => {
|
||||||
|
for (const id of form.inboundIds || []) {
|
||||||
|
const ib = (inbounds || []).find((row) => row.id === id);
|
||||||
|
const method = ib?.ssMethod;
|
||||||
|
if (method && method.substring(0, 4) === '2022') return method;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}, [form.inboundIds, inbounds]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showFlow && form.flow) {
|
if (!showFlow && form.flow) {
|
||||||
|
|
||||||
@@ -153,7 +162,9 @@ export default function ClientBulkAddModal({
|
|||||||
email,
|
email,
|
||||||
subId: form.subId || RandomUtil.randomLowerAndNum(16),
|
subId: form.subId || RandomUtil.randomLowerAndNum(16),
|
||||||
id: RandomUtil.randomUUID(),
|
id: RandomUtil.randomUUID(),
|
||||||
password: RandomUtil.randomLowerAndNum(16),
|
password: ss2022Method
|
||||||
|
? RandomUtil.randomShadowsocksPassword(ss2022Method)
|
||||||
|
: RandomUtil.randomLowerAndNum(16),
|
||||||
auth: RandomUtil.randomLowerAndNum(16),
|
auth: RandomUtil.randomLowerAndNum(16),
|
||||||
flow: showFlow ? (form.flow || '') : '',
|
flow: showFlow ? (form.flow || '') : '',
|
||||||
totalGB: Math.round((form.totalGB || 0) * SizeFormatter.ONE_GB),
|
totalGB: Math.round((form.totalGB || 0) * SizeFormatter.ONE_GB),
|
||||||
|
|||||||
@@ -228,6 +228,21 @@ export default function ClientFormModal({
|
|||||||
return ids;
|
return ids;
|
||||||
}, [inbounds]);
|
}, [inbounds]);
|
||||||
|
|
||||||
|
const ss2022Method = useMemo(() => {
|
||||||
|
for (const id of form.inboundIds || []) {
|
||||||
|
const ib = (inbounds || []).find((row) => row.id === id);
|
||||||
|
const method = ib?.ssMethod;
|
||||||
|
if (method && method.substring(0, 4) === '2022') return method;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}, [form.inboundIds, inbounds]);
|
||||||
|
|
||||||
|
function regeneratePassword() {
|
||||||
|
update('password', ss2022Method
|
||||||
|
? RandomUtil.randomShadowsocksPassword(ss2022Method)
|
||||||
|
: RandomUtil.randomLowerAndNum(16));
|
||||||
|
}
|
||||||
|
|
||||||
const showFlow = useMemo(
|
const showFlow = useMemo(
|
||||||
() => (form.inboundIds || []).some((id) => flowCapableIds.has(id)),
|
() => (form.inboundIds || []).some((id) => flowCapableIds.has(id)),
|
||||||
[form.inboundIds, flowCapableIds],
|
[form.inboundIds, flowCapableIds],
|
||||||
@@ -257,6 +272,15 @@ export default function ClientFormModal({
|
|||||||
}
|
}
|
||||||
}, [showReverseTag, form.reverseTag]);
|
}, [showReverseTag, form.reverseTag]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ss2022Method) return;
|
||||||
|
setForm((prev) => (
|
||||||
|
RandomUtil.isShadowsocks2022Password(prev.password, ss2022Method)
|
||||||
|
? prev
|
||||||
|
: { ...prev, password: RandomUtil.randomShadowsocksPassword(ss2022Method) }
|
||||||
|
));
|
||||||
|
}, [ss2022Method]);
|
||||||
|
|
||||||
const inboundOptions = useMemo(
|
const inboundOptions = useMemo(
|
||||||
() => (inbounds || [])
|
() => (inbounds || [])
|
||||||
.filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || ''))
|
.filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || ''))
|
||||||
@@ -433,7 +457,7 @@ export default function ClientFormModal({
|
|||||||
<Form.Item label={t('pages.clients.password')}>
|
<Form.Item label={t('pages.clients.password')}>
|
||||||
<Space.Compact style={{ display: 'flex' }}>
|
<Space.Compact style={{ display: 'flex' }}>
|
||||||
<Input value={form.password} style={{ flex: 1 }} onChange={(e) => update('password', e.target.value)} />
|
<Input value={form.password} style={{ flex: 1 }} onChange={(e) => update('password', e.target.value)} />
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => update('password', RandomUtil.randomLowerAndNum(16))} />
|
<Button icon={<ReloadOutlined />} onClick={regeneratePassword} />
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const InboundOptionSchema = z.object({
|
|||||||
protocol: z.string().optional(),
|
protocol: z.string().optional(),
|
||||||
port: z.number().optional(),
|
port: z.number().optional(),
|
||||||
tlsFlowCapable: z.boolean().optional(),
|
tlsFlowCapable: z.boolean().optional(),
|
||||||
|
ssMethod: z.string().optional(),
|
||||||
}).loose();
|
}).loose();
|
||||||
|
|
||||||
export const InboundOptionsSchema = z.array(InboundOptionSchema);
|
export const InboundOptionsSchema = z.array(InboundOptionSchema);
|
||||||
|
|||||||
@@ -183,15 +183,22 @@ export class RandomUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static randomShadowsocksPassword(method: string = '2022-blake3-aes-256-gcm'): string {
|
static randomShadowsocksPassword(method: string = '2022-blake3-aes-256-gcm'): string {
|
||||||
let length = 32;
|
const length = method === '2022-blake3-aes-128-gcm' ? 16 : 32;
|
||||||
if (method === '2022-blake3-aes-128-gcm') {
|
|
||||||
length = 16;
|
|
||||||
}
|
|
||||||
const array = new Uint8Array(length);
|
const array = new Uint8Array(length);
|
||||||
window.crypto.getRandomValues(array);
|
window.crypto.getRandomValues(array);
|
||||||
return Base64.alternativeEncode(String.fromCharCode(...array));
|
return Base64.alternativeEncode(String.fromCharCode(...array));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isShadowsocks2022Password(password: string, method: string): boolean {
|
||||||
|
if (!method || method.substring(0, 4) !== '2022') return true;
|
||||||
|
const expected = method === '2022-blake3-aes-128-gcm' ? 16 : 32;
|
||||||
|
try {
|
||||||
|
return window.atob(password).length === expected;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static randomBase64(length: number = 16): string {
|
static randomBase64(length: number = 16): string {
|
||||||
const array = new Uint8Array(length);
|
const array = new Uint8Array(length);
|
||||||
window.crypto.getRandomValues(array);
|
window.crypto.getRandomValues(array);
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func emitExamples(w io.Writer, schemas []Schema, aliases []Alias) error {
|
||||||
|
byName := make(map[string]Schema, len(schemas))
|
||||||
|
for _, s := range schemas {
|
||||||
|
byName[s.Name] = s
|
||||||
|
}
|
||||||
|
aliasByName := make(map[string]Alias, len(aliases))
|
||||||
|
for _, a := range aliases {
|
||||||
|
aliasByName[a.Name] = a
|
||||||
|
}
|
||||||
|
|
||||||
|
gen := &exampleGen{byName: byName, aliasByName: aliasByName}
|
||||||
|
|
||||||
|
out := make(map[string]any, len(schemas))
|
||||||
|
for _, s := range schemas {
|
||||||
|
out[s.Name] = gen.forSchema(s, map[string]bool{})
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.MarshalIndent(out, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintln(w, examplesHeader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(w, "export const EXAMPLES: Record<string, unknown> = %s;\n", payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type exampleGen struct {
|
||||||
|
byName map[string]Schema
|
||||||
|
aliasByName map[string]Alias
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *exampleGen) forSchema(s Schema, visited map[string]bool) map[string]any {
|
||||||
|
obj := make(map[string]any, len(s.Fields))
|
||||||
|
for _, f := range s.Fields {
|
||||||
|
obj[f.JSONName] = g.forField(f, visited)
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *exampleGen) forField(f Field, visited map[string]bool) any {
|
||||||
|
if f.Example != "" {
|
||||||
|
return coerceExample(f.Example, baseKind(f.Type))
|
||||||
|
}
|
||||||
|
if v, ok := firstOneOf(f.Validate); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
bk := baseKind(f.Type)
|
||||||
|
if bk.Kind == KindInt || bk.Kind == KindNumber {
|
||||||
|
if v, ok := numericFloor(bk.Kind, f.Validate); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return g.forType(f.Type, visited)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *exampleGen) forType(t TypeRef, visited map[string]bool) any {
|
||||||
|
switch t.Kind {
|
||||||
|
case KindString:
|
||||||
|
if t.Name == "datetime" {
|
||||||
|
return "2025-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
case KindInt, KindNumber:
|
||||||
|
return 0
|
||||||
|
case KindBool:
|
||||||
|
return false
|
||||||
|
case KindArray:
|
||||||
|
if isVisitedRef(*t.Element, visited) {
|
||||||
|
return []any{}
|
||||||
|
}
|
||||||
|
return []any{g.forType(*t.Element, visited)}
|
||||||
|
case KindMap:
|
||||||
|
return map[string]any{}
|
||||||
|
case KindRef:
|
||||||
|
if t.Name == "nullable" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if alias, ok := g.aliasByName[t.Name]; ok {
|
||||||
|
return g.forType(alias.Underlying, visited)
|
||||||
|
}
|
||||||
|
schema, ok := g.byName[t.Name]
|
||||||
|
if !ok || visited[t.Name] {
|
||||||
|
return map[string]any{}
|
||||||
|
}
|
||||||
|
next := cloneVisited(visited)
|
||||||
|
next[t.Name] = true
|
||||||
|
return g.forSchema(schema, next)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func baseKind(t TypeRef) TypeRef {
|
||||||
|
if t.Kind == KindRef && t.Name == "nullable" && t.Inner != nil {
|
||||||
|
return *t.Inner
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func isVisitedRef(t TypeRef, visited map[string]bool) bool {
|
||||||
|
return t.Kind == KindRef && t.Name != "nullable" && visited[t.Name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneVisited(in map[string]bool) map[string]bool {
|
||||||
|
out := make(map[string]bool, len(in)+1)
|
||||||
|
for k, v := range in {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func numericFloor(kind TypeKind, rules []ValidateRule) (any, bool) {
|
||||||
|
for _, r := range rules {
|
||||||
|
if (r.Name == "gte" || r.Name == "min") && r.Param != "" {
|
||||||
|
return coerceExample(r.Param, TypeRef{Kind: kind}), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstOneOf(rules []ValidateRule) (string, bool) {
|
||||||
|
for _, r := range rules {
|
||||||
|
if r.Name == "oneof" {
|
||||||
|
fields := strings.Fields(r.Param)
|
||||||
|
if len(fields) > 0 {
|
||||||
|
return fields[0], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func coerceExample(ex string, t TypeRef) any {
|
||||||
|
switch t.Kind {
|
||||||
|
case KindInt:
|
||||||
|
if n, err := strconv.ParseInt(ex, 10, 64); err == nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case KindNumber:
|
||||||
|
if n, err := strconv.ParseFloat(ex, 64); err == nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
case KindBool:
|
||||||
|
return ex == "true"
|
||||||
|
case KindString:
|
||||||
|
return ex
|
||||||
|
default:
|
||||||
|
var parsed any
|
||||||
|
if err := json.Unmarshal([]byte(ex), &parsed); err == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
return ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const examplesHeader = `// Code generated by tools/openapigen. DO NOT EDIT.`
|
||||||
@@ -69,6 +69,14 @@ func run(root, outDir string) error {
|
|||||||
"ClientTraffic",
|
"ClientTraffic",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Path: resolveRel(root, "web/service"),
|
||||||
|
StructAllow: setOf(
|
||||||
|
"InboundOption",
|
||||||
|
"ApiTokenView",
|
||||||
|
"ProbeResultUI",
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
schemas, aliases, err := walkPackages(requests)
|
schemas, aliases, err := walkPackages(requests)
|
||||||
@@ -94,6 +102,10 @@ func run(root, outDir string) error {
|
|||||||
if err := emitTypes(typesBuf, schemas, aliases); err != nil {
|
if err := emitTypes(typesBuf, schemas, aliases); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
examplesBuf := &bytes.Buffer{}
|
||||||
|
if err := emitExamples(examplesBuf, schemas, aliases); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(filepath.Join(target, "zod.ts"), zodBuf.Bytes(), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(target, "zod.ts"), zodBuf.Bytes(), 0o644); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -101,6 +113,9 @@ func run(root, outDir string) error {
|
|||||||
if err := os.WriteFile(filepath.Join(target, "types.ts"), typesBuf.Bytes(), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(target, "types.ts"), typesBuf.Bytes(), 0o644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(target, "examples.ts"), examplesBuf.Bytes(), 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("openapigen: wrote %d schemas to %s\n", len(schemas), target)
|
fmt.Printf("openapigen: wrote %d schemas to %s\n", len(schemas), target)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type Field struct {
|
|||||||
Skip bool
|
Skip bool
|
||||||
Validate []ValidateRule
|
Validate []ValidateRule
|
||||||
Doc string
|
Doc string
|
||||||
|
Example string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TypeRef struct {
|
type TypeRef struct {
|
||||||
@@ -59,10 +60,11 @@ type ValidateRule struct {
|
|||||||
Param string
|
Param string
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseStructTag(raw string) (json string, validate string, gormHasDash bool) {
|
func parseStructTag(raw string) (json string, validate string, example string, gormHasDash bool) {
|
||||||
tag := reflect.StructTag(strings.Trim(raw, "`"))
|
tag := reflect.StructTag(strings.Trim(raw, "`"))
|
||||||
json = tag.Get("json")
|
json = tag.Get("json")
|
||||||
validate = tag.Get("validate")
|
validate = tag.Get("validate")
|
||||||
|
example = tag.Get("example")
|
||||||
if g := tag.Get("gorm"); g != "" {
|
if g := tag.Get("gorm"); g != "" {
|
||||||
for part := range strings.SplitSeq(g, ";") {
|
for part := range strings.SplitSeq(g, ";") {
|
||||||
if strings.TrimSpace(part) == "-" {
|
if strings.TrimSpace(part) == "-" {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func buildFields(fld *ast.Field, overrides []walkOverride) []Field {
|
|||||||
if fld.Tag != nil {
|
if fld.Tag != nil {
|
||||||
tag = fld.Tag.Value
|
tag = fld.Tag.Value
|
||||||
}
|
}
|
||||||
jsonTag, validateTag, gormDash := parseStructTag(tag)
|
jsonTag, validateTag, exampleTag, gormDash := parseStructTag(tag)
|
||||||
if gormDash && jsonTag == "" {
|
if gormDash && jsonTag == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -132,6 +132,7 @@ func buildFields(fld *ast.Field, overrides []walkOverride) []Field {
|
|||||||
Optional: omitempty || isPointer(fld.Type),
|
Optional: omitempty || isPointer(fld.Type),
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Doc: doc,
|
Doc: doc,
|
||||||
|
Example: exampleTag,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +155,7 @@ func buildFields(fld *ast.Field, overrides []walkOverride) []Field {
|
|||||||
Optional: omitempty || isPointer(fld.Type),
|
Optional: omitempty || isPointer(fld.Type),
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Doc: doc,
|
Doc: doc,
|
||||||
|
Example: exampleTag,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ type ApiTokenService struct{}
|
|||||||
const apiTokenLength = 48
|
const apiTokenLength = 48
|
||||||
|
|
||||||
type ApiTokenView struct {
|
type ApiTokenView struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id" example:"2"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" example:"central-panel-a"`
|
||||||
Token string `json:"token,omitempty"`
|
Token string `json:"token,omitempty" example:"new-token-string"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled" example:"true"`
|
||||||
CreatedAt int64 `json:"createdAt"`
|
CreatedAt int64 `json:"createdAt" example:"1736000000"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// toView builds the metadata view returned by List. It never carries the
|
// toView builds the metadata view returned by List. It never carries the
|
||||||
|
|||||||
+26
-7
@@ -356,12 +356,13 @@ func (s *InboundService) annotateFallbackParents(db *gorm.DB, inbounds []*model.
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InboundOption struct {
|
type InboundOption struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id" example:"1"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark" example:"VLESS-443"`
|
||||||
Tag string `json:"tag"`
|
Tag string `json:"tag" example:"in-443-tcp"`
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol" example:"vless"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port" example:"443"`
|
||||||
TlsFlowCapable bool `json:"tlsFlowCapable"`
|
TlsFlowCapable bool `json:"tlsFlowCapable" example:"true"`
|
||||||
|
SsMethod string `json:"ssMethod"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error) {
|
func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error) {
|
||||||
@@ -373,9 +374,10 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error)
|
|||||||
Protocol string `gorm:"column:protocol"`
|
Protocol string `gorm:"column:protocol"`
|
||||||
Port int `gorm:"column:port"`
|
Port int `gorm:"column:port"`
|
||||||
StreamSettings string `gorm:"column:stream_settings"`
|
StreamSettings string `gorm:"column:stream_settings"`
|
||||||
|
Settings string `gorm:"column:settings"`
|
||||||
}
|
}
|
||||||
err := db.Table("inbounds").
|
err := db.Table("inbounds").
|
||||||
Select("id, remark, tag, protocol, port, stream_settings").
|
Select("id, remark, tag, protocol, port, stream_settings, settings").
|
||||||
Where("user_id = ?", userId).
|
Where("user_id = ?", userId).
|
||||||
Order("id ASC").
|
Order("id ASC").
|
||||||
Scan(&rows).Error
|
Scan(&rows).Error
|
||||||
@@ -391,11 +393,28 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error)
|
|||||||
Protocol: r.Protocol,
|
Protocol: r.Protocol,
|
||||||
Port: r.Port,
|
Port: r.Port,
|
||||||
TlsFlowCapable: inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings),
|
TlsFlowCapable: inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings),
|
||||||
|
SsMethod: inboundShadowsocksMethod(r.Protocol, r.Settings),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inboundShadowsocksMethod extracts settings.method for Shadowsocks inbounds so
|
||||||
|
// the client UI can generate a valid PSK (base64 of the method's key length)
|
||||||
|
// for Shadowsocks 2022 ciphers. Returns "" for non-Shadowsocks inbounds.
|
||||||
|
func inboundShadowsocksMethod(protocol, settings string) string {
|
||||||
|
if protocol != string(model.Shadowsocks) || settings == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var s struct {
|
||||||
|
Method string `json:"method"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(settings), &s); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s.Method
|
||||||
|
}
|
||||||
|
|
||||||
// inboundCanEnableTlsFlow mirrors Inbound.canEnableTlsFlow() from the frontend:
|
// inboundCanEnableTlsFlow mirrors Inbound.canEnableTlsFlow() from the frontend:
|
||||||
// XTLS Vision is only valid for VLESS on TCP with tls or reality.
|
// XTLS Vision is only valid for VLESS on TCP with tls or reality.
|
||||||
func inboundCanEnableTlsFlow(protocol, streamSettings string) bool {
|
func inboundCanEnableTlsFlow(protocol, streamSettings string) bool {
|
||||||
|
|||||||
+7
-7
@@ -635,13 +635,13 @@ func (s *NodeService) Probe(ctx context.Context, n *model.Node) (HeartbeatPatch,
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProbeResultUI struct {
|
type ProbeResultUI struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status" example:"online"`
|
||||||
LatencyMs int `json:"latencyMs"`
|
LatencyMs int `json:"latencyMs" example:"42"`
|
||||||
XrayVersion string `json:"xrayVersion"`
|
XrayVersion string `json:"xrayVersion" example:"25.10.31"`
|
||||||
PanelVersion string `json:"panelVersion"`
|
PanelVersion string `json:"panelVersion" example:"v3.x.x"`
|
||||||
CpuPct float64 `json:"cpuPct"`
|
CpuPct float64 `json:"cpuPct" example:"12.5"`
|
||||||
MemPct float64 `json:"memPct"`
|
MemPct float64 `json:"memPct" example:"45.2"`
|
||||||
UptimeSecs uint64 `json:"uptimeSecs"`
|
UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -3,16 +3,16 @@ package xray
|
|||||||
// ClientTraffic represents traffic statistics and limits for a specific client.
|
// ClientTraffic represents traffic statistics and limits for a specific client.
|
||||||
// It tracks upload/download usage, expiry times, and online status for inbound clients.
|
// It tracks upload/download usage, expiry times, and online status for inbound clients.
|
||||||
type ClientTraffic struct {
|
type ClientTraffic struct {
|
||||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement" example:"14825"`
|
||||||
InboundId int `json:"inboundId" form:"inboundId"`
|
InboundId int `json:"inboundId" form:"inboundId" example:"1"`
|
||||||
Enable bool `json:"enable" form:"enable"`
|
Enable bool `json:"enable" form:"enable" example:"true"`
|
||||||
Email string `json:"email" form:"email" gorm:"unique"`
|
Email string `json:"email" form:"email" gorm:"unique" example:"user1"`
|
||||||
UUID string `json:"uuid" form:"uuid" gorm:"-"`
|
UUID string `json:"uuid" form:"uuid" gorm:"-" example:"e18c9a96-71bf-48d4-933f-8b9a46d4290c"`
|
||||||
SubId string `json:"subId" form:"subId" gorm:"-"`
|
SubId string `json:"subId" form:"subId" gorm:"-" example:"i7tvdpeffi0hvvf1"`
|
||||||
Up int64 `json:"up" form:"up"`
|
Up int64 `json:"up" form:"up" example:"1048576"`
|
||||||
Down int64 `json:"down" form:"down"`
|
Down int64 `json:"down" form:"down" example:"2097152"`
|
||||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
ExpiryTime int64 `json:"expiryTime" form:"expiryTime" example:"1735689600000"`
|
||||||
Total int64 `json:"total" form:"total"`
|
Total int64 `json:"total" form:"total" example:"10737418240"`
|
||||||
Reset int `json:"reset" form:"reset" gorm:"default:0"`
|
Reset int `json:"reset" form:"reset" gorm:"default:0" example:"0"`
|
||||||
LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"`
|
LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0" example:"1735680000000"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user