1
0

model.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  1. // Package model defines the database models and data structures used by the 3x-ui panel.
  2. package model
  3. import (
  4. "bytes"
  5. "encoding/json"
  6. "fmt"
  7. "strings"
  8. "github.com/mhsanaei/3x-ui/v3/util/json_util"
  9. "github.com/mhsanaei/3x-ui/v3/xray"
  10. )
  11. // Protocol represents the protocol type for Xray inbounds.
  12. type Protocol string
  13. // Protocol constants for different Xray inbound protocols.
  14. // Hysteria v2 is not a distinct protocol — it is plain "hysteria"
  15. // with streamSettings.version = 2. The share-link URI scheme
  16. // "hysteria2://" is independent of this and is still emitted by the
  17. // link generator when the stream version is 2.
  18. const (
  19. VMESS Protocol = "vmess"
  20. VLESS Protocol = "vless"
  21. Tunnel Protocol = "tunnel"
  22. HTTP Protocol = "http"
  23. Trojan Protocol = "trojan"
  24. Shadowsocks Protocol = "shadowsocks"
  25. Mixed Protocol = "mixed"
  26. WireGuard Protocol = "wireguard"
  27. Hysteria Protocol = "hysteria"
  28. )
  29. // User represents a user account in the 3x-ui panel.
  30. type User struct {
  31. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  32. Username string `json:"username"`
  33. Password string `json:"password"`
  34. LoginEpoch int64 `json:"-" gorm:"default:0"`
  35. }
  36. // Inbound represents an Xray inbound configuration with traffic statistics and settings.
  37. type Inbound struct {
  38. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` // Unique identifier
  39. UserId int `json:"-"` // Associated user ID
  40. Up int64 `json:"up" form:"up"` // Upload traffic in bytes
  41. Down int64 `json:"down" form:"down"` // Download traffic in bytes
  42. Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
  43. Remark string `json:"remark" form:"remark"` // Human-readable remark
  44. Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1"` // Whether the inbound is enabled
  45. ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
  46. 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
  47. LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
  48. ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
  49. // Xray configuration fields
  50. Listen string `json:"listen" form:"listen"`
  51. Port int `json:"port" form:"port" validate:"gte=0,lte=65535"`
  52. Protocol Protocol `json:"protocol" form:"protocol" validate:"required,oneof=vmess vless trojan shadowsocks wireguard hysteria http mixed tunnel tun"`
  53. Settings string `json:"settings" form:"settings"`
  54. StreamSettings string `json:"streamSettings" form:"streamSettings"`
  55. Tag string `json:"tag" form:"tag" gorm:"unique"`
  56. Sniffing string `json:"sniffing" form:"sniffing"`
  57. NodeID *int `json:"nodeId,omitempty" form:"nodeId" gorm:"index"`
  58. // OriginNodeGuid is the panelGuid of the node that physically hosts this
  59. // inbound, propagated up across hops (#4983). Empty for an inbound that
  60. // lives on this panel's own xray; set to the originating node's GUID when
  61. // the inbound was synced from a node (kept as-is across further hops). Lets
  62. // the master attribute a deeply nested inbound to the real node instead of
  63. // the intermediate one it was fetched through.
  64. OriginNodeGuid string `json:"originNodeGuid,omitempty" form:"originNodeGuid" gorm:"column:origin_node_guid;index"`
  65. // FallbackParent is populated by the API layer when this inbound is
  66. // attached as a fallback child of a VLESS/Trojan TCP-TLS master.
  67. // The frontend uses it to rewrite client-share links so they advertise
  68. // the master's externally reachable endpoint instead of the child's
  69. // loopback listen. Not persisted.
  70. FallbackParent *FallbackParentInfo `json:"fallbackParent,omitempty" gorm:"-"`
  71. }
  72. // FallbackParentInfo carries everything the frontend needs to rewrite a
  73. // child inbound's client link: where to connect (the master's address
  74. // and port) and which path matched on the master's fallbacks array.
  75. // The frontend already has the master inbound in its dbInbounds list,
  76. // so we only ship identifiers + the match path here.
  77. type FallbackParentInfo struct {
  78. MasterId int `json:"masterId"`
  79. Path string `json:"path,omitempty"`
  80. }
  81. // OutboundTraffics tracks traffic statistics for Xray outbound connections.
  82. type OutboundTraffics struct {
  83. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  84. Tag string `json:"tag" form:"tag" gorm:"unique"`
  85. Up int64 `json:"up" form:"up" gorm:"default:0"`
  86. Down int64 `json:"down" form:"down" gorm:"default:0"`
  87. Total int64 `json:"total" form:"total" gorm:"default:0"`
  88. }
  89. // InboundClientIps stores IP addresses associated with inbound clients for access control.
  90. type InboundClientIps struct {
  91. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  92. ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
  93. Ips string `json:"ips" form:"ips"`
  94. }
  95. // MarshalJSON emits the Ips column as a real JSON array instead of an escaped
  96. // JSON-text string. Empty or unparseable storage renders as null so API
  97. // consumers don't have to special-case the legacy double-encoded shape.
  98. func (ic InboundClientIps) MarshalJSON() ([]byte, error) {
  99. type alias InboundClientIps
  100. return json.Marshal(struct {
  101. alias
  102. Ips json.RawMessage `json:"ips"`
  103. }{
  104. alias: alias(ic),
  105. Ips: jsonStringFieldToRaw(ic.Ips),
  106. })
  107. }
  108. // UnmarshalJSON accepts ips as either a JSON array (modern shape) or a
  109. // JSON-encoded string (legacy shape), normalising back to the JSON-text the
  110. // column stores.
  111. func (ic *InboundClientIps) UnmarshalJSON(data []byte) error {
  112. type alias InboundClientIps
  113. aux := struct {
  114. *alias
  115. Ips json.RawMessage `json:"ips"`
  116. }{
  117. alias: (*alias)(ic),
  118. }
  119. if err := json.Unmarshal(data, &aux); err != nil {
  120. return err
  121. }
  122. ic.Ips = jsonStringFieldFromRaw(aux.Ips)
  123. return nil
  124. }
  125. // HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.
  126. type HistoryOfSeeders struct {
  127. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  128. SeederName string `json:"seederName"`
  129. }
  130. type ApiToken struct {
  131. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  132. Name string `json:"name" gorm:"uniqueIndex;not null"`
  133. Token string `json:"token" gorm:"not null"` // SHA-256 hash; the plaintext is shown only once at creation
  134. Enabled bool `json:"enabled" gorm:"default:true"`
  135. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  136. }
  137. // MarshalJSON emits settings, streamSettings, and sniffing as nested JSON
  138. // objects rather than escaped strings, so API consumers don't need to JSON.parse
  139. // a string inside a string. Empty fields render as null; fields whose stored
  140. // text isn't valid JSON fall back to a JSON-encoded string so no data is lost.
  141. func (i Inbound) MarshalJSON() ([]byte, error) {
  142. type alias Inbound
  143. return json.Marshal(struct {
  144. alias
  145. Settings json.RawMessage `json:"settings"`
  146. StreamSettings json.RawMessage `json:"streamSettings"`
  147. Sniffing json.RawMessage `json:"sniffing"`
  148. }{
  149. alias: alias(i),
  150. Settings: jsonStringFieldToRaw(i.Settings),
  151. StreamSettings: jsonStringFieldToRaw(i.StreamSettings),
  152. Sniffing: jsonStringFieldToRaw(i.Sniffing),
  153. })
  154. }
  155. // UnmarshalJSON accepts settings, streamSettings, and sniffing as either a raw
  156. // JSON object/array (the modern shape MarshalJSON emits) or a JSON-encoded
  157. // string (the legacy shape). Either form is normalised back to the JSON-text
  158. // string the DB column stores.
  159. func (i *Inbound) UnmarshalJSON(data []byte) error {
  160. type alias Inbound
  161. aux := struct {
  162. *alias
  163. Settings json.RawMessage `json:"settings"`
  164. StreamSettings json.RawMessage `json:"streamSettings"`
  165. Sniffing json.RawMessage `json:"sniffing"`
  166. }{
  167. alias: (*alias)(i),
  168. }
  169. if err := json.Unmarshal(data, &aux); err != nil {
  170. return err
  171. }
  172. i.Settings = jsonStringFieldFromRaw(aux.Settings)
  173. i.StreamSettings = jsonStringFieldFromRaw(aux.StreamSettings)
  174. i.Sniffing = jsonStringFieldFromRaw(aux.Sniffing)
  175. return nil
  176. }
  177. func jsonStringFieldToRaw(s string) json.RawMessage {
  178. trimmed := strings.TrimSpace(s)
  179. if trimmed == "" {
  180. return json.RawMessage("null")
  181. }
  182. if json.Valid([]byte(trimmed)) {
  183. return json.RawMessage(trimmed)
  184. }
  185. b, _ := json.Marshal(s)
  186. return b
  187. }
  188. func jsonStringFieldFromRaw(r json.RawMessage) string {
  189. trimmed := bytes.TrimSpace(r)
  190. if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
  191. return ""
  192. }
  193. if trimmed[0] == '"' {
  194. var s string
  195. if err := json.Unmarshal(trimmed, &s); err == nil {
  196. return s
  197. }
  198. }
  199. return string(trimmed)
  200. }
  201. // GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
  202. func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
  203. listen := i.Listen
  204. if listen == "" {
  205. listen = "0.0.0.0"
  206. }
  207. listen = fmt.Sprintf("\"%v\"", listen)
  208. protocol := string(i.Protocol)
  209. settings := i.Settings
  210. switch i.Protocol {
  211. case Shadowsocks:
  212. if healed, ok := HealShadowsocksClientMethods(settings); ok {
  213. settings = healed
  214. }
  215. case VMESS:
  216. if stripped, ok := StripVmessClientSecurity(settings); ok {
  217. settings = stripped
  218. }
  219. case VLESS:
  220. if stripped, ok := StripVlessInboundEncryption(settings); ok {
  221. settings = stripped
  222. }
  223. }
  224. return &xray.InboundConfig{
  225. Listen: json_util.RawMessage(listen),
  226. Port: i.Port,
  227. Protocol: protocol,
  228. Settings: json_util.RawMessage(settings),
  229. StreamSettings: json_util.RawMessage(i.StreamSettings),
  230. Tag: i.Tag,
  231. Sniffing: json_util.RawMessage(i.Sniffing),
  232. }
  233. }
  234. func StripVmessClientSecurity(settings string) (string, bool) {
  235. if settings == "" {
  236. return settings, false
  237. }
  238. var parsed map[string]any
  239. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  240. return settings, false
  241. }
  242. clients, ok := parsed["clients"].([]any)
  243. if !ok {
  244. return settings, false
  245. }
  246. changed := false
  247. for i := range clients {
  248. cm, ok := clients[i].(map[string]any)
  249. if !ok {
  250. continue
  251. }
  252. if _, has := cm["security"]; has {
  253. delete(cm, "security")
  254. clients[i] = cm
  255. changed = true
  256. }
  257. }
  258. if !changed {
  259. return settings, false
  260. }
  261. out, err := json.MarshalIndent(parsed, "", " ")
  262. if err != nil {
  263. return settings, false
  264. }
  265. return string(out), true
  266. }
  267. func StripVlessInboundEncryption(settings string) (string, bool) {
  268. if settings == "" {
  269. return settings, false
  270. }
  271. var parsed map[string]any
  272. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  273. return settings, false
  274. }
  275. if _, has := parsed["encryption"]; !has {
  276. return settings, false
  277. }
  278. delete(parsed, "encryption")
  279. out, err := json.MarshalIndent(parsed, "", " ")
  280. if err != nil {
  281. return settings, false
  282. }
  283. return string(out), true
  284. }
  285. // HealShadowsocksClientMethods normalises the per-client `method` field
  286. // on a shadowsocks inbound's settings JSON before it leaves for xray-core:
  287. // - Legacy ciphers (aes-*, chacha20-*): every client must carry a
  288. // per-user `method` matching the inbound's top-level method, otherwise
  289. // xray fails with "unsupported cipher method:".
  290. // - Shadowsocks 2022 (2022-blake3-*): xray's multi-user code rejects the
  291. // inbound with "users must have empty method" when a client carries
  292. // one — strip stale entries left over from a switch off a legacy
  293. // cipher.
  294. //
  295. // Returns the rewritten settings string and true when anything changed.
  296. func HealShadowsocksClientMethods(settings string) (string, bool) {
  297. if settings == "" {
  298. return settings, false
  299. }
  300. var parsed map[string]any
  301. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  302. return settings, false
  303. }
  304. method, _ := parsed["method"].(string)
  305. clients, ok := parsed["clients"].([]any)
  306. if !ok {
  307. return settings, false
  308. }
  309. is2022 := strings.HasPrefix(method, "2022-blake3-")
  310. changed := false
  311. for i := range clients {
  312. cm, ok := clients[i].(map[string]any)
  313. if !ok {
  314. continue
  315. }
  316. if is2022 {
  317. if _, hasKey := cm["method"]; hasKey {
  318. delete(cm, "method")
  319. clients[i] = cm
  320. changed = true
  321. }
  322. continue
  323. }
  324. if method == "" {
  325. continue
  326. }
  327. existing, _ := cm["method"].(string)
  328. if existing == method {
  329. continue
  330. }
  331. cm["method"] = method
  332. clients[i] = cm
  333. changed = true
  334. }
  335. if !changed {
  336. return settings, false
  337. }
  338. out, err := json.MarshalIndent(parsed, "", " ")
  339. if err != nil {
  340. return settings, false
  341. }
  342. return string(out), true
  343. }
  344. // Setting stores key-value configuration settings for the 3x-ui panel.
  345. type Setting struct {
  346. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  347. Key string `json:"key" form:"key"`
  348. Value string `json:"value" form:"value"`
  349. }
  350. // Node represents a remote 3x-ui panel registered with the central panel.
  351. // The central panel polls each node's existing /panel/api/server/status
  352. // endpoint over HTTP using the per-node ApiToken to populate the runtime
  353. // status fields below.
  354. type Node struct {
  355. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  356. Name string `json:"name" form:"name" gorm:"uniqueIndex" validate:"required"`
  357. Remark string `json:"remark" form:"remark"`
  358. Scheme string `json:"scheme" form:"scheme" validate:"omitempty,oneof=http https"`
  359. Address string `json:"address" form:"address" validate:"required"`
  360. Port int `json:"port" form:"port" validate:"gte=1,lte=65535"`
  361. BasePath string `json:"basePath" form:"basePath"`
  362. ApiToken string `json:"apiToken" form:"apiToken" validate:"required"`
  363. Enable bool `json:"enable" form:"enable" gorm:"default:true"`
  364. AllowPrivateAddress bool `json:"allowPrivateAddress" form:"allowPrivateAddress" gorm:"default:false"`
  365. TlsVerifyMode string `json:"tlsVerifyMode" form:"tlsVerifyMode" gorm:"column:tls_verify_mode;default:verify" validate:"omitempty,oneof=verify skip pin"`
  366. PinnedCertSha256 string `json:"pinnedCertSha256" form:"pinnedCertSha256" gorm:"column:pinned_cert_sha256"`
  367. // Guid is the remote panel's stable self-identifier (its panelGuid),
  368. // learned from each heartbeat. It is the globally stable node identity used
  369. // to attribute online clients/inbounds to the physical node across a chain
  370. // of nodes (#4983); panel-local autoincrement ids don't survive a hop.
  371. // Observed-state only — never user-edited.
  372. Guid string `json:"guid" gorm:"column:guid;index"`
  373. // Heartbeat-updated fields. UpdatedAt advances on every probe even when
  374. // the row is otherwise unchanged so the UI's "last seen" tooltip is
  375. // truthful without us having to read LastHeartbeat separately.
  376. Status string `json:"status" gorm:"default:unknown"` // online|offline|unknown
  377. LastHeartbeat int64 `json:"lastHeartbeat"` // unix seconds, 0 = never
  378. LatencyMs int `json:"latencyMs"`
  379. XrayVersion string `json:"xrayVersion"`
  380. PanelVersion string `json:"panelVersion" gorm:"column:panel_version"`
  381. CpuPct float64 `json:"cpuPct"`
  382. MemPct float64 `json:"memPct"`
  383. UptimeSecs uint64 `json:"uptimeSecs"`
  384. LastError string `json:"lastError"`
  385. ConfigDirty bool `json:"configDirty" gorm:"default:false"`
  386. ConfigDirtyAt int64 `json:"configDirtyAt"`
  387. InboundCount int `json:"inboundCount" gorm:"-"`
  388. ClientCount int `json:"clientCount" gorm:"-"`
  389. OnlineCount int `json:"onlineCount" gorm:"-"`
  390. DepletedCount int `json:"depletedCount" gorm:"-"`
  391. // ParentGuid + Transitive are set only when a node is surfaced as part of a
  392. // node tree (#4983): direct nodes carry the master panel's own GUID, a
  393. // transitive sub-node carries its parent node's GUID. Transitive nodes are
  394. // read-only projections (Id == 0, not persisted) — never edited or deployed.
  395. ParentGuid string `json:"parentGuid,omitempty" gorm:"-"`
  396. Transitive bool `json:"transitive,omitempty" gorm:"-"`
  397. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  398. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  399. }
  400. // NodeSummary is the read-only identity of a node as published one hop up: the
  401. // view a panel exposes about the nodes it directly manages, so a master can
  402. // surface transitive sub-nodes in a chained topology (#4983). Counts are
  403. // computed by the consuming master from its own per-GUID data, never trusted
  404. // from the child, so this carries identity/health only.
  405. type NodeSummary struct {
  406. Guid string `json:"guid"`
  407. ParentGuid string `json:"parentGuid"`
  408. Name string `json:"name"`
  409. Address string `json:"address"`
  410. Scheme string `json:"scheme"`
  411. Port int `json:"port"`
  412. Status string `json:"status"`
  413. LastHeartbeat int64 `json:"lastHeartbeat"`
  414. LatencyMs int `json:"latencyMs"`
  415. PanelVersion string `json:"panelVersion"`
  416. XrayVersion string `json:"xrayVersion"`
  417. }
  418. type CustomGeoResource struct {
  419. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  420. Type string `json:"type" gorm:"not null;uniqueIndex:idx_custom_geo_type_alias;column:geo_type"`
  421. Alias string `json:"alias" gorm:"not null;uniqueIndex:idx_custom_geo_type_alias"`
  422. Url string `json:"url" gorm:"not null"`
  423. LocalPath string `json:"localPath" gorm:"column:local_path"`
  424. LastUpdatedAt int64 `json:"lastUpdatedAt" gorm:"default:0;column:last_updated_at"`
  425. LastModified string `json:"lastModified" gorm:"column:last_modified"`
  426. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli;column:created_at"`
  427. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli;column:updated_at"`
  428. }
  429. type ClientReverse struct {
  430. Tag string `json:"tag"`
  431. }
  432. // Client represents a client configuration for Xray inbounds with traffic limits and settings.
  433. type Client struct {
  434. ID string `json:"id,omitempty"` // Unique client identifier
  435. Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
  436. Password string `json:"password,omitempty"` // Client password
  437. Flow string `json:"flow,omitempty"` // Flow control (XTLS)
  438. Reverse *ClientReverse `json:"reverse,omitempty"` // VLESS simple reverse proxy settings
  439. Auth string `json:"auth,omitempty"` // Auth password (Hysteria)
  440. Email string `json:"email"` // Client email identifier
  441. LimitIP int `json:"limitIp"` // IP limit for this client
  442. TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
  443. ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
  444. Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
  445. TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
  446. SubID string `json:"subId" form:"subId"` // Subscription identifier
  447. Group string `json:"group,omitempty" form:"group"` // Logical grouping label
  448. Comment string `json:"comment" form:"comment"` // Client comment
  449. Reset int `json:"reset" form:"reset"` // Reset period in days
  450. CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
  451. UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
  452. }
  453. type ClientRecord struct {
  454. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  455. Email string `json:"email" gorm:"uniqueIndex;not null"`
  456. SubID string `json:"subId" gorm:"index;column:sub_id"`
  457. UUID string `json:"uuid" gorm:"column:uuid"`
  458. Password string `json:"password"`
  459. Auth string `json:"auth"`
  460. Flow string `json:"flow"`
  461. Security string `json:"security"`
  462. Reverse string `json:"reverse" gorm:"column:reverse"`
  463. LimitIP int `json:"limitIp" gorm:"column:limit_ip"`
  464. TotalGB int64 `json:"totalGB" gorm:"column:total_gb"`
  465. ExpiryTime int64 `json:"expiryTime" gorm:"column:expiry_time"`
  466. Enable bool `json:"enable" gorm:"default:true"`
  467. TgID int64 `json:"tgId" gorm:"column:tg_id"`
  468. Group string `json:"group" gorm:"column:group_name;default:''"`
  469. Comment string `json:"comment"`
  470. Reset int `json:"reset" gorm:"default:0"`
  471. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  472. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  473. }
  474. func (ClientRecord) TableName() string { return "clients" }
  475. type ClientGroup struct {
  476. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  477. Name string `json:"name" gorm:"uniqueIndex;not null"`
  478. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  479. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  480. }
  481. func (ClientGroup) TableName() string { return "client_groups" }
  482. // MarshalJSON emits the reverse column as a nested JSON object rather than an
  483. // escaped JSON-text string, matching the same convention Inbound uses for its
  484. // JSON-text columns. Empty storage renders as null.
  485. func (r ClientRecord) MarshalJSON() ([]byte, error) {
  486. type alias ClientRecord
  487. return json.Marshal(struct {
  488. alias
  489. Reverse json.RawMessage `json:"reverse"`
  490. }{
  491. alias: alias(r),
  492. Reverse: jsonStringFieldToRaw(r.Reverse),
  493. })
  494. }
  495. // UnmarshalJSON accepts reverse as either a JSON object (modern shape) or a
  496. // JSON-encoded string (legacy shape).
  497. func (r *ClientRecord) UnmarshalJSON(data []byte) error {
  498. type alias ClientRecord
  499. aux := struct {
  500. *alias
  501. Reverse json.RawMessage `json:"reverse"`
  502. }{
  503. alias: (*alias)(r),
  504. }
  505. if err := json.Unmarshal(data, &aux); err != nil {
  506. return err
  507. }
  508. r.Reverse = jsonStringFieldFromRaw(aux.Reverse)
  509. return nil
  510. }
  511. type ClientInbound struct {
  512. ClientId int `json:"clientId" gorm:"primaryKey;column:client_id;index"`
  513. InboundId int `json:"inboundId" gorm:"primaryKey;column:inbound_id;index"`
  514. FlowOverride string `json:"flowOverride" gorm:"column:flow_override"`
  515. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  516. }
  517. func (ClientInbound) TableName() string { return "client_inbounds" }
  518. type InboundFallback struct {
  519. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  520. MasterId int `json:"masterId" gorm:"index;not null;column:master_id"`
  521. ChildId int `json:"childId" gorm:"index;not null;column:child_id"`
  522. Name string `json:"name"`
  523. Alpn string `json:"alpn"`
  524. Path string `json:"path"`
  525. Dest string `json:"dest"`
  526. Xver int `json:"xver"`
  527. SortOrder int `json:"sortOrder" gorm:"default:0;column:sort_order"`
  528. }
  529. func (InboundFallback) TableName() string { return "inbound_fallbacks" }
  530. func (c *Client) ToRecord() *ClientRecord {
  531. rec := &ClientRecord{
  532. Email: c.Email,
  533. SubID: c.SubID,
  534. UUID: c.ID,
  535. Password: c.Password,
  536. Auth: c.Auth,
  537. Flow: c.Flow,
  538. Security: c.Security,
  539. LimitIP: c.LimitIP,
  540. TotalGB: c.TotalGB,
  541. ExpiryTime: c.ExpiryTime,
  542. Enable: c.Enable,
  543. TgID: c.TgID,
  544. Group: c.Group,
  545. Comment: c.Comment,
  546. Reset: c.Reset,
  547. CreatedAt: c.CreatedAt,
  548. UpdatedAt: c.UpdatedAt,
  549. }
  550. if c.Reverse != nil {
  551. if b, err := json.Marshal(c.Reverse); err == nil {
  552. rec.Reverse = string(b)
  553. }
  554. }
  555. return rec
  556. }
  557. func (r *ClientRecord) ToClient() *Client {
  558. c := &Client{
  559. ID: r.UUID,
  560. Email: r.Email,
  561. SubID: r.SubID,
  562. Password: r.Password,
  563. Auth: r.Auth,
  564. Flow: r.Flow,
  565. Security: r.Security,
  566. LimitIP: r.LimitIP,
  567. TotalGB: r.TotalGB,
  568. ExpiryTime: r.ExpiryTime,
  569. Enable: r.Enable,
  570. TgID: r.TgID,
  571. Group: r.Group,
  572. Comment: r.Comment,
  573. Reset: r.Reset,
  574. CreatedAt: r.CreatedAt,
  575. UpdatedAt: r.UpdatedAt,
  576. }
  577. if r.Reverse != "" {
  578. var rev ClientReverse
  579. if err := json.Unmarshal([]byte(r.Reverse), &rev); err == nil {
  580. c.Reverse = &rev
  581. }
  582. }
  583. return c
  584. }
  585. type ClientMergeConflict struct {
  586. Field string
  587. Old any
  588. New any
  589. Kept any
  590. }
  591. func MergeClientRecord(existing *ClientRecord, incoming *ClientRecord) []ClientMergeConflict {
  592. var conflicts []ClientMergeConflict
  593. keep := func(field string, oldV, newV, kept any) {
  594. conflicts = append(conflicts, ClientMergeConflict{Field: field, Old: oldV, New: newV, Kept: kept})
  595. }
  596. const redacted = "<redacted>"
  597. keepSecret := func(field string) {
  598. conflicts = append(conflicts, ClientMergeConflict{Field: field, Old: redacted, New: redacted, Kept: redacted})
  599. }
  600. incomingNewer := incoming.UpdatedAt > existing.UpdatedAt ||
  601. (incoming.UpdatedAt == existing.UpdatedAt && incoming.CreatedAt > existing.CreatedAt)
  602. if existing.UUID != incoming.UUID && incoming.UUID != "" {
  603. if incomingNewer || existing.UUID == "" {
  604. existing.UUID = incoming.UUID
  605. }
  606. keepSecret("uuid")
  607. }
  608. if existing.Password != incoming.Password && incoming.Password != "" {
  609. if incomingNewer || existing.Password == "" {
  610. existing.Password = incoming.Password
  611. keepSecret("password")
  612. }
  613. }
  614. if existing.Auth != incoming.Auth && incoming.Auth != "" {
  615. if incomingNewer || existing.Auth == "" {
  616. existing.Auth = incoming.Auth
  617. keepSecret("auth")
  618. }
  619. }
  620. if existing.Flow != incoming.Flow && incoming.Flow != "" {
  621. if incomingNewer || existing.Flow == "" {
  622. keep("flow", existing.Flow, incoming.Flow, incoming.Flow)
  623. existing.Flow = incoming.Flow
  624. }
  625. }
  626. if existing.Security != incoming.Security && incoming.Security != "" {
  627. if incomingNewer || existing.Security == "" {
  628. keep("security", existing.Security, incoming.Security, incoming.Security)
  629. existing.Security = incoming.Security
  630. }
  631. }
  632. if existing.SubID != incoming.SubID && incoming.SubID != "" {
  633. if incomingNewer || existing.SubID == "" {
  634. existing.SubID = incoming.SubID
  635. keepSecret("subId")
  636. }
  637. }
  638. if existing.TotalGB != incoming.TotalGB {
  639. picked := existing.TotalGB
  640. if existing.TotalGB == 0 || (incoming.TotalGB != 0 && incoming.TotalGB > existing.TotalGB) {
  641. picked = incoming.TotalGB
  642. }
  643. if picked != existing.TotalGB {
  644. keep("totalGB", existing.TotalGB, incoming.TotalGB, picked)
  645. existing.TotalGB = picked
  646. }
  647. }
  648. if existing.ExpiryTime != incoming.ExpiryTime {
  649. picked := existing.ExpiryTime
  650. if existing.ExpiryTime == 0 || (incoming.ExpiryTime != 0 && incoming.ExpiryTime > existing.ExpiryTime) {
  651. picked = incoming.ExpiryTime
  652. }
  653. if picked != existing.ExpiryTime {
  654. keep("expiryTime", existing.ExpiryTime, incoming.ExpiryTime, picked)
  655. existing.ExpiryTime = picked
  656. }
  657. }
  658. if existing.LimitIP != incoming.LimitIP && incoming.LimitIP != 0 {
  659. picked := existing.LimitIP
  660. if existing.LimitIP == 0 || incoming.LimitIP > existing.LimitIP {
  661. picked = incoming.LimitIP
  662. }
  663. if picked != existing.LimitIP {
  664. keep("limitIp", existing.LimitIP, incoming.LimitIP, picked)
  665. existing.LimitIP = picked
  666. }
  667. }
  668. if existing.TgID != incoming.TgID && incoming.TgID != 0 {
  669. if incomingNewer || existing.TgID == 0 {
  670. keep("tgId", existing.TgID, incoming.TgID, incoming.TgID)
  671. existing.TgID = incoming.TgID
  672. }
  673. }
  674. if existing.Reset != incoming.Reset && incoming.Reset != 0 {
  675. if incomingNewer || existing.Reset == 0 {
  676. keep("reset", existing.Reset, incoming.Reset, incoming.Reset)
  677. existing.Reset = incoming.Reset
  678. }
  679. }
  680. if existing.Reverse != incoming.Reverse && incoming.Reverse != "" {
  681. if incomingNewer || existing.Reverse == "" {
  682. keep("reverse", existing.Reverse, incoming.Reverse, incoming.Reverse)
  683. existing.Reverse = incoming.Reverse
  684. }
  685. }
  686. if existing.Comment != incoming.Comment && incoming.Comment != "" {
  687. if incomingNewer || existing.Comment == "" {
  688. keep("comment", existing.Comment, incoming.Comment, incoming.Comment)
  689. existing.Comment = incoming.Comment
  690. }
  691. }
  692. if existing.Group != incoming.Group && incoming.Group != "" {
  693. if incomingNewer || existing.Group == "" {
  694. keep("group", existing.Group, incoming.Group, incoming.Group)
  695. existing.Group = incoming.Group
  696. }
  697. }
  698. if existing.Enable != incoming.Enable {
  699. if incoming.Enable {
  700. if !existing.Enable {
  701. keep("enable", existing.Enable, incoming.Enable, true)
  702. existing.Enable = true
  703. }
  704. }
  705. }
  706. if incoming.CreatedAt != 0 && (existing.CreatedAt == 0 || incoming.CreatedAt < existing.CreatedAt) {
  707. existing.CreatedAt = incoming.CreatedAt
  708. }
  709. if incoming.UpdatedAt > existing.UpdatedAt {
  710. existing.UpdatedAt = incoming.UpdatedAt
  711. }
  712. return conflicts
  713. }