mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-09-11 12:46:19 +00:00
Merge branch 'main' of https://github.com/izzzzzi/3x-ui
This commit is contained in:
commit
e8ed1949a7
30 changed files with 895 additions and 143 deletions
49
go.mod
49
go.mod
|
@ -1,46 +1,45 @@
|
|||
module x-ui
|
||||
|
||||
go 1.23.4
|
||||
go 1.23.5
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/gzip v1.1.0
|
||||
github.com/gin-contrib/gzip v1.2.0
|
||||
github.com/gin-contrib/sessions v1.0.2
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/goccy/go-json v0.10.4
|
||||
github.com/mymmrac/telego v0.31.5
|
||||
github.com/mymmrac/telego v0.32.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.1
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.2.3
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.24.12
|
||||
github.com/valyala/fasthttp v1.58.0
|
||||
github.com/xtls/xray-core v1.8.25-0.20250101120305-e80ca67feed9
|
||||
github.com/xtls/xray-core v1.8.25-0.20250119133207-ca9a90221348
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/text v0.21.0
|
||||
google.golang.org/grpc v1.69.2
|
||||
google.golang.org/grpc v1.69.4
|
||||
gorm.io/driver/sqlite v1.5.7
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||
github.com/bytedance/sonic v1.12.7 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
||||
github.com/cloudflare/circl v1.5.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
|
||||
github.com/ebitengine/purego v0.8.1 // indirect
|
||||
github.com/fasthttp/router v1.5.3 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/fasthttp/router v1.5.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.23.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.24.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
|
||||
github.com/google/pprof v0.0.0-20250121033306-997b0b79cac0 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0 // indirect
|
||||
|
@ -83,20 +82,20 @@ require (
|
|||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.12.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
|
||||
golang.org/x/arch v0.13.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241230172942-26aa7a208def // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
|
||||
google.golang.org/protobuf v1.36.3 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
)
|
||||
|
|
93
go.sum
93
go.sum
|
@ -4,34 +4,33 @@ github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJS
|
|||
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
|
||||
github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
|
||||
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
|
||||
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
|
||||
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
|
||||
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fasthttp/router v1.5.3 h1:BFWXqa3e4thRI3MgPKTNtz0Oiq6UYN2OsEtb+YQ5TMI=
|
||||
github.com/fasthttp/router v1.5.3/go.mod h1:b864KkDIapOYh77AVG/SNkwfRZ6k6ecWvD+ZRXmP5pw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM=
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8=
|
||||
github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/gzip v1.1.0 h1:kVw7Nr9M+Z6Ch4qo7aGMbiqxDeyQFru+07MgAcUF62M=
|
||||
github.com/gin-contrib/gzip v1.1.0/go.mod h1:iHJXCup4CWiKyPUEl+GwkHjchl+YyYuMKbOCiXujPIA=
|
||||
github.com/gin-contrib/gzip v1.2.0 h1:JzN6DT3/xYL5zAdviN1ORNzKeklrwafXCIDKIR+qmUA=
|
||||
github.com/gin-contrib/gzip v1.2.0/go.mod h1:C1a5cacjlDsS20cKnHlZRCPUu57D3qH6B2pV0rl+Y/s=
|
||||
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
|
||||
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
|
||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||
|
@ -51,8 +50,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
|
@ -68,8 +67,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
|||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/pprof v0.0.0-20250121033306-997b0b79cac0 h1:EinjE47mmVVsxcjIwVKQWNY+3P+5R2BhkbULjhEDThc=
|
||||
github.com/google/pprof v0.0.0-20250121033306-997b0b79cac0/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||
|
@ -113,8 +112,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mymmrac/telego v0.31.5 h1:5CpSeAxGtFTmEJWFvPmu8f4Lqg3u2DSDYn4R80Mo+rs=
|
||||
github.com/mymmrac/telego v0.31.5/go.mod h1:1U+D+TeoB7PnkOi024apOivATL4Q3MnUSZlY+rcMPiE=
|
||||
github.com/mymmrac/telego v0.32.0 h1:4X8C1l3k+opkk86r95+eQE8DxiS2LYlR61L/G7yreDY=
|
||||
github.com/mymmrac/telego v0.32.0/go.mod h1:qS6NaRhJgcuEEBEMVCV79S2xCAuHq9O+ixwfLuRW31M=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.1 h1:zwzjtX4uYyiaU02K5Ia3zSkpJZrByARkRB4V3YPrr0g=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.1/go.mod h1:++Pl70FR6Cki7hdzZRnEEqdc2dJt+SAGotyFg/SvZMk=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
|
||||
|
@ -191,8 +190,8 @@ github.com/xtls/quic-go v0.48.2 h1:59Gs+E9qtc9s0uniXYDA649gNEZlMWcNpFLyp9jfkuE=
|
|||
github.com/xtls/quic-go v0.48.2/go.mod h1:rcyY5J0JT+1d5pa5Y+FbCsXM7Zu79jE87ZSFOBfiH7Q=
|
||||
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 h1:g1Cj7d+my6k/HHxLAyxPwyX8i7FGRr6ulBDMkBzg2BM=
|
||||
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463/go.mod h1:BjIOLmkEEtAgloAiVUcYj0Mt+YU00JARZw8AEU0IwAg=
|
||||
github.com/xtls/xray-core v1.8.25-0.20250101120305-e80ca67feed9 h1:lCjZeLXt/k1DGAqt2+D1kzMMaDELuS1v7TqpeNS/JR8=
|
||||
github.com/xtls/xray-core v1.8.25-0.20250101120305-e80ca67feed9/go.mod h1:ZQNlyI+v398/dKXjacCwhGfQlhjPThvmPToC/RBy7oM=
|
||||
github.com/xtls/xray-core v1.8.25-0.20250119133207-ca9a90221348 h1:x54/hGG4meg32o8wR0EkIKyVJy7Fv+ey+bmSZuXWkJE=
|
||||
github.com/xtls/xray-core v1.8.25-0.20250119133207-ca9a90221348/go.mod h1:51y3AycMua6m1VFYnc3vJmgXeiL0qTsRGsDpjEaoXXc=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
|
@ -213,16 +212,16 @@ go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
|||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
|
||||
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -231,24 +230,24 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241230172942-26aa7a208def h1:4P81qv5JXI/sDNae2ClVx88cgDDA6DPilADkG9tYKz8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241230172942-26aa7a208def/go.mod h1:bdAgzvd4kFrpykc5/AC2eLUiegK9T/qxZHD4hXYf/ho=
|
||||
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
|
||||
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
||||
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
|
||||
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
@ -262,8 +261,8 @@ gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
|||
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE=
|
||||
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
|
||||
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0 h1:P+U/06iIKPQ3DLcg+zBfSCia1luZ2msPZrJ8jYDFPs0=
|
||||
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
|
||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
|
|
@ -493,6 +493,7 @@ class xHTTPStreamSettings extends XrayCommonClass {
|
|||
headers = [],
|
||||
scMaxBufferedPosts = 30,
|
||||
scMaxEachPostBytes = "1000000",
|
||||
scStreamUpServerSecs = "0",
|
||||
noSSEHeader = false,
|
||||
xPaddingBytes = "100-1000",
|
||||
mode = MODE_OPTION.AUTO,
|
||||
|
@ -503,6 +504,7 @@ class xHTTPStreamSettings extends XrayCommonClass {
|
|||
this.headers = headers;
|
||||
this.scMaxBufferedPosts = scMaxBufferedPosts;
|
||||
this.scMaxEachPostBytes = scMaxEachPostBytes;
|
||||
this.scStreamUpServerSecs = scStreamUpServerSecs;
|
||||
this.noSSEHeader = noSSEHeader;
|
||||
this.xPaddingBytes = xPaddingBytes;
|
||||
this.mode = mode;
|
||||
|
@ -523,6 +525,7 @@ class xHTTPStreamSettings extends XrayCommonClass {
|
|||
XrayCommonClass.toHeaders(json.headers),
|
||||
json.scMaxBufferedPosts,
|
||||
json.scMaxEachPostBytes,
|
||||
json.scStreamUpServerSecs,
|
||||
json.noSSEHeader,
|
||||
json.xPaddingBytes,
|
||||
json.mode,
|
||||
|
@ -536,6 +539,7 @@ class xHTTPStreamSettings extends XrayCommonClass {
|
|||
headers: XrayCommonClass.toV2Headers(this.headers, false),
|
||||
scMaxBufferedPosts: this.scMaxBufferedPosts,
|
||||
scMaxEachPostBytes: this.scMaxEachPostBytes,
|
||||
scStreamUpServerSecs: this.scStreamUpServerSecs,
|
||||
noSSEHeader: this.noSSEHeader,
|
||||
xPaddingBytes: this.xPaddingBytes,
|
||||
mode: this.mode,
|
||||
|
|
|
@ -26,6 +26,7 @@ class AllSetting {
|
|||
this.xrayTemplateConfig = "";
|
||||
this.secretEnable = false;
|
||||
this.subEnable = false;
|
||||
this.subSyncEnable = true;
|
||||
this.subListen = "";
|
||||
this.subPort = 2096;
|
||||
this.subPath = "/sub/";
|
||||
|
|
|
@ -70,6 +70,41 @@ class HttpUtil {
|
|||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
static async jsonPost(url, data) {
|
||||
let msg;
|
||||
try {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
const resp = await fetch(url, requestOptions);
|
||||
const response = await resp.json();
|
||||
|
||||
msg = this._respToMsg({data : response});
|
||||
} catch (e) {
|
||||
msg = new Msg(false, e.toString());
|
||||
}
|
||||
this._handleMsg(msg);
|
||||
return msg;
|
||||
}
|
||||
|
||||
static async postWithModalJson(url, data, modal) {
|
||||
if (modal) {
|
||||
modal.loading(true);
|
||||
}
|
||||
const msg = await this.jsonPost(url, data);
|
||||
if (modal) {
|
||||
modal.loading(false);
|
||||
if (msg instanceof Msg && msg.success) {
|
||||
modal.close();
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
class PromiseUtil {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"x-ui/database/model"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
@ -33,9 +33,13 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
|||
g.POST("/clientIps/:email", a.getClientIps)
|
||||
g.POST("/clearClientIps/:email", a.clearClientIps)
|
||||
g.POST("/addClient", a.addInboundClient)
|
||||
g.POST("/addGroupClient", a.addGroupInboundClient)
|
||||
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
||||
g.POST("/delGroupClients", a.delGroupClients)
|
||||
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
||||
g.POST("/updateClients", a.updateGroupInboundClient)
|
||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||
g.POST("/resetGroupClientTraffic", a.resetGroupClientTraffic)
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
|
@ -190,6 +194,34 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) addGroupInboundClient(c *gin.Context) {
|
||||
var requestData []model.Inbound
|
||||
|
||||
err := c.ShouldBindJSON(&requestData)
|
||||
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
|
||||
needRestart := true
|
||||
|
||||
for _, data := range requestData {
|
||||
|
||||
needRestart, err = a.inboundService.AddInboundClient(&data)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
jsonMsg(c, "Client(s) added", nil)
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
|
@ -211,6 +243,38 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) delGroupClients(c *gin.Context) {
|
||||
var requestData []struct {
|
||||
InboundID int `json:"inboundId"`
|
||||
ClientID string `json:"clientId"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&requestData); err != nil {
|
||||
jsonMsg(c, "Invalid request data", err)
|
||||
return
|
||||
}
|
||||
|
||||
needRestart := false
|
||||
|
||||
for _, req := range requestData {
|
||||
needRestartTmp, err := a.inboundService.DelInboundClient(req.InboundID, req.ClientID)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Failed to delete client", err)
|
||||
return
|
||||
}
|
||||
|
||||
if needRestartTmp {
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
|
||||
jsonMsg(c, "Clients deleted successfully", nil)
|
||||
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
clientId := c.Param("clientId")
|
||||
|
||||
|
@ -234,6 +298,56 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) updateGroupInboundClient(c *gin.Context) {
|
||||
var requestData []map[string]interface{}
|
||||
|
||||
if err := c.ShouldBindJSON(&requestData); err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
|
||||
needRestart := false
|
||||
|
||||
for _, item := range requestData {
|
||||
|
||||
inboundMap, ok := item["inbound"].(map[string]interface{})
|
||||
if !ok {
|
||||
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'inbound' to map"))
|
||||
return
|
||||
}
|
||||
|
||||
clientId, ok := item["clientId"].(string)
|
||||
if !ok {
|
||||
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'clientId' to string"))
|
||||
return
|
||||
}
|
||||
|
||||
inboundJSON, err := json.Marshal(inboundMap)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
|
||||
var inboundModel model.Inbound
|
||||
if err := json.Unmarshal(inboundJSON, &inboundModel); err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
|
||||
if restart, err := a.inboundService.UpdateInboundClient(&inboundModel, clientId); err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
} else {
|
||||
needRestart = needRestart || restart
|
||||
}
|
||||
}
|
||||
|
||||
jsonMsg(c, "Client updated", nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
|
@ -253,6 +367,44 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) resetGroupClientTraffic(c *gin.Context) {
|
||||
var requestData []struct {
|
||||
InboundID int `json:"inboundId"` // Map JSON "inboundId" to struct field "InboundID"
|
||||
Email string `json:"email"` // Map JSON "email" to struct field "Email"
|
||||
}
|
||||
|
||||
// Parse JSON body directly using ShouldBindJSON
|
||||
if err := c.ShouldBindJSON(&requestData); err != nil {
|
||||
jsonMsg(c, "Invalid request data", err)
|
||||
return
|
||||
}
|
||||
|
||||
needRestart := false
|
||||
|
||||
// Process each request data
|
||||
for _, req := range requestData {
|
||||
needRestartTmp, err := a.inboundService.ResetClientTraffic(req.InboundID, req.Email)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Failed to reset client traffic", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If any request requires a restart, set needRestart to true
|
||||
if needRestartTmp {
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
|
||||
// Send response back to the client
|
||||
jsonMsg(c, "Traffic reset for all clients", nil)
|
||||
|
||||
// Restart the service if required
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||
err := a.inboundService.ResetAllTraffics()
|
||||
if err != nil {
|
||||
|
|
|
@ -40,6 +40,7 @@ type AllSetting struct {
|
|||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||
SubSyncEnable bool `json:"subSyncEnable" form:"subSyncEnable"`
|
||||
SubListen string `json:"subListen" form:"subListen"`
|
||||
SubPort int `json:"subPort" form:"subPort"`
|
||||
SubPath string `json:"subPath" form:"subPath"`
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
</template>
|
||||
<template v-if="!isJustSub">
|
||||
<template v-for="(row, index) in qrModal.qrcodes">
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
|
||||
|
@ -31,6 +32,7 @@
|
|||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
</template>
|
||||
</template>
|
||||
</tr-qr-modal>
|
||||
</a-modal>
|
||||
|
||||
|
@ -43,12 +45,14 @@
|
|||
qrcodes: [],
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
isJustSub: false,
|
||||
subId: '',
|
||||
show: function(title = '', dbInbound, client) {
|
||||
show: function(title = '', dbInbound, client, isJustSub = false) {
|
||||
this.title = title;
|
||||
this.dbInbound = dbInbound;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.client = client;
|
||||
this.isJustSub = isJustSub;
|
||||
this.subId = '';
|
||||
this.qrcodes = [];
|
||||
if (this.inbound.protocol == Protocols.WIREGUARD) {
|
||||
|
@ -76,7 +80,9 @@
|
|||
delimiters: ['[[', ']]'],
|
||||
el: '#qrcode-modal',
|
||||
data: {
|
||||
qrModal: qrModal,
|
||||
qrModal: qrModal,get isJustSub(){
|
||||
return qrModal.isJustSub
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(elementId, content) {
|
||||
|
|
|
@ -16,7 +16,16 @@
|
|||
title: '',
|
||||
okText: '',
|
||||
isEdit: false,
|
||||
group: {
|
||||
canGroup: true,
|
||||
isGroup: false,
|
||||
currentClient: null,
|
||||
inbounds: [],
|
||||
clients: [],
|
||||
editIds: []
|
||||
},
|
||||
dbInbound: new DBInbound(),
|
||||
dbInbounds: null,
|
||||
inbound: new Inbound(),
|
||||
clients: [],
|
||||
clientStats: [],
|
||||
|
@ -25,32 +34,120 @@
|
|||
clientIps: null,
|
||||
delayedStart: false,
|
||||
ok() {
|
||||
if (app.subSettings.enable && clientModal.group.isGroup && clientModal.group.canGroup) {
|
||||
const currentClient = clientModal.group.currentClient;
|
||||
const { limitIp, totalGB, expiryTime, reset, enable, subId, tgId, flow } = currentClient;
|
||||
const uniqueEmails = clientModalApp.makeGroupEmailsUnique(clientModal.dbInbounds, currentClient.email, clientModal.group.clients);
|
||||
|
||||
clientModal.group.clients.forEach((client, index) => {
|
||||
client.email = uniqueEmails[index];
|
||||
client.limitIp = limitIp;
|
||||
client.totalGB = totalGB;
|
||||
client.expiryTime = expiryTime;
|
||||
client.reset = reset;
|
||||
client.enable = enable;
|
||||
|
||||
if (subId) {
|
||||
client.subId = subId;
|
||||
}
|
||||
if (tgId) {
|
||||
client.tgId = tgId;
|
||||
}
|
||||
if (flow) {
|
||||
client.flow = flow;
|
||||
}
|
||||
});
|
||||
|
||||
if (clientModal.isEdit) {
|
||||
ObjectUtil.execute(clientModal.confirm, clientModal.group.clients, clientModal.group.inbounds, clientModal.group.editIds);
|
||||
} else {
|
||||
ObjectUtil.execute(clientModal.confirm, clientModal.group.clients, clientModal.group.inbounds);
|
||||
}
|
||||
} else {
|
||||
if (clientModal.isEdit) {
|
||||
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
|
||||
} else {
|
||||
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {
|
||||
show({
|
||||
title = '',
|
||||
okText = '{{ i18n "sure" }}',
|
||||
index = null,
|
||||
dbInbound = null,
|
||||
dbInbounds = null,
|
||||
confirm = () => {
|
||||
},
|
||||
isEdit = false
|
||||
}) {
|
||||
this.group = {
|
||||
canGroup: true,
|
||||
isGroup: false,
|
||||
currentClient: null,
|
||||
inbounds: [],
|
||||
clients: [],
|
||||
editIds: []
|
||||
}
|
||||
this.dbInbounds = dbInbounds;
|
||||
this.visible = true;
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.isEdit = isEdit;
|
||||
if (app.subSettings.enable && dbInbounds !== null && Array.isArray(dbInbounds)) {
|
||||
if (isEdit) {
|
||||
this.showProcess(dbInbound, index);
|
||||
let processSingleEdit = true
|
||||
if (this.group.canGroup) {
|
||||
this.group.currentClient = this.clients[this.index]
|
||||
const response = app.getSubGroupClients(dbInbounds, this.group.currentClient)
|
||||
if (response.clients.length > 1) {
|
||||
this.group.isGroup = true;
|
||||
this.group.inbounds = response.inbounds
|
||||
this.group.clients = response.clients
|
||||
this.group.editIds = response.editIds
|
||||
if (this.clients[index].expiryTime < 0) {
|
||||
this.delayedStart = true;
|
||||
}
|
||||
processSingleEdit = false
|
||||
}
|
||||
}
|
||||
if (processSingleEdit) {
|
||||
this.singleEditClientProcess(index)
|
||||
}
|
||||
} else {
|
||||
this.group.isGroup = true;
|
||||
dbInbounds.forEach((dbInboundItem) => {
|
||||
this.showProcess(dbInboundItem);
|
||||
this.addClient(this.inbound.protocol, this.clients);
|
||||
this.group.inbounds.push(dbInboundItem.id)
|
||||
this.group.clients.push(this.clients[this.index])
|
||||
})
|
||||
this.group.currentClient = this.clients[this.index]
|
||||
}
|
||||
} else {
|
||||
this.showProcess(dbInbound, index);
|
||||
if (isEdit) {
|
||||
this.singleEditClientProcess(index)
|
||||
} else {
|
||||
this.addClient(this.inbound.protocol, this.clients);
|
||||
}
|
||||
}
|
||||
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
||||
this.confirm = confirm;
|
||||
},
|
||||
showProcess(dbInbound, index = null) {
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.clients = this.inbound.clients;
|
||||
this.index = index === null ? this.clients.length : index;
|
||||
this.delayedStart = false;
|
||||
if (isEdit) {
|
||||
},
|
||||
singleEditClientProcess(index) {
|
||||
if (this.clients[index].expiryTime < 0) {
|
||||
this.delayedStart = true;
|
||||
}
|
||||
this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
|
||||
} else {
|
||||
this.addClient(this.inbound.protocol, this.clients);
|
||||
}
|
||||
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
||||
this.confirm = confirm;
|
||||
this.oldClientId = this.getClientId(this.dbInbound.protocol, this.clients[index]);
|
||||
},
|
||||
getClientId(protocol, client) {
|
||||
switch (protocol) {
|
||||
|
@ -94,6 +191,18 @@
|
|||
get isEdit() {
|
||||
return this.clientModal.isEdit;
|
||||
},
|
||||
get isGroup() {
|
||||
return this.clientModal.group.isGroup;
|
||||
},
|
||||
get isGroupEdit() {
|
||||
return this.clientModal.group.canGroup;
|
||||
},
|
||||
set isGroupEdit(value) {
|
||||
this.clientModal.group.canGroup = value;
|
||||
if (!value) {
|
||||
this.clientModal.singleEditClientProcess(this.clientModal.index)
|
||||
}
|
||||
},
|
||||
get datepicker() {
|
||||
return app.datepicker;
|
||||
},
|
||||
|
@ -120,22 +229,35 @@
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
async getDBClientIps(email) {
|
||||
const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`);
|
||||
if (!msg.success) {
|
||||
document.getElementById("clientIPs").value = msg.obj;
|
||||
return;
|
||||
makeGroupEmailsUnique(dbInbounds, baseEmail, groupClients) {
|
||||
// Extract the base part of the email (before the "__" if present)
|
||||
const match = baseEmail.match(/^(.*?)__/);
|
||||
const base = match ? match[1] : baseEmail;
|
||||
|
||||
// Generate initial emails for each client in the group
|
||||
const generatedEmails = groupClients.map((_, index) => `${base}__${index + 1}`);
|
||||
|
||||
// Function to check if an email already exists in dbInbounds but belongs to a different subId
|
||||
const isDuplicate = (emailToCheck, clientSubId) => {
|
||||
return dbInbounds.some((dbInbound) => {
|
||||
const settings = JSON.parse(dbInbound.settings);
|
||||
const clients = settings && settings.clients ? settings.clients : [];
|
||||
return clients.some(client => client.email === emailToCheck && client.subId !== clientSubId);
|
||||
});
|
||||
};
|
||||
|
||||
// Check if any of the generated emails are duplicates
|
||||
const hasDuplicates = generatedEmails.some((email, index) => {
|
||||
return isDuplicate(email, groupClients[index].subId);
|
||||
});
|
||||
|
||||
// If duplicates exist, add a random string to the base email to ensure uniqueness
|
||||
if (hasDuplicates) {
|
||||
const randomString = `-${RandomUtil.randomLowerAndNum(4)}`;
|
||||
return groupClients.map((_, index) => `${base}${randomString}__${index + 1}`);
|
||||
}
|
||||
let ips = msg.obj;
|
||||
if (typeof ips === 'string' && ips.startsWith('[') && ips.endsWith(']')) {
|
||||
try {
|
||||
ips = JSON.parse(ips);
|
||||
ips = Array.isArray(ips) ? ips.join("\n") : ips;
|
||||
} catch (e) {
|
||||
console.error('Error parsing JSON:', e);
|
||||
}
|
||||
}
|
||||
document.getElementById("clientIPs").value = ips;
|
||||
|
||||
return generatedEmails;
|
||||
},
|
||||
async clearDBClientIps(email) {
|
||||
try {
|
||||
|
@ -147,7 +269,22 @@
|
|||
} catch (error) {
|
||||
}
|
||||
},
|
||||
resetClientTraffic(email, dbInboundId, iconElement) {
|
||||
async resetClientTrafficHandler(client, dbInboundId, clients = []) {
|
||||
if (clients.length > 0) {
|
||||
const resetRequests = clients
|
||||
.filter(client => {
|
||||
const inbound = clientModal.dbInbounds.find(inbound => inbound.id === client.inboundId);
|
||||
return inbound && app.hasClientStats(inbound, client.email);
|
||||
}).map(client => ({ inboundId: client.inboundId, email: client.email}));
|
||||
|
||||
return HttpUtil.postWithModalJson('/panel/inbound/resetGroupClientTraffic', resetRequests, null)
|
||||
} else {
|
||||
return HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email)
|
||||
}
|
||||
},
|
||||
resetClientTraffic(client, dbInboundId, iconElement) {
|
||||
const subGroup = app.subSettings.enable && clientModal.group.isGroup && clientModal.group.canGroup && clientModal.dbInbounds && clientModal.dbInbounds.length > 0 ? app.getSubGroupClients(clientModal.dbInbounds, client) : [];
|
||||
const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
|
@ -156,8 +293,8 @@
|
|||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: async () => {
|
||||
iconElement.disabled = true;
|
||||
const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email);
|
||||
if (msg.success) {
|
||||
const msg = await this.resetClientTrafficHandler(client, dbInboundId, clients);
|
||||
if (msg && msg.success) {
|
||||
this.clientModal.clientStats.up = 0;
|
||||
this.clientModal.clientStats.down = 0;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,18 @@
|
|||
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
|
||||
<a-switch v-model="client.enable"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="isEdit && app.subSettings.enable && isGroup">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.client.isGroupEditDesc" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.client.isGroupEdit" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-switch v-model="isGroupEdit"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
|
@ -134,7 +146,7 @@
|
|||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||
<a-icon type="retweet"
|
||||
@click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
|
||||
@click="resetClientTraffic(client,clientStats.inboundId,$event.target)"
|
||||
v-if="client.email.length > 0"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-form-item>
|
||||
|
|
|
@ -33,6 +33,9 @@
|
|||
<a-form-item label="Max Upload Size (Byte)" v-if="inbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Stream-Up Server">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Bytes">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.xPaddingBytes"></a-input>
|
||||
</a-form-item>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<template slot="title">{{ i18n "info" }}</template>
|
||||
<a-icon style="font-size: 24px;" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<a-tooltip v-if="hasClientStats(record, client.email)">
|
||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: var(--color-primary-100)' : 'color: var(--color-primary-100)'"></a-icon>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
confirmLoading: false,
|
||||
okText: '{{ i18n "sure" }}',
|
||||
isEdit: false,
|
||||
isGroup: false,
|
||||
confirm: null,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
|
@ -61,6 +62,9 @@
|
|||
get isEdit() {
|
||||
return inModal.isEdit;
|
||||
},
|
||||
get isGroup() {
|
||||
return inModal.isGroup;
|
||||
},
|
||||
get client() {
|
||||
return inModal.inbound.clients[0];
|
||||
},
|
||||
|
|
|
@ -224,6 +224,10 @@
|
|||
<a-icon type="rest"></a-icon>
|
||||
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="subSettings.enable && dbInbounds.length > 0" key="addGroupClient">
|
||||
<a-icon type="usergroup-add"></a-icon>
|
||||
{{ i18n "pages.client.groupAdd"}}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
|
@ -859,6 +863,9 @@
|
|||
case "delDepletedClients":
|
||||
this.delDepletedClients(-1)
|
||||
break;
|
||||
case "addGroupClient":
|
||||
this.openGroupAddClient()
|
||||
break;
|
||||
}
|
||||
},
|
||||
clickAction(action, dbInbound) {
|
||||
|
@ -1004,6 +1011,21 @@
|
|||
|
||||
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
|
||||
},
|
||||
openGroupAddClient() {
|
||||
clientModal.show({
|
||||
title: '{{ i18n "pages.client.groupAdd"}}',
|
||||
okText: '{{ i18n "pages.client.submitAdd"}}',
|
||||
dbInbounds: this.dbInbounds,
|
||||
confirm: async (clients, dbInboundIds) => {
|
||||
await this.addGroupClient(clients, dbInboundIds, clientModal).then((res) => {
|
||||
if(res){
|
||||
this.showQrcode(dbInboundIds[0],clients[0], true)
|
||||
}
|
||||
});
|
||||
},
|
||||
isEdit: false
|
||||
});
|
||||
},
|
||||
openAddClient(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
clientModal.show({
|
||||
|
@ -1011,7 +1033,11 @@
|
|||
okText: '{{ i18n "pages.client.submitAdd"}}',
|
||||
dbInbound: dbInbound,
|
||||
confirm: async (clients, dbInboundId) => {
|
||||
await this.addClient(clients, dbInboundId, clientModal);
|
||||
await this.addClient(clients, dbInboundId, clientModal).then((res) => {
|
||||
if(res){
|
||||
this.showQrcode(dbInboundId,clients)
|
||||
}
|
||||
});
|
||||
},
|
||||
isEdit: false
|
||||
});
|
||||
|
@ -1034,6 +1060,7 @@
|
|||
clientModal.show({
|
||||
title: '{{ i18n "pages.client.edit"}}',
|
||||
okText: '{{ i18n "pages.client.submitEdit"}}',
|
||||
dbInbounds: this.dbInbounds,
|
||||
dbInbound: dbInbound,
|
||||
index: index,
|
||||
confirm: async (client, dbInboundId, clientId) => {
|
||||
|
@ -1059,12 +1086,36 @@
|
|||
};
|
||||
await this.submit(`/panel/inbound/addClient`, data, modal);
|
||||
},
|
||||
async addGroupClient(clients, dbInboundIds, modal) {
|
||||
const data = []
|
||||
dbInboundIds.forEach((dbInboundId, index) => {
|
||||
data.push({
|
||||
id: dbInboundId,
|
||||
settings: '{"clients": [' + clients[index].toString() + ']}',
|
||||
})
|
||||
})
|
||||
return await this.submit(`/panel/inbound/addGroupClient`, data, modal, true)
|
||||
},
|
||||
async updateClient(client, dbInboundId, clientId) {
|
||||
if (Array.isArray(client) && Array.isArray(dbInboundId) && Array.isArray(clientId)){
|
||||
const data = []
|
||||
client.forEach((client, index) => {
|
||||
data.push({
|
||||
clientId: clientId[index],
|
||||
inbound: {
|
||||
id: dbInboundId[index],
|
||||
settings: '{"clients": [' + client.toString() + ']}',
|
||||
}
|
||||
})
|
||||
})
|
||||
await this.submit(`/panel/inbound/updateClients`, data, clientModal, true);
|
||||
}else{
|
||||
const data = {
|
||||
id: dbInboundId,
|
||||
settings: '{"clients": [' + client.toString() + ']}',
|
||||
};
|
||||
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
|
||||
}
|
||||
},
|
||||
resetTraffic(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
|
@ -1092,22 +1143,87 @@
|
|||
onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
|
||||
});
|
||||
},
|
||||
delClient(dbInboundId, client,confirmation = true) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
clientId = this.getClientId(dbInbound.protocol, client);
|
||||
async delClientHandler(dbInboundId, currentClient, clients = []) {
|
||||
if (clients.length > 0) {
|
||||
const deleteRequestData = [];
|
||||
|
||||
for (const client of clients) {
|
||||
const dbInbound = this.dbInbounds.find(inbound => inbound.id === client.inboundId);
|
||||
if (dbInbound) {
|
||||
const inbound = dbInbound.toInbound();
|
||||
if (inbound && inbound.clients && inbound.clients.length === 1) {
|
||||
let newClient = Inbound.Settings.getSettings(inbound.protocol).toString();
|
||||
newClient = JSON.parse(newClient);
|
||||
newClient = newClient && newClient.clients && newClient.clients.length > 0 ? JSON.stringify(newClient.clients[0], null, 2) : null;
|
||||
if (newClient) {
|
||||
const data = {
|
||||
id: client.inboundId,
|
||||
settings: '{"clients": [' + newClient + ']}',
|
||||
};
|
||||
await this.submit(`/panel/inbound/addClient`, data, null);
|
||||
}
|
||||
}
|
||||
|
||||
deleteRequestData.push({
|
||||
inboundId: client.inboundId,
|
||||
clientId: client.clientId,
|
||||
});
|
||||
}
|
||||
}
|
||||
await this.submit('/panel/inbound/delGroupClients', deleteRequestData, null, true);
|
||||
} else {
|
||||
const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
const clientId = this.getClientId(dbInbound.protocol, currentClient);
|
||||
await this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
|
||||
}
|
||||
},
|
||||
delClient(dbInboundId, currentClient, confirmation = true) {
|
||||
const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, currentClient) : [];
|
||||
const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
|
||||
if (confirmation){
|
||||
const clientEmails = clients.length > 0 ? clients.map(item => item.email) : currentClient.email
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
|
||||
title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + clientEmails,
|
||||
content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
|
||||
onOk: () => this.delClientHandler(dbInboundId, currentClient, clients),
|
||||
});
|
||||
} else {
|
||||
this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
|
||||
this.delClientHandler(dbInboundId, currentClient, clients)
|
||||
}
|
||||
},
|
||||
getSubGroupClients(dbInbounds, currentClient) {
|
||||
const response = {
|
||||
inbounds: [],
|
||||
clients: [],
|
||||
editIds: []
|
||||
}
|
||||
if (dbInbounds && dbInbounds.length > 0 && currentClient) {
|
||||
dbInbounds.forEach((dbInboundItem) => {
|
||||
const dbInbound = new DBInbound(dbInboundItem);
|
||||
if (dbInbound) {
|
||||
const inbound = dbInbound.toInbound();
|
||||
if (inbound) {
|
||||
const clients = inbound.clients;
|
||||
if (clients.length > 0) {
|
||||
clients.forEach((client) => {
|
||||
if (client['subId'] === currentClient['subId']) {
|
||||
client['inboundId'] = dbInboundItem.id
|
||||
client['clientId'] = this.getClientId(dbInbound.protocol, client)
|
||||
response.inbounds.push(dbInboundItem.id)
|
||||
response.clients.push(client)
|
||||
response.editIds.push(client['clientId'])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return response;
|
||||
},
|
||||
getClientId(protocol, client) {
|
||||
switch (protocol) {
|
||||
case Protocols.TROJAN: return client.password;
|
||||
|
@ -1135,10 +1251,10 @@
|
|||
}
|
||||
return newDbInbound;
|
||||
},
|
||||
showQrcode(dbInboundId, client) {
|
||||
showQrcode(dbInboundId, client, isJustSub = false) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
|
||||
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client, isJustSub);
|
||||
},
|
||||
showInfo(dbInboundId, client) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
|
@ -1158,6 +1274,13 @@
|
|||
},
|
||||
async switchEnableClient(dbInboundId, client) {
|
||||
this.loading()
|
||||
const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, client) : [];
|
||||
if (subGroup && subGroup.clients && subGroup.clients.length > 0){
|
||||
await this.updateClient(subGroup.clients.map(item => {
|
||||
item.enable = !item.enable
|
||||
return item
|
||||
}), subGroup.inbounds, subGroup.editIds);
|
||||
}else{
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
inbound = dbInbound.toInbound();
|
||||
clients = inbound.clients;
|
||||
|
@ -1165,29 +1288,47 @@
|
|||
clients[index].enable = !clients[index].enable;
|
||||
clientId = this.getClientId(dbInbound.protocol, clients[index]);
|
||||
await this.updateClient(clients[index], dbInboundId, clientId);
|
||||
}
|
||||
this.loading(false);
|
||||
},
|
||||
async submit(url, data, modal) {
|
||||
const msg = await HttpUtil.postWithModal(url, data, modal);
|
||||
async submit(url, data, model, isJson = false) {
|
||||
const msg = isJson ? await HttpUtil.postWithModalJson(url, data, model) : await HttpUtil.postWithModal(url, data, model);
|
||||
if (msg.success) {
|
||||
await this.getDBInbounds();
|
||||
}
|
||||
return msg
|
||||
},
|
||||
getInboundClients(dbInbound) {
|
||||
return dbInbound.toInbound().clients;
|
||||
},
|
||||
resetClientTrafficHandler(client, dbInboundId, clients = []) {
|
||||
if (clients.length > 0){
|
||||
const resetRequests = clients
|
||||
.filter(client => {
|
||||
const inbound = this.dbInbounds.find(inbound => inbound.id === client.inboundId);
|
||||
return inbound && this.hasClientStats(inbound, client.email);
|
||||
}).map(client => ({ inboundId: client.inboundId, email: client.email}));
|
||||
|
||||
this.submit('/panel/inbound/resetGroupClientTraffic', resetRequests, null, true)
|
||||
}else {
|
||||
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
|
||||
}
|
||||
},
|
||||
resetClientTraffic(client, dbInboundId, confirmation = true) {
|
||||
const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, client) : [];
|
||||
const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
|
||||
if (confirmation){
|
||||
const clientEmails = clients.length > 0 ? clients.map(item => item.email) : client.email
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + client.email,
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + clientEmails,
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
|
||||
onOk: () => this.resetClientTrafficHandler(client, dbInboundId, clients),
|
||||
})
|
||||
} else {
|
||||
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
|
||||
this.resetClientTrafficHandler(client, dbInboundId, clients);
|
||||
}
|
||||
},
|
||||
resetAllTraffic() {
|
||||
|
@ -1223,6 +1364,10 @@
|
|||
isExpiry(dbInbound, index) {
|
||||
return dbInbound.toInbound().isExpiry(index);
|
||||
},
|
||||
hasClientStats(dbInbound, email) {
|
||||
if (email.length == 0) return 0;
|
||||
return !!dbInbound.clientStats.find(stats => stats.email === email);
|
||||
},
|
||||
getUpStats(dbInbound, email) {
|
||||
if (email.length == 0) return 0;
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
||||
|
|
|
@ -268,6 +268,7 @@
|
|||
<a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}'>
|
||||
<a-list item-layout="horizontal">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subSyncEnable"}}' desc='{{ i18n "pages.settings.subSyncEnableDesc"}}' v-model="allSetting.subSyncEnable"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
|
||||
|
|
178
web/job/client_traffic_sync_job.go
Normal file
178
web/job/client_traffic_sync_job.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
package job
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/xray"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SyncClientTrafficJob struct {
|
||||
subClientsCollection map[string][]string
|
||||
}
|
||||
|
||||
func NewClientTrafficSyncJob() *SyncClientTrafficJob {
|
||||
return new(SyncClientTrafficJob)
|
||||
}
|
||||
func (j *SyncClientTrafficJob) Run() {
|
||||
// Step 1: Group clients by SubID
|
||||
subClientsCollection := j.collectClientsGroupedBySubId()
|
||||
|
||||
// Step 2: Sync client traffics for each SubID group
|
||||
for subId, emails := range subClientsCollection {
|
||||
err := j.syncClientTraffics(map[string][]string{subId: emails})
|
||||
if err != nil {
|
||||
logger.Error("Failed to sync traffics for SubID ", subId, ": ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collectClientsGroupedBySubId groups clients by their SubIDs
|
||||
func (j *SyncClientTrafficJob) collectClientsGroupedBySubId() map[string][]string {
|
||||
db := database.GetDB()
|
||||
result := make(map[string][]string)
|
||||
|
||||
// Fetch all inbounds
|
||||
var inbounds []*model.Inbound
|
||||
if err := db.Model(&model.Inbound{}).Find(&inbounds).Error; err != nil {
|
||||
logger.Error("Error fetching inbounds: ", err)
|
||||
return result // Return empty map on error
|
||||
}
|
||||
|
||||
// Process each inbound
|
||||
for _, inbound := range inbounds {
|
||||
if inbound.Settings == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
settingsMap, err := parseSettings(inbound.Settings, uint(inbound.Id))
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
clients, ok := settingsMap["clients"].([]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
processClients(clients, result)
|
||||
}
|
||||
|
||||
// Remove SubIDs with one or fewer emails
|
||||
filterSingleEmailSubIDs(result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// parseSettings unmarshals the JSON settings and returns it as a map
|
||||
func parseSettings(settings string, inboundID uint) (map[string]interface{}, error) {
|
||||
if !json.Valid([]byte(settings)) {
|
||||
return nil, fmt.Errorf("Invalid JSON format in Settings for inbound ID %d", inboundID)
|
||||
}
|
||||
|
||||
var tempData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(settings), &tempData); err != nil {
|
||||
return nil, fmt.Errorf("Error unmarshalling settings for inbound ID %d: %v", inboundID, err)
|
||||
}
|
||||
|
||||
return tempData, nil
|
||||
}
|
||||
|
||||
// processClients extracts SubID and email from the clients and populates the result map
|
||||
func processClients(clients []interface{}, result map[string][]string) {
|
||||
for _, client := range clients {
|
||||
clientMap, ok := client.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
subId, ok := clientMap["subId"].(string)
|
||||
if !ok || subId == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
email, ok := clientMap["email"].(string)
|
||||
if !ok || email == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
result[subId] = append(result[subId], email)
|
||||
}
|
||||
}
|
||||
|
||||
// filterSingleEmailSubIDs removes SubIDs with one or fewer emails from the result map
|
||||
func filterSingleEmailSubIDs(result map[string][]string) {
|
||||
for subId, emails := range result {
|
||||
if len(emails) <= 1 {
|
||||
delete(result, subId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// syncClientTraffics synchronizes traffic data for each SubID group
|
||||
func (j *SyncClientTrafficJob) syncClientTraffics(result map[string][]string) error {
|
||||
for subId, emails := range result {
|
||||
db := database.GetDB()
|
||||
|
||||
// Step 1: Calculate maxUp and maxDown (outside transaction)
|
||||
var maxUp, maxDown int64
|
||||
err := calculateMaxTraffic(db, emails, &maxUp, &maxDown)
|
||||
if err != nil {
|
||||
logger.Error("Failed to calculate max traffic for SubID ", subId, ": ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Step 2: Update traffic data with retry mechanism
|
||||
err = retryOperation(func() error {
|
||||
return updateTraffic(db, emails, maxUp, maxDown)
|
||||
}, 5, 100*time.Millisecond)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("Failed to update client traffics for SubID ", subId, ": ", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateMaxTraffic calculates max up and down traffic for a group of emails
|
||||
func calculateMaxTraffic(db *gorm.DB, emails []string, maxUp, maxDown *int64) error {
|
||||
return db.Model(&xray.ClientTraffic{}).
|
||||
Where("email IN ?", emails).
|
||||
Select("MAX(up) AS max_up, MAX(down) AS max_down").
|
||||
Row().
|
||||
Scan(maxUp, maxDown)
|
||||
}
|
||||
|
||||
// updateTraffic updates the traffic data in the database within a transaction
|
||||
func updateTraffic(db *gorm.DB, emails []string, maxUp, maxDown int64) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
return tx.Model(&xray.ClientTraffic{}).
|
||||
Where("email IN ?", emails).
|
||||
Updates(map[string]interface{}{
|
||||
"up": maxUp,
|
||||
"down": maxDown,
|
||||
}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// retryOperation retries an operation multiple times with a delay
|
||||
func retryOperation(operation func() error, maxRetries int, delay time.Duration) error {
|
||||
var err error
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
err = operation()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Retry %d/%d failed: %v", i+1, maxRetries, err))
|
||||
time.Sleep(delay)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -50,6 +50,7 @@ var defaultValueMap = map[string]string{
|
|||
"tgLang": "en-US",
|
||||
"secretEnable": "false",
|
||||
"subEnable": "false",
|
||||
"subSyncEnable": "true",
|
||||
"subListen": "",
|
||||
"subPort": "2096",
|
||||
"subPath": "/sub/",
|
||||
|
@ -416,6 +417,14 @@ func (s *SettingService) GetSubEnable() (bool, error) {
|
|||
return s.getBool("subEnable")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubSyncEnable() (bool, error) {
|
||||
return s.getBool("subSyncEnable")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetSubSyncEnable(value bool) error {
|
||||
return s.setBool("subSyncEnable", value)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubListen() (string, error) {
|
||||
return s.getString("subListen")
|
||||
}
|
||||
|
@ -544,6 +553,7 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
|
|||
"defaultKey": func() (interface{}, error) { return s.GetKeyFile() },
|
||||
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotEnabled() },
|
||||
"subEnable": func() (interface{}, error) { return s.GetSubEnable() },
|
||||
"subSyncEnable": func() (interface{}, error) { return s.GetSubSyncEnable() },
|
||||
"subURI": func() (interface{}, error) { return s.GetSubURI() },
|
||||
"subJsonURI": func() (interface{}, error) { return s.GetSubJsonURI() },
|
||||
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
|
||||
|
|
|
@ -190,6 +190,9 @@
|
|||
|
||||
[pages.client]
|
||||
"add" = "Add Client"
|
||||
"groupAdd" = "Add subscription user"
|
||||
"isGroupEdit" = "Group editing"
|
||||
"isGroupEditDesc" = "All clients with the same subscription are edited"
|
||||
"edit" = "Edit Client"
|
||||
"submitAdd" = "Add Client"
|
||||
"submitEdit" = "Save Changes"
|
||||
|
@ -287,6 +290,8 @@
|
|||
"subSettings" = "Subscription"
|
||||
"subEnable" = "Enable Subscription Service"
|
||||
"subEnableDesc" = "Enables the subscription service."
|
||||
"subSyncEnable" = "Enable Subscription Sync"
|
||||
"subSyncEnableDesc" = "Traffic from clients with the same subscription will be synchronized every 10 seconds."
|
||||
"subListen" = "Listen IP"
|
||||
"subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)"
|
||||
"subPort" = "Listen Port"
|
||||
|
|
|
@ -190,6 +190,7 @@
|
|||
|
||||
[pages.client]
|
||||
"add" = "Agregar Cliente"
|
||||
"groupAdd" = "Agregar usuario de suscripción"
|
||||
"edit" = "Editar Cliente"
|
||||
"submitAdd" = "Agregar Cliente"
|
||||
"submitEdit" = "Guardar Cambios"
|
||||
|
@ -287,6 +288,8 @@
|
|||
"subSettings" = "Suscripción"
|
||||
"subEnable" = "Habilitar Servicio"
|
||||
"subEnableDesc" = "Función de suscripción con configuración separada."
|
||||
"subSyncEnable" = "Habilitar sincronización de suscripciones"
|
||||
"subSyncEnableDesc" = "El tráfico de los clientes con la misma suscripción se sincronizará cada 10 segundos."
|
||||
"subListen" = "Listening IP"
|
||||
"subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs."
|
||||
"subPort" = "Puerto de Suscripción"
|
||||
|
|
|
@ -190,6 +190,9 @@
|
|||
|
||||
[pages.client]
|
||||
"add" = "کاربر جدید"
|
||||
"groupAdd" = "افزودن کاربر سابسکریپشن"
|
||||
"isGroupEdit" = "ویرایش گروهی"
|
||||
"isGroupEditDesc" = "همه کاربران با سابسکریپشن یکسان ویرایش می شوند"
|
||||
"edit" = "ویرایش کاربر"
|
||||
"submitAdd" = "اضافه کردن"
|
||||
"submitEdit" = "ذخیره تغییرات"
|
||||
|
@ -287,6 +290,8 @@
|
|||
"subSettings" = "سابسکریپشن"
|
||||
"subEnable" = "فعالسازی سرویس سابسکریپشن"
|
||||
"subEnableDesc" = "سرویس سابسکریپشن را فعالمیکند"
|
||||
"subSyncEnable" = "فعالسازی همگام سازی سابسکریپشن"
|
||||
"subSyncEnableDesc" = "ترافیک کلاینت هایی که سابسکریپشن یکسان دارند هر ۱۰ ثانیه همگام میشوند."
|
||||
"subListen" = "آدرس آیپی"
|
||||
"subListenDesc" = "آدرس آیپی برای سرویس سابسکریپشن. برای گوش دادن بهتمام آیپیها خالیبگذارید"
|
||||
"subPort" = "پورت"
|
||||
|
|
|
@ -190,6 +190,9 @@
|
|||
|
||||
[pages.client]
|
||||
"add" = "Tambah Klien"
|
||||
"groupAdd" = "Tambahkan pengguna langganan"
|
||||
"isGroupEdit" = "Pengeditan grup"
|
||||
"isGroupEditDesc" = "Semua klien dengan langganan yang sama akan diedit"
|
||||
"edit" = "Edit Klien"
|
||||
"submitAdd" = "Tambah Klien"
|
||||
"submitEdit" = "Simpan Perubahan"
|
||||
|
@ -287,6 +290,8 @@
|
|||
"subSettings" = "Langganan"
|
||||
"subEnable" = "Aktifkan Layanan Langganan"
|
||||
"subEnableDesc" = "Mengaktifkan layanan langganan."
|
||||
"subSyncEnable" = "Aktifkan Sinkronisasi Langganan"
|
||||
"subSyncEnableDesc" = "Lalu lintas dari klien dengan langganan yang sama akan disinkronkan setiap 10 detik."
|
||||
"subListen" = "IP Pendengar"
|
||||
"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)"
|
||||
"subPort" = "Port Pendengar"
|
||||
|
|
|
@ -190,6 +190,9 @@
|
|||
|
||||
[pages.client]
|
||||
"add" = "クライアント追加"
|
||||
"groupAdd" = "サブスクリプション ユーザーの追加"
|
||||
"isGroupEdit" = "グループの編集"
|
||||
"isGroupEditDesc" = "同じサブスクリプションを持つすべてのクライアントが編集されます"
|
||||
"edit" = "クライアント編集"
|
||||
"submitAdd" = "クライアント追加"
|
||||
"submitEdit" = "変更を保存"
|
||||
|
@ -287,6 +290,8 @@
|
|||
"subSettings" = "サブスクリプション設定"
|
||||
"subEnable" = "サブスクリプションサービスを有効にする"
|
||||
"subEnableDesc" = "サブスクリプションサービス機能を有効にする"
|
||||
"subSyncEnable" = "サブスクリプション同期を有効にする"
|
||||
"subSyncEnableDesc" = "同じサブスクリプションを持つクライアントからのトラフィックは 10 秒ごとに同期されます。"
|
||||
"subListen" = "監視IP"
|
||||
"subListenDesc" = "サブスクリプションサービスが監視するIPアドレス(空白にするとすべてのIPを監視)"
|
||||
"subPort" = "監視ポート"
|
||||
|
|
|
@ -190,6 +190,9 @@
|
|||
|
||||
[pages.client]
|
||||
"add" = "Adicionar Cliente"
|
||||
"groupAdd" = "Adicionar usuário de assinatura"
|
||||
"isGroupEdit" = "Edição de grupo"
|
||||
"isGroupEditDesc" = "Todos os clientes com a mesma assinatura são editados"
|
||||
"edit" = "Editar Cliente"
|
||||
"submitAdd" = "Adicionar Cliente"
|
||||
"submitEdit" = "Salvar Alterações"
|
||||
|
@ -287,6 +290,8 @@
|
|||
"subSettings" = "Assinatura"
|
||||
"subEnable" = "Ativar Serviço de Assinatura"
|
||||
"subEnableDesc" = "Ativa o serviço de assinatura."
|
||||
"subSyncEnable" = "Habilitar sincronização de assinatura"
|
||||
"subSyncEnableDesc" = "O tráfego de clientes com a mesma assinatura será sincronizado a cada 10 segundos."
|
||||
"subListen" = "IP de Escuta"
|
||||
"subListenDesc" = "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)"
|
||||
"subPort" = "Porta de Escuta"
|
||||
|
|
|
@ -190,6 +190,9 @@
|
|||
|
||||
[pages.client]
|
||||
"add" = "Добавить пользователя"
|
||||
"groupAdd" = "Добавить пользователя подписки"
|
||||
"isGroupEdit" = "Групповое редактирование"
|
||||
"isGroupEditDesc" = "Все клиенты с одинаковой подпиской редактируются"
|
||||
"edit" = "Редактировать пользователя"
|
||||
"submitAdd" = "Добавить пользователя"
|
||||
"submitEdit" = "Сохранить изменения"
|
||||
|
@ -287,6 +290,8 @@
|
|||
"subSettings" = "Подписка"
|
||||
"subEnable" = "Включить службу"
|
||||
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
|
||||
"subSyncEnable" = "Включить синхронизацию подписки"
|
||||
"subSyncEnableDesc" = "Трафик от клиентов с одинаковой подпиской будет синхронизироваться каждые 10 секунд."
|
||||
"subListen" = "Прослушивание IP"
|
||||
"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса"
|
||||
"subPort" = "Порт подписки"
|
||||
|
|
|
@ -190,6 +190,9 @@
|
|||
|
||||
[pages.client]
|
||||
"add" = "Müşteri Ekle"
|
||||
"groupAdd" = "Abonelik kullanıcısı ekle"
|
||||
"isGroupEdit" = "Grup düzenleme"
|
||||
"isGroupEditDesc" = "Aynı aboneliğe sahip tüm istemciler düzenlendi"
|
||||
"edit" = "Müşteriyi Düzenle"
|
||||
"submitAdd" = "Müşteri Ekle"
|
||||
"submitEdit" = "Değişiklikleri Kaydet"
|
||||
|
@ -287,6 +290,8 @@
|
|||
"subSettings" = "Abonelik"
|
||||
"subEnable" = "Abonelik Hizmetini Etkinleştir"
|
||||
"subEnableDesc" = "Abonelik hizmetini etkinleştirir."
|
||||
"subSyncEnable" = "Abonelik Senkronizasyonunu Etkinleştir"
|
||||
"subSyncEnableDesc" = "Aynı aboneliğe sahip istemcilerden gelen trafik her 10 saniyede bir senkronize edilecektir."
|
||||
"subListen" = "Dinleme IP"
|
||||
"subListenDesc" = "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)"
|
||||
"subPort" = "Dinleme Portu"
|
||||
|
|
|
@ -190,6 +190,9 @@
|
|||
|
||||
[pages.client]
|
||||
"add" = "Додати клієнта"
|
||||
"groupAdd" = "Додати підписаного користувача"
|
||||
"isGroupEdit" = "Редагування групи"
|
||||
"isGroupEditDesc" = "Всі клієнти з однаковою підпискою редагуються"
|
||||
"edit" = "Редагувати клієнта"
|
||||
"submitAdd" = "Додати клієнта"
|
||||
"submitEdit" = "Зберегти зміни"
|
||||
|
@ -287,6 +290,8 @@
|
|||
"subSettings" = "Підписка"
|
||||
"subEnable" = "Увімкнути службу підписки"
|
||||
"subEnableDesc" = "Вмикає службу підписки."
|
||||
"subSyncEnable" = "Увімкнути синхронізацію підписки"
|
||||
"subSyncEnableDesc" = "Трафік від клієнтів з однаковою підпискою буде синхронізовано кожні 10 секунд."
|
||||
"subListen" = "Слухати IP"
|
||||
"subListenDesc" = "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)"
|
||||
"subPort" = "Слухати порт"
|
||||
|
|
|
@ -190,6 +190,9 @@
|
|||
|
||||
[pages.client]
|
||||
"add" = "Thêm người dùng"
|
||||
"groupAdd" = "Thêm người dùng đăng ký"
|
||||
"isGroupEdit" = "Chỉnh sửa nhóm"
|
||||
"isGroupEditDesc" = "Tất cả người dùng có cùng đăng ký đều có thể được chỉnh sửa"
|
||||
"edit" = "Chỉnh sửa người dùng"
|
||||
"submitAdd" = "Thêm"
|
||||
"submitEdit" = "Lưu thay đổi"
|
||||
|
@ -287,6 +290,8 @@
|
|||
"subSettings" = "Gói đăng ký"
|
||||
"subEnable" = "Bật dịch vụ"
|
||||
"subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng"
|
||||
"subSyncEnable" = "Bật đồng bộ đăng ký"
|
||||
"subSyncEnableDesc" = "Lưu lượng truy cập từ các máy khách có cùng đăng ký sẽ được đồng bộ sau mỗi 10 giây."
|
||||
"subListen" = "Listening IP"
|
||||
"subListenDesc" = "Mặc định để trống để nghe tất cả các IP"
|
||||
"subPort" = "Cổng gói đăng ký"
|
||||
|
|
|
@ -190,6 +190,9 @@
|
|||
|
||||
[pages.client]
|
||||
"add" = "添加客户端"
|
||||
"groupAdd" = "添加订阅用户"
|
||||
"isGroupEdit" = "群组编辑"
|
||||
"isGroupEditDesc" = "所有具有相同订阅的客户端均被编辑"
|
||||
"edit" = "编辑客户端"
|
||||
"submitAdd" = "添加客户端"
|
||||
"submitEdit" = "保存修改"
|
||||
|
@ -287,6 +290,8 @@
|
|||
"subSettings" = "订阅设置"
|
||||
"subEnable" = "启用订阅服务"
|
||||
"subEnableDesc" = "启用订阅服务功能"
|
||||
"subSyncEnable" = "启用订阅同步"
|
||||
"subSyncEnableDesc" = "具有相同订阅的客户端的流量将每 10 秒同步一次。"
|
||||
"subListen" = "监听 IP"
|
||||
"subListenDesc" = "订阅服务监听的 IP 地址(留空表示监听所有 IP)"
|
||||
"subPort" = "监听端口"
|
||||
|
|
|
@ -190,6 +190,9 @@
|
|||
|
||||
[pages.client]
|
||||
"add" = "新增客戶端"
|
||||
"groupAdd" = "新增訂閱使用者"
|
||||
"isGroupEdit" = "群組編輯"
|
||||
"isGroupEditDesc" = "所有具有相同訂閱的用戶端都被編輯"
|
||||
"edit" = "編輯客戶端"
|
||||
"submitAdd" = "新增客戶端"
|
||||
"submitEdit" = "儲存修改"
|
||||
|
@ -287,6 +290,8 @@
|
|||
"subSettings" = "訂閱設定"
|
||||
"subEnable" = "啟用訂閱服務"
|
||||
"subEnableDesc" = "啟用訂閱服務功能"
|
||||
"subSyncEnable" = "啟用訂閱同步"
|
||||
"subSyncEnableDesc" = "來自具有相同訂閱的客戶端的流量將每 10 秒同步一次。"
|
||||
"subListen" = "監聽 IP"
|
||||
"subListenDesc" = "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP)"
|
||||
"subPort" = "監聽埠"
|
||||
|
|
|
@ -260,6 +260,13 @@ func (s *Server) startTask() {
|
|||
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
|
||||
}()
|
||||
|
||||
isSubEnable, err1 := s.settingService.GetSubEnable()
|
||||
isSubSyncEnable, err2 := s.settingService.GetSubSyncEnable()
|
||||
if err1 == nil && err2 == nil && isSubEnable && isSubSyncEnable {
|
||||
// Sync the client traffic with the same SubId every 10 seconds
|
||||
s.cron.AddJob("@every 10s", job.NewClientTrafficSyncJob())
|
||||
}
|
||||
|
||||
// check client ips from log file every 10 sec
|
||||
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())
|
||||
|
||||
|
|
Loading…
Reference in a new issue