mirror of
https://github.com/alireza0/x-ui.git
synced 2026-03-18 23:05:48 +00:00
Compare commits
330 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f20b71c45 | ||
|
|
4779b37e6e | ||
|
|
7a20d2c83c | ||
|
|
7ead999f12 | ||
|
|
812c9fbe17 | ||
|
|
5adbfd9528 | ||
|
|
1b93c7c0f2 | ||
|
|
58e7f51313 | ||
|
|
78e97af4db | ||
|
|
7c37319438 | ||
|
|
22605edb20 | ||
|
|
cb4c843dbf | ||
|
|
c2541d65f9 | ||
|
|
a99035bd3d | ||
|
|
57e297d6cf | ||
|
|
97b603c4a7 | ||
|
|
8216de011e | ||
|
|
f3cdc35452 | ||
|
|
a118bae974 | ||
|
|
e01a3ac893 | ||
|
|
f9e2d9f61e | ||
|
|
28233884bb | ||
|
|
a0a4d7571d | ||
|
|
7dde506862 | ||
|
|
1f5a785806 | ||
|
|
df13bf1a97 | ||
|
|
4f289fbbad | ||
|
|
ef0a48de23 | ||
|
|
545d7a73c8 | ||
|
|
58474373d9 | ||
|
|
eb7fda21be | ||
|
|
68b0cb1312 | ||
|
|
520ce4b557 | ||
|
|
63eda1ed27 | ||
|
|
74595b2785 | ||
|
|
b79ff596a2 | ||
|
|
e5f0cf9b76 | ||
|
|
0248b981d7 | ||
|
|
bcb90ac14a | ||
|
|
58ae8fadad | ||
|
|
33e41f1bda | ||
|
|
b87705f50b | ||
|
|
2349c9fdbe | ||
|
|
b00d33830c | ||
|
|
d14c5f4f67 | ||
|
|
c538301d42 | ||
|
|
161c4c950b | ||
|
|
243273defd | ||
|
|
ceb0e0837f | ||
|
|
710c2280a8 | ||
|
|
8a9b3eddfe | ||
|
|
0c4bf6fac8 | ||
|
|
5e8556be33 | ||
|
|
d86c75b925 | ||
|
|
c8c0bbc455 | ||
|
|
6d453fa91b | ||
|
|
abc82cef4c | ||
|
|
14270caa16 | ||
|
|
aa74f05c52 | ||
|
|
953a3ae315 | ||
|
|
a097733ccc | ||
|
|
7edaf89596 | ||
|
|
f8fe396ed5 | ||
|
|
128e027dac | ||
|
|
7a36eda6b4 | ||
|
|
00746c7864 | ||
|
|
c1fb8712d6 | ||
|
|
0bd44a64ea | ||
|
|
d532eb6bd8 | ||
|
|
8960e5450a | ||
|
|
d1d7ee7f7c | ||
|
|
8ddf7963c7 | ||
|
|
5bb0372aee | ||
|
|
788a1b9d6b | ||
|
|
9d1aa0d45f | ||
|
|
b125f1835c | ||
|
|
5e3c0d6ecc | ||
|
|
c197165da7 | ||
|
|
ec1efabcaa | ||
|
|
3ee57fd51d | ||
|
|
26b748338a | ||
|
|
af432828f1 | ||
|
|
cb14d298c0 | ||
|
|
994a9f4907 | ||
|
|
01bfd36316 | ||
|
|
4a332c6723 | ||
|
|
c3d1824ea2 | ||
|
|
57a32ab524 | ||
|
|
2c5bb94894 | ||
|
|
39c1a4276d | ||
|
|
4736786c6f | ||
|
|
c89dbed88e | ||
|
|
402c713f06 | ||
|
|
a371bec2aa | ||
|
|
427d008bd1 | ||
|
|
e69d17be67 | ||
|
|
09c61976ea | ||
|
|
6e17c282e0 | ||
|
|
700973655c | ||
|
|
dcb54267f2 | ||
|
|
19e851d5c8 | ||
|
|
3f7ef07b8e | ||
|
|
89be2c8fec | ||
|
|
dd11585074 | ||
|
|
7ecb73af8c | ||
|
|
ba083ecc7e | ||
|
|
365ec1a704 | ||
|
|
63969d5fd6 | ||
|
|
0449a35409 | ||
|
|
453594ee9e | ||
|
|
a2d8bec80a | ||
|
|
b0ca8a8e6c | ||
|
|
99c9d777c0 | ||
|
|
97d109c900 | ||
|
|
35ad91a9e0 | ||
|
|
8bc16b020b | ||
|
|
6db9bd0ad9 | ||
|
|
5baa397d1c | ||
|
|
76e1243da3 | ||
|
|
7b7a0f9aa7 | ||
|
|
134e2236a6 | ||
|
|
aab672976e | ||
|
|
f7aee9fe18 | ||
|
|
a19b58675b | ||
|
|
7a229b27f5 | ||
|
|
abf68446e5 | ||
|
|
b4b7ec565e | ||
|
|
4b1b920bf4 | ||
|
|
11bff57f23 | ||
|
|
4838b0f6f0 | ||
|
|
bb8807129a | ||
|
|
cb050bfe34 | ||
|
|
76a996cc2d | ||
|
|
7fec1dd5cf | ||
|
|
a24e9089b6 | ||
|
|
b6474eaef6 | ||
|
|
76f90b0a46 | ||
|
|
42abffdaef | ||
|
|
73d518dfe3 | ||
|
|
4592a8d201 | ||
|
|
6ab99326ce | ||
|
|
6c5b098e1d | ||
|
|
fe670ae885 | ||
|
|
f4f1d477b3 | ||
|
|
7455a2baa3 | ||
|
|
4c5576c38b | ||
|
|
18ee201412 | ||
|
|
772a1b341e | ||
|
|
5923c765a9 | ||
|
|
d7074cc3c9 | ||
|
|
f4f8c0d6df | ||
|
|
0c755be648 | ||
|
|
c8dc4a96b4 | ||
|
|
8d9f6ccc11 | ||
|
|
3d7a8e372e | ||
|
|
2fe4f1ad07 | ||
|
|
04095b6ec4 | ||
|
|
6d404e4b27 | ||
|
|
f9f8431dd2 | ||
|
|
f996b9ee2b | ||
|
|
a3e1ee1f35 | ||
|
|
e16391ad05 | ||
|
|
48b543becb | ||
|
|
80f052697c | ||
|
|
fb15248030 | ||
|
|
6a18ba48aa | ||
|
|
c13b7b2a18 | ||
|
|
838bdaf314 | ||
|
|
f6f0cf3657 | ||
|
|
dd5e9a97a6 | ||
|
|
edfbb91dff | ||
|
|
eb239b33af | ||
|
|
4b9940fb06 | ||
|
|
98d5e960a4 | ||
|
|
61fd029e28 | ||
|
|
2be3df59be | ||
|
|
7cf192e179 | ||
|
|
3a002c886d | ||
|
|
4b05c333ca | ||
|
|
bfc5b37bb1 | ||
|
|
07089455b5 | ||
|
|
7add969312 | ||
|
|
501f778c9e | ||
|
|
ac52340c51 | ||
|
|
f5f90d81a3 | ||
|
|
e6c23caa1d | ||
|
|
bf3a64c77d | ||
|
|
e2e88d8652 | ||
|
|
91b453fff7 | ||
|
|
9e955df76a | ||
|
|
58ca5f7a51 | ||
|
|
4f469f9ad5 | ||
|
|
1969120c94 | ||
|
|
cdbf742b66 | ||
|
|
02a4f0c7d4 | ||
|
|
e0a64a8257 | ||
|
|
38db94f507 | ||
|
|
d0ce67a8f0 | ||
|
|
fcc3f67181 | ||
|
|
357630b077 | ||
|
|
1246b54ffa | ||
|
|
2e3119ab9b | ||
|
|
b834ac5d93 | ||
|
|
92df5659c9 | ||
|
|
f85bd39a0a | ||
|
|
67fe79a7eb | ||
|
|
5cd3ee10de | ||
|
|
e3e7b0f736 | ||
|
|
19280cdfbd | ||
|
|
17a483266e | ||
|
|
1e7d2010a8 | ||
|
|
97a0e2cae9 | ||
|
|
e9c3c330f2 | ||
|
|
294894eca0 | ||
|
|
f3df93a950 | ||
|
|
1780f06242 | ||
|
|
50671bfce3 | ||
|
|
7332258dce | ||
|
|
d9523cfd37 | ||
|
|
47f75f6382 | ||
|
|
a6f2dc577a | ||
|
|
36ddde3491 | ||
|
|
f1a87c7d92 | ||
|
|
48f7b88ede | ||
|
|
b7048cdab8 | ||
|
|
df2ef6cdde | ||
|
|
efd1b06593 | ||
|
|
28050aba23 | ||
|
|
27de7b030b | ||
|
|
9ae49ed562 | ||
|
|
cee117e1e8 | ||
|
|
8f0a701d9e | ||
|
|
921a72b5cf | ||
|
|
9e902fc82f | ||
|
|
6ed8ff8e05 | ||
|
|
f333ad23d9 | ||
|
|
3f49aa5541 | ||
|
|
0f487dc10f | ||
|
|
f3c0edac07 | ||
|
|
394c857bb6 | ||
|
|
6080709fc7 | ||
|
|
17c47f0711 | ||
|
|
dac234357a | ||
|
|
2d15073a90 | ||
|
|
1b2e165a65 | ||
|
|
e1f8e8efd2 | ||
|
|
738c7064a9 | ||
|
|
c24483ad44 | ||
|
|
046f071378 | ||
|
|
d40a16e29d | ||
|
|
2f1f28cc0b | ||
|
|
70ff51d629 | ||
|
|
7472c31c34 | ||
|
|
04a8d5c177 | ||
|
|
7b2bd4247d | ||
|
|
5aee1c886b | ||
|
|
7a9a30a038 | ||
|
|
eb17cec289 | ||
|
|
22e107c746 | ||
|
|
36a93e2959 | ||
|
|
073247f054 | ||
|
|
96c8932961 | ||
|
|
3d4b87d72f | ||
|
|
5fac7df174 | ||
|
|
8bba9f3c25 | ||
|
|
268e8c7eb1 | ||
|
|
14e8f6cbfa | ||
|
|
7f0ba495db | ||
|
|
af07a9bf15 | ||
|
|
6b8ca0c321 | ||
|
|
48554ab497 | ||
|
|
dddebf02ef | ||
|
|
0c469848dc | ||
|
|
cb85d4f83f | ||
|
|
df29570dc1 | ||
|
|
7e9c336a17 | ||
|
|
585f43498c | ||
|
|
2a7a09d12f | ||
|
|
98759e964c | ||
|
|
9ae3d62afb | ||
|
|
24b718dca3 | ||
|
|
d0c4d0a941 | ||
|
|
f829213dd1 | ||
|
|
4d5a73512e | ||
|
|
6dccc5b891 | ||
|
|
f642830dc8 | ||
|
|
073b2ee8a8 | ||
|
|
8bcfa0b101 | ||
|
|
cd9e903ec3 | ||
|
|
b9fecaa918 | ||
|
|
eb516575ec | ||
|
|
aeb5c9acaa | ||
|
|
68a7b38117 | ||
|
|
e084518f4f | ||
|
|
e6460d1d00 | ||
|
|
0218af8b4f | ||
|
|
552bc6c1f6 | ||
|
|
3001107282 | ||
|
|
ed6f45f341 | ||
|
|
83853a963e | ||
|
|
2774de744c | ||
|
|
7306be3cb6 | ||
|
|
b2d16b87bb | ||
|
|
4ab1135d89 | ||
|
|
8a6782273e | ||
|
|
b2075e9548 | ||
|
|
8efa776cd7 | ||
|
|
b641951759 | ||
|
|
f539cbec45 | ||
|
|
645b1535ac | ||
|
|
3729ac5e3e | ||
|
|
f736f069a0 | ||
|
|
cd3d42c7ee | ||
|
|
88cc4aed19 | ||
|
|
2ae1455a1f | ||
|
|
c20d13ea96 | ||
|
|
998d861d0a | ||
|
|
e1c76fdc7a | ||
|
|
6749d63222 | ||
|
|
546b66a61c | ||
|
|
2e2f066c4b | ||
|
|
5bac00b419 | ||
|
|
d07a2119ce | ||
|
|
2951b5f94c | ||
|
|
a9c25cddb3 | ||
|
|
edc911c2f3 | ||
|
|
c053991a37 | ||
|
|
73803af14c | ||
|
|
6dc823e215 | ||
|
|
5b1eaf9cc3 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
10
.github/ISSUE_TEMPLATE/question-template.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/question-template.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
name: Question template
|
||||||
|
about: Ask if it is not clear that it is a bug
|
||||||
|
title: ''
|
||||||
|
labels: question
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
29
.github/workflows/release.yml
vendored
29
.github/workflows/release.yml
vendored
@@ -8,13 +8,13 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
linuxamd64build:
|
linuxamd64build:
|
||||||
name: build x-ui amd64 version
|
name: build x-ui amd64 version
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: 'stable'
|
||||||
- name: build linux amd64 version
|
- name: build linux amd64 version
|
||||||
run: |
|
run: |
|
||||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
|
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
|
||||||
@@ -26,11 +26,12 @@ jobs:
|
|||||||
mv xui-release x-ui
|
mv xui-release x-ui
|
||||||
mkdir bin
|
mkdir bin
|
||||||
cd bin
|
cd bin
|
||||||
wget https://github.com/xtls/xray-core/releases/latest/download/Xray-linux-64.zip
|
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-64.zip
|
||||||
unzip Xray-linux-64.zip
|
unzip Xray-linux-64.zip
|
||||||
rm -f Xray-linux-64.zip geoip.dat geosite.dat
|
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
|
||||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||||
|
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
|
||||||
mv xray xray-linux-amd64
|
mv xray xray-linux-amd64
|
||||||
cd ..
|
cd ..
|
||||||
cd ..
|
cd ..
|
||||||
@@ -46,11 +47,11 @@ jobs:
|
|||||||
prerelease: true
|
prerelease: true
|
||||||
linuxarm64build:
|
linuxarm64build:
|
||||||
name: build x-ui arm64 version
|
name: build x-ui arm64 version
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: '1.20'
|
||||||
- name: build linux arm64 version
|
- name: build linux arm64 version
|
||||||
@@ -66,11 +67,12 @@ jobs:
|
|||||||
mv xui-release x-ui
|
mv xui-release x-ui
|
||||||
mkdir bin
|
mkdir bin
|
||||||
cd bin
|
cd bin
|
||||||
wget https://github.com/xtls/xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
|
wget https://github.com/xtls/xray-core/releases/download/v1.8.1/Xray-linux-arm64-v8a.zip
|
||||||
unzip Xray-linux-arm64-v8a.zip
|
unzip Xray-linux-arm64-v8a.zip
|
||||||
rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat
|
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
|
||||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||||
|
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
|
||||||
mv xray xray-linux-arm64
|
mv xray xray-linux-arm64
|
||||||
cd ..
|
cd ..
|
||||||
cd ..
|
cd ..
|
||||||
@@ -86,11 +88,11 @@ jobs:
|
|||||||
prerelease: true
|
prerelease: true
|
||||||
linuxs390xbuild:
|
linuxs390xbuild:
|
||||||
name: build x-ui s390x version
|
name: build x-ui s390x version
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: '1.20'
|
||||||
- name: build linux s390x version
|
- name: build linux s390x version
|
||||||
@@ -106,11 +108,12 @@ jobs:
|
|||||||
mv xui-release x-ui
|
mv xui-release x-ui
|
||||||
mkdir bin
|
mkdir bin
|
||||||
cd bin
|
cd bin
|
||||||
wget https://github.com/xtls/xray-core/releases/latest/download/Xray-linux-s390x.zip
|
wget https://github.com/xtls/xray-core/releases/download/v1.8.1/Xray-linux-s390x.zip
|
||||||
unzip Xray-linux-s390x.zip
|
unzip Xray-linux-s390x.zip
|
||||||
rm -f Xray-linux-s390x.zip geoip.dat geosite.dat
|
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
|
||||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||||
|
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
|
||||||
mv xray xray-linux-s390x
|
mv xray xray-linux-s390x
|
||||||
cd ..
|
cd ..
|
||||||
cd ..
|
cd ..
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
tmp
|
tmp
|
||||||
|
backup/
|
||||||
bin/
|
bin/
|
||||||
dist/
|
dist/
|
||||||
x-ui-*.tar.gz
|
x-ui-*.tar.gz
|
||||||
@@ -10,4 +11,5 @@ x-ui-*.tar.gz
|
|||||||
main
|
main
|
||||||
release/
|
release/
|
||||||
access.log
|
access.log
|
||||||
|
error.log
|
||||||
.cache
|
.cache
|
||||||
|
|||||||
21
DockerInitFiles.sh
Executable file
21
DockerInitFiles.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
if [ $1 == "amd64" ]; then
|
||||||
|
ARCH="64";
|
||||||
|
FNAME="amd64";
|
||||||
|
elif [ $1 == "arm64" ]; then
|
||||||
|
ARCH="arm64-v8a"
|
||||||
|
FNAME="arm64";
|
||||||
|
else
|
||||||
|
ARCH="64";
|
||||||
|
FNAME="amd64";
|
||||||
|
fi
|
||||||
|
mkdir -p build/bin
|
||||||
|
cd build/bin
|
||||||
|
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-${ARCH}.zip"
|
||||||
|
unzip "Xray-linux-${ARCH}.zip"
|
||||||
|
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat iran.dat
|
||||||
|
mv xray "xray-linux-${FNAME}"
|
||||||
|
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
|
||||||
|
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
|
||||||
|
wget "https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat"
|
||||||
|
cd ../../
|
||||||
12
Dockerfile
12
Dockerfile
@@ -1,16 +1,18 @@
|
|||||||
FROM golang:1.20-alpine AS builder
|
FROM golang:1.20-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV CGO_ENABLED 1
|
ARG TARGETARCH
|
||||||
RUN apk add gcc && apk --no-cache --update add build-base
|
RUN apk --no-cache --update add build-base gcc wget unzip
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go build main.go
|
RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
|
||||||
|
RUN ./DockerInitFiles.sh "$TARGETARCH"
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
LABEL org.opencontainers.image.authors="alireza7@gmail.com"
|
LABEL org.opencontainers.image.authors="alireza7@gmail.com"
|
||||||
ENV TZ=Asia/Tehran
|
ENV TZ=Asia/Tehran
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add ca-certificates tzdata && mkdir bin
|
RUN apk add ca-certificates tzdata
|
||||||
COPY --from=builder /app/main /app/x-ui
|
|
||||||
|
COPY --from=builder /app/build/ /app/
|
||||||
VOLUME [ "/etc/x-ui" ]
|
VOLUME [ "/etc/x-ui" ]
|
||||||
CMD [ "./x-ui" ]
|
CMD [ "./x-ui" ]
|
||||||
196
README.md
196
README.md
@@ -1,51 +1,44 @@
|
|||||||
# x-ui
|
# x-ui
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
[](https://goreportcard.com/report/github.com/alireza0/x-ui)
|
[](https://goreportcard.com/report/github.com/alireza0/x-ui)
|
||||||
[](https://img.shields.io/github/downloads/alireza0/x-ui/total.svg)
|
[](https://img.shields.io/github/downloads/alireza0/x-ui/total.svg)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|
||||||
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
|
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
|
||||||
|
|
||||||
|
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
|
||||||
|
|
||||||
# Temporary downgrade to 0.2.4 due to fixing bugs
|
| Features | Enable? |
|
||||||
Coming back soon with new features
|
| ------------------------------------ | :----------------: |
|
||||||
|
| Multi-lang | :heavy_check_mark: |
|
||||||
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
|
| Dark/Light Theme | :heavy_check_mark: |
|
||||||
|
| Search in deep | :heavy_check_mark: |
|
||||||
| Features | Enable? |
|
| Inbound Multi User | :heavy_check_mark: |
|
||||||
| ------------- |:-------------:|
|
|
||||||
| Multi-lang | :heavy_check_mark: |
|
|
||||||
| Search in deep | :heavy_check_mark: |
|
|
||||||
| Inbound Multi User | :heavy_check_mark: |
|
|
||||||
| Multi User Traffic & Expiration time | :heavy_check_mark: |
|
| Multi User Traffic & Expiration time | :heavy_check_mark: |
|
||||||
| REST API | :heavy_check_mark: |
|
| REST API | :heavy_check_mark: |
|
||||||
| Telegram BOT | :heavy_check_mark: |
|
| Telegram BOT (admin + clients) | :heavy_check_mark: |
|
||||||
|
| Backup database using Telegram BOT | :heavy_check_mark: |
|
||||||
|
| Subscription link + userInfo | :heavy_check_mark: |
|
||||||
|
| Calculate expire date on first usage | :heavy_check_mark: |
|
||||||
|
|
||||||
**If you think this project is helpful to you, you may wish to give a** :star2:
|
**If you think this project is helpful to you, you may wish to give a** :star2:
|
||||||
|
|
||||||
# Features
|
# Install & Upgrade to latest version
|
||||||
|
|
||||||
- System Status Monitoring
|
|
||||||
- Search within all inbounds and clients
|
|
||||||
- Support multi-user multi-protocol, web page visualization operation
|
|
||||||
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
|
|
||||||
- Support for configuring more transport configurations
|
|
||||||
- Traffic statistics, limit traffic, limit expiration time
|
|
||||||
- Customizable xray configuration templates
|
|
||||||
- Support https access panel (self-provided domain name + ssl certificate)
|
|
||||||
- Support one-click SSL certificate application and automatic renewal
|
|
||||||
- For more advanced configuration items, please refer to the panel
|
|
||||||
|
|
||||||
# Screenshot from Inbouds page
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
# Install & Upgrade
|
|
||||||
|
|
||||||
```
|
```
|
||||||
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh)
|
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Install custom version
|
||||||
|
|
||||||
|
To install your desired version you can add the version to the end of install command. Example for ver `0.5.2`:
|
||||||
|
|
||||||
|
```
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.5.2
|
||||||
|
```
|
||||||
|
|
||||||
## Manual install & upgrade
|
## Manual install & upgrade
|
||||||
|
|
||||||
1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases , generally choose Architecture `amd64`
|
1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases , generally choose Architecture `amd64`
|
||||||
@@ -68,8 +61,6 @@ systemctl restart x-ui
|
|||||||
|
|
||||||
## Install using docker
|
## Install using docker
|
||||||
|
|
||||||
> This docker tutorial and docker image are provided by [alireza0](https://github.com/alireza0)
|
|
||||||
|
|
||||||
1. install docker
|
1. install docker
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@@ -80,11 +71,13 @@ curl -fsSL https://get.docker.com | sh
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
mkdir x-ui && cd x-ui
|
mkdir x-ui && cd x-ui
|
||||||
docker run -itd --network=host \
|
docker run -itd \
|
||||||
|
-p 54321:54321 -p 443:443 -p 80:80 \
|
||||||
|
-e XRAY_VMESS_AEAD_FORCED=false \
|
||||||
-v $PWD/db/:/etc/x-ui/ \
|
-v $PWD/db/:/etc/x-ui/ \
|
||||||
-v $PWD/cert/:/root/cert/ \
|
-v $PWD/cert/:/root/cert/ \
|
||||||
--name x-ui --restart=unless-stopped \
|
--name x-ui --restart=unless-stopped \
|
||||||
alireza0/x-ui:latest
|
alireza7/x-ui:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
> Build your own image
|
> Build your own image
|
||||||
@@ -93,11 +86,74 @@ docker run -itd --network=host \
|
|||||||
docker build -t x-ui .
|
docker build -t x-ui .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
- System Status Monitoring
|
||||||
|
- Search within all inbounds and clients
|
||||||
|
- Support Dark/Light theme UI
|
||||||
|
- Support multi-user multi-protocol, web page visualization operation
|
||||||
|
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
|
||||||
|
- Support for configuring more transport configurations
|
||||||
|
- Traffic statistics, limit traffic, limit expiration time
|
||||||
|
- Customizable xray configuration templates
|
||||||
|
- Support subscription ( multi ) link
|
||||||
|
- Detect users which are expiring or exceed traffic limit soon
|
||||||
|
- Support https access panel (self-provided domain name + ssl certificate)
|
||||||
|
- Support one-click SSL certificate application and automatic renewal
|
||||||
|
- For more advanced configuration items, please refer to the panel
|
||||||
|
- Support export/import database from panel
|
||||||
|
|
||||||
|
## suggestion system
|
||||||
|
|
||||||
|
- CentOS 8+
|
||||||
|
- Ubuntu 20+
|
||||||
|
- Debian 10+
|
||||||
|
- Fedora 36+
|
||||||
|
|
||||||
|
## API routes
|
||||||
|
|
||||||
|
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
|
||||||
|
- `/xui/API/inbounds` base for following actions:
|
||||||
|
|
||||||
|
| Method | Path | Action |
|
||||||
|
| :----: | ------------------------------- | ----------------------------------------- |
|
||||||
|
| `GET` | `"/"` | Get all inbounds |
|
||||||
|
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
|
||||||
|
| `POST` | `"/add"` | Add inbound |
|
||||||
|
| `POST` | `"/del/:id"` | Delete Inbound |
|
||||||
|
| `POST` | `"/update/:id"` | Update Inbound |
|
||||||
|
| `POST` | `"/addClient/"` | Add Client to inbound |
|
||||||
|
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId\* |
|
||||||
|
| `POST` | `"/updateClient/:clientId"` | Update Client by clientId\* |
|
||||||
|
| `POST` | `"/getClientTraffics/:email"` | Get Client's Traffic |
|
||||||
|
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
|
||||||
|
| `POST` | `"/resetAllClientTraffics/:id"` | Reset inbound clients traffics (-1: all) |
|
||||||
|
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
|
||||||
|
|
||||||
|
\*- The field `clientId` should be filled by:
|
||||||
|
|
||||||
|
- `client.id` for VMESS and VLESS
|
||||||
|
- `client.password` for TROJAN
|
||||||
|
- `client.email` for Shadowsocks
|
||||||
|
|
||||||
|
# Environment Variables
|
||||||
|
|
||||||
|
| Variable | Type | Default |
|
||||||
|
| -------------- | :--------------------------------------------: | :------------ |
|
||||||
|
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||||
|
| XUI_DEBUG | `boolean` | `false` |
|
||||||
|
| XUI_BIN_FOLDER | `string` | `"bin"` |
|
||||||
|
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
|
||||||
|
|
||||||
|
# Screenshot from Inbouds page
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
## SSL certificate application
|
## SSL certificate application
|
||||||
|
|
||||||
### Cloudflare
|
<details>
|
||||||
|
<summary>Click for details</summary>
|
||||||
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
|
|
||||||
|
|
||||||
### Certbot
|
### Certbot
|
||||||
|
|
||||||
@@ -109,42 +165,52 @@ ln -s /snap/bin/certbot /usr/bin/certbot
|
|||||||
certbot certonly --standalone --register-unsafely-without-email --non-interactive --agree-tos -d <Your Domain Name>
|
certbot certonly --standalone --register-unsafely-without-email --non-interactive --agree-tos -d <Your Domain Name>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tg robot use (under development, temporarily unavailable)
|
</details>
|
||||||
|
|
||||||
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
|
## Tg robot use
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click for details</summary>
|
||||||
|
|
||||||
X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
|
X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
|
||||||
Set the robot-related parameters in the panel background, including:
|
Set the robot-related parameters in the panel background, including:
|
||||||
|
|
||||||
- Tg Robot Token
|
- Tg robot Token
|
||||||
- Tg Robot ChatId
|
- Tg robot ChatId
|
||||||
- Tg robot cycle runtime, in crontab syntax
|
- Tg robot cycle runtime, in crontab syntax
|
||||||
|
- Tg robot Expiration threshold
|
||||||
|
- Tg robot Traffic threshold
|
||||||
|
- Tg robot Enable send backup in cycle runtime
|
||||||
|
- Tg robot Enable CPU usage alarm threshold
|
||||||
|
|
||||||
Reference syntax:
|
Reference syntax:
|
||||||
|
|
||||||
- 30 * * * * * //Notify at the 30s of each point
|
- 30 \* \* \* \* \* //Notify at the 30s of each point
|
||||||
|
- 0 \*/10 \* \* \* \* //Notify at the first second of each 10 minutes
|
||||||
- @hourly // hourly notification
|
- @hourly // hourly notification
|
||||||
- @daily // Daily notification (00:00 in the morning)
|
- @daily // Daily notification (00:00 in the morning)
|
||||||
- @every 8h // notify every 8 hours
|
- @every 8h // notify every 8 hours
|
||||||
- TG notification content:
|
|
||||||
|
|
||||||
- Node traffic usage
|
### Telegram Bot Features
|
||||||
- Panel login reminder
|
|
||||||
- Node expiration reminder
|
|
||||||
- Traffic warning reminder
|
|
||||||
|
|
||||||
More features are planned...
|
- Report periodic
|
||||||
|
- Login notification
|
||||||
|
- CPU threshold notification
|
||||||
|
- Threshold for Expiration time and Traffic to report in advance
|
||||||
|
- Support client report menu if client's telegram username added to the user's configurations
|
||||||
|
- Support telegram traffic report searched with UID (VMESS/VLESS) or Password (TROJAN) - anonymously
|
||||||
|
- Menu based bot
|
||||||
|
- Search client by email ( only admin )
|
||||||
|
- Check all inbounds
|
||||||
|
- Check server status
|
||||||
|
- Check depleted users
|
||||||
|
- Receive backup by request and in periodic reports
|
||||||
|
</details>
|
||||||
|
|
||||||
|
# Common problem
|
||||||
|
|
||||||
## suggestion system
|
<details>
|
||||||
|
<summary>Click for details</summary>
|
||||||
- CentOS 7+
|
|
||||||
- Ubuntu 16+
|
|
||||||
- Debian 8+
|
|
||||||
|
|
||||||
# common problem
|
|
||||||
|
|
||||||
## Migrating from v2-ui
|
## Migrating from v2-ui
|
||||||
|
|
||||||
First install the latest version of x-ui on the server where v2-ui is installed, and then use the following command to migrate, which will migrate the native v2-ui `All inbound account data` to x-ui,`Panel settings and username passwords are not migrated`
|
First install the latest version of x-ui on the server where v2-ui is installed, and then use the following command to migrate, which will migrate the native v2-ui `All inbound account data` to x-ui,`Panel settings and username passwords are not migrated`
|
||||||
@@ -160,11 +226,14 @@ x-ui v2-ui
|
|||||||
**If you upgrade from an old version or other forks, for enable traffic for users you should do :**
|
**If you upgrade from an old version or other forks, for enable traffic for users you should do :**
|
||||||
|
|
||||||
find this in config :
|
find this in config :
|
||||||
``` json
|
|
||||||
|
```json
|
||||||
"policy": {
|
"policy": {
|
||||||
"system": {
|
"system": {
|
||||||
```
|
```
|
||||||
**and add this just after ` "policy": {` :**
|
|
||||||
|
**and add this just after ` "policy": {` :**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"levels": {
|
"levels": {
|
||||||
"0": {
|
"0": {
|
||||||
@@ -174,8 +243,8 @@ find this in config :
|
|||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
**the final output is like :**
|
**the final output is like :**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"policy": {
|
"policy": {
|
||||||
"levels": {
|
"levels": {
|
||||||
@@ -192,11 +261,14 @@ find this in config :
|
|||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
```
|
```
|
||||||
restart panel
|
|
||||||
|
restart panel
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
# a special thanks to
|
# a special thanks to
|
||||||
|
|
||||||
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
|
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
|
||||||
- [FranzKafkaYu](https://github.com/FranzKafkaYu)
|
|
||||||
- [MHSanaei](https://github.com/MHSanaei)
|
- [MHSanaei](https://github.com/MHSanaei)
|
||||||
|
|
||||||
## Stargazers over time
|
## Stargazers over time
|
||||||
|
|||||||
@@ -45,6 +45,22 @@ func IsDebug() bool {
|
|||||||
return os.Getenv("XUI_DEBUG") == "true"
|
return os.Getenv("XUI_DEBUG") == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDBPath() string {
|
func GetBinFolderPath() string {
|
||||||
return fmt.Sprintf("/etc/%s/%s.db", GetName(), GetName())
|
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
|
||||||
|
if binFolderPath == "" {
|
||||||
|
binFolderPath = "bin"
|
||||||
|
}
|
||||||
|
return binFolderPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDBFolderPath() string {
|
||||||
|
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
|
||||||
|
if dbFolderPath == "" {
|
||||||
|
dbFolderPath = "/etc/x-ui"
|
||||||
|
}
|
||||||
|
return dbFolderPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDBPath() string {
|
||||||
|
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.3.2
|
1.3.1
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -98,3 +100,13 @@ func GetDB() *gorm.DB {
|
|||||||
func IsNotFound(err error) bool {
|
func IsNotFound(err error) bool {
|
||||||
return err == gorm.ErrRecordNotFound
|
return err == gorm.ErrRecordNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsSQLiteDB(file io.Reader) (bool, error) {
|
||||||
|
signature := []byte("SQLite format 3\x00")
|
||||||
|
buf := make([]byte, len(signature))
|
||||||
|
_, err := file.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return bytes.Equal(buf, signature), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -74,4 +74,7 @@ type Client struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
||||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||||
|
Enable bool `json:"enable" form:"enable"`
|
||||||
|
TgID string `json:"tgId" form:"tgId"`
|
||||||
|
SubID string `json:"subId" form:"subId"`
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
db/x-ui.db
BIN
db/x-ui.db
Binary file not shown.
@@ -1,10 +1,16 @@
|
|||||||
version: '3.9'
|
---
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
xui:
|
xui:
|
||||||
image: alireza7/x-ui
|
image: alireza7/x-ui
|
||||||
container_name: x-ui
|
container_name: x-ui
|
||||||
|
hostname: yourhostname
|
||||||
volumes:
|
volumes:
|
||||||
- $PWD/db/:/etc/x-ui/
|
- $PWD/db/:/etc/x-ui/
|
||||||
- $PWD/cert/:/root/cert/
|
- $PWD/cert/:/root/cert/
|
||||||
restart: unless-stopped
|
environment:
|
||||||
|
XRAY_VMESS_AEAD_FORCED: "false"
|
||||||
|
tty: true
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
restart: unless-stopped
|
||||||
|
|||||||
49
go.mod
49
go.mod
@@ -7,55 +7,56 @@ require (
|
|||||||
github.com/gin-contrib/sessions v0.0.4
|
github.com/gin-contrib/sessions v0.0.4
|
||||||
github.com/gin-gonic/gin v1.9.0
|
github.com/gin-gonic/gin v1.9.0
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||||
|
github.com/goccy/go-json v0.10.2
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.2.1
|
github.com/nicksnyder/go-i18n/v2 v2.2.1
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
github.com/pelletier/go-toml/v2 v2.0.7
|
github.com/pelletier/go-toml/v2 v2.0.7
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/shirou/gopsutil/v3 v3.23.1
|
github.com/shirou/gopsutil/v3 v3.23.4
|
||||||
github.com/xtls/xray-core v1.7.5
|
github.com/xtls/xray-core v1.8.1
|
||||||
go.uber.org/atomic v1.10.0
|
go.uber.org/atomic v1.11.0
|
||||||
golang.org/x/text v0.7.0
|
golang.org/x/text v0.9.0
|
||||||
google.golang.org/grpc v1.53.0
|
google.golang.org/grpc v1.55.0
|
||||||
gorm.io/driver/sqlite v1.4.4
|
gorm.io/driver/sqlite v1.5.0
|
||||||
gorm.io/gorm v1.24.5
|
gorm.io/gorm v1.25.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||||
github.com/bytedance/sonic v1.8.0 // indirect
|
github.com/bytedance/sonic v1.8.7 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.11.2 // indirect
|
github.com/go-playground/validator/v10 v10.12.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.0 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
|
||||||
github.com/gorilla/context v1.1.1 // indirect
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
github.com/gorilla/sessions v1.2.1 // indirect
|
github.com/gorilla/sessions v1.2.1 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/leodido/go-urn v1.2.3 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
|
||||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pires/go-proxyproto v0.6.2 // indirect
|
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.5 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.9 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/crypto v0.5.0 // indirect
|
golang.org/x/crypto v0.8.0 // indirect
|
||||||
golang.org/x/net v0.7.0 // indirect
|
golang.org/x/net v0.9.0 // indirect
|
||||||
golang.org/x/sys v0.5.0 // indirect
|
golang.org/x/sys v0.7.0 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57 // indirect
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||||
google.golang.org/protobuf v1.28.1 // indirect
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
147
go.sum
147
go.sum
@@ -3,14 +3,14 @@ github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak
|
|||||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
|
github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
|
||||||
github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
||||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
|
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
|
||||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
|
github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
|
||||||
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
@@ -19,6 +19,7 @@ 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
|
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
|
||||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||||
|
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
|
||||||
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 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||||
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
|
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
|
||||||
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
|
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
|
||||||
@@ -39,18 +40,18 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
|||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
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/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
|
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
|
||||||
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
|
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||||
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
@@ -58,7 +59,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
|
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ=
|
||||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
@@ -70,32 +71,31 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
|
|||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
||||||
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
|
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
|
||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||||
|
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -104,47 +104,50 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
|
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
|
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
|
||||||
github.com/onsi/ginkgo/v2 v2.8.0 h1:pAM+oBNPrpXRs+E/8spkeGx9QgekbRVyr74EUvRVOUI=
|
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
|
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
|
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||||
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
|
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||||
github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U=
|
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
|
||||||
github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk=
|
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
|
||||||
github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI=
|
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
|
||||||
github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA=
|
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
|
||||||
github.com/refraction-networking/utls v1.2.2-0.20230207151345-a75a4b484849 h1:vNEcNapWFwnYJTBcVkHJa8VrdL40PNDLDbSGVY+ZV7I=
|
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk=
|
||||||
github.com/sagernet/sing v0.1.6 h1:Qy63OUfKpcqKjfd5rPmUlj0RGjHZSK/PJn0duyCCsRg=
|
github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw=
|
||||||
github.com/sagernet/sing-shadowsocks v0.1.1-0.20230202035033-e3123545f2f7 h1:Plup6oEiyLzY3HDqQ+QsUBzgBGdVmcsgf3t8h940z9U=
|
|
||||||
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
|
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
||||||
github.com/shirou/gopsutil/v3 v3.23.1 h1:a9KKO+kGLKEvcPIs4W62v0nu3sciVDOOOPUD0Hz7z/4=
|
github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
|
||||||
github.com/shirou/gopsutil/v3 v3.23.1/go.mod h1:NN6mnm5/0k8jw4cBfCnJtr5L7ErOTg18tMNpgFkn0hA=
|
github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8=
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ=
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
|
||||||
|
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
|
||||||
|
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
|
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
|
||||||
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||||
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
||||||
@@ -155,38 +158,39 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||||
github.com/xtls/go v0.0.0-20230107031059-4610f88d00f3 h1:a3Y4WVjCxwoyO4E2xdNvq577tW8lkSBgyrA8E9+2NtM=
|
github.com/xtls/reality v0.0.0-20230331223127-176a94313eda h1:psRJD2RrZbnI0OWyHvXfgYCPqlRM5q5SPDcjDoDBWhE=
|
||||||
github.com/xtls/xray-core v1.7.5 h1:Ukr3hXnOG2ciViQL7kfYRl9S3GVej2dkV7DzabmoLL4=
|
github.com/xtls/xray-core v1.8.1 h1:iSTTqXj82ZdwC1ah+eV331X4JTcnrDz+WuKuB/EB3P4=
|
||||||
github.com/xtls/xray-core v1.7.5/go.mod h1:Mx1QzIDvSk4eZ8hKa3AYsSPfyZJNQXWVXTJxJRJ98wI=
|
github.com/xtls/xray-core v1.8.1/go.mod h1:AXxSso0MZwUE4NhRocCfHCg73BtJ+T2dSpQVo1Cg9VM=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.starlark.net v0.0.0-20230128213706-3f75dec8e403 h1:jPeC7Exc+m8OBJUlWbBLh0O5UZPM7yU5W4adnhhbG4U=
|
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
|
||||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
|
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||||
golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg=
|
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -202,11 +206,10 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -214,28 +217,28 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
|
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57 h1:vArvWooPH749rNHpBGgVl+U9B9dATjiEhJzcWGlovNs=
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||||
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
|
||||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
@@ -243,11 +246,11 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
|
gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
|
||||||
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
|
||||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE=
|
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
|
||||||
gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
|
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
|
||||||
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
|
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|||||||
90
install.sh
90
install.sh
@@ -8,26 +8,20 @@ plain='\033[0m'
|
|||||||
cur_dir=$(pwd)
|
cur_dir=$(pwd)
|
||||||
|
|
||||||
# check root
|
# check root
|
||||||
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error:${plain} Please run this script with root privilege \n " && exit 1
|
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
|
||||||
|
|
||||||
# check os
|
# Check OS and set release variable
|
||||||
if [[ -f /etc/redhat-release ]]; then
|
if [[ -f /etc/os-release ]]; then
|
||||||
release="centos"
|
source /etc/os-release
|
||||||
elif cat /etc/issue | grep -Eqi "debian"; then
|
release=$ID
|
||||||
release="debian"
|
elif [[ -f /usr/lib/os-release ]]; then
|
||||||
elif cat /etc/issue | grep -Eqi "ubuntu"; then
|
source /usr/lib/os-release
|
||||||
release="ubuntu"
|
release=$ID
|
||||||
elif cat /etc/issue | grep -Eqi "centos|red hat|redhat"; then
|
|
||||||
release="centos"
|
|
||||||
elif cat /proc/version | grep -Eqi "debian"; then
|
|
||||||
release="debian"
|
|
||||||
elif cat /proc/version | grep -Eqi "ubuntu"; then
|
|
||||||
release="ubuntu"
|
|
||||||
elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then
|
|
||||||
release="centos"
|
|
||||||
else
|
else
|
||||||
echo -e "${red} Check system OS failed, please contact the author! ${plain}\n" && exit 1
|
echo "Failed to check the system OS, please contact the author!" >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
echo "The OS release is: $release"
|
||||||
|
|
||||||
arch=$(arch)
|
arch=$(arch)
|
||||||
|
|
||||||
@@ -39,42 +33,44 @@ elif [[ $arch == "s390x" ]]; then
|
|||||||
arch="s390x"
|
arch="s390x"
|
||||||
else
|
else
|
||||||
arch="amd64"
|
arch="amd64"
|
||||||
echo -e "${red} Fail to check system arch, will use default arch: ${arch}${plain}"
|
echo -e "${red} Failed to check system arch, will use default arch: ${arch}${plain}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "arch: ${arch}"
|
echo "arch: ${arch}"
|
||||||
|
|
||||||
if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then
|
if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then
|
||||||
echo "x-ui dosen't support 32-bit(x86) system, please use 64 bit operating system(x86_64) instead,if there is something wrong, plz let me know"
|
echo "x-ui dosen't support 32-bit(x86) system, please use 64 bit operating system(x86_64) instead, if there is something wrong, please get in touch with me!"
|
||||||
exit -1
|
exit -1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
os_version=""
|
os_version=""
|
||||||
|
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
|
||||||
|
|
||||||
# os version
|
if [[ "${release}" == "centos" ]]; then
|
||||||
if [[ -f /etc/os-release ]]; then
|
|
||||||
os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release)
|
|
||||||
fi
|
|
||||||
if [[ -z "$os_version" && -f /etc/lsb-release ]]; then
|
|
||||||
os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ x"${release}" == x"centos" ]]; then
|
|
||||||
if [[ ${os_version} -le 6 ]]; then
|
|
||||||
echo -e "${red} Please use CentOS 7 or higher version ${plain}\n" && exit 1
|
|
||||||
fi
|
|
||||||
elif [[ x"${release}" == x"ubuntu" ]]; then
|
|
||||||
if [[ ${os_version} -lt 16 ]]; then
|
|
||||||
echo -e "${red} Please use Ubuntu 16 or higher version ${plain}\n" && exit 1
|
|
||||||
fi
|
|
||||||
elif [[ x"${release}" == x"debian" ]]; then
|
|
||||||
if [[ ${os_version} -lt 8 ]]; then
|
if [[ ${os_version} -lt 8 ]]; then
|
||||||
echo -e "${red} Please use Debian 8 or higher version ${plain}\n" && exit 1
|
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
|
||||||
fi
|
fi
|
||||||
|
elif [[ "${release}" == "ubuntu" ]]; then
|
||||||
|
if [[ ${os_version} -lt 20 ]]; then
|
||||||
|
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "${release}" == "fedora" ]]; then
|
||||||
|
if [[ ${os_version} -lt 36 ]]; then
|
||||||
|
echo -e "${red}please use Fedora 36 or higher version! ${plain}\n" && exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "${release}" == "debian" ]]; then
|
||||||
|
if [[ ${os_version} -lt 10 ]]; then
|
||||||
|
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
install_base() {
|
install_base() {
|
||||||
if [[ x"${release}" == x"centos" ]]; then
|
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]] ; then
|
||||||
yum install wget curl tar -y
|
yum install wget curl tar -y
|
||||||
else
|
else
|
||||||
apt install wget curl tar -y
|
apt install wget curl tar -y
|
||||||
@@ -85,7 +81,7 @@ install_base() {
|
|||||||
config_after_install() {
|
config_after_install() {
|
||||||
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
|
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
|
||||||
read -p "Do you want to continue with the modification [y/n]? ": config_confirm
|
read -p "Do you want to continue with the modification [y/n]? ": config_confirm
|
||||||
if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then
|
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
|
||||||
read -p "Please set up your username:" config_account
|
read -p "Please set up your username:" config_account
|
||||||
echo -e "${yellow}Your username will be:${config_account}${plain}"
|
echo -e "${yellow}Your username will be:${config_account}${plain}"
|
||||||
read -p "Please set up your password:" config_password
|
read -p "Please set up your password:" config_password
|
||||||
@@ -98,8 +94,22 @@ config_after_install() {
|
|||||||
/usr/local/x-ui/x-ui setting -port ${config_port}
|
/usr/local/x-ui/x-ui setting -port ${config_port}
|
||||||
echo -e "${yellow}Panel port set successfully!${plain}"
|
echo -e "${yellow}Panel port set successfully!${plain}"
|
||||||
else
|
else
|
||||||
echo -e "${red}Canceled, will use the default settings.${plain}"
|
echo -e "${red}cancel...${plain}"
|
||||||
|
if [[ ! -f "/etc/x-ui/x-ui.db" ]]; then
|
||||||
|
local usernameTemp=$(head -c 6 /dev/urandom | base64)
|
||||||
|
local passwordTemp=$(head -c 6 /dev/urandom | base64)
|
||||||
|
/usr/local/x-ui/x-ui setting -username ${usernameTemp} -password ${passwordTemp}
|
||||||
|
echo -e "this is a fresh installation,will generate random login info for security concerns:"
|
||||||
|
echo -e "###############################################"
|
||||||
|
echo -e "${green}username:${usernameTemp}${plain}"
|
||||||
|
echo -e "${green}password:${passwordTemp}${plain}"
|
||||||
|
echo -e "###############################################"
|
||||||
|
echo -e "${red}if you forgot your login info,you can type x-ui and then type 7 to check after installation${plain}"
|
||||||
|
else
|
||||||
|
echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 7 to check${plain}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
/usr/local/x-ui/x-ui migrate
|
||||||
}
|
}
|
||||||
|
|
||||||
install_x-ui() {
|
install_x-ui() {
|
||||||
|
|||||||
43
main.go
43
main.go
@@ -51,8 +51,8 @@ func runWebServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
//信号量捕获处理
|
// Trap shutdown signals
|
||||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
|
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
|
||||||
for {
|
for {
|
||||||
sig := <-sigCh
|
sig := <-sigCh
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ func showSetting(show bool) {
|
|||||||
settingService := service.SettingService{}
|
settingService := service.SettingService{}
|
||||||
port, err := settingService.GetPort()
|
port, err := settingService.GetPort()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("get current port fialed,error info:", err)
|
fmt.Println("get current port failed,error info:", err)
|
||||||
}
|
}
|
||||||
userService := service.UserService{}
|
userService := service.UserService{}
|
||||||
userModel, err := userService.GetFirstUser()
|
userModel, err := userService.GetFirstUser()
|
||||||
@@ -109,7 +109,7 @@ func showSetting(show bool) {
|
|||||||
if (username == "") || (userpasswd == "") {
|
if (username == "") || (userpasswd == "") {
|
||||||
fmt.Println("current username or password is empty")
|
fmt.Println("current username or password is empty")
|
||||||
}
|
}
|
||||||
fmt.Println("current pannel settings as follows:")
|
fmt.Println("current panel settings as follows:")
|
||||||
fmt.Println("username:", username)
|
fmt.Println("username:", username)
|
||||||
fmt.Println("userpasswd:", userpasswd)
|
fmt.Println("userpasswd:", userpasswd)
|
||||||
fmt.Println("port:", port)
|
fmt.Println("port:", port)
|
||||||
@@ -133,10 +133,9 @@ func updateTgbotEnableSts(status bool) {
|
|||||||
logger.Infof("SetTgbotenabled[%v] success", status)
|
logger.Infof("SetTgbotenabled[%v] success", status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string) {
|
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
||||||
err := database.InitDB(config.GetDBPath())
|
err := database.InitDB(config.GetDBPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@@ -165,7 +164,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tgBotChatid != 0 {
|
if tgBotChatid != "" {
|
||||||
err := settingService.SetTgBotChatId(tgBotChatid)
|
err := settingService.SetTgBotChatId(tgBotChatid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@@ -204,6 +203,18 @@ func updateSetting(port int, username string, password string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateDb() {
|
||||||
|
inboundService := service.InboundService{}
|
||||||
|
|
||||||
|
err := database.InitDB(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println("Start migrating database...")
|
||||||
|
inboundService.MigrateDB()
|
||||||
|
fmt.Println("Migration done!")
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
runWebServer()
|
runWebServer()
|
||||||
@@ -217,14 +228,14 @@ func main() {
|
|||||||
|
|
||||||
v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError)
|
v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError)
|
||||||
var dbPath string
|
var dbPath string
|
||||||
v2uiCmd.StringVar(&dbPath, "db", "/etc/v2-ui/v2-ui.db", "set v2-ui db file path")
|
v2uiCmd.StringVar(&dbPath, "db", fmt.Sprintf("%s/v2-ui.db", config.GetDBFolderPath()), "set v2-ui db file path")
|
||||||
|
|
||||||
settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
|
settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
|
||||||
var port int
|
var port int
|
||||||
var username string
|
var username string
|
||||||
var password string
|
var password string
|
||||||
var tgbottoken string
|
var tgbottoken string
|
||||||
var tgbotchatid int
|
var tgbotchatid string
|
||||||
var enabletgbot bool
|
var enabletgbot bool
|
||||||
var tgbotRuntime string
|
var tgbotRuntime string
|
||||||
var reset bool
|
var reset bool
|
||||||
@@ -234,9 +245,9 @@ func main() {
|
|||||||
settingCmd.IntVar(&port, "port", 0, "set panel port")
|
settingCmd.IntVar(&port, "port", 0, "set panel port")
|
||||||
settingCmd.StringVar(&username, "username", "", "set login username")
|
settingCmd.StringVar(&username, "username", "", "set login username")
|
||||||
settingCmd.StringVar(&password, "password", "", "set login password")
|
settingCmd.StringVar(&password, "password", "", "set login password")
|
||||||
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
|
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegram bot token")
|
||||||
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
|
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegram bot cron time")
|
||||||
settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id")
|
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegram bot chat id")
|
||||||
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
|
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
|
||||||
|
|
||||||
oldUsage := flag.Usage
|
oldUsage := flag.Usage
|
||||||
@@ -246,6 +257,7 @@ func main() {
|
|||||||
fmt.Println("Commands:")
|
fmt.Println("Commands:")
|
||||||
fmt.Println(" run run web panel")
|
fmt.Println(" run run web panel")
|
||||||
fmt.Println(" v2-ui migrate form v2-ui")
|
fmt.Println(" v2-ui migrate form v2-ui")
|
||||||
|
fmt.Println(" migrate migrate form other/old x-ui")
|
||||||
fmt.Println(" setting set settings")
|
fmt.Println(" setting set settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +275,8 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
runWebServer()
|
runWebServer()
|
||||||
|
case "migrate":
|
||||||
|
migrateDb()
|
||||||
case "v2-ui":
|
case "v2-ui":
|
||||||
err := v2uiCmd.Parse(os.Args[2:])
|
err := v2uiCmd.Parse(os.Args[2:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -287,9 +301,12 @@ func main() {
|
|||||||
if show {
|
if show {
|
||||||
showSetting(show)
|
showSetting(show)
|
||||||
}
|
}
|
||||||
if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") {
|
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
|
||||||
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
|
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
|
||||||
}
|
}
|
||||||
|
if enabletgbot {
|
||||||
|
updateTgbotEnableSts(enabletgbot)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
|
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|||||||
BIN
media/inbounds-dark.png
Normal file
BIN
media/inbounds-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 316 KiB |
@@ -6,8 +6,6 @@ import (
|
|||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var CtxDone = errors.New("context done")
|
|
||||||
|
|
||||||
func NewErrorf(format string, a ...interface{}) error {
|
func NewErrorf(format string, a ...interface{}) error {
|
||||||
msg := fmt.Sprintf(format, a...)
|
msg := fmt.Sprintf(format, a...)
|
||||||
return errors.New(msg)
|
return errors.New(msg)
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import "sort"
|
|
||||||
|
|
||||||
func IsSubString(target string, str_array []string) bool {
|
|
||||||
sort.Strings(str_array)
|
|
||||||
index := sort.SearchStrings(str_array, target)
|
|
||||||
return index < len(str_array) && str_array[index] == target
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
func IsDone(ctx context.Context) bool {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
type RawMessage []byte
|
type RawMessage []byte
|
||||||
|
|
||||||
// MarshalJSON 自定义 json.RawMessage 默认行为
|
// MarshalJSON: Customize json.RawMessage default behavior
|
||||||
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||||
if len(m) == 0 {
|
if len(m) == 0 {
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
@@ -14,7 +14,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON sets *m to a copy of data.
|
// UnmarshalJSON: sets *m to a copy of data.
|
||||||
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
|
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
|
||||||
|
|||||||
2
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
2
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,5 +1,23 @@
|
|||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-space {
|
.ant-space {
|
||||||
@@ -10,6 +28,12 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.ant-layout-sider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ant-card {
|
.ant-card {
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
}
|
}
|
||||||
@@ -142,6 +166,11 @@
|
|||||||
transform: translateY(-30px)
|
transform: translateY(-30px)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-list-item-meta-title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-progress-inner {
|
.ant-progress-inner {
|
||||||
background-color: #EBEEF5;
|
background-color: #EBEEF5;
|
||||||
}
|
}
|
||||||
@@ -150,3 +179,268 @@
|
|||||||
color:rgb(255, 255, 255) !important;
|
color:rgb(255, 255, 255) !important;
|
||||||
background-color: rgb(255, 127, 127);
|
background-color: rgb(255, 127, 127);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody>tr>td,
|
||||||
|
.ant-table-thead>tr>th{
|
||||||
|
padding:16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-expand-icon-th,
|
||||||
|
.ant-table-row-expand-icon-cell {
|
||||||
|
width: 30px;
|
||||||
|
min-width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu-dark,
|
||||||
|
.ant-menu-dark .ant-menu-sub,
|
||||||
|
.ant-layout-header,
|
||||||
|
.ant-layout-sider-dark,
|
||||||
|
.ant-layout-sider-zero-width-trigger,
|
||||||
|
.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu,
|
||||||
|
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
|
||||||
|
background:#161b22
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background-color: #1a212a;
|
||||||
|
border-color:rgba(0,0,0,.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark:hover {
|
||||||
|
border-color: #e8e8e8;
|
||||||
|
box-shadow: 0 2px 8px rgba(255,255,255,.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-setting-textarea {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark-box-nohover{
|
||||||
|
padding: 0 20px 20px !important;
|
||||||
|
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
|
||||||
|
}
|
||||||
|
.ant-card-dark-box-nohover:hover{
|
||||||
|
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-table-thead th {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background-color: #161b22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-table-tbody tr td,
|
||||||
|
.ant-card-dark .ant-modal-title {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-collapse-content,
|
||||||
|
.ant-card-dark .ant-calendar,
|
||||||
|
.ant-card-dark .ant-table-placeholder,
|
||||||
|
.ant-card-dark .ant-input-group-addon {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background-color: #262f3d;
|
||||||
|
border: 1px solid rgb(0 150 112 / 0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-list-item-meta-title,
|
||||||
|
.ant-card-dark .ant-list-item-meta-description,
|
||||||
|
.ant-card-dark .ant-form-item-label>label,
|
||||||
|
.ant-card-dark .ant-form-item,
|
||||||
|
.ant-card-dark .ant-divider-inner-text,
|
||||||
|
.ant-card-dark .ant-modal-confirm-content,
|
||||||
|
.ant-card-dark .ant-modal-confirm-title,
|
||||||
|
.ant-card-dark .ant-progress-text,
|
||||||
|
.ant-card-dark .ant-modal-close,
|
||||||
|
.ant-card-dark i,
|
||||||
|
.ant-card-dark .ant-select-dropdown-menu-item,
|
||||||
|
.ant-card-dark .ant-calendar-day-select,
|
||||||
|
.ant-card-dark .ant-calendar-month-select,
|
||||||
|
.ant-card-dark .ant-calendar-year-select,
|
||||||
|
.ant-card-dark .ant-calendar-date,
|
||||||
|
.ant-card-dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,
|
||||||
|
.ant-card-dark .ant-empty-normal,
|
||||||
|
.ant-card-dark .ant-checkbox+span {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
|
||||||
|
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
|
||||||
|
.ant-card-dark .ant-calendar-date:hover,
|
||||||
|
.ant-card-dark .ant-select-dropdown-menu-item-active,
|
||||||
|
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
|
||||||
|
background-color: #11314d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark tbody .ant-table-expanded-row,
|
||||||
|
.ant-card-dark .ant-calendar-time-picker-inner {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background-color: #1a212a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input-number {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-input,
|
||||||
|
.ant-card-dark .ant-input-number,
|
||||||
|
.ant-card-dark .ant-input-number-handler-wrap,
|
||||||
|
.ant-card-dark .ant-calendar-input,
|
||||||
|
.ant-card-dark .ant-select-dropdown-menu-item-selected,
|
||||||
|
.ant-card-dark .ant-select-selection,
|
||||||
|
.ant-card-dark .ant-calendar-picker-clear {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background-color: #193752;
|
||||||
|
border: 1px solid rgba(0, 65, 150, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-select-disabled .ant-select-selection {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background-color: #242c3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-collapse-item {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background-color: #161b22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dropdown-menu-dark,
|
||||||
|
.ant-card-dark .ant-modal-content {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||||
|
box-shadow: 0 2px 8px rgba(255,255,255,.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-modal-content,
|
||||||
|
.ant-card-dark .ant-modal-body,
|
||||||
|
.ant-card-dark .ant-modal-header {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background-color: #222a37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
|
||||||
|
background-color: #1668dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-calendar-time-picker-select li:hover {
|
||||||
|
background: #1668dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-table-header {
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-table-odd-row {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .client-table-header {
|
||||||
|
background-color: #1a212a;
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .client-table-odd-row {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background-color: #242c3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-calendar-last-month-cell .ant-calendar-date,
|
||||||
|
.ant-card-dark .ant-calendar-next-month-btn-day .ant-calendar-date {
|
||||||
|
color: hsla(0,0%,100%,.30);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-dark {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-dark .ant-drawer-wrapper-body,
|
||||||
|
.ant-drawer-dark .drawer-handle {
|
||||||
|
background-color: #1a212a;
|
||||||
|
border: 1px solid hsla(0,0%,100%,.30);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-tag {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background: rgba(255,255,255,.04);
|
||||||
|
border-color: #434343;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-tag-blue {
|
||||||
|
color: #3c9ae8;
|
||||||
|
background: #111d2c;
|
||||||
|
border-color: #15395b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-tag-green {
|
||||||
|
color: #6abe39;
|
||||||
|
background: #162312;
|
||||||
|
border-color: #274916;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-tag-cyan {
|
||||||
|
color: #33bcb7;
|
||||||
|
background: #112123;
|
||||||
|
border-color: #144848;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-tag-red {
|
||||||
|
color: #e84749;
|
||||||
|
background: #2a1215;
|
||||||
|
border-color: #58181c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-tag-orange {
|
||||||
|
color: #e89a3c;
|
||||||
|
background: #2b1d11;
|
||||||
|
border-color: #593815;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-table-row-expand-icon,
|
||||||
|
.ant-card-dark .ant-checkbox-inner {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-switch-checked {
|
||||||
|
background-color: #0c61b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-btn,
|
||||||
|
.ant-card-dark .ant-radio-button-wrapper {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background: none;
|
||||||
|
border: 1px solid hsla(0,0%,100%,.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-radio-button-wrapper:hover {
|
||||||
|
color: #177ddc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-btn-primary {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background-color: #073763;
|
||||||
|
border-color: #1890ff;
|
||||||
|
text-shadow: 0 -1px 0 rgba(255,255,255,.12);
|
||||||
|
box-shadow: 0 2px 0 rgba(255,255,255,.045);
|
||||||
|
}
|
||||||
|
.ant-card-dark .ant-btn-primary:hover {
|
||||||
|
background-color: #40a9ff;
|
||||||
|
border-color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dark .ant-popover-content {
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(255,255,255,.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dark .ant-popover-inner {
|
||||||
|
background: #222a37;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dark .ant-popover-title,
|
||||||
|
.ant-dark .ant-popover-inner-content {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dark .ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow {
|
||||||
|
border-color: transparent #2e3b52 #2e3b52 transparent;
|
||||||
|
}
|
||||||
BIN
web/assets/favicon.ico
Normal file
BIN
web/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
@@ -2,11 +2,15 @@ axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded
|
|||||||
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
|
|
||||||
axios.interceptors.request.use(
|
axios.interceptors.request.use(
|
||||||
config => {
|
(config) => {
|
||||||
config.data = Qs.stringify(config.data, {
|
if (config.data instanceof FormData) {
|
||||||
arrayFormat: 'repeat'
|
config.headers['Content-Type'] = 'multipart/form-data';
|
||||||
});
|
} else {
|
||||||
|
config.data = Qs.stringify(config.data, {
|
||||||
|
arrayFormat: 'repeat',
|
||||||
|
});
|
||||||
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
error => Promise.reject(error)
|
(error) => Promise.reject(error),
|
||||||
);
|
);
|
||||||
@@ -1,36 +1,41 @@
|
|||||||
supportLangs = [
|
const supportLangs = [
|
||||||
{
|
{
|
||||||
name : "English",
|
name: 'English',
|
||||||
value : "en-US",
|
value: 'en-US',
|
||||||
icon : "🇺🇸"
|
icon: '🇺🇸',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : "Farsi",
|
name: 'فارسی',
|
||||||
value : "fa_IR",
|
value: 'fa_IR',
|
||||||
icon : "🇮🇷"
|
icon: '🇮🇷',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : "汉语",
|
name: '汉语',
|
||||||
value : "zh-Hans",
|
value: 'zh-Hans',
|
||||||
icon : "🇨🇳"
|
icon: '🇨🇳',
|
||||||
},
|
},
|
||||||
]
|
{
|
||||||
|
name: 'Русский',
|
||||||
|
value: 'ru_RU',
|
||||||
|
icon: '🇷🇺',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function getLang(){
|
function getLang() {
|
||||||
let lang = getCookie('lang')
|
let lang = getCookie('lang');
|
||||||
|
|
||||||
if (! lang){
|
if (!lang) {
|
||||||
if (window.navigator){
|
if (window.navigator) {
|
||||||
lang = window.navigator.language || window.navigator.userLanguage;
|
lang = window.navigator.language || window.navigator.userLanguage;
|
||||||
|
|
||||||
if (isSupportLang(lang)){
|
if (isSupportLang(lang)) {
|
||||||
setCookie('lang' , lang , 150)
|
setCookie('lang', lang, 150);
|
||||||
}else{
|
} else {
|
||||||
setCookie('lang' , 'en-US' , 150)
|
setCookie('lang', 'en-US', 150);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
setCookie('lang' , 'en-US' , 150)
|
setCookie('lang', 'en-US', 150);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,47 +43,21 @@ function getLang(){
|
|||||||
return lang;
|
return lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLang(lang){
|
function setLang(lang) {
|
||||||
|
if (!isSupportLang(lang)) {
|
||||||
if (!isSupportLang(lang)){
|
|
||||||
lang = 'en-US';
|
lang = 'en-US';
|
||||||
}
|
}
|
||||||
|
|
||||||
setCookie('lang' , lang , 150)
|
setCookie('lang', lang, 150);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSupportLang(lang){
|
function isSupportLang(lang) {
|
||||||
for (l of supportLangs){
|
for (l of supportLangs) {
|
||||||
if (l.value === lang){
|
if (l.value === lang) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function getCookie(cname) {
|
|
||||||
let name = cname + "=";
|
|
||||||
let decodedCookie = decodeURIComponent(document.cookie);
|
|
||||||
let ca = decodedCookie.split(';');
|
|
||||||
for(let i = 0; i <ca.length; i++) {
|
|
||||||
let c = ca[i];
|
|
||||||
while (c.charAt(0) == ' ') {
|
|
||||||
c = c.substring(1);
|
|
||||||
}
|
|
||||||
if (c.indexOf(name) == 0) {
|
|
||||||
return c.substring(name.length, c.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCookie(cname, cvalue, exdays) {
|
|
||||||
const d = new Date();
|
|
||||||
d.setTime(d.getTime() + (exdays*24*60*60*1000));
|
|
||||||
let expires = "expires="+ d.toUTCString();
|
|
||||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
|
||||||
}
|
|
||||||
@@ -170,10 +170,15 @@ class AllSetting {
|
|||||||
this.webCertFile = "";
|
this.webCertFile = "";
|
||||||
this.webKeyFile = "";
|
this.webKeyFile = "";
|
||||||
this.webBasePath = "/";
|
this.webBasePath = "/";
|
||||||
|
this.sessionMaxAge = "";
|
||||||
|
this.expireDiff = "";
|
||||||
|
this.trafficDiff = "";
|
||||||
this.tgBotEnable = false;
|
this.tgBotEnable = false;
|
||||||
this.tgBotToken = "";
|
this.tgBotToken = "";
|
||||||
this.tgBotChatId = 0;
|
this.tgBotChatId = "";
|
||||||
this.tgRunTime = "";
|
this.tgRunTime = "@daily";
|
||||||
|
this.tgBotBackup = false;
|
||||||
|
this.tgCpu = "";
|
||||||
this.xrayTemplateConfig = "";
|
this.xrayTemplateConfig = "";
|
||||||
|
|
||||||
this.timeLocation = "Asia/Tehran";
|
this.timeLocation = "Asia/Tehran";
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -56,14 +56,61 @@ function toFixed(num, n) {
|
|||||||
return Math.round(num * n) / n;
|
return Math.round(num * n) / n;
|
||||||
}
|
}
|
||||||
|
|
||||||
function debounce (fn, delay) {
|
function debounce(fn, delay) {
|
||||||
var timeoutID = null
|
var timeoutID = null;
|
||||||
return function () {
|
return function () {
|
||||||
clearTimeout(timeoutID)
|
clearTimeout(timeoutID);
|
||||||
var args = arguments
|
var args = arguments;
|
||||||
var that = this
|
var that = this;
|
||||||
timeoutID = setTimeout(function () {
|
timeoutID = setTimeout(function () {
|
||||||
fn.apply(that, args)
|
fn.apply(that, args);
|
||||||
}, delay)
|
}, delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(cname) {
|
||||||
|
let name = cname + "=";
|
||||||
|
let decodedCookie = decodeURIComponent(document.cookie);
|
||||||
|
let ca = decodedCookie.split(";");
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) == " ") {
|
||||||
|
c = c.substring(1);
|
||||||
|
}
|
||||||
|
if (c.indexOf(name) == 0) {
|
||||||
|
return c.substring(name.length, c.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCookie(cname, cvalue, exdays) {
|
||||||
|
const d = new Date();
|
||||||
|
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
|
||||||
|
let expires = "expires=" + d.toUTCString();
|
||||||
|
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||||
|
}
|
||||||
|
|
||||||
|
function usageColor(data, threshold, total) {
|
||||||
|
switch (true) {
|
||||||
|
case data === null:
|
||||||
|
return "blue";
|
||||||
|
case total <= 0:
|
||||||
|
return "blue";
|
||||||
|
case data < total - threshold:
|
||||||
|
return "cyan";
|
||||||
|
case data < total:
|
||||||
|
return "orange";
|
||||||
|
default:
|
||||||
|
return "red";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doAllItemsExist(array1, array2) {
|
||||||
|
for (let i = 0; i < array1.length; i++) {
|
||||||
|
if (!array2.includes(array1[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,67 +1,67 @@
|
|||||||
const oneMinute = 1000 * 60; // 一分钟的毫秒数
|
const oneMinute = 1000 * 60; // 一The millise times of minutes
|
||||||
const oneHour = oneMinute * 60; // 一小时的毫秒数
|
const oneHour = oneMinute * 60; // 一Hours of millise times
|
||||||
const oneDay = oneHour * 24; // 一天的毫秒数
|
const oneDay = oneHour * 24; // 一Day's milliseconds
|
||||||
const oneWeek = oneDay * 7; // 一星期的毫秒数
|
const oneWeek = oneDay * 7; // 一Number of millise times on week
|
||||||
const oneMonth = oneDay * 30; // 一个月的毫秒数
|
const oneMonth = oneDay * 30; // 一Number of millise times a month
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按天数减少
|
* Decrease by day
|
||||||
*
|
*
|
||||||
* @param days 要减少的天数
|
* @param days A few days to reduce
|
||||||
*/
|
*/
|
||||||
Date.prototype.minusDays = function (days) {
|
Date.prototype.minusDays = function (days) {
|
||||||
return this.minusMillis(oneDay * days);
|
return this.minusMillis(oneDay * days);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按天数增加
|
* Increase by day
|
||||||
*
|
*
|
||||||
* @param days 要增加的天数
|
* @param days The number of days to be increased
|
||||||
*/
|
*/
|
||||||
Date.prototype.plusDays = function (days) {
|
Date.prototype.plusDays = function (days) {
|
||||||
return this.plusMillis(oneDay * days);
|
return this.plusMillis(oneDay * days);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按小时减少
|
* Reduced
|
||||||
*
|
*
|
||||||
* @param hours 要减少的小时数
|
* @param hours The number of hours to be reduced
|
||||||
*/
|
*/
|
||||||
Date.prototype.minusHours = function (hours) {
|
Date.prototype.minusHours = function (hours) {
|
||||||
return this.minusMillis(oneHour * hours);
|
return this.minusMillis(oneHour * hours);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按小时增加
|
* Increase
|
||||||
*
|
*
|
||||||
* @param hours 要增加的小时数
|
* @param hours Increase the number of hours
|
||||||
*/
|
*/
|
||||||
Date.prototype.plusHours = function (hours) {
|
Date.prototype.plusHours = function (hours) {
|
||||||
return this.plusMillis(oneHour * hours);
|
return this.plusMillis(oneHour * hours);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按分钟减少
|
* Decrease by minute
|
||||||
*
|
*
|
||||||
* @param minutes 要减少的分钟数
|
* @param minutes The number of minutes to be reduced
|
||||||
*/
|
*/
|
||||||
Date.prototype.minusMinutes = function (minutes) {
|
Date.prototype.minusMinutes = function (minutes) {
|
||||||
return this.minusMillis(oneMinute * minutes);
|
return this.minusMillis(oneMinute * minutes);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按分钟增加
|
* Increase
|
||||||
*
|
*
|
||||||
* @param minutes 要增加的分钟数
|
* @param minutes The number of minutes to be increased
|
||||||
*/
|
*/
|
||||||
Date.prototype.plusMinutes = function (minutes) {
|
Date.prototype.plusMinutes = function (minutes) {
|
||||||
return this.plusMillis(oneMinute * minutes);
|
return this.plusMillis(oneMinute * minutes);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按毫秒减少
|
* Decrease by millisecond
|
||||||
*
|
*
|
||||||
* @param millis 要减少的毫秒数
|
* @param millis Number of milliligues to be reduced
|
||||||
*/
|
*/
|
||||||
Date.prototype.minusMillis = function(millis) {
|
Date.prototype.minusMillis = function(millis) {
|
||||||
let time = this.getTime() - millis;
|
let time = this.getTime() - millis;
|
||||||
@@ -71,9 +71,9 @@ Date.prototype.minusMillis = function(millis) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按毫秒增加
|
* Add in milliseconds
|
||||||
*
|
*
|
||||||
* @param millis 要增加的毫秒数
|
* @param millis To increase the millimeter number
|
||||||
*/
|
*/
|
||||||
Date.prototype.plusMillis = function(millis) {
|
Date.prototype.plusMillis = function(millis) {
|
||||||
let time = this.getTime() + millis;
|
let time = this.getTime() + millis;
|
||||||
@@ -83,7 +83,7 @@ Date.prototype.plusMillis = function(millis) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置时间为当天的 00:00:00.000
|
* Setting time is the day 00:00:00.000
|
||||||
*/
|
*/
|
||||||
Date.prototype.setMinTime = function () {
|
Date.prototype.setMinTime = function () {
|
||||||
this.setHours(0);
|
this.setHours(0);
|
||||||
@@ -94,7 +94,7 @@ Date.prototype.setMinTime = function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置时间为当天的 23:59:59.999
|
* Setting time is the day 23:59:59.999
|
||||||
*/
|
*/
|
||||||
Date.prototype.setMaxTime = function () {
|
Date.prototype.setMaxTime = function () {
|
||||||
this.setHours(23);
|
this.setHours(23);
|
||||||
@@ -105,14 +105,14 @@ Date.prototype.setMaxTime = function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化日期
|
* Formatting date
|
||||||
*/
|
*/
|
||||||
Date.prototype.formatDate = function () {
|
Date.prototype.formatDate = function () {
|
||||||
return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate());
|
return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate());
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化时间
|
* Formatting time
|
||||||
*/
|
*/
|
||||||
Date.prototype.formatTime = function () {
|
Date.prototype.formatTime = function () {
|
||||||
return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds());
|
return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds());
|
||||||
@@ -121,21 +121,20 @@ Date.prototype.formatTime = function () {
|
|||||||
/**
|
/**
|
||||||
* 格式化日期加时间
|
* 格式化日期加时间
|
||||||
*
|
*
|
||||||
* @param split 日期和时间之间的分隔符,默认是一个空格
|
* @param split Division between date and time, the default is a space
|
||||||
*/
|
*/
|
||||||
Date.prototype.formatDateTime = function (split = ' ') {
|
Date.prototype.formatDateTime = function (split = ' ') {
|
||||||
return this.formatDate() + split + this.formatTime();
|
return this.formatDate() + split + this.formatTime();
|
||||||
};
|
};
|
||||||
|
|
||||||
class DateUtil {
|
class DateUtil {
|
||||||
|
// String string to date object
|
||||||
// 字符串转 Date 对象
|
|
||||||
static parseDate(str) {
|
static parseDate(str) {
|
||||||
return new Date(str.replace(/-/g, '/'));
|
return new Date(str.replace(/-/g, '/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
static formatMillis(millis) {
|
static formatMillis(millis) {
|
||||||
return moment(millis).format('YYYY-M-D H:m:s')
|
return moment(millis).format('YYYY-M-D H:m:s');
|
||||||
}
|
}
|
||||||
|
|
||||||
static firstDayOfMonth() {
|
static firstDayOfMonth() {
|
||||||
|
|||||||
@@ -68,13 +68,11 @@ class HttpUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PromiseUtil {
|
class PromiseUtil {
|
||||||
|
|
||||||
static async sleep(timeout) {
|
static async sleep(timeout) {
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
setTimeout(resolve, timeout)
|
setTimeout(resolve, timeout)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const seq = [
|
const seq = [
|
||||||
@@ -90,7 +88,6 @@ const seq = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
class RandomUtil {
|
class RandomUtil {
|
||||||
|
|
||||||
static randomIntRange(min, max) {
|
static randomIntRange(min, max) {
|
||||||
return parseInt(Math.random() * (max - min) + min, 10);
|
return parseInt(Math.random() * (max - min) + min, 10);
|
||||||
}
|
}
|
||||||
@@ -137,19 +134,24 @@ class RandomUtil {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static randomText() {
|
static randomText(minLen = 6, varLen = 5) {
|
||||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||||
var string = '';
|
var string = '';
|
||||||
var len = 6 + Math.floor(Math.random() * 5)
|
var len = minLen + Math.floor(Math.random() * varLen);
|
||||||
for(var ii=0; ii<len; ii++){
|
for (var ii = 0; ii < len; ii++) {
|
||||||
string += chars[Math.floor(Math.random() * chars.length)];
|
string += chars[Math.floor(Math.random() * chars.length)];
|
||||||
}
|
}
|
||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static randomShadowsocksPassword() {
|
||||||
|
let array = new Uint8Array(32);
|
||||||
|
window.crypto.getRandomValues(array);
|
||||||
|
return btoa(String.fromCharCode.apply(null, array));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ObjectUtil {
|
class ObjectUtil {
|
||||||
|
|
||||||
static getPropIgnoreCase(obj, prop) {
|
static getPropIgnoreCase(obj, prop) {
|
||||||
for (const name in obj) {
|
for (const name in obj) {
|
||||||
if (!obj.hasOwnProperty(name)) {
|
if (!obj.hasOwnProperty(name)) {
|
||||||
@@ -297,5 +299,4 @@ class ObjectUtil {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import "github.com/gin-gonic/gin"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
type APIController struct {
|
type APIController struct {
|
||||||
BaseController
|
BaseController
|
||||||
|
|
||||||
inboundController *InboundController
|
inboundController *InboundController
|
||||||
settingController *SettingController
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPIController(g *gin.RouterGroup) *APIController {
|
func NewAPIController(g *gin.RouterGroup) *APIController {
|
||||||
@@ -23,9 +19,17 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
|||||||
|
|
||||||
g.GET("/", a.inbounds)
|
g.GET("/", a.inbounds)
|
||||||
g.GET("/get/:id", a.inbound)
|
g.GET("/get/:id", a.inbound)
|
||||||
|
g.GET("/getClientTraffics/:email", a.getClientTraffics)
|
||||||
g.POST("/add", a.addInbound)
|
g.POST("/add", a.addInbound)
|
||||||
g.POST("/del/:id", a.delInbound)
|
g.POST("/del/:id", a.delInbound)
|
||||||
g.POST("/update/:id", a.updateInbound)
|
g.POST("/update/:id", a.updateInbound)
|
||||||
|
g.POST("/addClient", a.addInboundClient)
|
||||||
|
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
||||||
|
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
||||||
|
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||||
|
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||||
|
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||||
|
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||||
|
|
||||||
a.inboundController = NewInboundController(g)
|
a.inboundController = NewInboundController(g)
|
||||||
}
|
}
|
||||||
@@ -36,6 +40,9 @@ func (a *APIController) inbounds(c *gin.Context) {
|
|||||||
func (a *APIController) inbound(c *gin.Context) {
|
func (a *APIController) inbound(c *gin.Context) {
|
||||||
a.inboundController.getInbound(c)
|
a.inboundController.getInbound(c)
|
||||||
}
|
}
|
||||||
|
func (a *APIController) getClientTraffics(c *gin.Context) {
|
||||||
|
a.inboundController.getClientTraffics(c)
|
||||||
|
}
|
||||||
func (a *APIController) addInbound(c *gin.Context) {
|
func (a *APIController) addInbound(c *gin.Context) {
|
||||||
a.inboundController.addInbound(c)
|
a.inboundController.addInbound(c)
|
||||||
}
|
}
|
||||||
@@ -45,3 +52,24 @@ func (a *APIController) delInbound(c *gin.Context) {
|
|||||||
func (a *APIController) updateInbound(c *gin.Context) {
|
func (a *APIController) updateInbound(c *gin.Context) {
|
||||||
a.inboundController.updateInbound(c)
|
a.inboundController.updateInbound(c)
|
||||||
}
|
}
|
||||||
|
func (a *APIController) addInboundClient(c *gin.Context) {
|
||||||
|
a.inboundController.addInboundClient(c)
|
||||||
|
}
|
||||||
|
func (a *APIController) delInboundClient(c *gin.Context) {
|
||||||
|
a.inboundController.delInboundClient(c)
|
||||||
|
}
|
||||||
|
func (a *APIController) updateInboundClient(c *gin.Context) {
|
||||||
|
a.inboundController.updateInboundClient(c)
|
||||||
|
}
|
||||||
|
func (a *APIController) resetClientTraffic(c *gin.Context) {
|
||||||
|
a.inboundController.resetClientTraffic(c)
|
||||||
|
}
|
||||||
|
func (a *APIController) resetAllTraffics(c *gin.Context) {
|
||||||
|
a.inboundController.resetAllTraffics(c)
|
||||||
|
}
|
||||||
|
func (a *APIController) resetAllClientTraffics(c *gin.Context) {
|
||||||
|
a.inboundController.resetAllClientTraffics(c)
|
||||||
|
}
|
||||||
|
func (a *APIController) delDepletedClients(c *gin.Context) {
|
||||||
|
a.inboundController.delDepletedClients(c)
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,10 +31,13 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
|||||||
g.POST("/add", a.addInbound)
|
g.POST("/add", a.addInbound)
|
||||||
g.POST("/del/:id", a.delInbound)
|
g.POST("/del/:id", a.delInbound)
|
||||||
g.POST("/update/:id", a.updateInbound)
|
g.POST("/update/:id", a.updateInbound)
|
||||||
g.POST("/addClient/", a.addInboundClient)
|
g.POST("/addClient", a.addInboundClient)
|
||||||
g.POST("/delClient/:email", a.delInboundClient)
|
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
||||||
g.POST("/updateClient/:index", a.updateInboundClient)
|
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
||||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||||
|
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||||
|
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||||
|
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,12 +76,21 @@ func (a *InboundController) getInbound(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
jsonObj(c, inbound, nil)
|
jsonObj(c, inbound, nil)
|
||||||
}
|
}
|
||||||
|
func (a *InboundController) getClientTraffics(c *gin.Context) {
|
||||||
|
email := c.Param("email")
|
||||||
|
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "Error getting traffics", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, clientTraffics, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *InboundController) addInbound(c *gin.Context) {
|
func (a *InboundController) addInbound(c *gin.Context) {
|
||||||
inbound := &model.Inbound{}
|
inbound := &model.Inbound{}
|
||||||
err := c.ShouldBind(inbound)
|
err := c.ShouldBind(inbound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.addTo"), err)
|
jsonMsg(c, I18n(c, "pages.inbounds.create"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := session.GetLoginUser(c)
|
user := session.GetLoginUser(c)
|
||||||
@@ -86,7 +98,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
|||||||
inbound.Enable = true
|
inbound.Enable = true
|
||||||
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||||
inbound, err = a.inboundService.AddInbound(inbound)
|
inbound, err = a.inboundService.AddInbound(inbound)
|
||||||
jsonMsgObj(c, I18n(c, "pages.inbounds.addTo"), inbound, err)
|
jsonMsgObj(c, I18n(c, "pages.inbounds.create"), inbound, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
a.xrayService.SetToNeedRestart()
|
a.xrayService.SetToNeedRestart()
|
||||||
}
|
}
|
||||||
@@ -108,7 +120,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
|
|||||||
func (a *InboundController) updateInbound(c *gin.Context) {
|
func (a *InboundController) updateInbound(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
inbound := &model.Inbound{
|
inbound := &model.Inbound{
|
||||||
@@ -116,47 +128,46 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
err = c.ShouldBind(inbound)
|
err = c.ShouldBind(inbound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
inbound, err = a.inboundService.UpdateInbound(inbound)
|
inbound, err = a.inboundService.UpdateInbound(inbound)
|
||||||
jsonMsgObj(c, I18n(c, "pages.inbounds.revise"), inbound, err)
|
jsonMsgObj(c, I18n(c, "pages.inbounds.update"), inbound, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
a.xrayService.SetToNeedRestart()
|
a.xrayService.SetToNeedRestart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *InboundController) addInboundClient(c *gin.Context) {
|
func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||||
inbound := &model.Inbound{}
|
data := &model.Inbound{}
|
||||||
err := c.ShouldBind(inbound)
|
err := c.ShouldBind(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.inboundService.AddInboundClient(inbound)
|
err = a.inboundService.AddInboundClient(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, "something worng!", err)
|
jsonMsg(c, "Something went wrong!", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jsonMsg(c, "Client added", nil)
|
jsonMsg(c, "Client(s) added", nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
a.xrayService.SetToNeedRestart()
|
a.xrayService.SetToNeedRestart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||||
email := c.Param("email")
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
inbound := &model.Inbound{}
|
|
||||||
err := c.ShouldBind(inbound)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
clientId := c.Param("clientId")
|
||||||
|
|
||||||
err = a.inboundService.DelInboundClient(inbound, email)
|
err = a.inboundService.DelInboundClient(id, clientId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, "something worng!", err)
|
jsonMsg(c, "Something went wrong!", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jsonMsg(c, "Client deleted", nil)
|
jsonMsg(c, "Client deleted", nil)
|
||||||
@@ -166,22 +177,18 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||||
index, err := strconv.Atoi(c.Param("index"))
|
clientId := c.Param("clientId")
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
inbound := &model.Inbound{}
|
inbound := &model.Inbound{}
|
||||||
err = c.ShouldBind(inbound)
|
err := c.ShouldBind(inbound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.inboundService.UpdateInboundClient(inbound, index)
|
err = a.inboundService.UpdateInboundClient(inbound, clientId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, "something worng!", err)
|
jsonMsg(c, "Something went wrong!", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jsonMsg(c, "Client updated", nil)
|
jsonMsg(c, "Client updated", nil)
|
||||||
@@ -193,14 +200,14 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
|||||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
email := c.Param("email")
|
email := c.Param("email")
|
||||||
|
|
||||||
err = a.inboundService.ResetClientTraffic(id, email)
|
err = a.inboundService.ResetClientTraffic(id, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, "something worng!", err)
|
jsonMsg(c, "Something went wrong!", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jsonMsg(c, "traffic reseted", nil)
|
jsonMsg(c, "traffic reseted", nil)
|
||||||
@@ -208,3 +215,41 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
|||||||
a.xrayService.SetToNeedRestart()
|
a.xrayService.SetToNeedRestart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||||
|
err := a.inboundService.ResetAllTraffics()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "Something went wrong!", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, "All traffics reseted", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.inboundService.ResetAllClientTraffics(id)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "Something went wrong!", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, "All traffics of client reseted", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *InboundController) delDepletedClients(c *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = a.inboundService.DelDepletedClients(id)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "Something went wrong!", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, "All delpeted clients are deleted", nil)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
"x-ui/web/job"
|
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
"x-ui/web/session"
|
"x-ui/web/session"
|
||||||
|
|
||||||
@@ -19,7 +18,9 @@ type LoginForm struct {
|
|||||||
type IndexController struct {
|
type IndexController struct {
|
||||||
BaseController
|
BaseController
|
||||||
|
|
||||||
userService service.UserService
|
settingService service.SettingService
|
||||||
|
userService service.UserService
|
||||||
|
tgbot service.Tgbot
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
||||||
@@ -60,13 +61,25 @@ func (a *IndexController) login(c *gin.Context) {
|
|||||||
user := a.userService.CheckUser(form.Username, form.Password)
|
user := a.userService.CheckUser(form.Username, form.Password)
|
||||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||||
if user == nil {
|
if user == nil {
|
||||||
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
||||||
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
||||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
|
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
|
||||||
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
|
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionMaxAge, err := a.settingService.GetSessionMaxAge()
|
||||||
|
if err != nil {
|
||||||
|
logger.Infof("Unable to get session's max age from DB")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionMaxAge > 0 {
|
||||||
|
err = session.SetMaxAge(c, sessionMaxAge*60)
|
||||||
|
if err != nil {
|
||||||
|
logger.Infof("Unable to set session's max age")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = session.SetLoginUser(c, user)
|
err = session.SetLoginUser(c, user)
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
"x-ui/web/global"
|
"x-ui/web/global"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
|
||||||
|
|
||||||
type ServerController struct {
|
type ServerController struct {
|
||||||
BaseController
|
BaseController
|
||||||
|
|
||||||
@@ -34,7 +40,14 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
|||||||
g.Use(a.checkLogin)
|
g.Use(a.checkLogin)
|
||||||
g.POST("/status", a.status)
|
g.POST("/status", a.status)
|
||||||
g.POST("/getXrayVersion", a.getXrayVersion)
|
g.POST("/getXrayVersion", a.getXrayVersion)
|
||||||
|
g.POST("/stopXrayService", a.stopXrayService)
|
||||||
|
g.POST("/restartXrayService", a.restartXrayService)
|
||||||
g.POST("/installXray/:version", a.installXray)
|
g.POST("/installXray/:version", a.installXray)
|
||||||
|
g.POST("/logs/:count", a.getLogs)
|
||||||
|
g.POST("/getConfigJson", a.getConfigJson)
|
||||||
|
g.GET("/getDb", a.getDb)
|
||||||
|
g.POST("/importDB", a.importDB)
|
||||||
|
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerController) refreshStatus() {
|
func (a *ServerController) refreshStatus() {
|
||||||
@@ -83,3 +96,94 @@ func (a *ServerController) installXray(c *gin.Context) {
|
|||||||
err := a.serverService.UpdateXray(version)
|
err := a.serverService.UpdateXray(version)
|
||||||
jsonMsg(c, I18n(c, "install")+" xray", err)
|
jsonMsg(c, I18n(c, "install")+" xray", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ServerController) stopXrayService(c *gin.Context) {
|
||||||
|
a.lastGetStatusTime = time.Now()
|
||||||
|
err := a.serverService.StopXrayService()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, "Xray stoped", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ServerController) restartXrayService(c *gin.Context) {
|
||||||
|
err := a.serverService.RestartXrayService()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, "Xray restarted", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ServerController) getLogs(c *gin.Context) {
|
||||||
|
count := c.Param("count")
|
||||||
|
logs, err := a.serverService.GetLogs(count)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "getLogs", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, logs, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ServerController) getConfigJson(c *gin.Context) {
|
||||||
|
configJson, err := a.serverService.GetConfigJson()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "get config.json", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, configJson, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ServerController) getDb(c *gin.Context) {
|
||||||
|
db, err := a.serverService.GetDb()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "get Database", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := "x-ui.db"
|
||||||
|
|
||||||
|
if !filenameRegex.MatchString(filename) {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the headers for the response
|
||||||
|
c.Header("Content-Type", "application/octet-stream")
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||||
|
|
||||||
|
// Write the file contents to the response
|
||||||
|
c.Writer.Write(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ServerController) importDB(c *gin.Context) {
|
||||||
|
// Get the file from the request body
|
||||||
|
file, _, err := c.Request.FormFile("db")
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "Error reading db file", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
// Always restart Xray before return
|
||||||
|
defer a.serverService.RestartXrayService()
|
||||||
|
defer func() {
|
||||||
|
a.lastGetStatusTime = time.Now()
|
||||||
|
}()
|
||||||
|
// Import it
|
||||||
|
err = a.serverService.ImportDB(file)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, "Import DB", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
|
||||||
|
cert, err := a.serverService.GetNewX25519Cert()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "get x25519 certificate", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, cert, nil)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"time"
|
"time"
|
||||||
"x-ui/web/entity"
|
"x-ui/web/entity"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
"x-ui/web/session"
|
"x-ui/web/session"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type updateUserForm struct {
|
type updateUserForm struct {
|
||||||
@@ -32,45 +33,77 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
|||||||
g = g.Group("/setting")
|
g = g.Group("/setting")
|
||||||
|
|
||||||
g.POST("/all", a.getAllSetting)
|
g.POST("/all", a.getAllSetting)
|
||||||
|
g.POST("/defaultSettings", a.getDefaultSettings)
|
||||||
g.POST("/update", a.updateSetting)
|
g.POST("/update", a.updateSetting)
|
||||||
g.POST("/updateUser", a.updateUser)
|
g.POST("/updateUser", a.updateUser)
|
||||||
g.POST("/restartPanel", a.restartPanel)
|
g.POST("/restartPanel", a.restartPanel)
|
||||||
|
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SettingController) getAllSetting(c *gin.Context) {
|
func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||||
allSetting, err := a.settingService.GetAllSetting()
|
allSetting, err := a.settingService.GetAllSetting()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
|
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jsonObj(c, allSetting, nil)
|
jsonObj(c, allSetting, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||||
|
expireDiff, err := a.settingService.GetExpireDiff()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trafficDiff, err := a.settingService.GetTrafficDiff()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defaultCert, err := a.settingService.GetCertFile()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defaultKey, err := a.settingService.GetKeyFile()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"expireDiff": expireDiff,
|
||||||
|
"trafficDiff": trafficDiff,
|
||||||
|
"defaultCert": defaultCert,
|
||||||
|
"defaultKey": defaultKey,
|
||||||
|
}
|
||||||
|
jsonObj(c, result, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *SettingController) updateSetting(c *gin.Context) {
|
func (a *SettingController) updateSetting(c *gin.Context) {
|
||||||
allSetting := &entity.AllSetting{}
|
allSetting := &entity.AllSetting{}
|
||||||
err := c.ShouldBind(allSetting)
|
err := c.ShouldBind(allSetting)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
|
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = a.settingService.UpdateAllSetting(allSetting)
|
err = a.settingService.UpdateAllSetting(allSetting)
|
||||||
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
|
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SettingController) updateUser(c *gin.Context) {
|
func (a *SettingController) updateUser(c *gin.Context) {
|
||||||
form := &updateUserForm{}
|
form := &updateUserForm{}
|
||||||
err := c.ShouldBind(form)
|
err := c.ShouldBind(form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
|
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := session.GetLoginUser(c)
|
user := session.GetLoginUser(c)
|
||||||
if user.Username != form.OldUsername || user.Password != form.OldPassword {
|
if user.Username != form.OldUsername || user.Password != form.OldPassword {
|
||||||
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.originalUserPassIncorrect")))
|
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.originalUserPassIncorrect")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if form.NewUsername == "" || form.NewPassword == "" {
|
if form.NewUsername == "" || form.NewPassword == "" {
|
||||||
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.userPassMustBeNotEmpty")))
|
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
|
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
|
||||||
@@ -79,10 +112,19 @@ func (a *SettingController) updateUser(c *gin.Context) {
|
|||||||
user.Password = form.NewPassword
|
user.Password = form.NewPassword
|
||||||
session.SetLoginUser(c, user)
|
session.SetLoginUser(c, user)
|
||||||
}
|
}
|
||||||
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), err)
|
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SettingController) restartPanel(c *gin.Context) {
|
func (a *SettingController) restartPanel(c *gin.Context) {
|
||||||
err := a.panelService.RestartPanel(time.Second * 3)
|
err := a.panelService.RestartPanel(time.Second * 3)
|
||||||
jsonMsg(c, I18n(c, "pages.setting.restartPanel"), err)
|
jsonMsg(c, I18n(c, "pages.settings.restartPanel"), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||||
|
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, defaultJsonConfig, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
48
web/controller/sub.go
Normal file
48
web/controller/sub.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"x-ui/web/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SUBController struct {
|
||||||
|
BaseController
|
||||||
|
|
||||||
|
subService service.SubService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSUBController(g *gin.RouterGroup) *SUBController {
|
||||||
|
a := &SUBController{}
|
||||||
|
a.initRouter(g)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||||
|
g = g.Group("/sub")
|
||||||
|
|
||||||
|
g.GET("/:subid", a.subs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *SUBController) subs(c *gin.Context) {
|
||||||
|
subId := c.Param("subid")
|
||||||
|
host := strings.Split(c.Request.Host, ":")[0]
|
||||||
|
subs, headers, err := a.subService.GetSubs(subId, host)
|
||||||
|
if err != nil || len(subs) == 0 {
|
||||||
|
c.String(400, "Error!")
|
||||||
|
} else {
|
||||||
|
result := ""
|
||||||
|
for _, sub := range subs {
|
||||||
|
result += sub + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
|
||||||
|
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
|
||||||
|
c.Writer.Header().Set("Profile-Title", headers[2])
|
||||||
|
|
||||||
|
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,16 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"x-ui/config"
|
"x-ui/config"
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
"x-ui/web/entity"
|
"x-ui/web/entity"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getUriId(c *gin.Context) int64 {
|
|
||||||
s := struct {
|
|
||||||
Id int64 `uri:"id"`
|
|
||||||
}{}
|
|
||||||
|
|
||||||
_ = c.BindUri(&s)
|
|
||||||
return s.Id
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRemoteIp(c *gin.Context) string {
|
func getRemoteIp(c *gin.Context) string {
|
||||||
value := c.GetHeader("X-Forwarded-For")
|
value := c.GetHeader("X-Forwarded-For")
|
||||||
if value != "" {
|
if value != "" {
|
||||||
@@ -75,6 +67,7 @@ func html(c *gin.Context, name string, title string, data gin.H) {
|
|||||||
data = gin.H{}
|
data = gin.H{}
|
||||||
}
|
}
|
||||||
data["title"] = title
|
data["title"] = title
|
||||||
|
data["host"] = strings.Split(c.Request.Host, ":")[0]
|
||||||
data["request_uri"] = c.Request.RequestURI
|
data["request_uri"] = c.Request.RequestURI
|
||||||
data["base_path"] = c.GetString("base_path")
|
data["base_path"] = c.GetString("base_path")
|
||||||
c.HTML(http.StatusOK, name, getContext(data))
|
c.HTML(http.StatusOK, name, getContext(data))
|
||||||
@@ -84,10 +77,8 @@ func getContext(h gin.H) gin.H {
|
|||||||
a := gin.H{
|
a := gin.H{
|
||||||
"cur_ver": config.GetVersion(),
|
"cur_ver": config.GetVersion(),
|
||||||
}
|
}
|
||||||
if h != nil {
|
for key, value := range h {
|
||||||
for key, value := range h {
|
a[key] = value
|
||||||
a[key] = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
|||||||
|
|
||||||
g.GET("/", a.index)
|
g.GET("/", a.index)
|
||||||
g.GET("/inbounds", a.inbounds)
|
g.GET("/inbounds", a.inbounds)
|
||||||
g.GET("/setting", a.setting)
|
g.GET("/settings", a.settings)
|
||||||
|
|
||||||
a.inboundController = NewInboundController(g)
|
a.inboundController = NewInboundController(g)
|
||||||
a.settingController = NewSettingController(g)
|
a.settingController = NewSettingController(g)
|
||||||
@@ -37,6 +37,6 @@ func (a *XUIController) inbounds(c *gin.Context) {
|
|||||||
html(c, "inbounds.html", "pages.inbounds.title", nil)
|
html(c, "inbounds.html", "pages.inbounds.title", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *XUIController) setting(c *gin.Context) {
|
func (a *XUIController) settings(c *gin.Context) {
|
||||||
html(c, "setting.html", "pages.setting.title", nil)
|
html(c, "settings.html", "pages.settings.title", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,17 @@ type AllSetting struct {
|
|||||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
||||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
||||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
||||||
|
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
|
||||||
|
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
|
||||||
|
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
|
||||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
||||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
||||||
TgBotChatId int `json:"tgBotChatId" form:"tgBotChatId"`
|
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
||||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
||||||
|
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
||||||
|
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
||||||
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
||||||
|
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AllSetting) CheckValid() error {
|
func (s *AllSetting) CheckValid() error {
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
|
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
|
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
|
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
|
||||||
|
<link rel=”icon” type=”image/x-icon” href="{{ .base_path }}assets/favicon.ico">
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="{{ .base_path }}assets/favicon.ico">
|
||||||
<style>
|
<style>
|
||||||
[v-cloak] {
|
[v-cloak] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<title>{{ i18n .title}}</title>
|
<title>{{ .host }}-{{ i18n .title}}</title>
|
||||||
</head>
|
</head>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{{define "promptModal"}}
|
{{define "promptModal"}}
|
||||||
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
|
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
|
||||||
:closable="true" @ok="promptModal.ok" :mask-closable="false"
|
:closable="true" @ok="promptModal.ok" :mask-closable="false"
|
||||||
|
:class="themeSwitcher.darkCardClass"
|
||||||
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
|
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
|
||||||
<a-input id="prompt-modal-input" :type="promptModal.type"
|
<a-input id="prompt-modal-input" :type="promptModal.type"
|
||||||
v-model="promptModal.value"
|
v-model="promptModal.value"
|
||||||
@@ -35,11 +36,11 @@
|
|||||||
},
|
},
|
||||||
confirm() {},
|
confirm() {},
|
||||||
open({
|
open({
|
||||||
title='',
|
title = '',
|
||||||
type='text',
|
type = 'text',
|
||||||
value='',
|
value = '',
|
||||||
okText='{{ i18n "sure"}}',
|
okText = '{{ i18n "sure"}}',
|
||||||
confirm=() => {},
|
confirm = () => {},
|
||||||
}) {
|
}) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
{{define "qrcodeModal"}}
|
{{define "qrcodeModal"}}
|
||||||
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
|
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
|
||||||
:closable="true" width="300px" :ok-text="qrModal.okText"
|
:closable="true"
|
||||||
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
|
:class="themeSwitcher.darkCardClass"
|
||||||
<canvas id="qrCode" style="width: 100%; height: 100%;"></canvas>
|
:footer="null"
|
||||||
|
width="300px">
|
||||||
|
<a-tag v-if="qrModal.clientName" color="orange" style="margin-bottom: 10px;display: block;text-align: center;">
|
||||||
|
{{ i18n "pages.inbounds.email" }}: "[[ qrModal.clientName ]]"
|
||||||
|
</a-tag>
|
||||||
|
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas>
|
||||||
|
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -12,17 +18,17 @@
|
|||||||
content: '',
|
content: '',
|
||||||
inbound: new Inbound(),
|
inbound: new Inbound(),
|
||||||
dbInbound: new DBInbound(),
|
dbInbound: new DBInbound(),
|
||||||
okText: '',
|
|
||||||
copyText: '',
|
copyText: '',
|
||||||
|
clientName: null,
|
||||||
qrcode: null,
|
qrcode: null,
|
||||||
clipboard: null,
|
clipboard: null,
|
||||||
visible: false,
|
visible: false,
|
||||||
show: function (title='', content='', dbInbound=new DBInbound(),okText='{{ i18n "copy" }}', copyText='') {
|
show: function (title = '', content = '', dbInbound = new DBInbound(), copyText = '', clientName = null) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.dbInbound = dbInbound;
|
this.dbInbound = dbInbound;
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
this.okText = okText;
|
this.clientName = clientName;
|
||||||
if (ObjectUtil.isEmpty(copyText)) {
|
if (ObjectUtil.isEmpty(copyText)) {
|
||||||
this.copyText = content;
|
this.copyText = content;
|
||||||
} else {
|
} else {
|
||||||
@@ -30,12 +36,6 @@
|
|||||||
}
|
}
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
qrModalApp.$nextTick(() => {
|
qrModalApp.$nextTick(() => {
|
||||||
if (this.clipboard === null) {
|
|
||||||
this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
|
|
||||||
text: () => this.copyText,
|
|
||||||
});
|
|
||||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
|
||||||
}
|
|
||||||
if (this.qrcode === null) {
|
if (this.qrcode === null) {
|
||||||
this.qrcode = new QRious({
|
this.qrcode = new QRious({
|
||||||
element: document.querySelector('#qrCode'),
|
element: document.querySelector('#qrCode'),
|
||||||
@@ -53,10 +53,22 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const qrModalApp = new Vue({
|
const qrModalApp = new Vue({
|
||||||
|
delimiters: ['[[', ']]'],
|
||||||
el: '#qrcode-modal',
|
el: '#qrcode-modal',
|
||||||
data: {
|
data: {
|
||||||
qrModal: qrModal,
|
qrModal: qrModal,
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
copyToClipboard() {
|
||||||
|
this.qrModal.clipboard = new ClipboardJS('#qrCode', {
|
||||||
|
text: () => this.qrModal.copyText,
|
||||||
|
});
|
||||||
|
this.qrModal.clipboard.on('success', () => {
|
||||||
|
app.$message.success('{{ i18n "copied" }}')
|
||||||
|
this.qrModal.clipboard.destroy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{{define "textModal"}}
|
{{define "textModal"}}
|
||||||
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
|
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
|
||||||
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
|
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
|
||||||
|
:class="themeSwitcher.darkCardClass"
|
||||||
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
|
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
|
||||||
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
|
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
|
||||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName">
|
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
|
||||||
|
:download="txtModal.fileName">
|
||||||
{{ i18n "download" }} [[ txtModal.fileName ]]
|
{{ i18n "download" }} [[ txtModal.fileName ]]
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-input type="textarea" v-model="txtModal.content"
|
<a-input type="textarea" v-model="txtModal.content"
|
||||||
@@ -19,7 +21,7 @@
|
|||||||
qrcode: null,
|
qrcode: null,
|
||||||
clipboard: null,
|
clipboard: null,
|
||||||
visible: false,
|
visible: false,
|
||||||
show: function (title='', content='', fileName='') {
|
show: function (title = '', content = '', fileName = '') {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.fileName = fileName;
|
this.fileName = fileName;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #fff;
|
|
||||||
margin: 20px 0 50px 0;
|
margin: 20px 0 50px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +17,12 @@
|
|||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-input-group-addon {
|
||||||
|
border-radius: 0 30px 30px 0;
|
||||||
|
width: 50px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-input-affix-wrapper .ant-input-prefix {
|
.ant-input-affix-wrapper .ant-input-prefix {
|
||||||
left: 23px;
|
left: 23px;
|
||||||
}
|
}
|
||||||
@@ -26,20 +31,26 @@
|
|||||||
padding-left: 50px;
|
padding-left: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectLang{
|
.centered {
|
||||||
display: flex;
|
display: flex;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<a-layout id="app" v-cloak>
|
<a-layout id="app" v-cloak :class="themeSwitcher.darkCardClass">
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-row type="flex" justify="center">
|
<a-row type="flex" justify="center">
|
||||||
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
|
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
|
||||||
<h1>{{ i18n "pages.login.title" }}</h1>
|
<h1 class="title" :style="themeSwitcher.textStyle">{{ i18n "pages.login.title" }}</h1>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-row type="flex" justify="center">
|
<a-row type="flex" justify="center">
|
||||||
@@ -48,40 +59,38 @@
|
|||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
|
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
|
||||||
@keydown.enter.native="login" autofocus>
|
@keydown.enter.native="login" autofocus>
|
||||||
<a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/>
|
<a-icon slot="prefix" type="user" :style="'font-size: 16px;' + themeSwitcher.textStyle"/>
|
||||||
</a-input>
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-input type="password" v-model.trim="user.password"
|
<password-input icon="lock" v-model.trim="user.password"
|
||||||
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
|
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
|
||||||
<a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
|
</password-input>
|
||||||
</a-input>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-button block @click="login" :loading="loading">{{ i18n "login" }}</a-button>
|
<a-row justify="center" class="centered">
|
||||||
|
<a-button type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined"
|
||||||
|
:style="loading ? { width: '50px' } : { display: 'block', width: '100%' }">
|
||||||
|
[[ loading ? '' : '{{ i18n "login" }}' ]]
|
||||||
|
</a-button>
|
||||||
|
</a-row>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
|
<a-row justify="center" class="centered">
|
||||||
<a-row justify="center" class="selectLang">
|
<a-col :span="12">
|
||||||
<a-col :span="4"><span>Language : </span></a-col>
|
<a-select ref="selectLang" v-model="lang" @change="setLang(lang)" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
|
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
|
||||||
<a-col :span="6">
|
|
||||||
<a-select
|
|
||||||
ref="selectLang"
|
|
||||||
v-model="lang"
|
|
||||||
@change="setLang(lang)"
|
|
||||||
>
|
|
||||||
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" >
|
|
||||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||||
<span v-text="l.name"></span>
|
<span v-text="l.name"></span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
</a-row>
|
</a-row>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-row justify="center" class="centered">
|
||||||
|
<theme-switch />
|
||||||
|
</a-row>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -90,22 +99,21 @@
|
|||||||
</transition>
|
</transition>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
{{template "js" .}}
|
{{template "js" .}}
|
||||||
|
{{template "component/themeSwitcher" .}}
|
||||||
|
{{template "component/password" .}}
|
||||||
<script>
|
<script>
|
||||||
const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16);
|
|
||||||
const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16);
|
|
||||||
const deg = RandomUtil.randomIntRange(0, 360);
|
|
||||||
const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`;
|
|
||||||
document.querySelector('#app').style.background = background;
|
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
delimiters: ['[[', ']]'],
|
delimiters: ['[[', ']]'],
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
|
themeSwitcher,
|
||||||
loading: false,
|
loading: false,
|
||||||
user: new User(),
|
user: new User(),
|
||||||
lang : ""
|
lang: ""
|
||||||
},
|
},
|
||||||
created(){
|
created() {
|
||||||
this.lang = getLang();
|
this.updateBackground();
|
||||||
|
this.lang = getLang();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async login() {
|
async login() {
|
||||||
@@ -115,8 +123,16 @@
|
|||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
location.href = basePath + 'xui/';
|
location.href = basePath + 'xui/';
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
updateBackground() {
|
||||||
|
document.querySelector('#app').style.background = colors[this.themeSwitcher.currentTheme].bg;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'themeSwitcher.isDarkTheme'(newVal, oldVal) {
|
||||||
|
this.updateBackground();
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,60 +1,194 @@
|
|||||||
{{define "clientsBulkModal"}}
|
{{define "clientsBulkModal"}}
|
||||||
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
|
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
|
||||||
:closable="true" :mask-closable="false"
|
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||||
|
:class="themeSwitcher.darkCardClass"
|
||||||
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
|
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item>
|
<table width="100%" class="ant-table-tbody">
|
||||||
<span slot="label">{{ i18n "pages.client.clientCount" }}</span>
|
<tr>
|
||||||
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
|
<td>{{ i18n "pages.client.method" }}</td>
|
||||||
</a-form-item>
|
<td>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 250px"
|
||||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
:dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
<a-tooltip>
|
<a-select-option :value="0">Random</a-select-option>
|
||||||
<template slot="title">
|
<a-select-option :value="1">Random+Prefix</a-select-option>
|
||||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
|
||||||
</template>
|
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
|
||||||
</a-tooltip>
|
</a-select>
|
||||||
</span>
|
</a-form-item>
|
||||||
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
|
</td>
|
||||||
</a-form-item>
|
</tr>
|
||||||
<a-form-item>
|
<tr v-if="clientsBulkModal.emailMethod>1">
|
||||||
<span slot="label">
|
<td>{{ i18n "pages.client.first" }}</td>
|
||||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
<td>
|
||||||
<a-tooltip>
|
<a-form-item>
|
||||||
<template slot="title">
|
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
|
||||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
</a-form-item>
|
||||||
</template>
|
</td>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
</tr>
|
||||||
</a-tooltip>
|
<tr v-if="clientsBulkModal.emailMethod>1">
|
||||||
</span>
|
<td>{{ i18n "pages.client.last" }}</td>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
<td>
|
||||||
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
|
<a-form-item>
|
||||||
</a-form-item>
|
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="clientsBulkModal.emailMethod>0">
|
||||||
|
<td>{{ i18n "pages.client.prefix" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="clientsBulkModal.emailMethod>2">
|
||||||
|
<td>{{ i18n "pages.client.postfix" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="clientsBulkModal.emailMethod < 2">
|
||||||
|
<td>{{ i18n "pages.client.clientCount" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
|
||||||
|
<td>Flow</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-select v-model="clientsBulkModal.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
|
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||||
|
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Subscription</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="clientsBulkModal.subId" style="width: 250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Telegram Username</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="clientsBulkModal.tgId" style="width: 250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="clientsBulkModal.delayedStart">
|
||||||
|
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else>
|
||||||
|
<td>
|
||||||
|
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||||
|
v-model="clientsBulkModal.expiryTime" style="width: 250px;"></a-date-picker>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
const clientsBulkModal = {
|
const clientsBulkModal = {
|
||||||
visible: false,
|
visible: false,
|
||||||
|
confirmLoading: false,
|
||||||
title: '',
|
title: '',
|
||||||
okText: '',
|
okText: '',
|
||||||
confirm: null,
|
confirm: null,
|
||||||
dbInbound: new DBInbound(),
|
dbInbound: new DBInbound(),
|
||||||
inbound: new Inbound(),
|
inbound: new Inbound(),
|
||||||
clients: [],
|
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
totalGB: 0,
|
totalGB: 0,
|
||||||
expiryTime: '',
|
expiryTime: '',
|
||||||
|
emailMethod: 0,
|
||||||
|
firstNum: 1,
|
||||||
|
lastNum: 1,
|
||||||
|
emailPrefix: "",
|
||||||
|
emailPostfix: "",
|
||||||
|
subId: "",
|
||||||
|
tgId: "",
|
||||||
|
flow: "",
|
||||||
|
delayedStart: false,
|
||||||
ok() {
|
ok() {
|
||||||
for (let i = 0; i < clientsBulkModal.quantity; i++) {
|
clients = [];
|
||||||
|
method=clientsBulkModal.emailMethod;
|
||||||
|
if(method>1){
|
||||||
|
start=clientsBulkModal.firstNum;
|
||||||
|
end=clientsBulkModal.lastNum + 1;
|
||||||
|
} else {
|
||||||
|
start=0;
|
||||||
|
end=clientsBulkModal.quantity;
|
||||||
|
}
|
||||||
|
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : "";
|
||||||
|
useNum=(method>1);
|
||||||
|
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? clientsBulkModal.emailPostfix : "";
|
||||||
|
for (let i = start; i < end; i++) {
|
||||||
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
|
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
|
||||||
|
if(method==4) newClient.email = "";
|
||||||
|
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
|
||||||
|
newClient.subId = clientsBulkModal.subId;
|
||||||
|
newClient.tgId = clientsBulkModal.tgId;
|
||||||
newClient._totalGB = clientsBulkModal.totalGB;
|
newClient._totalGB = clientsBulkModal.totalGB;
|
||||||
newClient._expiryTime = clientsBulkModal.expiryTime;
|
newClient._expiryTime = clientsBulkModal.expiryTime;
|
||||||
clientsBulkModal.clients.push(newClient);
|
if(clientsBulkModal.inbound.canEnableTlsFlow()){
|
||||||
|
newClient.flow = clientsBulkModal.flow;
|
||||||
|
}
|
||||||
|
clients.push(newClient);
|
||||||
}
|
}
|
||||||
ObjectUtil.execute(clientsBulkModal.confirm, clientsBulkModal.inbound, clientsBulkModal.dbInbound);
|
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
|
||||||
},
|
},
|
||||||
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
|
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
@@ -63,10 +197,18 @@
|
|||||||
this.confirm = confirm;
|
this.confirm = confirm;
|
||||||
this.quantity = 1;
|
this.quantity = 1;
|
||||||
this.totalGB = 0;
|
this.totalGB = 0;
|
||||||
this.expiryTime = '';
|
this.expiryTime = 0;
|
||||||
|
this.emailMethod= 0;
|
||||||
|
this.firstNum= 1;
|
||||||
|
this.lastNum= 1;
|
||||||
|
this.emailPrefix= "";
|
||||||
|
this.emailPostfix= "";
|
||||||
|
this.subId= "";
|
||||||
|
this.tgId= "";
|
||||||
|
this.flow= "";
|
||||||
this.dbInbound = new DBInbound(dbInbound);
|
this.dbInbound = new DBInbound(dbInbound);
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
this.delayedStart = false;
|
||||||
},
|
},
|
||||||
getClients(protocol, clientSettings) {
|
getClients(protocol, clientSettings) {
|
||||||
switch(protocol){
|
switch(protocol){
|
||||||
@@ -101,6 +243,12 @@
|
|||||||
get inbound() {
|
get inbound() {
|
||||||
return this.clientsBulkModal.inbound;
|
return this.clientsBulkModal.inbound;
|
||||||
},
|
},
|
||||||
|
get delayedExpireDays() {
|
||||||
|
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
|
||||||
|
},
|
||||||
|
set delayedExpireDays(days){
|
||||||
|
this.clientsBulkModal.expiryTime = -86400000 * days;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{{define "clientsModal"}}
|
{{define "clientsModal"}}
|
||||||
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
|
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
|
||||||
:closable="true" :mask-closable="false"
|
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||||
|
:class="themeSwitcher.darkCardClass"
|
||||||
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
|
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||||
{{template "form/client"}}
|
{{template "form/client"}}
|
||||||
</a-modal>
|
</a-modal>
|
||||||
@@ -8,18 +9,25 @@
|
|||||||
|
|
||||||
const clientModal = {
|
const clientModal = {
|
||||||
visible: false,
|
visible: false,
|
||||||
|
confirmLoading: false,
|
||||||
title: '',
|
title: '',
|
||||||
okText: '',
|
okText: '',
|
||||||
|
isEdit: false,
|
||||||
dbInbound: new DBInbound(),
|
dbInbound: new DBInbound(),
|
||||||
inbound: new Inbound(),
|
inbound: new Inbound(),
|
||||||
clients: [],
|
clients: [],
|
||||||
clientStats: [],
|
clientStats: [],
|
||||||
|
oldClientId: "",
|
||||||
index: null,
|
index: null,
|
||||||
isExpired: false,
|
delayedStart: false,
|
||||||
ok() {
|
ok() {
|
||||||
ObjectUtil.execute(clientModal.confirm, clientModal.inbound, clientModal.dbInbound, clientModal.index);
|
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=(index, dbInbound)=>{}, isEdit=false }) {
|
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=()=>{}, isEdit=false }) {
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.okText = okText;
|
this.okText = okText;
|
||||||
@@ -28,8 +36,13 @@
|
|||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
||||||
this.index = index === null ? this.clients.length : index;
|
this.index = index === null ? this.clients.length : index;
|
||||||
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
|
this.delayedStart = false;
|
||||||
if (!isEdit){
|
if (isEdit){
|
||||||
|
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.addClient(this.inbound.protocol, this.clients);
|
||||||
}
|
}
|
||||||
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
||||||
@@ -40,14 +53,23 @@
|
|||||||
case Protocols.VMESS: return clientSettings.vmesses;
|
case Protocols.VMESS: return clientSettings.vmesses;
|
||||||
case Protocols.VLESS: return clientSettings.vlesses;
|
case Protocols.VLESS: return clientSettings.vlesses;
|
||||||
case Protocols.TROJAN: return clientSettings.trojans;
|
case Protocols.TROJAN: return clientSettings.trojans;
|
||||||
|
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getClientId(protocol, client) {
|
||||||
|
switch(protocol){
|
||||||
|
case Protocols.TROJAN: return client.password;
|
||||||
|
case Protocols.SHADOWSOCKS: return client.email;
|
||||||
|
default: return client.id;
|
||||||
|
}
|
||||||
|
},
|
||||||
addClient(protocol, clients) {
|
addClient(protocol, clients) {
|
||||||
switch (protocol) {
|
switch (protocol) {
|
||||||
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
|
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
|
||||||
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
||||||
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
||||||
|
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks());
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -79,19 +101,22 @@
|
|||||||
},
|
},
|
||||||
get isTrafficExhausted() {
|
get isTrafficExhausted() {
|
||||||
if(!clientStats) return false
|
if(!clientStats) return false
|
||||||
if(clientStats.total == 0) return false
|
if(clientStats.total <= 0) return false
|
||||||
if(clientStats.up + clientStats.down < clientStats.total) return false
|
if(clientStats.up + clientStats.down < clientStats.total) return false
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
get isExpiry() {
|
get isExpiry() {
|
||||||
return this.clientModal.isExpired
|
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
|
||||||
},
|
},
|
||||||
get statsColor() {
|
get statsColor() {
|
||||||
if(!clientStats) return 'blue'
|
return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
|
||||||
if(clientStats.total === 0) return 'blue'
|
},
|
||||||
else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan'
|
get delayedExpireDays() {
|
||||||
else return 'red'
|
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
|
||||||
}
|
},
|
||||||
|
set delayedExpireDays(days){
|
||||||
|
this.client.expiryTime = -86400000 * days;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getNewEmail(client) {
|
getNewEmail(client) {
|
||||||
@@ -103,6 +128,24 @@
|
|||||||
}
|
}
|
||||||
client.email = string;
|
client.email = string;
|
||||||
},
|
},
|
||||||
|
resetClientTraffic(email,dbInboundId,iconElement) {
|
||||||
|
this.$confirm({
|
||||||
|
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||||
|
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||||
|
class: themeSwitcher.darkCardClass,
|
||||||
|
okText: '{{ i18n "reset"}}',
|
||||||
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
|
onOk: async () => {
|
||||||
|
iconElement.disabled = true;
|
||||||
|
const msg = await HttpUtil.postWithModal('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ email);
|
||||||
|
if (msg.success) {
|
||||||
|
this.clientModal.clientStats.up = 0;
|
||||||
|
this.clientModal.clientStats.down = 0;
|
||||||
|
}
|
||||||
|
iconElement.disabled = false;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,24 +7,10 @@
|
|||||||
<a-icon type="user"></a-icon>
|
<a-icon type="user"></a-icon>
|
||||||
<span>{{ i18n "menu.inbounds"}}</span>
|
<span>{{ i18n "menu.inbounds"}}</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item key="{{ .base_path }}xui/setting">
|
<a-menu-item key="{{ .base_path }}xui/settings">
|
||||||
<a-icon type="setting"></a-icon>
|
<a-icon type="setting"></a-icon>
|
||||||
<span>{{ i18n "menu.setting"}}</span>
|
<span>{{ i18n "menu.settings"}}</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<!--<a-menu-item key="{{ .base_path }}xui/clients">-->
|
|
||||||
<!-- <a-icon type="laptop"></a-icon>-->
|
|
||||||
<!-- <span>Client</span>-->
|
|
||||||
<!--</a-menu-item>-->
|
|
||||||
<a-sub-menu>
|
|
||||||
<template slot="title">
|
|
||||||
<a-icon type="link"></a-icon>
|
|
||||||
<span>{{ i18n "menu.link"}}</span>
|
|
||||||
</template>
|
|
||||||
<a-menu-item key="https://github.com/alireza0/x-ui/">
|
|
||||||
<a-icon type="github"></a-icon>
|
|
||||||
<span>Github</span>
|
|
||||||
</a-menu-item>
|
|
||||||
</a-sub-menu>
|
|
||||||
<a-menu-item key="{{ .base_path }}logout">
|
<a-menu-item key="{{ .base_path }}logout">
|
||||||
<a-icon type="logout"></a-icon>
|
<a-icon type="logout"></a-icon>
|
||||||
<span>{{ i18n "menu.logout"}}</span>
|
<span>{{ i18n "menu.logout"}}</span>
|
||||||
@@ -33,25 +19,38 @@
|
|||||||
|
|
||||||
|
|
||||||
{{define "commonSider"}}
|
{{define "commonSider"}}
|
||||||
<a-layout-sider id="sider" collapsible breakpoint="md" collapsed-width="0">
|
<a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0">
|
||||||
<a-menu theme="dark" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||||
|
<a-menu-item mode="inline">
|
||||||
|
<a-icon type="bg-colors"></a-icon>
|
||||||
|
<theme-switch />
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||||
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||||
{{template "menuItems" .}}
|
{{template "menuItems" .}}
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</a-layout-sider>
|
</a-layout-sider>
|
||||||
<a-drawer id="sider-drawer" placement="left" :closable="false"
|
<a-drawer id="sider-drawer" placement="left" :closable="false"
|
||||||
@close="siderDrawer.close()"
|
@close="siderDrawer.close()"
|
||||||
:visible="siderDrawer.visible" :wrap-style="{ padding: 0 }">
|
:visible="siderDrawer.visible"
|
||||||
|
:wrap-class-name="themeSwitcher.darkDrawerClass"
|
||||||
|
:wrap-style="{ padding: 0 }">
|
||||||
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
|
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
|
||||||
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
|
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
|
||||||
</div>
|
</div>
|
||||||
<a-menu theme="light" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||||
|
<a-menu-item mode="inline">
|
||||||
|
<a-icon type="bg-colors"></a-icon>
|
||||||
|
<theme-switch />
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||||
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||||
{{template "menuItems" .}}
|
{{template "menuItems" .}}
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
const siderDrawer = {
|
const siderDrawer = {
|
||||||
visible: false,
|
visible: false,
|
||||||
show() {
|
show() {
|
||||||
|
|||||||
35
web/html/xui/component/password.html
Normal file
35
web/html/xui/component/password.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{{define "component/passwordInput"}}
|
||||||
|
<template>
|
||||||
|
<a-input :value="value" :type="showPassword ? 'text' : 'password'"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@input="$emit('input', $event.target.value)">
|
||||||
|
<template v-if="icon" #prefix>
|
||||||
|
<a-icon :type="icon" :style="'font-size: 16px;' + themeSwitcher.textStyle" />
|
||||||
|
</template>
|
||||||
|
<template #addonAfter>
|
||||||
|
<a-icon :type="showPassword ? 'eye-invisible' : 'eye'"
|
||||||
|
@click="toggleShowPassword"
|
||||||
|
:style="'font-size: 16px;' + themeSwitcher.textStyle" />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</template>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "component/password"}}
|
||||||
|
<script>
|
||||||
|
Vue.component('password-input', {
|
||||||
|
props: ["title", "value", "placeholder", "icon"],
|
||||||
|
template: `{{template "component/passwordInput"}}`,
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showPassword: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleShowPassword() {
|
||||||
|
this.showPassword = !this.showPassword;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
{{define "component/settingListItem"}}
|
{{define "component/settingListItem"}}
|
||||||
<a-list-item style="padding: 20px">
|
<a-list-item style="padding: 20px">
|
||||||
<a-row>
|
<a-row v-if="type === 'textarea'">
|
||||||
|
<a-col>
|
||||||
|
<a-list-item-meta :title="title" :description="desc"/>
|
||||||
|
<a-textarea class="ant-setting-textarea" :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 5 }"></a-textarea>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
<a-row v-else>
|
||||||
<a-col :lg="24" :xl="12">
|
<a-col :lg="24" :xl="12">
|
||||||
<a-list-item-meta :title="title" :description="desc"/>
|
<a-list-item-meta :title="title" :description="desc"/>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -9,10 +15,7 @@
|
|||||||
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
|
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="type === 'number'">
|
<template v-else-if="type === 'number'">
|
||||||
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)"></a-input>
|
<a-input-number :value="value" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number>
|
||||||
</template>
|
|
||||||
<template v-else-if="type === 'textarea'">
|
|
||||||
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="type === 'switch'">
|
<template v-else-if="type === 'switch'">
|
||||||
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
|
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
|
||||||
@@ -25,7 +28,7 @@
|
|||||||
{{define "component/setting"}}
|
{{define "component/setting"}}
|
||||||
<script>
|
<script>
|
||||||
Vue.component('setting-list-item', {
|
Vue.component('setting-list-item', {
|
||||||
props: ["type", "title", "desc", "value"],
|
props: ["type", "title", "desc", "value", "min"],
|
||||||
template: `{{template "component/settingListItem"}}`,
|
template: `{{template "component/settingListItem"}}`,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
58
web/html/xui/component/themeSwitch.html
Normal file
58
web/html/xui/component/themeSwitch.html
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{{define "component/themeSwitchTemplate"}}
|
||||||
|
<template>
|
||||||
|
<a-switch :default-checked="themeSwitcher.isDarkTheme"
|
||||||
|
checked-children="☀"
|
||||||
|
un-checked-children="🌙"
|
||||||
|
@change="themeSwitcher.toggleTheme()">
|
||||||
|
</a-switch>
|
||||||
|
</template>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "component/themeSwitcher"}}
|
||||||
|
<script>
|
||||||
|
const colors = {
|
||||||
|
dark: {
|
||||||
|
bg: "#242c3a",
|
||||||
|
text: "hsla(0,0%,100%,.65)"
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
bg: '#f0f2f5',
|
||||||
|
text: "rgba(0, 0, 0, 0.7)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createThemeSwitcher() {
|
||||||
|
const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
|
||||||
|
const theme = isDarkTheme ? 'dark' : 'light';
|
||||||
|
return {
|
||||||
|
isDarkTheme,
|
||||||
|
bgStyle: `background: ${colors[theme].bg};`,
|
||||||
|
textStyle: `color: ${colors[theme].text};`,
|
||||||
|
darkClass: isDarkTheme ? 'ant-dark' : '',
|
||||||
|
darkCardClass: isDarkTheme ? 'ant-card-dark' : '',
|
||||||
|
darkDrawerClass: isDarkTheme ? 'ant-drawer-dark' : '',
|
||||||
|
get currentTheme() {
|
||||||
|
return this.isDarkTheme ? 'dark' : 'light';
|
||||||
|
},
|
||||||
|
toggleTheme() {
|
||||||
|
this.isDarkTheme = !this.isDarkTheme;
|
||||||
|
this.theme = this.isDarkTheme ? 'dark' : 'light';
|
||||||
|
localStorage.setItem('dark-mode', this.isDarkTheme);
|
||||||
|
this.bgStyle = `background: ${colors[this.theme].bg};`;
|
||||||
|
this.textStyle = `color: ${colors[this.theme].text};`;
|
||||||
|
this.darkClass = this.isDarkTheme ? 'ant-dark' : '';
|
||||||
|
this.darkCardClass = this.isDarkTheme ? 'ant-card-dark' : '';
|
||||||
|
this.darkDrawerClass = this.isDarkTheme ? 'ant-drawer-dark' : '';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeSwitcher = createThemeSwitcher();
|
||||||
|
|
||||||
|
Vue.component('theme-switch', {
|
||||||
|
props: [],
|
||||||
|
template: `{{template "component/themeSwitchTemplate"}}`,
|
||||||
|
data: () => ({ themeSwitcher }),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
@@ -3,72 +3,149 @@
|
|||||||
<template v-if="isEdit">
|
<template v-if="isEdit">
|
||||||
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<a-form-item>
|
<table width="100%" class="ant-table-tbody">
|
||||||
<span slot="label">
|
<tr>
|
||||||
Email
|
<td>{{ i18n "pages.inbounds.enable" }}</td>
|
||||||
<a-tooltip>
|
<td>
|
||||||
<template slot="title">
|
<a-form-item>
|
||||||
The email must be completely unique
|
<a-switch v-model="client.enable"></a-switch>
|
||||||
</template>
|
</a-form-item>
|
||||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
</td>
|
||||||
</a-tooltip>
|
</tr>
|
||||||
</span>
|
<tr>
|
||||||
<a-input v-model.trim="client.email"></a-input>
|
<td>
|
||||||
</a-form-item>
|
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||||
<a-form-item label="password" v-if="inbound.protocol === Protocols.TROJAN">
|
<a-tooltip>
|
||||||
<a-input v-model.trim="client.password"></a-input>
|
<template slot="title">
|
||||||
</a-form-item>
|
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||||
<a-form-item label="id" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
|
</template>
|
||||||
<a-input v-model.trim="client.id"></a-input>
|
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||||
</a-form-item>
|
</a-tooltip>
|
||||||
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
|
</td>
|
||||||
<a-input type="number" v-model.number="client.alterId"></a-input>
|
<td>
|
||||||
</a-form-item>
|
<a-form-item>
|
||||||
<a-form-item v-if="inbound.xtls" label="flow">
|
<a-input v-model.trim="client.email" style="width: 250px"></a-input>
|
||||||
<a-select v-model="client.flow" style="width: 150px">
|
</a-form-item>
|
||||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
</td>
|
||||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
</tr>
|
||||||
</a-select>
|
<tr v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
|
||||||
</a-form-item>
|
<td>password
|
||||||
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="flow" layout="inline">
|
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||||
<a-select v-model="client.flow" style="width: 150px">
|
</td>
|
||||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
<td>
|
||||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
<a-form-item>
|
||||||
</a-select>
|
<a-input v-model.trim="client.password" style="width: 250px"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
</td>
|
||||||
<span slot="label">
|
</tr>
|
||||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
<tr v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
|
||||||
<a-tooltip>
|
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
|
||||||
<template slot="title">
|
<td>
|
||||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
<a-form-item>
|
||||||
</template>
|
<a-input v-model.trim="client.id" style="width: 250px"></a-input>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
</a-form-item>
|
||||||
</a-tooltip>
|
</td>
|
||||||
</span>
|
</tr>
|
||||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
<tr v-if="inbound.protocol === Protocols.VMESS">
|
||||||
<template v-if="isEdit && clientStats">
|
<td>{{ i18n "additional" }}</td>
|
||||||
{{ i18n "usage" }}:
|
<td>
|
||||||
<a-tag :color="statsColor">
|
<a-form-item>
|
||||||
[[ sizeFormat(clientStats.up) ]] /
|
<a-input-number v-model.number="client.alterId"></a-input-number>
|
||||||
[[ sizeFormat(clientStats.down) ]]
|
</a-form-item>
|
||||||
([[ sizeFormat(clientStats.up + clientStats.down) ]])
|
</td>
|
||||||
</a-tag>
|
</tr>
|
||||||
</template>
|
<tr v-if="client.email">
|
||||||
</a-form-item>
|
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||||
<a-form-item>
|
<td>
|
||||||
<span slot="label">
|
<a-form-item>
|
||||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
<a-input v-model.trim="client.subId" style="width: 250px"></a-input>
|
||||||
<a-tooltip>
|
</a-form-item>
|
||||||
<template slot="title">
|
</td>
|
||||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
</tr>
|
||||||
</template>
|
<tr v-if="client.email">
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<td>Telegram Username</td>
|
||||||
</a-tooltip>
|
<td>
|
||||||
</span>
|
<a-form-item>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
<a-input v-model.trim="client.tgId" style="width: 250px"></a-input>
|
||||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
</a-form-item>
|
||||||
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
|
</td>
|
||||||
</a-form-item>
|
</tr>
|
||||||
|
<tr v-if="inbound.canEnableTlsFlow()">
|
||||||
|
<td>Flow</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-select v-model="client.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
|
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||||
|
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="isEdit && clientStats">
|
||||||
|
<td>{{ i18n "usage" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-tag :color="statsColor">
|
||||||
|
[[ sizeFormat(clientStats.up) ]] /
|
||||||
|
[[ sizeFormat(clientStats.down) ]]
|
||||||
|
([[ sizeFormat(clientStats.up + clientStats.down) ]])
|
||||||
|
</a-tag>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||||
|
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" v-if="client.email.length > 0"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="clientModal.delayedStart">
|
||||||
|
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else>
|
||||||
|
<td>
|
||||||
|
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||||
|
v-model="client._expiryTime" style="width: 250px;"></a-date-picker>
|
||||||
|
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,57 +1,91 @@
|
|||||||
{{define "form/inbound"}}
|
{{define "form/inbound"}}
|
||||||
<!-- base -->
|
<!-- base -->
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "remark" }}'>
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-input v-model.trim="dbInbound.remark"></a-input>
|
<tr>
|
||||||
</a-form-item>
|
<td>{{ i18n "enable" }}</td>
|
||||||
<a-form-item label='{{ i18n "enable" }}'>
|
<td>
|
||||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
<a-form-item>
|
||||||
</a-form-item>
|
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||||
<a-form-item label='{{ i18n "protocol" }}'>
|
</a-form-item>
|
||||||
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit">
|
</td>
|
||||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
</tr>
|
||||||
</a-select>
|
<tr>
|
||||||
</a-form-item>
|
<td>{{ i18n "remark" }}</td>
|
||||||
<a-form-item>
|
<td>
|
||||||
<span slot="label">
|
<a-form-item>
|
||||||
{{ i18n "monitor" }}
|
<a-input v-model.trim="dbInbound.remark" style="width: 250px;"></a-input>
|
||||||
<a-tooltip>
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "protocol" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-select v-model="inbound.protocol" style="width: 250px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
|
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "monitor" }}
|
||||||
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<span>{{ i18n "pages.inbounds.monitorDesc" }}</span>
|
<span>{{ i18n "pages.inbounds.monitorDesc" }}</span>
|
||||||
</template>
|
</template>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</td>
|
||||||
<a-input v-model.trim="inbound.listen"></a-input>
|
<td>
|
||||||
</a-form-item>
|
<a-form-item>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
<a-input v-model.trim="inbound.listen" style="width: 250px;"></a-input>
|
||||||
<a-input type="number" v-model.number="inbound.port"></a-input>
|
</a-form-item>
|
||||||
</a-form-item>
|
</td>
|
||||||
<a-form-item>
|
</tr>
|
||||||
<span slot="label">
|
<tr>
|
||||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
<td>{{ i18n "pages.inbounds.port" }}</td>
|
||||||
<a-tooltip>
|
<td>
|
||||||
<template slot="title">
|
<a-form-item>
|
||||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
<a-input-number v-model.number="inbound.port"></a-input-number>
|
||||||
</template>
|
</a-form-item>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
</td>
|
||||||
</a-tooltip>
|
</tr>
|
||||||
</span>
|
<tr>
|
||||||
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
|
<td>
|
||||||
</a-form-item>
|
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||||
<a-form-item>
|
<a-tooltip>
|
||||||
<span slot="label">
|
<template slot="title">
|
||||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||||
<a-tooltip>
|
</template>
|
||||||
<template slot="title">
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
</a-tooltip>
|
||||||
</template>
|
</td>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<td>
|
||||||
</a-tooltip>
|
<a-form-item>
|
||||||
</span>
|
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
</a-form-item>
|
||||||
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
</td>
|
||||||
</a-form-item>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||||
|
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- vmess settings -->
|
<!-- vmess settings -->
|
||||||
|
|||||||
@@ -1,17 +1,42 @@
|
|||||||
{{define "form/dokodemo"}}
|
{{define "form/dokodemo"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
<tr>
|
||||||
</a-form-item>
|
<td>{{ i18n "pages.inbounds.targetAddress"}}</td>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
|
<td>
|
||||||
<a-input type="number" v-model.number="inbound.settings.port"></a-input>
|
<a-form-item>
|
||||||
</a-form-item>
|
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
|
</a-form-item>
|
||||||
<a-select v-model="inbound.settings.network" style="width: 100px;">
|
</td>
|
||||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
</tr>
|
||||||
<a-select-option value="tcp">tcp</a-select-option>
|
<tr>
|
||||||
<a-select-option value="udp">udp</a-select-option>
|
<td>{{ i18n "pages.inbounds.destinationPort"}}</td>
|
||||||
</a-select>
|
<td>
|
||||||
</a-form-item>
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="inbound.settings.port"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.inbounds.network"}}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
|
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
||||||
|
<a-select-option value="tcp">tcp</a-select-option>
|
||||||
|
<a-select-option value="udp">udp</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>FollowRedirect</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,10 +1,22 @@
|
|||||||
{{define "form/http"}}
|
{{define "form/http"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "username"}}'>
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
|
<tr>
|
||||||
</a-form-item>
|
<td>{{ i18n "username"}}</td>
|
||||||
<a-form-item label='{{ i18n "password" }}'>
|
<td>
|
||||||
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
|
<a-form-item>
|
||||||
</a-form-item>
|
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "password" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,19 +1,148 @@
|
|||||||
{{define "form/shadowsocks"}}
|
{{define "form/shadowsocks"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "encryption" }}'>
|
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
|
||||||
<a-select v-model="inbound.settings.method" style="width: 165px;">
|
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||||
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
<table width="100%" class="ant-table-tbody">
|
||||||
</a-select>
|
<tr>
|
||||||
</a-form-item>
|
<td>
|
||||||
<a-form-item label='{{ i18n "password" }}'>
|
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||||
<a-input v-model.trim="inbound.settings.password"></a-input>
|
<a-tooltip>
|
||||||
</a-form-item>
|
<template slot="title">
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||||
<a-select v-model="inbound.settings.network" style="width: 100px;">
|
</template>
|
||||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
||||||
<a-select-option value="tcp">tcp</a-select-option>
|
</a-tooltip>
|
||||||
<a-select-option value="udp">udp</a-select-option>
|
</td>
|
||||||
</a-select>
|
<td>
|
||||||
</a-form-item>
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>password
|
||||||
|
<a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="client.password" style="width: 200px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||||
|
<td>
|
||||||
|
<a-form-item v-if="client.email">
|
||||||
|
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Telegram Username</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item v-if="client.email">
|
||||||
|
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="delayedStart">
|
||||||
|
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else>
|
||||||
|
<td>
|
||||||
|
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||||
|
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
<a-collapse v-else>
|
||||||
|
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.shadowsockses.length">
|
||||||
|
<table width="100%">
|
||||||
|
<tr class="client-table-header">
|
||||||
|
<th v-for="col in Object.keys(inbound.settings.shadowsockses[0]).slice(0, 3)">[[ col ]]</th>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="(client, index) in inbound.settings.shadowsockses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||||
|
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
<table width="100%" class="ant-table-tbody">
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "encryption" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
|
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "password" }}
|
||||||
|
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.settings.password" style="width: 250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.inbounds.network" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
|
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
||||||
|
<a-select-option value="tcp">tcp</a-select-option>
|
||||||
|
<a-select-option value="udp">udp</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,24 +1,49 @@
|
|||||||
{{define "form/socks"}}
|
{{define "form/socks"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<!-- <a-form-item label="Password authentication">-->
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-form-item label='{{ i18n "password" }}'>
|
<tr>
|
||||||
<a-switch :checked="inbound.settings.auth === 'password'"
|
<td>{{ i18n "password" }}</td>
|
||||||
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
<td>
|
||||||
</a-form-item>
|
<a-form-item>
|
||||||
|
<a-switch :checked="inbound.settings.auth === 'password'"
|
||||||
|
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<template v-if="inbound.settings.auth === 'password'">
|
<template v-if="inbound.settings.auth === 'password'">
|
||||||
<a-form-item label='{{ i18n "username" }}'>
|
<tr>
|
||||||
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
|
<td>{{ i18n "username" }}</td>
|
||||||
</a-form-item>
|
<td>
|
||||||
<a-form-item label='{{ i18n "password" }}'>
|
<a-form-item>
|
||||||
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
|
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "password" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'>
|
<tr>
|
||||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
<td>{{ i18n "pages.inbounds.enable" }} udp</td>
|
||||||
</a-form-item>
|
<td>
|
||||||
<a-form-item v-if="inbound.settings.udp"
|
<a-form-item>
|
||||||
label="IP">
|
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||||
<a-input v-model.trim="inbound.settings.ip"></a-input>
|
</a-form-item>
|
||||||
</a-form-item>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>IP</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item v-if="inbound.settings.udp">
|
||||||
|
<a-input v-model.trim="inbound.settings.ip"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,64 +1,108 @@
|
|||||||
{{define "form/trojan"}}
|
{{define "form/trojan"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
|
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
|
||||||
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||||
<a-form layout="inline">
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-form-item>
|
<tr>
|
||||||
<span slot="label">
|
<td>
|
||||||
Email
|
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
The email must be completely unique
|
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||||
</template>
|
</template>
|
||||||
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</td>
|
||||||
<a-input v-model.trim="client.email"></a-input>
|
<td>
|
||||||
</a-form-item>
|
<a-form-item>
|
||||||
</a-form>
|
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||||
<a-form-item label="password">
|
</a-form-item>
|
||||||
<a-input v-model.trim="client.password"></a-input>
|
</td>
|
||||||
</a-form-item>
|
</tr>
|
||||||
<a-form-item v-if="inbound.xtls" label="flow">
|
<tr>
|
||||||
<a-select v-model="client.flow" style="width: 150px">
|
<td>password</td>
|
||||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
<td>
|
||||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
<a-form-item>
|
||||||
</a-select>
|
<a-input v-model.trim="client.password" style="width: 200px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
</td>
|
||||||
<span slot="label">
|
</tr>
|
||||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
<tr>
|
||||||
<a-tooltip>
|
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||||
<template slot="title">
|
<td>
|
||||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
<a-form-item v-if="client.email">
|
||||||
</template>
|
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
</a-form-item>
|
||||||
</a-tooltip>
|
</td>
|
||||||
</span>
|
</tr>
|
||||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
<tr>
|
||||||
</a-form-item>
|
<td>Telegram Username</td>
|
||||||
<a-form-item>
|
<td>
|
||||||
<span slot="label">
|
<a-form-item v-if="client.email">
|
||||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
|
||||||
<a-tooltip>
|
</a-form-item>
|
||||||
<template slot="title">
|
</td>
|
||||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
</tr>
|
||||||
</template>
|
<tr>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<td>
|
||||||
</a-tooltip>
|
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
|
||||||
</span>
|
<a-tooltip>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
<template slot="title">
|
||||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||||
</a-form-item>
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="delayedStart">
|
||||||
|
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else>
|
||||||
|
<td>
|
||||||
|
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||||
|
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-collapse v-else>
|
<a-collapse v-else>
|
||||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
|
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
|
||||||
<table width="100%">
|
<table width="100%">
|
||||||
<tr style="background-color: lightgrey;">
|
<tr class="client-table-header">
|
||||||
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
|
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="(client, index) in inbound.settings.trojans" :style="[ index % 2 == 1 ? {'background-color': '#fafafa'} : {}]">
|
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -96,7 +140,7 @@
|
|||||||
<a-input v-model="fallback.dest"></a-input>
|
<a-input v-model="fallback.dest"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="xver">
|
<a-form-item label="xver">
|
||||||
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
<a-input-number v-model.number="fallback.xver"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|||||||
@@ -2,69 +2,118 @@
|
|||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
||||||
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
||||||
<a-form layout="inline">
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-form-item>
|
<tr>
|
||||||
<span slot="label">
|
<td>
|
||||||
Email
|
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
The email must be completely unique
|
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||||
</template>
|
</template>
|
||||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</td>
|
||||||
<a-input v-model.trim="client.email"></a-input>
|
<td>
|
||||||
</a-form-item>
|
<a-form-item>
|
||||||
</a-form>
|
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||||
<a-form-item label="id">
|
</a-form-item>
|
||||||
<a-input v-model.trim="client.id"></a-input>
|
</td>
|
||||||
</a-form-item>
|
</tr>
|
||||||
<a-form-item v-if="inbound.xtls" label="flow">
|
<tr>
|
||||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
|
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
|
||||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
<td>
|
||||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
<a-form-item>
|
||||||
</a-select>
|
<a-input v-model.trim="client.id" style="width: 200px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="flow" layout="inline">
|
</td>
|
||||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
|
</tr>
|
||||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
<tr v-if="inbound.canEnableTlsFlow()">
|
||||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
<td>flow</td>
|
||||||
</a-select>
|
<td>
|
||||||
</a-form-item>
|
<a-form-item>
|
||||||
<a-form-item>
|
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
<span slot="label">
|
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||||
<a-tooltip>
|
</a-select>
|
||||||
<template slot="title">
|
</a-form-item>
|
||||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
</td>
|
||||||
</template>
|
</tr>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<tr>
|
||||||
</a-tooltip>
|
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||||
</span>
|
<td>
|
||||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
<a-form-item v-if="client.email">
|
||||||
</a-form-item>
|
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
|
||||||
<a-form-item>
|
</a-form-item>
|
||||||
<span slot="label">
|
</td>
|
||||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
</tr>
|
||||||
<a-tooltip>
|
<tr>
|
||||||
<template slot="title">
|
<td>Telegram Username</td>
|
||||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
<td>
|
||||||
</template>
|
<a-form-item v-if="client.email">
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
|
||||||
</a-tooltip>
|
</a-form-item>
|
||||||
</span>
|
</td>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
</tr>
|
||||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
<tr>
|
||||||
</a-form-item>
|
<td>
|
||||||
|
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="delayedStart">
|
||||||
|
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else>
|
||||||
|
<td>
|
||||||
|
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||||
|
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-collapse v-else>
|
<a-collapse v-else>
|
||||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
|
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
|
||||||
<table width="100%">
|
<table width="100%">
|
||||||
<tr style="background-color: lightgrey;">
|
<tr class="client-table-header">
|
||||||
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
|
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="(client, index) in inbound.settings.vlesses" :style="[ index % 2 == 1 ? {'background-color': '#fafafa'} : {}]">
|
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -102,7 +151,7 @@
|
|||||||
<a-input v-model="fallback.dest"></a-input>
|
<a-input v-model="fallback.dest"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="xver">
|
<a-form-item label="xver">
|
||||||
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
<a-input-number v-model.number="fallback.xver"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|||||||
@@ -2,60 +2,115 @@
|
|||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
|
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
|
||||||
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
||||||
<a-form layout="inline">
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-form-item>
|
<tr>
|
||||||
<span slot="label">
|
<td>
|
||||||
Email
|
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
The email must be completely unique
|
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||||
</template>
|
</template>
|
||||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</td>
|
||||||
<a-input v-model.trim="client.email"></a-input>
|
<td>
|
||||||
</a-form-item>
|
<a-form-item>
|
||||||
</a-form>
|
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||||
<a-form-item label="id">
|
</a-form-item>
|
||||||
<a-input v-model.trim="client.id"></a-input>
|
</td>
|
||||||
</a-form-item>
|
</tr>
|
||||||
<a-form-item label='{{ i18n "additional" }} ID'>
|
<tr>
|
||||||
<a-input type="number" v-model.number="client.alterId"></a-input>
|
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
|
||||||
</a-form-item>
|
<td>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<a-input v-model.trim="client.id" style="width: 200px;"></a-input>
|
||||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
</a-form-item>
|
||||||
<a-tooltip>
|
</td>
|
||||||
<template slot="title">
|
</tr>
|
||||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
<tr>
|
||||||
</template>
|
<td>{{ i18n "additional" }}</td>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<td>
|
||||||
</a-tooltip>
|
<a-form-item>
|
||||||
</span>
|
<a-input-number v-model.number="client.alterId"></a-input-number>
|
||||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
</a-form-item>
|
||||||
</a-form-item>
|
</td>
|
||||||
<a-form-item>
|
</tr>
|
||||||
<span slot="label">
|
<tr>
|
||||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||||
<a-tooltip>
|
<td>
|
||||||
<template slot="title">
|
<a-form-item v-if="client.email">
|
||||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
|
||||||
</template>
|
</a-form-item>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
</td>
|
||||||
</a-tooltip>
|
</tr>
|
||||||
</span>
|
<tr>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
<td>Telegram Username</td>
|
||||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
<td>
|
||||||
</a-form-item>
|
<a-form-item v-if="client.email">
|
||||||
|
<a-input v-model.trim="client.tgId" style="width: 200px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="delayedStart">
|
||||||
|
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else>
|
||||||
|
<td>
|
||||||
|
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||||
|
v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-collapse v-else>
|
<a-collapse v-else>
|
||||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vmesses.length">
|
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vmesses.length">
|
||||||
<table width="100%">
|
<table width="100%">
|
||||||
<tr style="background-color: lightgrey;">
|
<tr class="client-table-header">
|
||||||
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
|
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="(client, index) in inbound.settings.vmesses" :style="[ index % 2 == 1 ? {'background-color': '#fafafa'} : {}]">
|
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -63,8 +118,7 @@
|
|||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
|
||||||
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
|
<a-switch v-model="inbound.settings.disableInsecure"></a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -5,12 +5,17 @@
|
|||||||
sniffing
|
sniffing
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
|
<span>{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
|
||||||
</template>
|
</template>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
|
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-checkbox-group v-model="inbound.sniffing.destOverride" v-if="inbound.sniffing.enabled">
|
||||||
|
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
|
||||||
|
</a-checkbox-group>
|
||||||
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,7 +1,22 @@
|
|||||||
{{define "form/streamGRPC"}}
|
{{define "form/streamGRPC"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label="serviceName">
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
|
<tr>
|
||||||
</a-form-item>
|
<td>serviceName</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.stream.grpc.serviceName" style="width: 250px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>MultiMode</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,12 +1,24 @@
|
|||||||
{{define "form/streamHTTP"}}
|
{{define "form/streamHTTP"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "path" }}'>
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-input v-model.trim="inbound.stream.http.path"></a-input>
|
<tr>
|
||||||
</a-form-item>
|
<td>{{ i18n "path" }}</td>
|
||||||
<a-form-item label="host">
|
<td>
|
||||||
<a-row v-for="(host, index) in inbound.stream.http.host">
|
<a-form-item>
|
||||||
<a-input v-model.trim="inbound.stream.http.host[index]"></a-input>
|
<a-input v-model.trim="inbound.stream.http.path" style="width: 250px;"></a-input>
|
||||||
</a-row>
|
</a-form-item>
|
||||||
</a-form-item>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>host</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-row v-for="(host, index) in inbound.stream.http.host">
|
||||||
|
<a-input v-model.trim="inbound.stream.http.host[index]" style="width: 250px;"></a-input>
|
||||||
|
</a-row>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,38 +1,85 @@
|
|||||||
{{define "form/streamKCP"}}
|
{{define "form/streamKCP"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;">
|
<tr>
|
||||||
<a-select-option value="none">none(not camouflage)</a-select-option>
|
<td>{{ i18n "camouflage" }}</td>
|
||||||
<a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
|
<td>
|
||||||
<a-select-option value="utp">utp(camouflage BT download)</a-select-option>
|
<a-form-item>
|
||||||
<a-select-option value="wechat-video">wechat-video(camouflage WeChat video)</a-select-option>
|
<a-select v-model="inbound.stream.kcp.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
<a-select-option value="dtls">dtls(camouflage DTLS 1.2 packages)</a-select-option>
|
<a-select-option value="none">none (not camouflage)</a-select-option>
|
||||||
<a-select-option value="wireguard">wireguard(camouflage wireguard packages)</a-select-option>
|
<a-select-option value="srtp">srtp (video call)</a-select-option>
|
||||||
</a-select>
|
<a-select-option value="utp">utp (BT download)</a-select-option>
|
||||||
</a-form-item>
|
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
|
||||||
<a-form-item label='{{ i18n "password" }}'>
|
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
|
||||||
<a-input v-model.number="inbound.stream.kcp.seed"></a-input>
|
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
|
||||||
</a-form-item>
|
</a-select>
|
||||||
<a-form-item label="mtu">
|
</a-form-item>
|
||||||
<a-input type="number" v-model.number="inbound.stream.kcp.mtu"></a-input>
|
</td>
|
||||||
</a-form-item>
|
</tr>
|
||||||
<a-form-item label="tti (ms)">
|
<tr>
|
||||||
<a-input type="number" v-model.number="inbound.stream.kcp.tti"></a-input>
|
<td>{{ i18n "password" }}</td>
|
||||||
</a-form-item>
|
<td>
|
||||||
<a-form-item label="uplink capacity (MB/S)">
|
<a-form-item>
|
||||||
<a-input type="number" v-model.number="inbound.stream.kcp.upCap"></a-input>
|
<a-input v-model="inbound.stream.kcp.seed" style="width: 250px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="downlink capacity (MB/S)">
|
</td>
|
||||||
<a-input type="number" v-model.number="inbound.stream.kcp.downCap"></a-input>
|
</tr>
|
||||||
</a-form-item>
|
<tr>
|
||||||
<a-form-item label="congestion">
|
<td>mtu</td>
|
||||||
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
|
<td>
|
||||||
</a-form-item>
|
<a-form-item>
|
||||||
<a-form-item label="read buffer size (MB)">
|
<a-input-number v-model.number="inbound.stream.kcp.mtu"></a-input-number>
|
||||||
<a-input type="number" v-model.number="inbound.stream.kcp.readBuffer"></a-input>
|
</a-form-item>
|
||||||
</a-form-item>
|
</td>
|
||||||
<a-form-item label="write buffer size (MB)">
|
</tr>
|
||||||
<a-input type="number" v-model.number="inbound.stream.kcp.writeBuffer"></a-input>
|
<tr>
|
||||||
</a-form-item>
|
<td>tti (ms)</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="inbound.stream.kcp.tti"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>uplink capacity (MB/S)</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="inbound.stream.kcp.upCap"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>downlink capacity (MB/S)</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="inbound.stream.kcp.downCap"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>congestion</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>read buffer size (MB)</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="inbound.stream.kcp.readBuffer"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>write buffer size (MB)</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,24 +1,41 @@
|
|||||||
{{define "form/streamQUIC"}}
|
{{define "form/streamQUIC"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-select v-model="inbound.stream.quic.security" style="width: 165px;">
|
<tr>
|
||||||
<a-select-option value="none">none</a-select-option>
|
<td>{{ i18n "pages.inbounds.stream.quic.encryption" }}</td>
|
||||||
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
<td>
|
||||||
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
<a-form-item>
|
||||||
</a-select>
|
<a-select v-model="inbound.stream.quic.security" style="width: 200px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
</a-form-item>
|
<a-select-option value="none">none</a-select-option>
|
||||||
<a-form-item label='{{ i18n "password" }}'>
|
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
||||||
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
|
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
||||||
</a-form-item>
|
</a-select>
|
||||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
</a-form-item>
|
||||||
<a-select v-model="inbound.stream.quic.type" style="width: 280px;">
|
</td>
|
||||||
<a-select-option value="none">none(not camouflage)</a-select-option>
|
</tr>
|
||||||
<a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
|
<tr>
|
||||||
<a-select-option value="utp">utp(camouflage BT download)</a-select-option>
|
<td>{{ i18n "password" }}</td>
|
||||||
<a-select-option value="wechat-video">wechat-video(camouflage WeChat video)</a-select-option>
|
<td>
|
||||||
<a-select-option value="dtls">dtls(camouflage DTLS 1.2 packages)</a-select-option>
|
<a-form-item>
|
||||||
<a-select-option value="wireguard">wireguard(camouflage wireguard packages)</a-select-option>
|
<a-input v-model.trim="inbound.stream.quic.key" style="width: 200px;"></a-input>
|
||||||
</a-select>
|
</a-form-item>
|
||||||
</a-form-item>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "camouflage" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-select v-model="inbound.stream.quic.type" style="width: 200px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
|
<a-select-option value="none">none (not camouflage)</a-select-option>
|
||||||
|
<a-select-option value="srtp">srtp (video call)</a-select-option>
|
||||||
|
<a-select-option value="utp">utp (BT download)</a-select-option>
|
||||||
|
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
|
||||||
|
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
|
||||||
|
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
{{define "form/streamSettings"}}
|
{{define "form/streamSettings"}}
|
||||||
<!-- select stream network -->
|
<!-- select stream network -->
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "transmission" }}'>
|
<a-form-item label="{{ i18n "transmission" }}">
|
||||||
<a-select v-model="inbound.stream.network" @change="streamNetworkChange">
|
<a-select v-model="inbound.stream.network" @change="streamNetworkChange"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
<a-select-option value="tcp">tcp</a-select-option>
|
<a-select-option value="tcp">tcp</a-select-option>
|
||||||
<a-select-option value="kcp">kcp</a-select-option>
|
<a-select-option value="kcp">kcp</a-select-option>
|
||||||
<a-select-option value="ws">ws</a-select-option>
|
<a-select-option value="ws">ws</a-select-option>
|
||||||
|
|||||||
@@ -13,74 +13,109 @@
|
|||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- tcp request -->
|
<!-- tcp request -->
|
||||||
<a-form v-if="inbound.stream.tcp.type === 'http'"
|
<a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
|
||||||
layout="inline">
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'>
|
<tr>
|
||||||
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
|
<td>{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}</td>
|
||||||
</a-form-item>
|
<td>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}'>
|
<a-form-item>
|
||||||
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
|
<a-input v-model.trim="inbound.stream.tcp.request.version" style="width: 200px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestPath" }}'>
|
</td>
|
||||||
<a-row v-for="(path, index) in inbound.stream.tcp.request.path">
|
</tr>
|
||||||
<a-input v-model.trim="inbound.stream.tcp.request.path[index]"></a-input>
|
<tr>
|
||||||
</a-row>
|
<td>{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}</td>
|
||||||
</a-form-item>
|
<td>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
<a-form-item>
|
||||||
<a-row>
|
<a-input v-model.trim="inbound.stream.tcp.request.method" style="width: 200px;"></a-input>
|
||||||
<a-button size="small"
|
</a-form-item>
|
||||||
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
|
</td>
|
||||||
+
|
</tr>
|
||||||
</a-button>
|
<tr>
|
||||||
</a-row>
|
<td>{{ i18n "pages.inbounds.stream.tcp.requestPath" }}</td>
|
||||||
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
|
<td>
|
||||||
<a-input style="width: 50%" v-model.trim="header.name"
|
<a-form-item>
|
||||||
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
|
<a-row v-for="(path, index) in inbound.stream.tcp.request.path">
|
||||||
<a-input style="width: 50%" v-model.trim="header.value"
|
<a-input v-model.trim="inbound.stream.tcp.request.path[index]" style="width: 200px;"></a-input>
|
||||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
</a-row>
|
||||||
<template slot="addonAfter">
|
</a-form-item>
|
||||||
<a-button size="small"
|
</td>
|
||||||
@click="inbound.stream.tcp.request.removeHeader(index)">
|
</tr>
|
||||||
-
|
<tr>
|
||||||
</a-button>
|
<td colspan="2">
|
||||||
</template>
|
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||||
</a-input>
|
<a-row>
|
||||||
</a-input-group>
|
<a-button size="small"
|
||||||
</a-form-item>
|
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
|
||||||
</a-form>
|
+
|
||||||
|
</a-button>
|
||||||
<!-- tcp response -->
|
</a-row>
|
||||||
<a-form v-if="inbound.stream.tcp.type === 'http'"
|
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
|
||||||
layout="inline">
|
<a-input style="width: 50%" v-model.trim="header.name"
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'>
|
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
|
||||||
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
|
<a-input style="width: 50%" v-model.trim="header.value"
|
||||||
</a-form-item>
|
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}'>
|
<template slot="addonAfter">
|
||||||
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
|
<a-button size="small"
|
||||||
</a-form-item>
|
@click="inbound.stream.tcp.request.removeHeader(index)">
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'>
|
-
|
||||||
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
|
</a-button>
|
||||||
</a-form-item>
|
</template>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
|
</a-input>
|
||||||
<a-row>
|
</a-input-group>
|
||||||
<a-button size="small"
|
</a-form-item>
|
||||||
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
|
</td>
|
||||||
+
|
</tr>
|
||||||
</a-button>
|
<!-- tcp response -->
|
||||||
</a-row>
|
<tr>
|
||||||
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
|
<td>{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}</td>
|
||||||
<a-input style="width: 50%" v-model.trim="header.name"
|
<td>
|
||||||
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
|
<a-form-item>
|
||||||
<a-input style="width: 50%" v-model.trim="header.value"
|
<a-input v-model.trim="inbound.stream.tcp.response.version" style="width: 200px;"></a-input>
|
||||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
</a-form-item>
|
||||||
<template slot="addonAfter">
|
</td>
|
||||||
<a-button size="small"
|
</tr>
|
||||||
@click="inbound.stream.tcp.response.removeHeader(index)">
|
<tr>
|
||||||
-
|
<td>{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}</td>
|
||||||
</a-button>
|
<td>
|
||||||
</template>
|
<a-form-item>
|
||||||
</a-input>
|
<a-input v-model.trim="inbound.stream.tcp.response.status" style="width: 200px;"></a-input>
|
||||||
</a-input-group>
|
</a-form-item>
|
||||||
</a-form-item>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.stream.tcp.response.reason" style="width: 200px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
|
||||||
|
<a-row>
|
||||||
|
<a-button size="small"
|
||||||
|
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
|
||||||
|
+
|
||||||
|
</a-button>
|
||||||
|
</a-row>
|
||||||
|
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
|
||||||
|
<a-input style="width: 50%" v-model.trim="header.name"
|
||||||
|
addon-before='{{ i18n "pages.inbounds.stream.general.name" }}'></a-input>
|
||||||
|
<a-input style="width: 50%" v-model.trim="header.value"
|
||||||
|
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||||
|
<template slot="addonAfter">
|
||||||
|
<a-button size="small"
|
||||||
|
@click="inbound.stream.tcp.response.removeHeader(index)">
|
||||||
|
-
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</a-input-group>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,33 +1,47 @@
|
|||||||
{{define "form/streamWS"}}
|
{{define "form/streamWS"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label="acceptProxyProtocol">
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
<tr>
|
||||||
</a-form-item>
|
<td>acceptProxyProtocol</td>
|
||||||
</a-form>
|
<td>
|
||||||
<a-form layout="inline">
|
<a-form-item>
|
||||||
<a-form-item label='{{ i18n "path" }}'>
|
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
</a-form-item>
|
||||||
</a-form-item>
|
</td>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
</tr>
|
||||||
<a-row>
|
<tr>
|
||||||
<a-button size="small"
|
<td>{{ i18n "path" }}</td>
|
||||||
@click="inbound.stream.ws.addHeader('Host', '')">
|
<td>
|
||||||
+
|
<a-form-item>
|
||||||
</a-button>
|
<a-input v-model.trim="inbound.stream.ws.path" style="width: 250px;"></a-input>
|
||||||
</a-row>
|
</a-form-item>
|
||||||
<a-input-group v-for="(header, index) in inbound.stream.ws.headers">
|
</td>
|
||||||
<a-input style="width: 50%" v-model.trim="header.name"
|
</tr>
|
||||||
addon-before='{{ i18n "pages.inbounds.stream.general.name"}}'></a-input>
|
<tr>
|
||||||
<a-input style="width: 50%" v-model.trim="header.value"
|
<td colspan="2">
|
||||||
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||||
<template slot="addonAfter">
|
<a-row>
|
||||||
<a-button size="small"
|
<a-button size="small"
|
||||||
@click="inbound.stream.ws.removeHeader(index)">
|
@click="inbound.stream.ws.addHeader('Host', '')">
|
||||||
-
|
+
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</a-row>
|
||||||
</a-input>
|
<a-input-group v-for="(header, index) in inbound.stream.ws.headers">
|
||||||
</a-input-group>
|
<a-input style="width: 50%" v-model.trim="header.name"
|
||||||
</a-form-item>
|
addon-before='{{ i18n "pages.inbounds.stream.general.name"}}'></a-input>
|
||||||
|
<a-input style="width: 50%" v-model.trim="header.value"
|
||||||
|
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||||
|
<template slot="addonAfter">
|
||||||
|
<a-button size="small"
|
||||||
|
@click="inbound.stream.ws.removeHeader(index)">
|
||||||
|
-
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</a-input-group>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1,60 +1,246 @@
|
|||||||
{{define "form/tlsSettings"}}
|
{{define "form/tlsSettings"}}
|
||||||
<!-- tls enable -->
|
<!-- tls enable -->
|
||||||
<a-form layout="inline" v-if="inbound.canSetTls()">
|
<a-form v-if="inbound.canSetTls()" layout="inline">
|
||||||
<a-form-item label="tls">
|
<a-form-item label="TLS">
|
||||||
<a-switch v-model="inbound.tls">
|
<a-switch v-model="inbound.tls">
|
||||||
</a-switch>
|
</a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item v-if="inbound.canEnableXTls()" label="xtls">
|
<a-form-item v-if="inbound.canEnableReality()" label="Reality">
|
||||||
<a-switch v-model="inbound.xtls"></a-switch>
|
<a-switch v-model="inbound.reality"></a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- tls settings -->
|
<!-- tls settings -->
|
||||||
<a-form v-if="inbound.tls || inbound.xtls" layout="inline">
|
<a-form v-if="inbound.tls" layout="inline">
|
||||||
<a-form-item label="MinVersion">
|
<table width="100%" class="ant-table-tbody">
|
||||||
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px">
|
<tr>
|
||||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
<td>SNI</td>
|
||||||
</a-select>
|
<td>
|
||||||
</a-form-item>
|
<a-form-item placeholder="Server Name Indication" v-if="inbound.tls">
|
||||||
<a-form-item label="MaxVersion">
|
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
|
||||||
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px">
|
</a-form-item>
|
||||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
</td>
|
||||||
</a-select>
|
</tr>
|
||||||
</a-form-item>
|
<tr>
|
||||||
<a-form-item label="CipherSuites">
|
<td>CipherSuites</td>
|
||||||
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px">
|
<td>
|
||||||
<a-select-option value="">auto</a-select-option>
|
<a-form-item>
|
||||||
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
|
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
</a-select>
|
<a-select-option value="">auto</a-select-option>
|
||||||
</a-form-item>
|
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||||
<a-form-item label='{{ i18n "domainName" }}'>
|
</a-select>
|
||||||
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
|
</a-form-item>
|
||||||
</a-form-item>
|
</td>
|
||||||
<a-form-item label="alpn" placeholder="http/1.1,h2">
|
</tr>
|
||||||
<a-input v-model.trim="inbound.stream.tls.alpn"></a-input>
|
<tr>
|
||||||
</a-form-item>
|
<td>MinVersion</td>
|
||||||
<a-form-item label='{{ i18n "certificate" }}'>
|
<td>
|
||||||
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
|
<a-form-item>
|
||||||
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
<a-select v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-radio-group>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<template v-if="inbound.stream.tls.certs[0].useFile">
|
</td>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
|
</tr>
|
||||||
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile"></a-input>
|
<tr>
|
||||||
</a-form-item>
|
<td>MaxVersion</td>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
<td>
|
||||||
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile"></a-input>
|
<a-form-item>
|
||||||
</a-form-item>
|
<a-select v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
</template>
|
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||||
<template v-else>
|
</a-select>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
</a-form-item>
|
||||||
<a-input type="textarea" :rows="2" v-model="inbound.stream.tls.certs[0].cert"></a-input>
|
</td>
|
||||||
</a-form-item>
|
</tr>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
|
<tr>
|
||||||
<a-input type="textarea" :rows="2" v-model="inbound.stream.tls.certs[0].key"></a-input>
|
<td>uTLS</td>
|
||||||
</a-form-item>
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-select v-model="inbound.stream.tls.settings.fingerprint"
|
||||||
|
style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
|
<a-select-option value=''>None</a-select-option>
|
||||||
|
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "domainName" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Alpn</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-checkbox-group v-model="inbound.stream.tls.alpn">
|
||||||
|
<a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox>
|
||||||
|
</a-checkbox-group>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Allow insecure</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<template v-for="cert,index in inbound.stream.tls.certs">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<a-form-item label='{{ i18n "certificate" }}'>
|
||||||
|
<a-radio-group v-model="cert.useFile" button-style="solid">
|
||||||
|
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||||
|
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
|
||||||
|
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<template v-if="cert.useFile">
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.inbounds.publicKeyPath" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="cert.certFile" style="width:250px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.inbounds.keyPath" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="cert.keyFile" style="width:250px;"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.inbounds.publicKeyContent" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.cert"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.inbounds.keyContent" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.key"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
</table>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<!-- reality settings -->
|
||||||
|
<a-form v-if="inbound.reality" layout="inline">
|
||||||
|
<table width="100%" class="ant-table-tbody">
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "domainName" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Show</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-switch v-model="inbound.stream.reality.show"></a-switch>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Xver</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>uTLS</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-select v-model="inbound.stream.reality.settings.fingerprint"
|
||||||
|
style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
|
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Dest</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Server Names</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Short Ids</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.stream.reality.shortIds" style="width:250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>SpiderX</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.stream.reality.settings.spiderX" style="width:250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Private Key</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Public Key</td>
|
||||||
|
<td>
|
||||||
|
<a-form-item>
|
||||||
|
<a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 250px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get new cert</a-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<template slot="actions" slot-scope="text, client, index">
|
<template slot="actions" slot-scope="text, client, index">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "qrCode" }}</template>
|
<template slot="title">{{ i18n "qrCode" }}</template>
|
||||||
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
|
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index,'',client.email);"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
||||||
@@ -21,24 +21,27 @@
|
|||||||
<a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
|
<a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
<template slot="enable" slot-scope="text, client, index">
|
||||||
|
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||||
|
</template>
|
||||||
<template slot="client" slot-scope="text, client">
|
<template slot="client" slot-scope="text, client">
|
||||||
[[ client.email ]]
|
[[ client.email ]]
|
||||||
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "disabled" }}</a-tag>
|
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template slot="traffic" slot-scope="text, client">
|
<template slot="traffic" slot-scope="text, client">
|
||||||
<a-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
|
<a-tag :color="statsColor(record, client.email)" @click="alert(usageColor(0,1024,512))">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
|
||||||
<template v-if="client._totalGB > 0">
|
<template v-if="client._totalGB > 0">
|
||||||
<a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]]GB</a-tag>
|
<a-tag :color="statsColor(record, client.email)">[[client._totalGB]]GB</a-tag>
|
||||||
<a-tag v-else color="cyan">[[client._totalGB]]GB</a-tag>
|
|
||||||
</template>
|
</template>
|
||||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template slot="expiryTime" slot-scope="text, client, index">
|
<template slot="expiryTime" slot-scope="text, client, index">
|
||||||
<template v-if="client._expiryTime > 0">
|
<template v-if="client.expiryTime > 0">
|
||||||
<a-tag :color="isExpiry(record, index)? 'red' : 'blue'">
|
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)">
|
||||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
<a-tag v-else-if="client.expiryTime < 0" color="cyan">[[ client._expiryTime ]] {{ i18n "pages.client.days" }}</a-tag>
|
||||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
|
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
|
||||||
:closable="true"
|
:closable="true"
|
||||||
:mask-closable="true"
|
:mask-closable="true"
|
||||||
|
:class="themeSwitcher.darkCardClass"
|
||||||
:footer="null"
|
:footer="null"
|
||||||
width="600px"
|
width="600px"
|
||||||
>
|
>
|
||||||
@@ -40,35 +41,47 @@
|
|||||||
|
|
||||||
<template v-if="inbound.isGrpc">
|
<template v-if="inbound.isGrpc">
|
||||||
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
|
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
|
||||||
|
<tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
|
||||||
</template>
|
</template>
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr colspan="2">
|
<tr colspan="2" v-if="dbInbound.hasLink()">
|
||||||
<td v-if="inbound.tls">
|
<td v-if="inbound.tls">
|
||||||
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
||||||
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||||
</td>
|
</td>
|
||||||
<td v-else-if="inbound.xtls">
|
<td v-else-if="inbound.reality">
|
||||||
xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
||||||
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
|
||||||
</td>
|
</td>
|
||||||
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
|
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<template v-if="infoModal.clientSettings">
|
||||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||||
<table style="margin-bottom: 10px; width: 100%;">
|
<table style="margin-bottom: 10px;">
|
||||||
<tr>
|
<tr v-for="col,index in Object.keys(infoModal.clientSettings).slice(0, 3)">
|
||||||
<th v-for="col in Object.keys(infoModal.clientSettings).slice(0, 3)">[[ col ]]</th>
|
<td>[[ col ]]</td>
|
||||||
|
<td><a-tag color="green">[[ infoModal.clientSettings[col] ]]</a-tag></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td v-for="col in Object.values(infoModal.clientSettings).slice(0, 3)"><a-tag color="green">[[ col ]]</a-tag></td>
|
<td>{{ i18n "status" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
|
||||||
|
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
|
||||||
|
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<table style="margin-bottom: 10px; width: 100%;">
|
<table style="margin-bottom: 10px; width: 100%;">
|
||||||
<tr><th>{{ i18n "usage" }}</th><th>{{ i18n "pages.inbounds.totalFlow" }}</th><th>{{ i18n "pages.inbounds.expireDate" }}</th><th>{{ i18n "enable" }}</th></tr>
|
<tr>
|
||||||
|
<th>{{ i18n "usage" }}</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)">
|
<a-tag v-if="infoModal.clientStats" color="green">
|
||||||
[[ sizeFormat(infoModal.clientStats['up']) ]] /
|
[[ sizeFormat(infoModal.clientStats['up']) ]] /
|
||||||
[[ sizeFormat(infoModal.clientStats['down']) ]]
|
[[ sizeFormat(infoModal.clientStats['down']) ]]
|
||||||
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
|
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
|
||||||
@@ -80,55 +93,121 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<template v-if="infoModal.clientSettings.expiryTime > 0">
|
<template v-if="infoModal.clientSettings.expiryTime > 0">
|
||||||
<a-tag :color="infoModal.isExpired ? 'red' : 'blue'">
|
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
|
||||||
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
|
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="cyan">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
|
||||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
|
|
||||||
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<table v-if="infoModal.clientSettings.subId + infoModal.clientSettings.tgId" style="margin-bottom: 10px;">
|
||||||
|
<tr v-if="infoModal.clientSettings.subId">
|
||||||
|
<td>Subscription link</td>
|
||||||
|
<td>
|
||||||
|
<a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a>
|
||||||
|
<a-icon id="copy-sub-link" type="snippets" @click="copyToClipboard('copy-sub-link', subBase + infoModal.clientSettings.subId)"></a-icon>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="infoModal.clientSettings.tgId">
|
||||||
|
<td>Telegram Username</td>
|
||||||
|
<td><a :href="[[ tgBase + infoModal.clientSettings.tgId ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-divider></a-divider>
|
||||||
|
<table v-if="inbound.protocol == Protocols.SHADOWSOCKS" style="margin-bottom: 10px; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<th>{{ i18n "encryption" }}</th>
|
||||||
|
<th>{{ i18n "password" }}</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.network" }}</th>
|
||||||
|
</tr><tr>
|
||||||
|
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
|
||||||
|
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
|
||||||
|
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.network" }}</th>
|
||||||
|
<th>FollowRedirect</th>
|
||||||
|
</tr><tr>
|
||||||
|
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
|
||||||
|
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
|
||||||
|
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
||||||
|
<td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</table>
|
||||||
|
<table v-if="inbound.protocol == Protocols.SOCKS" style="margin-bottom: 10px; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<th>{{ i18n "password" }} Auth</th>
|
||||||
|
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
|
||||||
|
<th>IP</th>
|
||||||
|
</tr><tr>
|
||||||
|
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
|
||||||
|
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
|
||||||
|
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
|
||||||
|
</tr><tr v-if="inbound.settings.auth == 'password'">
|
||||||
|
<td> </td>
|
||||||
|
<td>{{ i18n "username" }}</td>
|
||||||
|
<td>{{ i18n "password" }}</td>
|
||||||
|
</tr><tr v-for="account,index in inbound.settings.accounts">
|
||||||
|
<td><a-tag color="green">[[ index ]]</a-tag></td>
|
||||||
|
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
|
||||||
|
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</table>
|
||||||
|
<table v-if="inbound.protocol == Protocols.HTTP" style="margin-bottom: 10px; width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<th> </th>
|
||||||
|
<th>{{ i18n "username" }}</th>
|
||||||
|
<th>{{ i18n "password" }}</th>
|
||||||
|
</tr><tr v-for="account,index in inbound.settings.accounts">
|
||||||
|
<td><a-tag color="green">[[ index ]]</a-tag></td>
|
||||||
|
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
|
||||||
|
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
<div v-if="dbInbound.hasLink()">
|
<div v-if="dbInbound.hasLink()">
|
||||||
<a-divider>URL</a-divider>
|
<a-divider>URL</a-divider>
|
||||||
<p>[[ infoModal.link ]]</p>
|
<p>[[ infoModal.link ]]</p>
|
||||||
<button class="ant-btn ant-btn-primary" id="copy-url-link"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button>
|
<button class="ant-btn ant-btn-primary" id="copy-url-link" @click="copyToClipboard('copy-url-link', infoModal.link)">
|
||||||
|
<a-icon type="snippets"></a-icon>{{ i18n "copy" }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
const infoModal = {
|
const infoModal = {
|
||||||
visible: false,
|
visible: false,
|
||||||
inbound: new Inbound(),
|
inbound: new Inbound(),
|
||||||
dbInbound: new DBInbound(),
|
dbInbound: new DBInbound(),
|
||||||
clientSettings: new Inbound.Settings(),
|
settings: null,
|
||||||
|
clientSettings: null,
|
||||||
clientStats: [],
|
clientStats: [],
|
||||||
upStats: 0,
|
upStats: 0,
|
||||||
downStats: 0,
|
downStats: 0,
|
||||||
clipboard: null,
|
clipboard: null,
|
||||||
link: null,
|
link: null,
|
||||||
index: 0,
|
index: null,
|
||||||
isExpired: false,
|
isExpired: false,
|
||||||
show(dbInbound, index=0) {
|
show(dbInbound, index) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
this.dbInbound = new DBInbound(dbInbound);
|
this.dbInbound = new DBInbound(dbInbound);
|
||||||
this.link = dbInbound.genLink(index);
|
this.link = dbInbound.genLink(index);
|
||||||
this.clientSettings = Object.values(JSON.parse(this.inbound.settings).clients)[index];
|
this.settings = JSON.parse(this.inbound.settings);
|
||||||
|
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
|
||||||
this.isExpired = this.inbound.isExpiry(index);
|
this.isExpired = this.inbound.isExpiry(index);
|
||||||
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email);
|
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
infoModalApp.$nextTick(() => {
|
|
||||||
if (this.clipboard === null) {
|
|
||||||
this.clipboard = new ClipboardJS('#copy-url-link', {
|
|
||||||
text: () => this.link,
|
|
||||||
});
|
|
||||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
infoModal.visible = false;
|
infoModal.visible = false;
|
||||||
@@ -146,24 +225,27 @@
|
|||||||
get inbound() {
|
get inbound() {
|
||||||
return this.infoModal.inbound;
|
return this.infoModal.inbound;
|
||||||
},
|
},
|
||||||
get isEnable() {
|
get isActive() {
|
||||||
if(infoModal.clientStats){
|
if(infoModal.clientStats){
|
||||||
return infoModal.clientStats.enable;
|
return infoModal.clientStats.enable;
|
||||||
}
|
}
|
||||||
return true;
|
return infoModal.dbInbound.isEnable;
|
||||||
}
|
},
|
||||||
|
get isEnable() {
|
||||||
|
if(infoModal.clientSettings){
|
||||||
|
return infoModal.clientSettings.enable;
|
||||||
|
}
|
||||||
|
return infoModal.dbInbound.isEnable;
|
||||||
|
},
|
||||||
|
get subBase() {
|
||||||
|
return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port:"") + basePath + "sub/";
|
||||||
|
},
|
||||||
|
get tgBase() {
|
||||||
|
return "https://t.me/"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setQrCode(elmentId,index) {
|
copyToClipboard(elmentId,content) {
|
||||||
content = infoModal.inbound.genLink(infoModal.dbInbound.address,infoModal.dbInbound.remark,index)
|
|
||||||
|
|
||||||
new QRious({
|
|
||||||
element: document.querySelector('#'+elmentId),
|
|
||||||
size: 260,
|
|
||||||
value: content,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
copyTextToClipboard(elmentId,content) {
|
|
||||||
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
|
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
|
||||||
text: () => content,
|
text: () => content,
|
||||||
});
|
});
|
||||||
@@ -173,10 +255,7 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
statsColor(stats) {
|
statsColor(stats) {
|
||||||
if(!stats) return 'blue'
|
return usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
|
||||||
if(stats['total'] === 0) return 'blue'
|
|
||||||
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
|
|
||||||
else return 'red'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{{define "inboundModal"}}
|
{{define "inboundModal"}}
|
||||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
|
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
|
||||||
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||||
|
:class="themeSwitcher.darkCardClass"
|
||||||
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||||
{{template "form/inbound"}}
|
{{template "form/inbound"}}
|
||||||
</a-modal>
|
</a-modal>
|
||||||
@@ -42,6 +43,15 @@
|
|||||||
loading(loading) {
|
loading(loading) {
|
||||||
inModal.confirmLoading = loading;
|
inModal.confirmLoading = loading;
|
||||||
},
|
},
|
||||||
|
getClients(protocol, clientSettings) {
|
||||||
|
switch(protocol){
|
||||||
|
case Protocols.VMESS: return clientSettings.vmesses;
|
||||||
|
case Protocols.VLESS: return clientSettings.vlesses;
|
||||||
|
case Protocols.TROJAN: return clientSettings.trojans;
|
||||||
|
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const protocols = {
|
const protocols = {
|
||||||
@@ -61,6 +71,7 @@
|
|||||||
inModal: inModal,
|
inModal: inModal,
|
||||||
Protocols: protocols,
|
Protocols: protocols,
|
||||||
SSMethods: SSMethods,
|
SSMethods: SSMethods,
|
||||||
|
delayedStart: false,
|
||||||
get inbound() {
|
get inbound() {
|
||||||
return inModal.inbound;
|
return inModal.inbound;
|
||||||
},
|
},
|
||||||
@@ -69,31 +80,39 @@
|
|||||||
},
|
},
|
||||||
get isEdit() {
|
get isEdit() {
|
||||||
return inModal.isEdit;
|
return inModal.isEdit;
|
||||||
}
|
},
|
||||||
|
get client() {
|
||||||
|
return inModal.getClients(this.inbound.protocol, this.inbound.settings)[0];
|
||||||
|
},
|
||||||
|
get delayedExpireDays() {
|
||||||
|
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
|
||||||
|
},
|
||||||
|
set delayedExpireDays(days){
|
||||||
|
this.client.expiryTime = -86400000 * days;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
streamNetworkChange(oldValue) {
|
streamNetworkChange() {
|
||||||
if (oldValue === 'kcp') {
|
if (!inModal.inbound.canSetTls()) {
|
||||||
this.inModal.inbound.tls = false;
|
this.inModal.inbound.stream.security = 'none';
|
||||||
|
}
|
||||||
|
if (!inModal.inbound.canEnableReality()) {
|
||||||
|
this.inModal.inbound.reality = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addClient(protocol, clients) {
|
setDefaultCertData(index){
|
||||||
switch (protocol) {
|
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
|
||||||
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
|
inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;
|
||||||
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
},
|
||||||
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
async getNewX25519Cert(){
|
||||||
default: return null;
|
inModal.loading(true);
|
||||||
|
const msg = await HttpUtil.post('/server/getNewX25519Cert');
|
||||||
|
inModal.loading(false);
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
},
|
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
|
||||||
removeClient(index, clients) {
|
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
|
||||||
clients.splice(index, 1);
|
|
||||||
},
|
|
||||||
isExpiry(index) {
|
|
||||||
return this.inbound.isExpiry(index)
|
|
||||||
},
|
|
||||||
isClientEnable(email) {
|
|
||||||
clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
|
|
||||||
return clientStats ? clientStats['enable'] : true
|
|
||||||
},
|
},
|
||||||
getNewEmail(client) {
|
getNewEmail(client) {
|
||||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||||
|
|||||||
@@ -12,10 +12,11 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<a-layout id="app" v-cloak>
|
<a-layout id="app" v-cloak>
|
||||||
{{ template "commonSider" . }}
|
{{ template "commonSider" . }}
|
||||||
<a-layout id="content-layout">
|
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-spin :spinning="spinning" :delay="500" tip="loading">
|
<a-spin :spinning="spinning" :delay="500" tip="loading">
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-card hoverable style="margin-bottom: 20px;">
|
<a-card hoverable style="margin-bottom: 20px;" :class="themeSwitcher.darkCardClass">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :xs="24" :sm="24" :lg="12">
|
<a-col :xs="24" :sm="24" :lg="12">
|
||||||
{{ i18n "pages.inbounds.totalDownUp" }}:
|
{{ i18n "pages.inbounds.totalDownUp" }}:
|
||||||
@@ -41,19 +42,79 @@
|
|||||||
<a-col :xs="24" :sm="24" :lg="12">
|
<a-col :xs="24" :sm="24" :lg="12">
|
||||||
{{ i18n "clients" }}:
|
{{ i18n "clients" }}:
|
||||||
<a-tag color="green">[[ total.clients ]]</a-tag>
|
<a-tag color="green">[[ total.clients ]]</a-tag>
|
||||||
<a-tag color="blue">{{ i18n "enabled" }} [[ total.active ]]</a-tag>
|
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||||
<a-tag color="red">{{ i18n "disabled" }} [[ total.deactive ]]</a-tag>
|
<template slot="content">
|
||||||
|
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
|
||||||
|
</a-popover>
|
||||||
|
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||||
|
<template slot="content">
|
||||||
|
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
|
||||||
|
</a-popover>
|
||||||
|
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||||
|
<template slot="content">
|
||||||
|
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
|
||||||
|
</a-popover>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-card>
|
</a-card>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-card hoverable>
|
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||||
<div slot="title">
|
<div slot="title">
|
||||||
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
|
<a-row>
|
||||||
<a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
|
<a-col :xs="24" :sm="24" :lg="12">
|
||||||
|
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
|
||||||
|
<a-dropdown :trigger="['click']">
|
||||||
|
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
|
||||||
|
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
|
||||||
|
<a-menu-item key="export">
|
||||||
|
<a-icon type="export"></a-icon>
|
||||||
|
{{ i18n "pages.inbounds.export" }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="resetInbounds">
|
||||||
|
<a-icon type="reload"></a-icon>
|
||||||
|
{{ i18n "pages.inbounds.resetAllTraffic" }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="resetClients">
|
||||||
|
<a-icon type="file-done"></a-icon>
|
||||||
|
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="delDepletedClients">
|
||||||
|
<a-icon type="rest"></a-icon>
|
||||||
|
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</a-dropdown>
|
||||||
|
</a-col>
|
||||||
|
<a-col :xs="24" :sm="24" :lg="12" style="text-align: right;">
|
||||||
|
<a-select v-model="refreshInterval"
|
||||||
|
v-if="isRefreshEnabled"
|
||||||
|
@change="changeRefreshInterval"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
|
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
<a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon>
|
||||||
|
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
</div>
|
</div>
|
||||||
<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
|
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
|
||||||
|
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
|
||||||
|
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
|
||||||
|
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
|
||||||
|
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
|
||||||
|
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
<a-switch v-model="enableFilter"
|
||||||
|
checked-children="{{ i18n "search" }}" un-checked-children="{{ i18n "filter" }}"
|
||||||
|
@change="toggleFilter">
|
||||||
|
</a-switch>
|
||||||
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||||
:data-source="searchedInbounds"
|
:data-source="searchedInbounds"
|
||||||
:loading="spinning" :scroll="{ x: 1300 }"
|
:loading="spinning" :scroll="{ x: 1300 }"
|
||||||
@@ -63,32 +124,45 @@
|
|||||||
<template slot="action" slot-scope="text, dbInbound">
|
<template slot="action" slot-scope="text, dbInbound">
|
||||||
<a-dropdown :trigger="['click']">
|
<a-dropdown :trigger="['click']">
|
||||||
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon>
|
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon>
|
||||||
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)">
|
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
|
||||||
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
|
|
||||||
<a-icon type="qrcode"></a-icon>
|
|
||||||
{{ i18n "qrCode" }}
|
|
||||||
</a-menu-item>
|
|
||||||
<a-menu-item key="edit">
|
<a-menu-item key="edit">
|
||||||
<a-icon type="edit"></a-icon>
|
<a-icon type="edit"></a-icon>
|
||||||
{{ i18n "edit" }}
|
{{ i18n "edit" }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
|
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS">
|
||||||
<a-menu-item key="addClient">
|
<a-menu-item key="addClient">
|
||||||
<a-icon type="user"></a-icon>
|
<a-icon type="user-add"></a-icon>
|
||||||
{{ i18n "pages.client.add"}}
|
{{ i18n "pages.client.add"}}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-menu-item key="addBulkClient">
|
<a-menu-item key="addBulkClient">
|
||||||
<a-icon type="team"></a-icon>
|
<a-icon type="usergroup-add"></a-icon>
|
||||||
{{ i18n "pages.client.bulk"}}
|
{{ i18n "pages.client.bulk"}}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
<a-menu-item key="resetClients">
|
||||||
|
<a-icon type="file-done"></a-icon>
|
||||||
|
{{ i18n "pages.inbounds.resetInboundClientTraffics"}}
|
||||||
|
</a-menu-item>
|
||||||
<a-menu-item key="export">
|
<a-menu-item key="export">
|
||||||
<a-icon type="export"></a-icon>
|
<a-icon type="export"></a-icon>
|
||||||
{{ i18n "pages.inbounds.export"}}
|
{{ i18n "pages.inbounds.export"}}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
<a-menu-item key="delDepletedClients">
|
||||||
|
<a-icon type="rest"></a-icon>
|
||||||
|
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||||
|
</a-menu-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-menu-item key="showInfo">
|
||||||
|
<a-icon type="info-circle"></a-icon>
|
||||||
|
{{ i18n "info"}}
|
||||||
|
</a-menu-item>
|
||||||
</template>
|
</template>
|
||||||
<a-menu-item key="resetTraffic">
|
<a-menu-item key="resetTraffic">
|
||||||
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
|
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
<a-menu-item key="clone">
|
||||||
|
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
|
||||||
|
</a-menu-item>
|
||||||
<a-menu-item key="delete">
|
<a-menu-item key="delete">
|
||||||
<span style="color: #FF4D4F">
|
<span style="color: #FF4D4F">
|
||||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||||
@@ -98,7 +172,35 @@
|
|||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</template>
|
</template>
|
||||||
<template slot="protocol" slot-scope="text, dbInbound">
|
<template slot="protocol" slot-scope="text, dbInbound">
|
||||||
<a-tag color="blue">[[ dbInbound.protocol ]]</a-tag>
|
<a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
|
||||||
|
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||||
|
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
|
||||||
|
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">tls</a-tag>
|
||||||
|
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">reality</a-tag>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template slot="clients" slot-scope="text, dbInbound">
|
||||||
|
<template v-if="clientCount[dbInbound.id]">
|
||||||
|
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
|
||||||
|
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||||
|
<template slot="content">
|
||||||
|
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
|
||||||
|
</a-popover>
|
||||||
|
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||||
|
<template slot="content">
|
||||||
|
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
|
||||||
|
</a-popover>
|
||||||
|
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||||
|
<template slot="content">
|
||||||
|
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
|
||||||
|
</a-popover>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template slot="traffic" slot-scope="text, dbInbound">
|
<template slot="traffic" slot-scope="text, dbInbound">
|
||||||
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
|
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
|
||||||
@@ -108,14 +210,6 @@
|
|||||||
</template>
|
</template>
|
||||||
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
|
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template slot="stream" slot-scope="text, dbInbound, index">
|
|
||||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
|
||||||
<a-tag color="green">[[ inbounds[index].stream.network ]]</a-tag>
|
|
||||||
<a-tag v-if="inbounds[index].stream.isTls" color="blue">tls</a-tag>
|
|
||||||
<a-tag v-if="inbounds[index].stream.isXTls" color="blue">xtls</a-tag>
|
|
||||||
</template>
|
|
||||||
<template v-else>{{ i18n "none" }}</template>
|
|
||||||
</template>
|
|
||||||
<template slot="enable" slot-scope="text, dbInbound">
|
<template slot="enable" slot-scope="text, dbInbound">
|
||||||
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
|
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
|
||||||
</template>
|
</template>
|
||||||
@@ -132,20 +226,20 @@
|
|||||||
</template>
|
</template>
|
||||||
<template slot="expandedRowRender" slot-scope="record">
|
<template slot="expandedRowRender" slot-scope="record">
|
||||||
<a-table
|
<a-table
|
||||||
v-if="(record.protocol === Protocols.VLESS) || (record.protocol === Protocols.VMESS)"
|
v-if="(record.protocol === Protocols.VLESS) || (record.protocol === Protocols.VMESS)"
|
||||||
:row-key="client => client.id"
|
:row-key="client => client.id"
|
||||||
:columns="innerColumns"
|
:columns="innerColumns"
|
||||||
:data-source="getInboundClients(record)"
|
:data-source="getInboundClients(record)"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
>
|
>
|
||||||
{{template "client_table"}}
|
{{template "client_table"}}
|
||||||
</a-table>
|
</a-table>
|
||||||
<a-table
|
<a-table
|
||||||
v-else-if="record.protocol === Protocols.TROJAN"
|
v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS"
|
||||||
:row-key="client => client.id"
|
:row-key="client => client.id"
|
||||||
:columns="innerTrojanColumns"
|
:columns="innerTrojanColumns"
|
||||||
:data-source="getInboundClients(record)"
|
:data-source="getInboundClients(record)"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
>
|
>
|
||||||
{{template "client_table"}}
|
{{template "client_table"}}
|
||||||
</a-table>
|
</a-table>
|
||||||
@@ -158,12 +252,12 @@
|
|||||||
</a-layout>
|
</a-layout>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
{{template "js" .}}
|
{{template "js" .}}
|
||||||
|
{{template "component/themeSwitcher" .}}
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
const columns = [{
|
const columns = [{
|
||||||
title: '{{ i18n "pages.inbounds.operate" }}',
|
title: '{{ i18n "pages.inbounds.operate" }}',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 30,
|
width: 40,
|
||||||
scopedSlots: { customRender: 'action' },
|
scopedSlots: { customRender: 'action' },
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.enable" }}',
|
title: '{{ i18n "pages.inbounds.enable" }}',
|
||||||
@@ -171,35 +265,35 @@
|
|||||||
width: 40,
|
width: 40,
|
||||||
scopedSlots: { customRender: 'enable' },
|
scopedSlots: { customRender: 'enable' },
|
||||||
}, {
|
}, {
|
||||||
title: "Id",
|
title: "ID",
|
||||||
align: 'center',
|
align: 'center',
|
||||||
dataIndex: "id",
|
dataIndex: "id",
|
||||||
width: 30,
|
width: 30,
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.remark" }}',
|
title: '{{ i18n "pages.inbounds.remark" }}',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 60,
|
width: 50,
|
||||||
dataIndex: "remark",
|
dataIndex: "remark",
|
||||||
}, {
|
|
||||||
title: '{{ i18n "pages.inbounds.protocol" }}',
|
|
||||||
align: 'center',
|
|
||||||
width: 40,
|
|
||||||
scopedSlots: { customRender: 'protocol' },
|
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.port" }}',
|
title: '{{ i18n "pages.inbounds.port" }}',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
dataIndex: "port",
|
dataIndex: "port",
|
||||||
width: 30,
|
width: 40,
|
||||||
|
}, {
|
||||||
|
title: '{{ i18n "pages.inbounds.protocol" }}',
|
||||||
|
align: 'left',
|
||||||
|
width: 70,
|
||||||
|
scopedSlots: { customRender: 'protocol' },
|
||||||
|
}, {
|
||||||
|
title: '{{ i18n "clients" }}',
|
||||||
|
align: 'left',
|
||||||
|
width: 50,
|
||||||
|
scopedSlots: { customRender: 'clients' },
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
|
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 150,
|
width: 120,
|
||||||
scopedSlots: { customRender: 'traffic' },
|
scopedSlots: { customRender: 'traffic' },
|
||||||
}, {
|
|
||||||
title: '{{ i18n "pages.inbounds.transportConfig" }}',
|
|
||||||
align: 'center',
|
|
||||||
width: 60,
|
|
||||||
scopedSlots: { customRender: 'stream' },
|
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
@@ -208,19 +302,21 @@
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
const innerColumns = [
|
const innerColumns = [
|
||||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 60, scopedSlots: { customRender: 'actions' } },
|
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||||
|
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 80, scopedSlots: { customRender: 'traffic' } },
|
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||||
{ title: 'UID', width: 150, dataIndex: "id" },
|
{ title: 'UID', width: 120, dataIndex: "id" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const innerTrojanColumns = [
|
const innerTrojanColumns = [
|
||||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 60, scopedSlots: { customRender: 'actions' } },
|
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||||
|
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 80, scopedSlots: { customRender: 'traffic' } },
|
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||||
{ title: 'Password', width: 150, dataIndex: "password" },
|
{ title: 'Password', width: 120, dataIndex: "password" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
@@ -228,36 +324,98 @@
|
|||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
siderDrawer,
|
siderDrawer,
|
||||||
|
themeSwitcher,
|
||||||
spinning: false,
|
spinning: false,
|
||||||
inbounds: [],
|
inbounds: [],
|
||||||
dbInbounds: [],
|
dbInbounds: [],
|
||||||
searchKey: '',
|
searchKey: '',
|
||||||
|
enableFilter: false,
|
||||||
|
filterBy: '',
|
||||||
searchedInbounds: [],
|
searchedInbounds: [],
|
||||||
|
expireDiff: 0,
|
||||||
|
trafficDiff: 0,
|
||||||
|
defaultCert: '',
|
||||||
|
defaultKey: '',
|
||||||
|
clientCount: {},
|
||||||
|
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
||||||
|
refreshing: false,
|
||||||
|
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loading(spinning=true) {
|
loading(spinning = true) {
|
||||||
this.spinning = spinning;
|
this.spinning = spinning;
|
||||||
},
|
},
|
||||||
async getDBInbounds() {
|
async getDBInbounds() {
|
||||||
this.loading();
|
this.refreshing = true;
|
||||||
const msg = await HttpUtil.post('/xui/inbound/list');
|
const msg = await HttpUtil.post('/xui/inbound/list');
|
||||||
this.loading(false);
|
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setInbounds(msg.obj);
|
this.setInbounds(msg.obj);
|
||||||
this.searchKey = '';
|
setTimeout(() => {
|
||||||
|
this.refreshing = false;
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
async getDefaultSettings() {
|
||||||
|
const msg = await HttpUtil.post('/xui/setting/defaultSettings');
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.expireDiff = msg.obj.expireDiff * 86400000;
|
||||||
|
this.trafficDiff = msg.obj.trafficDiff * 1073741824;
|
||||||
|
this.defaultCert = msg.obj.defaultCert;
|
||||||
|
this.defaultKey = msg.obj.defaultKey;
|
||||||
},
|
},
|
||||||
setInbounds(dbInbounds) {
|
setInbounds(dbInbounds) {
|
||||||
this.inbounds.splice(0);
|
this.inbounds.splice(0);
|
||||||
this.dbInbounds.splice(0);
|
this.dbInbounds.splice(0);
|
||||||
this.searchedInbounds.splice(0);
|
|
||||||
for (const inbound of dbInbounds) {
|
for (const inbound of dbInbounds) {
|
||||||
const dbInbound = new DBInbound(inbound);
|
const dbInbound = new DBInbound(inbound);
|
||||||
this.inbounds.push(dbInbound.toInbound());
|
to_inbound = dbInbound.toInbound()
|
||||||
|
this.inbounds.push(to_inbound);
|
||||||
this.dbInbounds.push(dbInbound);
|
this.dbInbounds.push(dbInbound);
|
||||||
this.searchedInbounds.push(dbInbound);
|
if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
|
||||||
|
this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if(this.enableFilter){
|
||||||
|
this.filterInbounds();
|
||||||
|
} else {
|
||||||
|
this.searchInbounds(this.searchKey);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getClientCounts(dbInbound, inbound) {
|
||||||
|
let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [];
|
||||||
|
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||||
|
clientStats = dbInbound.clientStats
|
||||||
|
now = new Date().getTime()
|
||||||
|
if (clients) {
|
||||||
|
clientCount = clients.length;
|
||||||
|
if (dbInbound.enable) {
|
||||||
|
clients.forEach(client => {
|
||||||
|
client.enable ? active.push(client.email) : deactive.push(client.email);
|
||||||
|
});
|
||||||
|
clientStats.forEach(client => {
|
||||||
|
if (!client.enable) {
|
||||||
|
depleted.push(client.email);
|
||||||
|
} else {
|
||||||
|
if ((client.expiryTime > 0 && (client.expiryTime - now < this.expireDiff)) ||
|
||||||
|
(client.total > 0 && (client.total - (client.up + client.down) < this.trafficDiff))) expiring.push(client.email);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
clients.forEach(client => {
|
||||||
|
deactive.push(client.email);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
clients: clientCount,
|
||||||
|
active: active,
|
||||||
|
deactive: deactive,
|
||||||
|
depleted: depleted,
|
||||||
|
expiring: expiring,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
searchInbounds(key) {
|
searchInbounds(key) {
|
||||||
if (ObjectUtil.isEmpty(key)) {
|
if (ObjectUtil.isEmpty(key)) {
|
||||||
@@ -268,10 +426,10 @@
|
|||||||
if (ObjectUtil.deepSearch(inbound, key)) {
|
if (ObjectUtil.deepSearch(inbound, key)) {
|
||||||
const newInbound = new DBInbound(inbound);
|
const newInbound = new DBInbound(inbound);
|
||||||
const inboundSettings = JSON.parse(inbound.settings);
|
const inboundSettings = JSON.parse(inbound.settings);
|
||||||
if (inboundSettings.hasOwnProperty('clients')){
|
if (inboundSettings.hasOwnProperty('clients')) {
|
||||||
const searchedSettings = { "clients": [] };
|
const searchedSettings = { "clients": [] };
|
||||||
inboundSettings.clients.forEach(client => {
|
inboundSettings.clients.forEach(client => {
|
||||||
if (ObjectUtil.deepSearch(client, key)){
|
if (ObjectUtil.deepSearch(client, key)) {
|
||||||
searchedSettings.clients.push(client);
|
searchedSettings.clients.push(client);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -282,11 +440,62 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
filterInbounds() {
|
||||||
|
if (ObjectUtil.isEmpty(this.filterBy)) {
|
||||||
|
this.searchedInbounds = this.dbInbounds.slice();
|
||||||
|
} else {
|
||||||
|
this.searchedInbounds.splice(0, this.searchedInbounds.length);
|
||||||
|
this.dbInbounds.forEach(inbound => {
|
||||||
|
const newInbound = new DBInbound(inbound);
|
||||||
|
const inboundSettings = JSON.parse(inbound.settings);
|
||||||
|
if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)){
|
||||||
|
const list = this.clientCount[inbound.id][this.filterBy];
|
||||||
|
if (list.length > 0) {
|
||||||
|
const filteredSettings = { "clients": [] };
|
||||||
|
inboundSettings.clients.forEach(client => {
|
||||||
|
if (list.includes(client.email)) {
|
||||||
|
filteredSettings.clients.push(client);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
|
||||||
|
this.searchedInbounds.push(newInbound);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleFilter(){
|
||||||
|
if(this.enableFilter) {
|
||||||
|
this.searchKey = '';
|
||||||
|
} else {
|
||||||
|
this.filterBy = '';
|
||||||
|
this.searchedInbounds = this.dbInbounds.slice();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
generalActions(action) {
|
||||||
|
switch (action.key) {
|
||||||
|
case "export":
|
||||||
|
this.exportAllLinks();
|
||||||
|
break;
|
||||||
|
case "resetInbounds":
|
||||||
|
this.resetAllTraffic();
|
||||||
|
break;
|
||||||
|
case "resetClients":
|
||||||
|
this.resetAllClientTraffics(-1);
|
||||||
|
break;
|
||||||
|
case "delDepletedClients":
|
||||||
|
this.delDepletedClients(-1)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
clickAction(action, dbInbound) {
|
clickAction(action, dbInbound) {
|
||||||
switch (action.key) {
|
switch (action.key) {
|
||||||
case "qrcode":
|
case "qrcode":
|
||||||
this.showQrcode(dbInbound);
|
this.showQrcode(dbInbound);
|
||||||
break;
|
break;
|
||||||
|
case "showInfo":
|
||||||
|
this.showInfo(dbInbound);
|
||||||
|
break;
|
||||||
case "edit":
|
case "edit":
|
||||||
this.openEditInbound(dbInbound.id);
|
this.openEditInbound(dbInbound.id);
|
||||||
break;
|
break;
|
||||||
@@ -302,15 +511,24 @@
|
|||||||
case "resetTraffic":
|
case "resetTraffic":
|
||||||
this.resetTraffic(dbInbound.id);
|
this.resetTraffic(dbInbound.id);
|
||||||
break;
|
break;
|
||||||
|
case "resetClients":
|
||||||
|
this.resetAllClientTraffics(dbInbound.id);
|
||||||
|
break;
|
||||||
|
case "clone":
|
||||||
|
this.openCloneInbound(dbInbound);
|
||||||
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
this.delInbound(dbInbound.id);
|
this.delInbound(dbInbound.id);
|
||||||
break;
|
break;
|
||||||
|
case "delDepletedClients":
|
||||||
|
this.delDepletedClients(dbInbound.id)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openAddInbound() {
|
openAddInbound() {
|
||||||
inModal.show({
|
inModal.show({
|
||||||
title: '{{ i18n "pages.inbounds.addInbound"}}',
|
title: '{{ i18n "pages.inbounds.addInbound"}}',
|
||||||
okText: '{{ i18n "pages.inbounds.addTo"}}',
|
okText: '{{ i18n "pages.inbounds.create"}}',
|
||||||
cancelText: '{{ i18n "close" }}',
|
cancelText: '{{ i18n "close" }}',
|
||||||
confirm: async (inbound, dbInbound) => {
|
confirm: async (inbound, dbInbound) => {
|
||||||
inModal.loading();
|
inModal.loading();
|
||||||
@@ -325,7 +543,7 @@
|
|||||||
const inbound = dbInbound.toInbound();
|
const inbound = dbInbound.toInbound();
|
||||||
inModal.show({
|
inModal.show({
|
||||||
title: '{{ i18n "pages.inbounds.modifyInbound"}}',
|
title: '{{ i18n "pages.inbounds.modifyInbound"}}',
|
||||||
okText: '{{ i18n "pages.inbounds.revise"}}',
|
okText: '{{ i18n "pages.inbounds.update"}}',
|
||||||
cancelText: '{{ i18n "close" }}',
|
cancelText: '{{ i18n "close" }}',
|
||||||
inbound: inbound,
|
inbound: inbound,
|
||||||
dbInbound: dbInbound,
|
dbInbound: dbInbound,
|
||||||
@@ -336,6 +554,38 @@
|
|||||||
},
|
},
|
||||||
isEdit: true
|
isEdit: true
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
openCloneInbound(dbInbound) {
|
||||||
|
this.$confirm({
|
||||||
|
title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
|
||||||
|
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
|
||||||
|
okText: '{{ i18n "pages.inbounds.update"}}',
|
||||||
|
cancelText: '{{ i18n "cancel" }}',
|
||||||
|
onOk: () => {
|
||||||
|
const baseInbound = dbInbound.toInbound();
|
||||||
|
dbInbound.up = 0;
|
||||||
|
dbInbound.down = 0;
|
||||||
|
this.cloneInbound(baseInbound, dbInbound);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async cloneInbound(baseInbound, dbInbound) {
|
||||||
|
const data = {
|
||||||
|
up: dbInbound.up,
|
||||||
|
down: dbInbound.down,
|
||||||
|
total: dbInbound.total,
|
||||||
|
remark: dbInbound.remark + " - Cloned",
|
||||||
|
enable: dbInbound.enable,
|
||||||
|
expiryTime: dbInbound.expiryTime,
|
||||||
|
|
||||||
|
listen: '',
|
||||||
|
port: RandomUtil.randomIntRange(10000, 60000),
|
||||||
|
protocol: baseInbound.protocol,
|
||||||
|
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
|
||||||
|
streamSettings: baseInbound.stream.toString(),
|
||||||
|
sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
|
||||||
|
};
|
||||||
|
await this.submit('/xui/inbound/add', data, inModal);
|
||||||
},
|
},
|
||||||
async addInbound(inbound, dbInbound) {
|
async addInbound(inbound, dbInbound) {
|
||||||
const data = {
|
const data = {
|
||||||
@@ -350,9 +600,10 @@
|
|||||||
port: inbound.port,
|
port: inbound.port,
|
||||||
protocol: inbound.protocol,
|
protocol: inbound.protocol,
|
||||||
settings: inbound.settings.toString(),
|
settings: inbound.settings.toString(),
|
||||||
streamSettings: inbound.stream.toString(),
|
|
||||||
sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
|
|
||||||
};
|
};
|
||||||
|
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
|
||||||
|
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
|
||||||
|
|
||||||
await this.submit('/xui/inbound/add', data, inModal);
|
await this.submit('/xui/inbound/add', data, inModal);
|
||||||
},
|
},
|
||||||
async updateInbound(inbound, dbInbound) {
|
async updateInbound(inbound, dbInbound) {
|
||||||
@@ -368,9 +619,10 @@
|
|||||||
port: inbound.port,
|
port: inbound.port,
|
||||||
protocol: inbound.protocol,
|
protocol: inbound.protocol,
|
||||||
settings: inbound.settings.toString(),
|
settings: inbound.settings.toString(),
|
||||||
streamSettings: inbound.stream.toString(),
|
|
||||||
sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
|
|
||||||
};
|
};
|
||||||
|
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
|
||||||
|
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
|
||||||
|
|
||||||
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
|
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
|
||||||
},
|
},
|
||||||
openAddClient(dbInboundId) {
|
openAddClient(dbInboundId) {
|
||||||
@@ -379,9 +631,9 @@
|
|||||||
title: '{{ i18n "pages.client.add"}}',
|
title: '{{ i18n "pages.client.add"}}',
|
||||||
okText: '{{ i18n "pages.client.submitAdd"}}',
|
okText: '{{ i18n "pages.client.submitAdd"}}',
|
||||||
dbInbound: dbInbound,
|
dbInbound: dbInbound,
|
||||||
confirm: async (inbound, dbInbound, index) => {
|
confirm: async (clients, dbInboundId) => {
|
||||||
clientModal.loading();
|
clientModal.loading();
|
||||||
await this.addClient(inbound, dbInbound);
|
await this.addClient(clients, dbInboundId);
|
||||||
clientModal.close();
|
clientModal.close();
|
||||||
},
|
},
|
||||||
isEdit: false
|
isEdit: false
|
||||||
@@ -393,9 +645,9 @@
|
|||||||
title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
|
title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
|
||||||
okText: '{{ i18n "pages.client.bulk"}}',
|
okText: '{{ i18n "pages.client.bulk"}}',
|
||||||
dbInbound: dbInbound,
|
dbInbound: dbInbound,
|
||||||
confirm: async (inbound, dbInbound) => {
|
confirm: async (clients, dbInboundId) => {
|
||||||
clientsBulkModal.loading();
|
clientsBulkModal.loading();
|
||||||
await this.addClient(inbound, dbInbound);
|
await this.addClient(clients, dbInboundId);
|
||||||
clientsBulkModal.close();
|
clientsBulkModal.close();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -409,37 +661,38 @@
|
|||||||
okText: '{{ i18n "pages.client.submitEdit"}}',
|
okText: '{{ i18n "pages.client.submitEdit"}}',
|
||||||
dbInbound: dbInbound,
|
dbInbound: dbInbound,
|
||||||
index: index,
|
index: index,
|
||||||
confirm: async (inbound, dbInbound, index) => {
|
confirm: async (client, dbInboundId, clientId) => {
|
||||||
clientModal.loading();
|
clientModal.loading();
|
||||||
await this.updateClient(inbound, dbInbound, index);
|
await this.updateClient(client, dbInboundId, clientId);
|
||||||
clientModal.close();
|
clientModal.close();
|
||||||
},
|
},
|
||||||
isEdit: true
|
isEdit: true
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
findIndexOfClient(clients,client) {
|
findIndexOfClient(clients, client) {
|
||||||
firstKey = Object.keys(client)[0];
|
firstKey = Object.keys(client)[0];
|
||||||
return clients.findIndex(c => c[firstKey] === client[firstKey]);
|
return clients.findIndex(c => c[firstKey] === client[firstKey]);
|
||||||
},
|
},
|
||||||
async addClient(inbound, dbInbound) {
|
async addClient(clients, dbInboundId) {
|
||||||
const data = {
|
const data = {
|
||||||
id: dbInbound.id,
|
id: dbInboundId,
|
||||||
settings: inbound.settings.toString(),
|
settings: '{"clients": [' + clients.toString() + ']}',
|
||||||
};
|
};
|
||||||
await this.submit('/xui/inbound/addClient', data);
|
await this.submit(`/xui/inbound/addClient`, data);
|
||||||
},
|
},
|
||||||
async updateClient(inbound, dbInbound, index) {
|
async updateClient(client, dbInboundId, clientId) {
|
||||||
const data = {
|
const data = {
|
||||||
id: dbInbound.id,
|
id: dbInboundId,
|
||||||
settings: inbound.settings.toString(),
|
settings: '{"clients": [' + client.toString() + ']}',
|
||||||
};
|
};
|
||||||
await this.submit(`/xui/inbound/updateClient/${index}`, data);
|
await this.submit(`/xui/inbound/updateClient/${clientId}`, data);
|
||||||
},
|
},
|
||||||
resetTraffic(dbInboundId) {
|
resetTraffic(dbInboundId) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
this.$confirm({
|
this.$confirm({
|
||||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||||
|
class: themeSwitcher.darkCardClass,
|
||||||
okText: '{{ i18n "reset"}}',
|
okText: '{{ i18n "reset"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
@@ -454,41 +707,44 @@
|
|||||||
this.$confirm({
|
this.$confirm({
|
||||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||||
|
class: themeSwitcher.darkCardClass,
|
||||||
okText: '{{ i18n "delete"}}',
|
okText: '{{ i18n "delete"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
|
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
delClient(dbInboundId,client) {
|
delClient(dbInboundId, client) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
newDbInbound = new DBInbound(dbInbound);
|
clientId = this.getClientId(dbInbound.protocol, client);
|
||||||
inbound = newDbInbound.toInbound();
|
|
||||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
|
||||||
index = this.findIndexOfClient(clients, client);
|
|
||||||
clients.splice(index, 1);
|
|
||||||
const data = {
|
|
||||||
id: dbInboundId,
|
|
||||||
settings: inbound.settings.toString(),
|
|
||||||
};
|
|
||||||
this.$confirm({
|
this.$confirm({
|
||||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||||
|
class: themeSwitcher.darkCardClass,
|
||||||
okText: '{{ i18n "delete"}}',
|
okText: '{{ i18n "delete"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => this.submit('/xui/inbound/delClient/' + client.email, data),
|
onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getClients(protocol, clientSettings) {
|
getClients(protocol, clientSettings) {
|
||||||
switch(protocol){
|
switch (protocol) {
|
||||||
case Protocols.VMESS: return clientSettings.vmesses;
|
case Protocols.VMESS: return clientSettings.vmesses;
|
||||||
case Protocols.VLESS: return clientSettings.vlesses;
|
case Protocols.VLESS: return clientSettings.vlesses;
|
||||||
case Protocols.TROJAN: return clientSettings.trojans;
|
case Protocols.TROJAN: return clientSettings.trojans;
|
||||||
|
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getClientId(protocol, client) {
|
||||||
|
switch (protocol) {
|
||||||
|
case Protocols.TROJAN: return client.password;
|
||||||
|
case Protocols.SHADOWSOCKS: return client.email;
|
||||||
|
default: return client.id;
|
||||||
|
}
|
||||||
|
},
|
||||||
showQrcode(dbInbound, clientIndex) {
|
showQrcode(dbInbound, clientIndex) {
|
||||||
|
const clientName = JSON.parse(dbInbound.settings).clients[clientIndex].email;
|
||||||
const link = dbInbound.genLink(clientIndex);
|
const link = dbInbound.genLink(clientIndex);
|
||||||
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
|
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound, '', clientName);
|
||||||
},
|
},
|
||||||
showInfo(dbInbound, index) {
|
showInfo(dbInbound, index) {
|
||||||
infoModal.show(dbInbound, index);
|
infoModal.show(dbInbound, index);
|
||||||
@@ -497,6 +753,17 @@
|
|||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound);
|
this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound);
|
||||||
},
|
},
|
||||||
|
async switchEnableClient(dbInboundId, client) {
|
||||||
|
this.loading()
|
||||||
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
|
inbound = dbInbound.toInbound();
|
||||||
|
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||||
|
index = this.findIndexOfClient(clients, client);
|
||||||
|
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) {
|
async submit(url, data) {
|
||||||
const msg = await HttpUtil.postWithModal(url, data);
|
const msg = await HttpUtil.postWithModal(url, data);
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
@@ -504,58 +771,117 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getInboundClients(dbInbound) {
|
getInboundClients(dbInbound) {
|
||||||
if(dbInbound.protocol == Protocols.VLESS) {
|
if (dbInbound.protocol == Protocols.VLESS) {
|
||||||
return dbInbound.toInbound().settings.vlesses
|
return dbInbound.toInbound().settings.vlesses;
|
||||||
} else if(dbInbound.protocol == Protocols.VMESS) {
|
} else if (dbInbound.protocol == Protocols.VMESS) {
|
||||||
return dbInbound.toInbound().settings.vmesses
|
return dbInbound.toInbound().settings.vmesses;
|
||||||
} else if(dbInbound.protocol == Protocols.TROJAN) {
|
} else if (dbInbound.protocol == Protocols.TROJAN) {
|
||||||
return dbInbound.toInbound().settings.trojans
|
return dbInbound.toInbound().settings.trojans;
|
||||||
|
} else if (dbInbound.protocol == Protocols.SHADOWSOCKS) {
|
||||||
|
return dbInbound.toInbound().settings.shadowsockses;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetClientTraffic(client,dbInboundId) {
|
resetClientTraffic(client, dbInboundId) {
|
||||||
this.$confirm({
|
this.$confirm({
|
||||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||||
|
class: themeSwitcher.darkCardClass,
|
||||||
okText: '{{ i18n "reset"}}',
|
okText: '{{ i18n "reset"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email),
|
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resetAllTraffic() {
|
||||||
|
this.$confirm({
|
||||||
|
title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
|
||||||
|
content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
|
||||||
|
class: themeSwitcher.darkCardClass,
|
||||||
|
okText: '{{ i18n "reset"}}',
|
||||||
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
|
onOk: () => this.submit('/xui/inbound/resetAllTraffics'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetAllClientTraffics(dbInboundId) {
|
||||||
|
this.$confirm({
|
||||||
|
title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
|
||||||
|
content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
|
||||||
|
class: themeSwitcher.darkCardClass,
|
||||||
|
okText: '{{ i18n "reset"}}',
|
||||||
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
|
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
delDepletedClients(dbInboundId) {
|
||||||
|
this.$confirm({
|
||||||
|
title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
|
||||||
|
content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
|
||||||
|
class: themeSwitcher.darkCardClass,
|
||||||
|
okText: '{{ i18n "reset"}}',
|
||||||
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
|
onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
isExpiry(dbInbound, index) {
|
isExpiry(dbInbound, index) {
|
||||||
return dbInbound.toInbound().isExpiry(index)
|
return dbInbound.toInbound().isExpiry(index)
|
||||||
},
|
},
|
||||||
getUpStats(dbInbound, email) {
|
getUpStats(dbInbound, email) {
|
||||||
if(email.length == 0) return 0
|
if (email.length == 0) return 0
|
||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
||||||
return clientStats ? clientStats.up : 0
|
return clientStats ? clientStats.up : 0
|
||||||
},
|
},
|
||||||
getDownStats(dbInbound, email) {
|
getDownStats(dbInbound, email) {
|
||||||
if(email.length == 0) return 0
|
if (email.length == 0) return 0
|
||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
||||||
return clientStats ? clientStats.down : 0
|
return clientStats ? clientStats.down : 0
|
||||||
},
|
},
|
||||||
isTrafficExhausted(dbInbound, email) {
|
statsColor(dbInbound, email) {
|
||||||
if(email.length == 0) return false
|
if (email.length == 0) return 'blue';
|
||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
||||||
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
|
return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
|
||||||
},
|
},
|
||||||
isClientEnabled(dbInbound, email) {
|
isClientEnabled(dbInbound, email) {
|
||||||
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
|
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
|
||||||
return clientStats ? clientStats['enable'] : true
|
return clientStats ? clientStats['enable'] : true
|
||||||
},
|
},
|
||||||
isRemovable(dbInbound_id){
|
isRemovable(dbInbound_id) {
|
||||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
|
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
|
||||||
},
|
},
|
||||||
inboundLinks(dbInboundId) {
|
inboundLinks(dbInboundId) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark);
|
txtModal.show('{{ i18n "pages.inbounds.export"}}', dbInbound.genInboundLinks, dbInbound.remark);
|
||||||
},
|
},
|
||||||
exportAllLinks() {
|
exportAllLinks() {
|
||||||
let copyText = '';
|
let copyText = '';
|
||||||
for (const dbInbound of this.dbInbounds) {
|
for (const dbInbound of this.dbInbounds) {
|
||||||
copyText += dbInbound.genInboundLinks
|
copyText += dbInbound.genInboundLinks
|
||||||
}
|
}
|
||||||
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
|
txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds');
|
||||||
|
},
|
||||||
|
async startDataRefreshLoop() {
|
||||||
|
while (this.isRefreshEnabled) {
|
||||||
|
try {
|
||||||
|
await this.getDBInbounds();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
await PromiseUtil.sleep(this.refreshInterval);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleRefresh() {
|
||||||
|
localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
|
||||||
|
if (this.isRefreshEnabled) {
|
||||||
|
this.startDataRefreshLoop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
changeRefreshInterval() {
|
||||||
|
localStorage.setItem("refreshInterval", this.refreshInterval);
|
||||||
|
},
|
||||||
|
async manualRefresh() {
|
||||||
|
if (!this.refreshing) {
|
||||||
|
this.spinning = true;
|
||||||
|
await this.getDBInbounds();
|
||||||
|
this.spinning = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -564,42 +890,41 @@
|
|||||||
}, 500)
|
}, 500)
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getDBInbounds();
|
this.loading();
|
||||||
|
this.getDefaultSettings();
|
||||||
|
if (this.isRefreshEnabled) {
|
||||||
|
this.startDataRefreshLoop();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.getDBInbounds();
|
||||||
|
}
|
||||||
|
this.loading(false);
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
total() {
|
total() {
|
||||||
let down = 0, up = 0;
|
let down = 0, up = 0;
|
||||||
let clients = 0, active = 0, deactive = 0;
|
let clients = 0, deactive = [], depleted = [], expiring = [];
|
||||||
this.dbInbounds.forEach(dbInbound => {
|
this.dbInbounds.forEach(dbInbound => {
|
||||||
down += dbInbound.down;
|
down += dbInbound.down;
|
||||||
up += dbInbound.up;
|
up += dbInbound.up;
|
||||||
inbound = dbInbound.toInbound();
|
if (this.clientCount[dbInbound.id]) {
|
||||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
clients += this.clientCount[dbInbound.id].clients;
|
||||||
if(clients){
|
deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
|
||||||
if(dbInbound.enable){
|
depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);
|
||||||
isClientEnable = false;
|
expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);
|
||||||
clients.forEach(client => {
|
|
||||||
isClientEnable = client.email == "" ? true: this.isClientEnabled(dbInbound,client.email);
|
|
||||||
isClientEnable ? active++ : deactive++;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
deactive += clients.length;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dbInbound.enable ? active++ : deactive++;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
down: down,
|
down: down,
|
||||||
up: up,
|
up: up,
|
||||||
clients: active + deactive,
|
clients: clients,
|
||||||
active: active,
|
|
||||||
deactive: deactive,
|
deactive: deactive,
|
||||||
|
depleted: depleted,
|
||||||
|
expiring: expiring,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{template "inboundModal"}}
|
{{template "inboundModal"}}
|
||||||
|
|||||||
@@ -11,28 +11,34 @@
|
|||||||
.ant-col-sm-24 {
|
.ant-col-sm-24 {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-card-dark h2 {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<a-layout id="app" v-cloak>
|
<a-layout id="app" v-cloak>
|
||||||
{{ template "commonSider" . }}
|
{{ template "commonSider" . }}
|
||||||
<a-layout id="content-layout">
|
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
|
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-card hoverable>
|
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="12" style="text-align: center">
|
<a-col :span="12" style="text-align: center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal"
|
||||||
:stroke-color="status.cpu.color"
|
:stroke-color="status.cpu.color"
|
||||||
|
:class="themeSwitcher.darkCardClass"
|
||||||
:percent="status.cpu.percent"></a-progress>
|
:percent="status.cpu.percent"></a-progress>
|
||||||
<div>CPU</div>
|
<div>CPU</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12" style="text-align: center">
|
<a-col :span="12" style="text-align: center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal"
|
||||||
:stroke-color="status.mem.color"
|
:stroke-color="status.mem.color"
|
||||||
|
:class="themeSwitcher.darkCardClass"
|
||||||
:percent="status.mem.percent"></a-progress>
|
:percent="status.mem.percent"></a-progress>
|
||||||
<div>
|
<div>
|
||||||
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
|
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
|
||||||
@@ -45,6 +51,7 @@
|
|||||||
<a-col :span="12" style="text-align: center">
|
<a-col :span="12" style="text-align: center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal"
|
||||||
:stroke-color="status.swap.color"
|
:stroke-color="status.swap.color"
|
||||||
|
:class="themeSwitcher.darkCardClass"
|
||||||
:percent="status.swap.percent"></a-progress>
|
:percent="status.swap.percent"></a-progress>
|
||||||
<div>
|
<div>
|
||||||
swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
|
swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
|
||||||
@@ -53,6 +60,7 @@
|
|||||||
<a-col :span="12" style="text-align: center">
|
<a-col :span="12" style="text-align: center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal"
|
||||||
:stroke-color="status.disk.color"
|
:stroke-color="status.disk.color"
|
||||||
|
:class="themeSwitcher.darkCardClass"
|
||||||
:percent="status.disk.percent"></a-progress>
|
:percent="status.disk.percent"></a-progress>
|
||||||
<div>
|
<div>
|
||||||
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
|
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
|
||||||
@@ -67,23 +75,15 @@
|
|||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-card hoverable>
|
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||||
{{ i18n "pages.index.xrayStatus" }}:
|
x-ui: <a href="https://github.com/alireza0/x-ui/releases" target="_blank"><a-tag color="green">{{ .cur_ver }}</a-tag></a>
|
||||||
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
|
xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
||||||
<a-tooltip v-if="status.xray.state === State.Error">
|
|
||||||
<template slot="title">
|
|
||||||
<p v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tag color="green" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
|
||||||
<a-tag color="blue" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch"}}</a-tag>
|
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-card hoverable>
|
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||||
{{ i18n "pages.index.operationHours" }}:
|
{{ i18n "pages.index.operationHours" }}:
|
||||||
<a-tag color="#87d068">[[ formatSecond(status.uptime) ]]</a-tag>
|
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
{{ i18n "pages.index.operationHoursDesc" }}
|
{{ i18n "pages.index.operationHoursDesc" }}
|
||||||
@@ -93,12 +93,35 @@
|
|||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-card hoverable>
|
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||||
|
{{ i18n "pages.index.xrayStatus" }}:
|
||||||
|
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
|
||||||
|
<a-tooltip v-if="status.xray.state === State.Error">
|
||||||
|
<template slot="title">
|
||||||
|
<p v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
|
||||||
|
<a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
|
||||||
|
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
<a-col :sm="24" :md="12">
|
||||||
|
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||||
|
{{ i18n "menu.link" }}:
|
||||||
|
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag>
|
||||||
|
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
|
||||||
|
<a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
<a-col :sm="24" :md="12">
|
||||||
|
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||||
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
|
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-card hoverable>
|
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||||
tcp / udp {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
|
tcp / udp {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
@@ -109,7 +132,7 @@
|
|||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-card hoverable>
|
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-icon type="arrow-up"></a-icon>
|
<a-icon type="arrow-up"></a-icon>
|
||||||
@@ -135,7 +158,7 @@
|
|||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-card hoverable>
|
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-icon type="cloud-upload"></a-icon>
|
<a-icon type="cloud-upload"></a-icon>
|
||||||
@@ -164,9 +187,11 @@
|
|||||||
</transition>
|
</transition>
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
|
|
||||||
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
||||||
:closable="true" @ok="() => versionModal.visible = false"
|
:closable="true" @ok="() => versionModal.visible = false"
|
||||||
ok-text='{{ i18n "confirm" }}' cancel-text='{{ i18n "cancel"}}'>
|
:class="themeSwitcher.darkCardClass"
|
||||||
|
footer="">
|
||||||
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
|
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
|
||||||
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
|
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
|
||||||
<template v-for="version, index in versionModal.versions">
|
<template v-for="version, index in versionModal.versions">
|
||||||
@@ -176,8 +201,59 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
|
||||||
|
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
|
||||||
|
:class="themeSwitcher.darkCardClass"
|
||||||
|
width="800px"
|
||||||
|
footer="">
|
||||||
|
<a-form layout="inline">
|
||||||
|
<a-form-item label="Count">
|
||||||
|
<a-select v-model="logModal.rows"
|
||||||
|
style="width: 80px"
|
||||||
|
@change="openLogs(logModal.rows)"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass">
|
||||||
|
<a-select-option value="10">10</a-select-option>
|
||||||
|
<a-select-option value="20">20</a-select-option>
|
||||||
|
<a-select-option value="50">50</a-select-option>
|
||||||
|
<a-select-option value="100">100</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<button class="ant-btn ant-btn-primary" @click="openLogs(logModal.rows)"><a-icon type="sync"></a-icon> Reload</button>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-button type="primary" style="margin-bottom: 10px;"
|
||||||
|
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs)" download="x-ui.log">
|
||||||
|
{{ i18n "download" }} x-ui.log
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
<a-input type="textarea" v-model="logModal.logs" disabled="true"
|
||||||
|
:autosize="{ minRows: 10, maxRows: 22}"></a-input>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
|
||||||
|
:closable="true" :class="themeSwitcher.darkCardClass"
|
||||||
|
@ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
|
||||||
|
<p style="color: inherit; font-size: 16px; padding: 4px 2px;">
|
||||||
|
<a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
|
||||||
|
[[ backupModal.description ]]
|
||||||
|
</p>
|
||||||
|
<a-space direction="horizontal" align="center" style="margin-bottom: 10px;">
|
||||||
|
<a-button type="primary" @click="exportDatabase()">
|
||||||
|
[[ backupModal.exportText ]]
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" @click="importDatabase()">
|
||||||
|
[[ backupModal.importText ]]
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
</a-layout>
|
</a-layout>
|
||||||
{{template "js" .}}
|
{{template "js" .}}
|
||||||
|
{{template "component/themeSwitcher" .}}
|
||||||
|
{{template "textModal"}}
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
const State = {
|
const State = {
|
||||||
@@ -269,13 +345,53 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logModal = {
|
||||||
|
visible: false,
|
||||||
|
logs: '',
|
||||||
|
rows: 20,
|
||||||
|
show(logs, rows) {
|
||||||
|
this.visible = true;
|
||||||
|
this.rows = rows;
|
||||||
|
this.logs = logs.join("\n");
|
||||||
|
},
|
||||||
|
hide() {
|
||||||
|
this.visible = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const backupModal = {
|
||||||
|
visible: false,
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
exportText: '',
|
||||||
|
importText: '',
|
||||||
|
show({
|
||||||
|
title = '{{ i18n "pages.index.backupTitle" }}',
|
||||||
|
description = '{{ i18n "pages.index.backupDescription" }}',
|
||||||
|
exportText = '{{ i18n "pages.index.exportDatabase" }}',
|
||||||
|
importText = '{{ i18n "pages.index.importDatabase" }}',
|
||||||
|
}) {
|
||||||
|
this.title = title;
|
||||||
|
this.description = description;
|
||||||
|
this.exportText = exportText;
|
||||||
|
this.importText = importText;
|
||||||
|
this.visible = true;
|
||||||
|
},
|
||||||
|
hide() {
|
||||||
|
this.visible = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
delimiters: ['[[', ']]'],
|
delimiters: ['[[', ']]'],
|
||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
siderDrawer,
|
siderDrawer,
|
||||||
|
themeSwitcher,
|
||||||
status: new Status(),
|
status: new Status(),
|
||||||
versionModal,
|
versionModal,
|
||||||
|
logModal,
|
||||||
|
backupModal,
|
||||||
spinning: false,
|
spinning: false,
|
||||||
loadingTip: '{{ i18n "loading"}}',
|
loadingTip: '{{ i18n "loading"}}',
|
||||||
},
|
},
|
||||||
@@ -307,15 +423,93 @@
|
|||||||
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
|
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
|
||||||
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
|
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
|
||||||
okText: '{{ i18n "confirm"}}',
|
okText: '{{ i18n "confirm"}}',
|
||||||
|
class: themeSwitcher.darkCardClass,
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
versionModal.hide();
|
versionModal.hide();
|
||||||
this.loading(true, '{{ i18n "pages.index.dontRefreshh"}}');
|
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
||||||
await HttpUtil.post(`/server/installXray/${version}`);
|
await HttpUtil.post(`/server/installXray/${version}`);
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async stopXrayService() {
|
||||||
|
this.loading(true);
|
||||||
|
const msg = await HttpUtil.post('server/stopXrayService');
|
||||||
|
this.loading(false);
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async restartXrayService() {
|
||||||
|
this.loading(true);
|
||||||
|
const msg = await HttpUtil.post('server/restartXrayService');
|
||||||
|
this.loading(false);
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async openLogs(rows){
|
||||||
|
this.loading(true);
|
||||||
|
const msg = await HttpUtil.post('server/logs/'+rows);
|
||||||
|
this.loading(false);
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logModal.show(msg.obj, rows);
|
||||||
|
},
|
||||||
|
async openConfig() {
|
||||||
|
this.loading(true);
|
||||||
|
const msg = await HttpUtil.post('server/getConfigJson');
|
||||||
|
this.loading(false);
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');
|
||||||
|
},
|
||||||
|
openBackup() {
|
||||||
|
backupModal.show({
|
||||||
|
title: '{{ i18n "pages.index.backupTitle" }}',
|
||||||
|
description: '{{ i18n "pages.index.backupDescription" }}',
|
||||||
|
exportText: '{{ i18n "pages.index.exportDatabase" }}',
|
||||||
|
importText: '{{ i18n "pages.index.importDatabase" }}',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
exportDatabase() {
|
||||||
|
window.location = basePath + 'server/getDb';
|
||||||
|
},
|
||||||
|
importDatabase() {
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = '.db';
|
||||||
|
fileInput.addEventListener('change', async (event) => {
|
||||||
|
const dbFile = event.target.files[0];
|
||||||
|
if (dbFile) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('db', dbFile);
|
||||||
|
backupModal.hide();
|
||||||
|
this.loading(true);
|
||||||
|
const uploadMsg = await HttpUtil.post('server/importDB', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.loading(false);
|
||||||
|
if (!uploadMsg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading(true);
|
||||||
|
const restartMsg = await HttpUtil.post("/xui/setting/restartPanel");
|
||||||
|
this.loading(false);
|
||||||
|
if (restartMsg.success) {
|
||||||
|
this.loading(true);
|
||||||
|
await PromiseUtil.sleep(5000);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fileInput.click();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -1,307 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
{{template "head" .}}
|
|
||||||
<style>
|
|
||||||
@media (min-width: 769px) {
|
|
||||||
.ant-layout-content {
|
|
||||||
margin: 24px 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-col-sm-24 {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-tabs-bar {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-list-item {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-tabs-top-bar {
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<body>
|
|
||||||
<a-layout id="app" v-cloak>
|
|
||||||
{{ template "commonSider" . }}
|
|
||||||
<a-layout id="content-layout">
|
|
||||||
<a-layout-content>
|
|
||||||
<a-spin :spinning="spinning" :delay="500" tip="loading">
|
|
||||||
<a-space direction="vertical">
|
|
||||||
<a-space direction="horizontal">
|
|
||||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.setting.save" }}</a-button>
|
|
||||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.setting.restartPanel" }}</a-button>
|
|
||||||
</a-space>
|
|
||||||
<a-tabs default-active-key="1">
|
|
||||||
<a-tab-pane key="1" tab='{{ i18n "pages.setting.panelConfig"}}'>
|
|
||||||
|
|
||||||
<a-list item-layout="horizontal" style="background: white">
|
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
|
||||||
<setting-list-item type="number" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
|
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
|
||||||
<a-list-item>
|
|
||||||
<a-row style="padding: 20px">
|
|
||||||
<a-col :lg="24" :xl="12">
|
|
||||||
<a-list-item-meta title="Language"/>
|
|
||||||
</a-col>
|
|
||||||
|
|
||||||
<a-col :lg="24" :xl="12">
|
|
||||||
<temlate>
|
|
||||||
<a-select
|
|
||||||
ref="selectLang"
|
|
||||||
v-model="lang"
|
|
||||||
@change="setLang(lang)"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" >
|
|
||||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
|
||||||
<span v-text="l.name"></span>
|
|
||||||
</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</temlate>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
|
|
||||||
</a-list-item>
|
|
||||||
</a-list>
|
|
||||||
</a-tab-pane>
|
|
||||||
<a-tab-pane key="2" tab='{{ i18n "pages.setting.userSetting"}}'>
|
|
||||||
<a-form style="background: white; padding: 20px">
|
|
||||||
<a-form-item label='{{ i18n "pages.setting.oldUsername"}}'>
|
|
||||||
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='{{ i18n "pages.setting.currentPassword"}}'>
|
|
||||||
<a-input type="password" v-model="user.oldPassword"
|
|
||||||
style="max-width: 300px"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='{{ i18n "pages.setting.newUsername"}}'>
|
|
||||||
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='{{ i18n "pages.setting.newPassword"}}'>
|
|
||||||
<a-input type="password" v-model="user.newPassword"
|
|
||||||
style="max-width: 300px"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<!-- <a-button type="primary" @click="updateUser">Revise</a-button>-->
|
|
||||||
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</a-tab-pane>
|
|
||||||
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
|
|
||||||
<a-list item-layout="horizontal" style="background: white">
|
|
||||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigTorrent"}}' desc='{{ i18n "pages.setting.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
|
|
||||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.setting.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
|
|
||||||
<a-divider>{{ i18n "pages.setting.advancedTemplate"}}</a-divider>
|
|
||||||
<a-collapse>
|
|
||||||
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigInbounds"}}">
|
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigInbounds"}}' desc='{{ i18n "pages.setting.xrayConfigInboundsDesc"}}' v-model ="inboundSettings"></setting-list-item>
|
|
||||||
</a-collapse-panel>
|
|
||||||
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigOutbounds"}}">
|
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigOutbounds"}}' desc='{{ i18n "pages.setting.xrayConfigOutboundsDesc"}}' v-model ="outboundSettings"></setting-list-item>
|
|
||||||
</a-collapse-panel>
|
|
||||||
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigRoutings"}}">
|
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigRoutings"}}' desc='{{ i18n "pages.setting.xrayConfigRoutingsDesc"}}' v-model ="routingRuleSettings"></setting-list-item>
|
|
||||||
</a-collapse-panel>
|
|
||||||
</a-collapse>
|
|
||||||
<a-divider>{{ i18n "pages.setting.completeTemplate"}}</a-divider>
|
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
|
||||||
</a-list>
|
|
||||||
</a-tab-pane>
|
|
||||||
<a-tab-pane key="4" tab='{{ i18n "pages.setting.TGReminder"}}'>
|
|
||||||
<a-list item-layout="horizontal" style="background: white">
|
|
||||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.telegramBotEnable" }}' desc='{{ i18n "pages.setting.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
|
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
|
||||||
<setting-list-item type="number" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model.number="allSetting.tgBotChatId"></setting-list-item>
|
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
|
||||||
</a-list>
|
|
||||||
</a-tab-pane>
|
|
||||||
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
|
|
||||||
<a-list item-layout="horizontal" style="background: white">
|
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.timeZonee"}}' desc='{{ i18n "pages.setting.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
|
|
||||||
</a-list>
|
|
||||||
</a-tab-pane>
|
|
||||||
</a-tabs>
|
|
||||||
</a-space>
|
|
||||||
</a-spin>
|
|
||||||
</a-layout-content>
|
|
||||||
</a-layout>
|
|
||||||
</a-layout>
|
|
||||||
{{template "js" .}}
|
|
||||||
{{template "component/setting"}}
|
|
||||||
<script>
|
|
||||||
|
|
||||||
const app = new Vue({
|
|
||||||
delimiters: ['[[', ']]'],
|
|
||||||
el: '#app',
|
|
||||||
data: {
|
|
||||||
siderDrawer,
|
|
||||||
spinning: false,
|
|
||||||
oldAllSetting: new AllSetting(),
|
|
||||||
allSetting: new AllSetting(),
|
|
||||||
saveBtnDisable: true,
|
|
||||||
user: {},
|
|
||||||
lang : getLang()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
loading(spinning = true) {
|
|
||||||
this.spinning = spinning;
|
|
||||||
},
|
|
||||||
async getAllSetting() {
|
|
||||||
this.loading(true);
|
|
||||||
const msg = await HttpUtil.post("/xui/setting/all");
|
|
||||||
this.loading(false);
|
|
||||||
if (msg.success) {
|
|
||||||
this.oldAllSetting = new AllSetting(msg.obj);
|
|
||||||
this.allSetting = new AllSetting(msg.obj);
|
|
||||||
this.saveBtnDisable = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async updateAllSetting() {
|
|
||||||
this.loading(true);
|
|
||||||
const msg = await HttpUtil.post("/xui/setting/update", this.allSetting);
|
|
||||||
this.loading(false);
|
|
||||||
if (msg.success) {
|
|
||||||
await this.getAllSetting();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async updateUser() {
|
|
||||||
this.loading(true);
|
|
||||||
const msg = await HttpUtil.post("/xui/setting/updateUser", this.user);
|
|
||||||
this.loading(false);
|
|
||||||
if (msg.success) {
|
|
||||||
this.user = {};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async restartPanel() {
|
|
||||||
await new Promise(resolve => {
|
|
||||||
this.$confirm({
|
|
||||||
title: '{{ i18n "pages.setting.restartPanel" }}',
|
|
||||||
content: '{{ i18n "pages.setting.restartPanelDesc" }}',
|
|
||||||
okText: '{{ i18n "sure" }}',
|
|
||||||
cancelText: '{{ i18n "cancel" }}',
|
|
||||||
onOk: () => resolve(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.loading(true);
|
|
||||||
const msg = await HttpUtil.post("/xui/setting/restartPanel");
|
|
||||||
this.loading(false);
|
|
||||||
if (msg.success) {
|
|
||||||
this.loading(true);
|
|
||||||
await PromiseUtil.sleep(5000);
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
await this.getAllSetting();
|
|
||||||
while (true) {
|
|
||||||
await PromiseUtil.sleep(1000);
|
|
||||||
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
templateSettings: {
|
|
||||||
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null ; },
|
|
||||||
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
|
|
||||||
},
|
|
||||||
inboundSettings: {
|
|
||||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
|
|
||||||
set: function (newValue) {
|
|
||||||
newTemplateSettings = this.templateSettings;
|
|
||||||
newTemplateSettings.inbounds = JSON.parse(newValue)
|
|
||||||
this.templateSettings = newTemplateSettings
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outboundSettings: {
|
|
||||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
|
|
||||||
set: function (newValue) {
|
|
||||||
newTemplateSettings = this.templateSettings;
|
|
||||||
newTemplateSettings.outbounds = JSON.parse(newValue)
|
|
||||||
this.templateSettings = newTemplateSettings
|
|
||||||
},
|
|
||||||
},
|
|
||||||
routingRuleSettings: {
|
|
||||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
|
|
||||||
set: function (newValue) {
|
|
||||||
newTemplateSettings = this.templateSettings;
|
|
||||||
newTemplateSettings.routing.rules = JSON.parse(newValue)
|
|
||||||
this.templateSettings = newTemplateSettings
|
|
||||||
},
|
|
||||||
},
|
|
||||||
torrentSettings: {
|
|
||||||
get: function () {
|
|
||||||
torrentFilter = false
|
|
||||||
if(this.templateSettings != null){
|
|
||||||
this.templateSettings.routing.rules.forEach(routingRule => {
|
|
||||||
if(routingRule.hasOwnProperty("protocol")){
|
|
||||||
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
|
|
||||||
torrentFilter = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return torrentFilter
|
|
||||||
},
|
|
||||||
set: function (newValue) {
|
|
||||||
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
|
|
||||||
if (newValue){
|
|
||||||
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"protocol\": [\"bittorrent\"],\"type\": \"field\"}"))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
newTemplateSettings.routing.rules = [];
|
|
||||||
this.templateSettings.routing.rules.forEach(routingRule => {
|
|
||||||
if (routingRule.hasOwnProperty('protocol')){
|
|
||||||
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newTemplateSettings.routing.rules.push(routingRule);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.templateSettings = newTemplateSettings
|
|
||||||
},
|
|
||||||
},
|
|
||||||
privateIpSettings: {
|
|
||||||
get: function () {
|
|
||||||
localIpFilter = false
|
|
||||||
if(this.templateSettings != null){
|
|
||||||
this.templateSettings.routing.rules.forEach(routingRule => {
|
|
||||||
if(routingRule.hasOwnProperty("ip")){
|
|
||||||
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
|
|
||||||
localIpFilter = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return localIpFilter
|
|
||||||
},
|
|
||||||
set: function (newValue) {
|
|
||||||
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
|
|
||||||
if (newValue){
|
|
||||||
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"ip\": [\"geoip:private\"],\"type\": \"field\"}"))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
newTemplateSettings.routing.rules = [];
|
|
||||||
this.templateSettings.routing.rules.forEach(routingRule => {
|
|
||||||
if (routingRule.hasOwnProperty('ip')){
|
|
||||||
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newTemplateSettings.routing.rules.push(routingRule);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.templateSettings = newTemplateSettings
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
830
web/html/xui/settings.html
Normal file
830
web/html/xui/settings.html
Normal file
@@ -0,0 +1,830 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
{{template "head" .}}
|
||||||
|
<style>
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.ant-layout-content {
|
||||||
|
margin: 24px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-col-sm-24 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-bar {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-list-item {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<a-layout id="app" v-cloak>
|
||||||
|
{{ template "commonSider" . }}
|
||||||
|
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
|
||||||
|
<a-layout-content>
|
||||||
|
<a-spin :spinning="spinning" :delay="500" tip="loading">
|
||||||
|
<a-space direction="vertical">
|
||||||
|
<a-space direction="horizontal">
|
||||||
|
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
|
||||||
|
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
|
||||||
|
</a-space>
|
||||||
|
<a-tabs default-active-key="1" :class="themeSwitcher.darkCardClass" :style="!themeSwitcher.isDarkTheme? 'background: white':''">
|
||||||
|
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelConfig"}}'>
|
||||||
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
|
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 20px 20px; text-align: center;">
|
||||||
|
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||||
|
{{ i18n "pages.settings.infoDesc" }}
|
||||||
|
</h2>
|
||||||
|
</a-row>
|
||||||
|
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
||||||
|
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
||||||
|
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
|
||||||
|
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
||||||
|
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
||||||
|
<setting-list-item type="text" title='{{ i18n "pages.settings.panelUrlPath"}}' desc='{{ i18n "pages.settings.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
||||||
|
<setting-list-item type="number" title='{{ i18n "pages.settings.sessionMaxAge" }}' desc='{{ i18n "pages.settings.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
|
||||||
|
<setting-list-item type="number" title='{{ i18n "pages.settings.expireTimeDiff" }}' desc='{{ i18n "pages.settings.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
|
||||||
|
<setting-list-item type="number" title='{{ i18n "pages.settings.trafficDiff" }}' desc='{{ i18n "pages.settings.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
|
||||||
|
<setting-list-item type="text" title='{{ i18n "pages.settings.timeZone"}}' desc='{{ i18n "pages.settings.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
|
||||||
|
<a-list-item>
|
||||||
|
<a-row style="padding: 20px">
|
||||||
|
<a-col :lg="24" :xl="12">
|
||||||
|
<a-list-item-meta title="Language"/>
|
||||||
|
</a-col>
|
||||||
|
|
||||||
|
<a-col :lg="24" :xl="12">
|
||||||
|
<template>
|
||||||
|
<a-select
|
||||||
|
ref="selectLang"
|
||||||
|
v-model="lang"
|
||||||
|
@change="setLang(lang)"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||||
|
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||||
|
<span v-text="l.name"></span>
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-list-item>
|
||||||
|
</a-list>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="2" tab='{{ i18n "pages.settings.userSettings"}}'>
|
||||||
|
<a-form :style="'padding: 20px;' + themeSwitcher.textStyle">
|
||||||
|
<a-form-item label='{{ i18n "pages.settings.oldUsername"}}'>
|
||||||
|
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label='{{ i18n "pages.settings.currentPassword"}}'>
|
||||||
|
<password-input v-model="user.oldPassword" style="max-width: 300px"></password-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label='{{ i18n "pages.settings.newUsername"}}'>
|
||||||
|
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label='{{ i18n "pages.settings.newPassword"}}'>
|
||||||
|
<password-input v-model="user.newPassword" style="max-width: 300px"></password-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-tab-pane>
|
||||||
|
|
||||||
|
<a-tab-pane key="3" tab='{{ i18n "pages.settings.xrayConfiguration"}}'>
|
||||||
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
|
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 20px 20px; text-align: center;">
|
||||||
|
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||||
|
{{ i18n "pages.settings.infoDesc" }}
|
||||||
|
</h2>
|
||||||
|
</a-row>
|
||||||
|
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
||||||
|
<a-tabs class="ant-card-dark-box-nohover" default-active-key="tpl-1" :class="themeSwitcher.darkCardClass" style="padding: 20px 20px;">
|
||||||
|
<a-tab-pane key="tpl-1" tab='{{ i18n "pages.settings.templates.basicTemplate"}}' style="padding-top: 20px;">
|
||||||
|
<a-collapse>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'>
|
||||||
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
|
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||||
|
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||||
|
{{ i18n "pages.settings.templates.generalConfigsDesc" }}
|
||||||
|
</h2>
|
||||||
|
</a-row>
|
||||||
|
<a-list-item>
|
||||||
|
<a-row style="padding: 20px">
|
||||||
|
<a-col :lg="24" :xl="12">
|
||||||
|
<a-list-item-meta
|
||||||
|
title='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategy" }}'
|
||||||
|
description='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategyDesc" }}'/>
|
||||||
|
</a-col>
|
||||||
|
<a-col :lg="24" :xl="12">
|
||||||
|
<template>
|
||||||
|
<a-select
|
||||||
|
v-model="freedomStrategy"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||||
|
style="width: 100%">
|
||||||
|
<a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-list-item>
|
||||||
|
<a-row style="padding: 20px">
|
||||||
|
<a-col :lg="24" :xl="12">
|
||||||
|
<a-list-item-meta
|
||||||
|
title='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategy" }}'
|
||||||
|
description='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategyDesc" }}'/>
|
||||||
|
</a-col>
|
||||||
|
<a-col :lg="24" :xl="12">
|
||||||
|
<template>
|
||||||
|
<a-select
|
||||||
|
v-model="routingStrategy"
|
||||||
|
:dropdown-class-name="themeSwitcher.darkCardClass"
|
||||||
|
style="width: 100%">
|
||||||
|
<a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.blockConfigs"}}'>
|
||||||
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
|
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||||
|
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||||
|
{{ i18n "pages.settings.templates.blockConfigsDesc" }}
|
||||||
|
</h2>
|
||||||
|
</a-row>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigTorrent"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigAds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigFamily"}}' desc='{{ i18n "pages.settings.templates.xrayConfigFamilyDesc"}}' v-model="familyProtectSettings"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.blockCountryConfigs"}}'>
|
||||||
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
|
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||||
|
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||||
|
{{ i18n "pages.settings.templates.blockCountryConfigsDesc" }}
|
||||||
|
</h2>
|
||||||
|
</a-row>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.directCountryConfigs"}}'>
|
||||||
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
|
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||||
|
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||||
|
{{ i18n "pages.settings.templates.directCountryConfigsDesc" }}
|
||||||
|
</h2>
|
||||||
|
</a-row>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRIpDesc"}}' v-model="IRIpDirectSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomainDesc"}}' v-model="IRDomainDirectSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIpDesc"}}' v-model="ChinaIpDirectSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomainDesc"}}' v-model="ChinaDomainDirectSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIpDesc"}}' v-model="RussiaIpDirectSettings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomainDesc"}}' v-model="RussiaDomainDirectSettings"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'>
|
||||||
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
|
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||||
|
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||||
|
{{ i18n "pages.settings.templates.ipv4ConfigsDesc" }}
|
||||||
|
</h2>
|
||||||
|
</a-row>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualLists"}}'>
|
||||||
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
|
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
|
||||||
|
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||||
|
{{ i18n "pages.settings.templates.manualListsDesc" }}
|
||||||
|
</h2>
|
||||||
|
</a-row>
|
||||||
|
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedIPs"}}' v-model="manualBlockedIPs"></setting-list-item>
|
||||||
|
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedDomains"}}' v-model="manualBlockedDomains"></setting-list-item>
|
||||||
|
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectIPs"}}' v-model="manualDirectIPs"></setting-list-item>
|
||||||
|
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectDomains"}}' v-model="manualDirectDomains"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
|
||||||
|
<a-space direction="horizontal" style="padding: 0 20px">
|
||||||
|
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
|
||||||
|
<a-collapse>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'>
|
||||||
|
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}'>
|
||||||
|
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOutboundsDesc"}}' v-model="outboundSettings"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}'>
|
||||||
|
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
|
||||||
|
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</a-list>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="4" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
|
||||||
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
|
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 20px 20px; text-align: center;">
|
||||||
|
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
|
||||||
|
{{ i18n "pages.settings.infoDesc" }}
|
||||||
|
</h2>
|
||||||
|
</a-row>
|
||||||
|
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
|
||||||
|
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
||||||
|
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
|
||||||
|
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramNotifyTime"}}' desc='{{ i18n "pages.settings.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
||||||
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyBackup" }}' desc='{{ i18n "pages.settings.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
|
||||||
|
<setting-list-item type="number" title='{{ i18n "pages.settings.tgNotifyCpu" }}' desc='{{ i18n "pages.settings.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
|
||||||
|
</a-list>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</a-space>
|
||||||
|
</a-spin>
|
||||||
|
</a-layout-content>
|
||||||
|
</a-layout>
|
||||||
|
</a-layout>
|
||||||
|
{{template "js" .}}
|
||||||
|
{{template "component/themeSwitcher" .}}
|
||||||
|
{{template "component/password" .}}
|
||||||
|
{{template "component/setting"}}
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const app = new Vue({
|
||||||
|
delimiters: ['[[', ']]'],
|
||||||
|
el: '#app',
|
||||||
|
data: {
|
||||||
|
siderDrawer,
|
||||||
|
themeSwitcher,
|
||||||
|
spinning: false,
|
||||||
|
oldAllSetting: new AllSetting(),
|
||||||
|
allSetting: new AllSetting(),
|
||||||
|
saveBtnDisable: true,
|
||||||
|
user: {},
|
||||||
|
lang: getLang(),
|
||||||
|
ipv4Settings: {
|
||||||
|
tag: "IPv4",
|
||||||
|
protocol: "freedom",
|
||||||
|
settings: {
|
||||||
|
domainStrategy: "UseIPv4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
directSettings: {
|
||||||
|
tag: "direct",
|
||||||
|
protocol: "freedom"
|
||||||
|
},
|
||||||
|
outboundDomainStrategies: ["AsIs", "UseIP", "UseIPv4", "UseIPv6"],
|
||||||
|
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
|
||||||
|
settingsData: {
|
||||||
|
protocols: {
|
||||||
|
bittorrent: ["bittorrent"],
|
||||||
|
},
|
||||||
|
ips: {
|
||||||
|
local: ["geoip:private"],
|
||||||
|
cn: ["geoip:cn"],
|
||||||
|
ir: ["geoip:ir"],
|
||||||
|
ru: ["geoip:ru"],
|
||||||
|
},
|
||||||
|
domains: {
|
||||||
|
ads: [
|
||||||
|
"geosite:category-ads-all",
|
||||||
|
"geosite:category-ads",
|
||||||
|
"geosite:google-ads",
|
||||||
|
"geosite:spotify-ads",
|
||||||
|
"ext:iran.dat:ads"
|
||||||
|
],
|
||||||
|
google: ["geosite:google"],
|
||||||
|
netflix: ["geosite:netflix"],
|
||||||
|
cn: [
|
||||||
|
"geosite:cn",
|
||||||
|
"regexp:.*\\.cn$"
|
||||||
|
],
|
||||||
|
ru: [
|
||||||
|
"geosite:category-gov-ru",
|
||||||
|
"regexp:.*\\.ru$"
|
||||||
|
],
|
||||||
|
ir: [
|
||||||
|
"regexp:.*\\.ir$",
|
||||||
|
"ext:iran.dat:ir",
|
||||||
|
"ext:iran.dat:other",
|
||||||
|
"geosite:category-ir"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
familyProtectDNS: {
|
||||||
|
"servers": [
|
||||||
|
"1.1.1.3",
|
||||||
|
"1.0.0.3",
|
||||||
|
"94.140.14.15",
|
||||||
|
"94.140.15.16"
|
||||||
|
],
|
||||||
|
"queryStrategy": "UseIPv4"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loading(spinning = true) {
|
||||||
|
this.spinning = spinning;
|
||||||
|
},
|
||||||
|
async getAllSetting() {
|
||||||
|
this.loading(true);
|
||||||
|
const msg = await HttpUtil.post("/xui/setting/all");
|
||||||
|
this.loading(false);
|
||||||
|
if (msg.success) {
|
||||||
|
this.oldAllSetting = new AllSetting(msg.obj);
|
||||||
|
this.allSetting = new AllSetting(msg.obj);
|
||||||
|
this.saveBtnDisable = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateAllSetting() {
|
||||||
|
this.loading(true);
|
||||||
|
const msg = await HttpUtil.post("/xui/setting/update", this.allSetting);
|
||||||
|
this.loading(false);
|
||||||
|
if (msg.success) {
|
||||||
|
await this.getAllSetting();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async updateUser() {
|
||||||
|
this.loading(true);
|
||||||
|
const msg = await HttpUtil.post("/xui/setting/updateUser", this.user);
|
||||||
|
this.loading(false);
|
||||||
|
if (msg.success) {
|
||||||
|
this.user = {};
|
||||||
|
window.location.replace(basePath + "logout")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async restartPanel() {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
this.$confirm({
|
||||||
|
title: '{{ i18n "pages.settings.restartPanel" }}',
|
||||||
|
content: '{{ i18n "pages.settings.restartPanelDesc" }}',
|
||||||
|
okText: '{{ i18n "sure" }}',
|
||||||
|
cancelText: '{{ i18n "cancel" }}',
|
||||||
|
onOk: () => resolve(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.loading(true);
|
||||||
|
const msg = await HttpUtil.post("/xui/setting/restartPanel");
|
||||||
|
this.loading(false);
|
||||||
|
if (msg.success) {
|
||||||
|
this.loading(true);
|
||||||
|
await PromiseUtil.sleep(5000);
|
||||||
|
window.location.replace(this.allSetting.webBasePath + "xui/settings");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async resetXrayConfigToDefault() {
|
||||||
|
this.loading(true);
|
||||||
|
const msg = await HttpUtil.get("/xui/setting/getDefaultJsonConfig");
|
||||||
|
this.loading(false);
|
||||||
|
if (msg.success) {
|
||||||
|
this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
|
||||||
|
this.saveBtnDisable = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
syncRulesWithOutbound(tag, setting) {
|
||||||
|
const newTemplateSettings = this.templateSettings;
|
||||||
|
const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag);
|
||||||
|
const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag);
|
||||||
|
if (!haveRules && outboundIndex > 0) {
|
||||||
|
newTemplateSettings.outbounds.splice(outboundIndex);
|
||||||
|
}
|
||||||
|
if (haveRules && outboundIndex < 0) {
|
||||||
|
newTemplateSettings.outbounds.push(setting);
|
||||||
|
}
|
||||||
|
this.templateSettings = newTemplateSettings;
|
||||||
|
},
|
||||||
|
templateRuleGetter(routeSettings) {
|
||||||
|
const { property, outboundTag } = routeSettings;
|
||||||
|
let result = [];
|
||||||
|
if (this.templateSettings != null) {
|
||||||
|
this.templateSettings.routing.rules.forEach(
|
||||||
|
(routingRule) => {
|
||||||
|
if (
|
||||||
|
routingRule.hasOwnProperty(property) &&
|
||||||
|
routingRule.hasOwnProperty("outboundTag") &&
|
||||||
|
routingRule.outboundTag === outboundTag
|
||||||
|
) {
|
||||||
|
result.push(...routingRule[property]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
templateRuleSetter(routeSettings) {
|
||||||
|
const { data, property, outboundTag } = routeSettings;
|
||||||
|
const oldTemplateSettings = this.templateSettings;
|
||||||
|
const newTemplateSettings = oldTemplateSettings;
|
||||||
|
currentProperty = this.templateRuleGetter({ outboundTag, property })
|
||||||
|
if (currentProperty.length == 0) {
|
||||||
|
const propertyRule = {
|
||||||
|
type: "field",
|
||||||
|
outboundTag,
|
||||||
|
[property]: data
|
||||||
|
};
|
||||||
|
newTemplateSettings.routing.rules.push(propertyRule);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const newRules = [];
|
||||||
|
insertedOnce = false;
|
||||||
|
newTemplateSettings.routing.rules.forEach(
|
||||||
|
(routingRule) => {
|
||||||
|
if (
|
||||||
|
routingRule.hasOwnProperty(property) &&
|
||||||
|
routingRule.hasOwnProperty("outboundTag") &&
|
||||||
|
routingRule.outboundTag === outboundTag
|
||||||
|
) {
|
||||||
|
if (!insertedOnce && data.length > 0) {
|
||||||
|
insertedOnce = true;
|
||||||
|
routingRule[property] = data;
|
||||||
|
newRules.push(routingRule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newRules.push(routingRule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
newTemplateSettings.routing.rules = newRules;
|
||||||
|
}
|
||||||
|
this.templateSettings = newTemplateSettings;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.getAllSetting();
|
||||||
|
while (true) {
|
||||||
|
await PromiseUtil.sleep(1000);
|
||||||
|
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
templateSettings: {
|
||||||
|
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; },
|
||||||
|
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
|
||||||
|
},
|
||||||
|
inboundSettings: {
|
||||||
|
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
|
||||||
|
set: function (newValue) {
|
||||||
|
newTemplateSettings = this.templateSettings;
|
||||||
|
newTemplateSettings.inbounds = JSON.parse(newValue)
|
||||||
|
this.templateSettings = newTemplateSettings
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outboundSettings: {
|
||||||
|
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
|
||||||
|
set: function (newValue) {
|
||||||
|
newTemplateSettings = this.templateSettings;
|
||||||
|
newTemplateSettings.outbounds = JSON.parse(newValue)
|
||||||
|
this.templateSettings = newTemplateSettings
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routingRuleSettings: {
|
||||||
|
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
|
||||||
|
set: function (newValue) {
|
||||||
|
newTemplateSettings = this.templateSettings;
|
||||||
|
newTemplateSettings.routing.rules = JSON.parse(newValue)
|
||||||
|
this.templateSettings = newTemplateSettings
|
||||||
|
},
|
||||||
|
},
|
||||||
|
freedomStrategy: {
|
||||||
|
get: function () {
|
||||||
|
if (!this.templateSettings) return "AsIs";
|
||||||
|
freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && !o.tag);
|
||||||
|
if (!freedomOutbound) return "AsIs";
|
||||||
|
if (!freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return "AsIs";
|
||||||
|
return freedomOutbound.settings.domainStrategy;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
newTemplateSettings = this.templateSettings;
|
||||||
|
freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && !o.tag);
|
||||||
|
if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) {
|
||||||
|
newTemplateSettings.outbounds[freedomOutboundIndex].settings = { "domainStrategy": newValue };
|
||||||
|
} else {
|
||||||
|
newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;
|
||||||
|
}
|
||||||
|
this.templateSettings = newTemplateSettings;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
routingStrategy: {
|
||||||
|
get: function () {
|
||||||
|
if (!this.templateSettings || !this.templateSettings.routing || !this.templateSettings.routing.domainStrategy) return "AsIs";
|
||||||
|
return this.templateSettings.routing.domainStrategy;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
newTemplateSettings = this.templateSettings;
|
||||||
|
newTemplateSettings.routing.domainStrategy = newValue;
|
||||||
|
this.templateSettings = newTemplateSettings;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
blockedIPs: {
|
||||||
|
get: function () {
|
||||||
|
return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.templateRuleSetter({ outboundTag: "blocked", property: "ip", data: newValue });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
blockedDomains: {
|
||||||
|
get: function () {
|
||||||
|
return this.templateRuleGetter({ outboundTag: "blocked", property: "domain" });
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.templateRuleSetter({ outboundTag: "blocked", property: "domain", data: newValue });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
blockedProtocols: {
|
||||||
|
get: function () {
|
||||||
|
return this.templateRuleGetter({ outboundTag: "blocked", property: "protocol" });
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.templateRuleSetter({ outboundTag: "blocked", property: "protocol", data: newValue });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
directIPs: {
|
||||||
|
get: function () {
|
||||||
|
return this.templateRuleGetter({ outboundTag: "direct", property: "ip" });
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.templateRuleSetter({ outboundTag: "direct", property: "ip", data: newValue });
|
||||||
|
this.syncRulesWithOutbound("direct", this.directSettings);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
directDomains: {
|
||||||
|
get: function () {
|
||||||
|
return this.templateRuleGetter({ outboundTag: "direct", property: "domain" });
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.templateRuleSetter({ outboundTag: "direct", property: "domain", data: newValue });
|
||||||
|
this.syncRulesWithOutbound("direct", this.directSettings);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
manualBlockedIPs: {
|
||||||
|
get: function () { return JSON.stringify(this.blockedIPs, null, 2); },
|
||||||
|
set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000)
|
||||||
|
},
|
||||||
|
manualBlockedDomains: {
|
||||||
|
get: function () { return JSON.stringify(this.blockedDomains, null, 2); },
|
||||||
|
set: debounce(function (value) { this.blockedDomains = JSON.parse(value); }, 1000)
|
||||||
|
},
|
||||||
|
manualDirectIPs: {
|
||||||
|
get: function () { return JSON.stringify(this.directIPs, null, 2); },
|
||||||
|
set: debounce(function (value) { this.directIPs = JSON.parse(value); }, 1000)
|
||||||
|
},
|
||||||
|
manualDirectDomains: {
|
||||||
|
get: function () { return JSON.stringify(this.directDomains, null, 2); },
|
||||||
|
set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000)
|
||||||
|
},
|
||||||
|
torrentSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.blockedProtocols = [...this.blockedProtocols, ...this.settingsData.protocols.bittorrent];
|
||||||
|
} else {
|
||||||
|
this.blockedProtocols = this.blockedProtocols.filter(data => !this.settingsData.protocols.bittorrent.includes(data));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
privateIpSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.ips.local, this.blockedIPs);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.local];
|
||||||
|
} else {
|
||||||
|
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.local.includes(data));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AdsSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.domains.ads, this.blockedDomains);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ads];
|
||||||
|
} else {
|
||||||
|
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ads.includes(data));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
familyProtectSettings: {
|
||||||
|
get: function () {
|
||||||
|
if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false;
|
||||||
|
return doAllItemsExist(this.templateSettings.dns.servers, this.settingsData.familyProtectDNS.servers);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
newTemplateSettings = this.templateSettings;
|
||||||
|
if (newValue) {
|
||||||
|
newTemplateSettings.dns = this.settingsData.familyProtectDNS;
|
||||||
|
} else {
|
||||||
|
delete newTemplateSettings.dns;
|
||||||
|
}
|
||||||
|
this.templateSettings = newTemplateSettings;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GoogleIPv4Settings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.domains.google, this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }));
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
oldData = this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
|
||||||
|
if (newValue) {
|
||||||
|
oldData = [...oldData, ...this.settingsData.domains.google];
|
||||||
|
} else {
|
||||||
|
oldData = oldData.filter(data => !this.settingsData.domains.google.includes(data))
|
||||||
|
}
|
||||||
|
this.templateRuleSetter({
|
||||||
|
outboundTag: "IPv4",
|
||||||
|
property: "domain",
|
||||||
|
data: oldData
|
||||||
|
});
|
||||||
|
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NetflixIPv4Settings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.domains.netflix, this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }));
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
oldData = this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
|
||||||
|
if (newValue) {
|
||||||
|
oldData = [...oldData, ...this.settingsData.domains.netflix];
|
||||||
|
} else {
|
||||||
|
oldData = oldData.filter(data => !this.settingsData.domains.netflix.includes(data))
|
||||||
|
}
|
||||||
|
this.templateRuleSetter({
|
||||||
|
outboundTag: "IPv4",
|
||||||
|
property: "domain",
|
||||||
|
data: oldData
|
||||||
|
});
|
||||||
|
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IRIpSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.ips.ir, this.blockedIPs);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ir];
|
||||||
|
} else {
|
||||||
|
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ir.includes(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IRDomainSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.domains.ir, this.blockedDomains);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ir];
|
||||||
|
} else {
|
||||||
|
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ir.includes(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ChinaIpSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.ips.cn, this.blockedIPs);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.cn];
|
||||||
|
} else {
|
||||||
|
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.cn.includes(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ChinaDomainSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.domains.cn, this.blockedDomains);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.cn];
|
||||||
|
} else {
|
||||||
|
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.cn.includes(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RussiaIpSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.ips.ru, this.blockedIPs);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ru];
|
||||||
|
} else {
|
||||||
|
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ru.includes(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RussiaDomainSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.domains.ru, this.blockedDomains);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ru];
|
||||||
|
} else {
|
||||||
|
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ru.includes(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IRIpDirectSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.ips.ir, this.directIPs);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.directIPs = [...this.directIPs, ...this.settingsData.ips.ir];
|
||||||
|
} else {
|
||||||
|
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ir.includes(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IRDomainDirectSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.domains.ir, this.directDomains);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.directDomains = [...this.directDomains, ...this.settingsData.domains.ir];
|
||||||
|
} else {
|
||||||
|
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ir.includes(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ChinaIpDirectSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.ips.cn, this.directIPs);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.directIPs = [...this.directIPs, ...this.settingsData.ips.cn];
|
||||||
|
} else {
|
||||||
|
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.cn.includes(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ChinaDomainDirectSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.domains.cn, this.directDomains);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.directDomains = [...this.directDomains, ...this.settingsData.domains.cn];
|
||||||
|
} else {
|
||||||
|
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.cn.includes(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RussiaIpDirectSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.ips.ru, this.directIPs);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.directIPs = [...this.directIPs, ...this.settingsData.ips.ru];
|
||||||
|
} else {
|
||||||
|
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ru.includes(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RussiaDomainDirectSettings: {
|
||||||
|
get: function () {
|
||||||
|
return doAllItemsExist(this.settingsData.domains.ru, this.directDomains);
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.directDomains = [...this.directDomains, ...this.settingsData.domains.ru];
|
||||||
|
} else {
|
||||||
|
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ru.includes(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
30
web/job/check_cpu_usage.go
Normal file
30
web/job/check_cpu_usage.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package job
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"x-ui/web/service"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CheckCpuJob struct {
|
||||||
|
tgbotService service.Tgbot
|
||||||
|
settingService service.SettingService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckCpuJob() *CheckCpuJob {
|
||||||
|
return new(CheckCpuJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here run is a interface method of Job interface
|
||||||
|
func (j *CheckCpuJob) Run() {
|
||||||
|
threshold, _ := j.settingService.GetTgCpu()
|
||||||
|
|
||||||
|
// get latest status of server
|
||||||
|
percent, err := cpu.Percent(1*time.Second, false)
|
||||||
|
if err == nil && percent[0] > float64(threshold) {
|
||||||
|
msg := fmt.Sprintf("🔴 CPU usage %.2f%% is more than threshold %d%%", percent[0], threshold)
|
||||||
|
j.tgbotService.SendMsgToTgbotAdmins(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,7 @@
|
|||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
"x-ui/logger"
|
|
||||||
"x-ui/util/common"
|
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
|
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginStatus byte
|
type LoginStatus byte
|
||||||
@@ -20,229 +12,18 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StatsNotifyJob struct {
|
type StatsNotifyJob struct {
|
||||||
enable bool
|
xrayService service.XrayService
|
||||||
xrayService service.XrayService
|
tgbotService service.Tgbot
|
||||||
inboundService service.InboundService
|
|
||||||
settingService service.SettingService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStatsNotifyJob() *StatsNotifyJob {
|
func NewStatsNotifyJob() *StatsNotifyJob {
|
||||||
return new(StatsNotifyJob)
|
return new(StatsNotifyJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *StatsNotifyJob) SendMsgToTgbot(msg string) {
|
|
||||||
//Telegram bot basic info
|
|
||||||
tgBottoken, err := j.settingService.GetTgBotToken()
|
|
||||||
if err != nil || tgBottoken == "" {
|
|
||||||
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tgBotid, err := j.settingService.GetTgBotChatId()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("sendMsgToTgbot failed,GetTgBotChatId fail:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bot, err := tgbotapi.NewBotAPI(tgBottoken)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("get tgbot error:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bot.Debug = true
|
|
||||||
fmt.Printf("Authorized on account %s", bot.Self.UserName)
|
|
||||||
info := tgbotapi.NewMessage(int64(tgBotid), msg)
|
|
||||||
//msg.ReplyToMessageID = int(tgBotid)
|
|
||||||
bot.Send(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here run is a interface method of Job interface
|
// Here run is a interface method of Job interface
|
||||||
func (j *StatsNotifyJob) Run() {
|
func (j *StatsNotifyJob) Run() {
|
||||||
if !j.xrayService.IsXrayRunning() {
|
if !j.xrayService.IsXrayRunning() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var info string
|
j.tgbotService.SendReport()
|
||||||
//get hostname
|
|
||||||
name, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("get hostname error:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
info = fmt.Sprintf("Hostname:%s\r\n", name)
|
|
||||||
//get ip address
|
|
||||||
var ip string
|
|
||||||
netInterfaces, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("net.Interfaces failed, err:", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(netInterfaces); i++ {
|
|
||||||
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
|
|
||||||
addrs, _ := netInterfaces[i].Addrs()
|
|
||||||
|
|
||||||
for _, address := range addrs {
|
|
||||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
|
||||||
if ipnet.IP.To4() != nil {
|
|
||||||
ip = ipnet.IP.String()
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
ip = ipnet.IP.String()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info += fmt.Sprintf("IP:%s\r\n \r\n", ip)
|
|
||||||
|
|
||||||
// get traffic
|
|
||||||
inbouds, err := j.inboundService.GetAllInbounds()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("StatsNotifyJob run failed:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// NOTE:If there no any sessions here,need to notify here
|
|
||||||
// TODO:Sub-node push, automatic conversion format
|
|
||||||
for _, inbound := range inbouds {
|
|
||||||
info += fmt.Sprintf("Node name:%s\r\nPort:%d\r\nUpload↑:%s\r\nDownload↓:%s\r\nTotal:%s\r\n", inbound.Remark, inbound.Port, common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down), common.FormatTraffic((inbound.Up + inbound.Down)))
|
|
||||||
if inbound.ExpiryTime == 0 {
|
|
||||||
info += fmt.Sprintf("Expire date:unlimited\r\n \r\n")
|
|
||||||
} else {
|
|
||||||
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
j.SendMsgToTgbot(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (j *StatsNotifyJob) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
|
|
||||||
if username == "" || ip == "" || time == "" {
|
|
||||||
logger.Warning("UserLoginNotify failed,invalid info")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var msg string
|
|
||||||
// Get hostname
|
|
||||||
name, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("get hostname error:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if status == LoginSuccess {
|
|
||||||
msg = fmt.Sprintf("Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
|
|
||||||
} else if status == LoginFail {
|
|
||||||
msg = fmt.Sprintf("Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
|
|
||||||
}
|
|
||||||
msg += fmt.Sprintf("Time:%s\r\n", time)
|
|
||||||
msg += fmt.Sprintf("Username:%s\r\n", username)
|
|
||||||
msg += fmt.Sprintf("IP:%s\r\n", ip)
|
|
||||||
j.SendMsgToTgbot(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
|
|
||||||
tgbotapi.NewInlineKeyboardRow(
|
|
||||||
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "get_usage"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
func (j *StatsNotifyJob) OnReceive() *StatsNotifyJob {
|
|
||||||
tgBottoken, err := j.settingService.GetTgBotToken()
|
|
||||||
if err != nil || tgBottoken == "" {
|
|
||||||
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
|
|
||||||
return j
|
|
||||||
}
|
|
||||||
bot, err := tgbotapi.NewBotAPI(tgBottoken)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("get tgbot error:", err)
|
|
||||||
return j
|
|
||||||
}
|
|
||||||
bot.Debug = false
|
|
||||||
u := tgbotapi.NewUpdate(0)
|
|
||||||
u.Timeout = 10
|
|
||||||
|
|
||||||
updates := bot.GetUpdatesChan(u)
|
|
||||||
|
|
||||||
for update := range updates {
|
|
||||||
if update.Message == nil {
|
|
||||||
|
|
||||||
if update.CallbackQuery != nil {
|
|
||||||
// Respond to the callback query, telling Telegram to show the user
|
|
||||||
// a message with the data received.
|
|
||||||
callback := tgbotapi.NewCallback(update.CallbackQuery.ID, update.CallbackQuery.Data)
|
|
||||||
if _, err := bot.Request(callback); err != nil {
|
|
||||||
logger.Warning(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// And finally, send a message containing the data received.
|
|
||||||
msg := tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, "")
|
|
||||||
|
|
||||||
switch update.CallbackQuery.Data {
|
|
||||||
case "get_usage":
|
|
||||||
msg.Text = "for get your usage send command like this : \n <code>/usage uuid | id</code> \n example : <code>/usage fc3239ed-8f3b-4151-ff51-b183d5182142</code>"
|
|
||||||
msg.ParseMode = "HTML"
|
|
||||||
}
|
|
||||||
if _, err := bot.Send(msg); err != nil {
|
|
||||||
logger.Warning(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !update.Message.IsCommand() { // ignore any non-command Messages
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new MessageConfig. We don't have text yet,
|
|
||||||
// so we leave it empty.
|
|
||||||
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "")
|
|
||||||
|
|
||||||
// Extract the command from the Message.
|
|
||||||
switch update.Message.Command() {
|
|
||||||
case "help":
|
|
||||||
msg.Text = "What you need?"
|
|
||||||
msg.ReplyMarkup = numericKeyboard
|
|
||||||
case "start":
|
|
||||||
msg.Text = "Hi :) \n What you need?"
|
|
||||||
msg.ReplyMarkup = numericKeyboard
|
|
||||||
|
|
||||||
case "status":
|
|
||||||
msg.Text = "bot is ok."
|
|
||||||
|
|
||||||
case "usage":
|
|
||||||
msg.Text = j.getClientUsage(update.Message.CommandArguments())
|
|
||||||
default:
|
|
||||||
msg.Text = "I don't know that command, /help"
|
|
||||||
msg.ReplyMarkup = numericKeyboard
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := bot.Send(msg); err != nil {
|
|
||||||
logger.Warning(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return j
|
|
||||||
|
|
||||||
}
|
|
||||||
func (j *StatsNotifyJob) getClientUsage(id string) string {
|
|
||||||
traffic, err := j.inboundService.GetClientTrafficById(id)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning(err)
|
|
||||||
return "something wrong!"
|
|
||||||
}
|
|
||||||
expiryTime := ""
|
|
||||||
if traffic.ExpiryTime == 0 {
|
|
||||||
expiryTime = fmt.Sprintf("unlimited")
|
|
||||||
} else {
|
|
||||||
expiryTime = fmt.Sprintf("%s", time.Unix((traffic.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
|
||||||
}
|
|
||||||
total := ""
|
|
||||||
if traffic.Total == 0 {
|
|
||||||
total = fmt.Sprintf("unlimited")
|
|
||||||
} else {
|
|
||||||
total = fmt.Sprintf("%s", common.FormatTraffic((traffic.Total)))
|
|
||||||
}
|
|
||||||
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
|
||||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
|
||||||
total, expiryTime)
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
{
|
{
|
||||||
"log": {
|
"log": {
|
||||||
"loglevel": "warning",
|
"loglevel": "warning"
|
||||||
"access": "./access.log"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"api": {
|
"api": {
|
||||||
"services": [
|
"tag": "api",
|
||||||
"HandlerService",
|
"services": ["HandlerService", "LoggerService", "StatsService"]
|
||||||
"LoggerService",
|
|
||||||
"StatsService"
|
|
||||||
],
|
|
||||||
"tag": "api"
|
|
||||||
},
|
},
|
||||||
"inbounds": [
|
"inbounds": [
|
||||||
{
|
{
|
||||||
|
"tag": "api",
|
||||||
"listen": "127.0.0.1",
|
"listen": "127.0.0.1",
|
||||||
"port": 62789,
|
"port": 62789,
|
||||||
"protocol": "dokodemo-door",
|
"protocol": "dokodemo-door",
|
||||||
"settings": {
|
"settings": {
|
||||||
"address": "127.0.0.1"
|
"address": "127.0.0.1"
|
||||||
},
|
}
|
||||||
"tag": "api"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outbounds": [
|
"outbounds": [
|
||||||
@@ -29,16 +23,16 @@
|
|||||||
"settings": {}
|
"settings": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"tag": "blocked",
|
||||||
"protocol": "blackhole",
|
"protocol": "blackhole",
|
||||||
"settings": {},
|
"settings": {}
|
||||||
"tag": "blocked"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"policy": {
|
"policy": {
|
||||||
"levels": {
|
"levels": {
|
||||||
"0": {
|
"0": {
|
||||||
"statsUserUplink": true,
|
"statsUserDownlink": true,
|
||||||
"statsUserDownlink": true
|
"statsUserUplink": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
@@ -47,27 +41,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"routing": {
|
"routing": {
|
||||||
|
"domainStrategy": "IPIfNonMatch",
|
||||||
"rules": [
|
"rules": [
|
||||||
{
|
{
|
||||||
"inboundTag": [
|
"type": "field",
|
||||||
"api"
|
"inboundTag": ["api"],
|
||||||
],
|
"outboundTag": "api"
|
||||||
"outboundTag": "api",
|
|
||||||
"type": "field"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ip": [
|
"type": "field",
|
||||||
"geoip:private"
|
|
||||||
],
|
|
||||||
"outboundTag": "blocked",
|
"outboundTag": "blocked",
|
||||||
"type": "field"
|
"ip": ["geoip:private"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"type": "field",
|
||||||
"outboundTag": "blocked",
|
"outboundTag": "blocked",
|
||||||
"protocol": [
|
"protocol": ["bittorrent"]
|
||||||
"bittorrent"
|
|
||||||
],
|
|
||||||
"type": "field"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"x-ui/database"
|
"x-ui/database"
|
||||||
"x-ui/database/model"
|
"x-ui/database/model"
|
||||||
@@ -64,28 +65,45 @@ func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, err
|
|||||||
return clients, nil
|
return clients, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) checkEmailsExist(emails map[string]bool, ignoreId int) (string, error) {
|
func (s *InboundService) getAllEmails() ([]string, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
var inbounds []*model.Inbound
|
var emails []string
|
||||||
db = db.Model(model.Inbound{}).Where("Protocol in ?", []model.Protocol{model.VMess, model.VLESS, model.Trojan})
|
err := db.Raw(`
|
||||||
if ignoreId > 0 {
|
SELECT JSON_EXTRACT(client.value, '$.email')
|
||||||
db = db.Where("id != ?", ignoreId)
|
FROM inbounds,
|
||||||
}
|
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
|
||||||
db = db.Find(&inbounds)
|
`).Scan(&emails).Error
|
||||||
if db.Error != nil {
|
|
||||||
return "", db.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, inbound := range inbounds {
|
if err != nil {
|
||||||
clients, err := s.getClients(inbound)
|
return nil, err
|
||||||
if err != nil {
|
}
|
||||||
return "", err
|
return emails, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) contains(slice []string, str string) bool {
|
||||||
|
for _, s := range slice {
|
||||||
|
if s == str {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
for _, client := range clients {
|
func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (string, error) {
|
||||||
if emails[client.Email] {
|
allEmails, err := s.getAllEmails()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var emails []string
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.Email != "" {
|
||||||
|
if s.contains(emails, client.Email) {
|
||||||
return client.Email, nil
|
return client.Email, nil
|
||||||
}
|
}
|
||||||
|
if s.contains(allEmails, client.Email) {
|
||||||
|
return client.Email, nil
|
||||||
|
}
|
||||||
|
emails = append(emails, client.Email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "", nil
|
return "", nil
|
||||||
@@ -96,16 +114,23 @@ func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (stri
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
emails := make(map[string]bool)
|
allEmails, err := s.getAllEmails()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var emails []string
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
if client.Email != "" {
|
if client.Email != "" {
|
||||||
if emails[client.Email] {
|
if s.contains(emails, client.Email) {
|
||||||
return client.Email, nil
|
return client.Email, nil
|
||||||
}
|
}
|
||||||
emails[client.Email] = true
|
if s.contains(allEmails, client.Email) {
|
||||||
|
return client.Email, nil
|
||||||
|
}
|
||||||
|
emails = append(emails, client.Email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return s.checkEmailsExist(emails, inbound.Id)
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, error) {
|
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, error) {
|
||||||
@@ -201,14 +226,6 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
|||||||
return inbound, common.NewError("Port already exists:", inbound.Port)
|
return inbound, common.NewError("Port already exists:", inbound.Port)
|
||||||
}
|
}
|
||||||
|
|
||||||
existEmail, err := s.checkEmailExistForInbound(inbound)
|
|
||||||
if err != nil {
|
|
||||||
return inbound, err
|
|
||||||
}
|
|
||||||
if existEmail != "" {
|
|
||||||
return inbound, common.NewError("Duplicate email:", existEmail)
|
|
||||||
}
|
|
||||||
|
|
||||||
oldInbound, err := s.GetInbound(inbound.Id)
|
oldInbound, err := s.GetInbound(inbound.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return inbound, err
|
return inbound, err
|
||||||
@@ -231,79 +248,124 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
|||||||
return inbound, db.Save(oldInbound).Error
|
return inbound, db.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) AddInboundClient(inbound *model.Inbound) error {
|
func (s *InboundService) AddInboundClient(data *model.Inbound) error {
|
||||||
existEmail, err := s.checkEmailExistForInbound(inbound)
|
clients, err := s.getClients(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var settings map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(data.Settings), &settings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaceClients := settings["clients"].([]interface{})
|
||||||
|
existEmail, err := s.checkEmailsExistForClients(clients)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if existEmail != "" {
|
if existEmail != "" {
|
||||||
return common.NewError("Duplicate email:", existEmail)
|
return common.NewError("Duplicate email:", existEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
clients, err := s.getClients(inbound)
|
oldInbound, err := s.GetInbound(data.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
oldInbound, err := s.GetInbound(inbound.Id)
|
var oldSettings map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
oldClients, err := s.getClients(oldInbound)
|
oldClients := oldSettings["clients"].([]interface{})
|
||||||
|
oldClients = append(oldClients, interfaceClients...)
|
||||||
|
|
||||||
|
oldSettings["clients"] = oldClients
|
||||||
|
|
||||||
|
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
oldInbound.Settings = inbound.Settings
|
oldInbound.Settings = string(newSettings)
|
||||||
|
|
||||||
if len(clients[len(clients)-1].Email) > 0 {
|
for _, client := range clients {
|
||||||
s.AddClientStat(inbound.Id, &clients[len(clients)-1])
|
if len(client.Email) > 0 {
|
||||||
}
|
s.AddClientStat(data.Id, &client)
|
||||||
for i := len(oldClients); i < len(clients); i++ {
|
|
||||||
if len(clients[i].Email) > 0 {
|
|
||||||
s.AddClientStat(inbound.Id, &clients[i])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
return db.Save(oldInbound).Error
|
return db.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) DelInboundClient(inbound *model.Inbound, email string) error {
|
func (s *InboundService) DelInboundClient(inboundId int, clientId string) error {
|
||||||
db := database.GetDB()
|
oldInbound, err := s.GetInbound(inboundId)
|
||||||
err := s.DelClientStat(db, email)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Delete stats Data Error")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldInbound, err := s.GetInbound(inbound.Id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Load Old Data Error")
|
logger.Error("Load Old Data Error")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var settings map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
oldInbound.Settings = inbound.Settings
|
email := ""
|
||||||
|
client_key := "id"
|
||||||
|
if oldInbound.Protocol == "trojan" {
|
||||||
|
client_key = "password"
|
||||||
|
}
|
||||||
|
if oldInbound.Protocol == "shadowsocks" {
|
||||||
|
client_key = "email"
|
||||||
|
}
|
||||||
|
|
||||||
|
inerfaceClients := settings["clients"].([]interface{})
|
||||||
|
var newClients []interface{}
|
||||||
|
for _, client := range inerfaceClients {
|
||||||
|
c := client.(map[string]interface{})
|
||||||
|
c_id := c[client_key].(string)
|
||||||
|
if c_id == clientId {
|
||||||
|
email = c["email"].(string)
|
||||||
|
} else {
|
||||||
|
newClients = append(newClients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings["clients"] = newClients
|
||||||
|
newSettings, err := json.MarshalIndent(settings, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldInbound.Settings = string(newSettings)
|
||||||
|
|
||||||
|
db := database.GetDB()
|
||||||
|
err = s.DelClientStat(db, email)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Delete stats Data Error")
|
||||||
|
return err
|
||||||
|
}
|
||||||
return db.Save(oldInbound).Error
|
return db.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) UpdateInboundClient(inbound *model.Inbound, index int) error {
|
func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) error {
|
||||||
existEmail, err := s.checkEmailExistForInbound(inbound)
|
clients, err := s.getClients(data)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if existEmail != "" {
|
|
||||||
return common.NewError("Duplicate email:", existEmail)
|
|
||||||
}
|
|
||||||
|
|
||||||
clients, err := s.getClients(inbound)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
oldInbound, err := s.GetInbound(inbound.Id)
|
var settings map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(data.Settings), &settings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
inerfaceClients := settings["clients"].([]interface{})
|
||||||
|
|
||||||
|
oldInbound, err := s.GetInbound(data.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -313,58 +375,100 @@ func (s *InboundService) UpdateInboundClient(inbound *model.Inbound, index int)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
oldInbound.Settings = inbound.Settings
|
oldEmail := ""
|
||||||
|
clientIndex := 0
|
||||||
|
for index, oldClient := range oldClients {
|
||||||
|
oldClientId := ""
|
||||||
|
if oldInbound.Protocol == "trojan" {
|
||||||
|
oldClientId = oldClient.Password
|
||||||
|
} else if oldInbound.Protocol == "shadowsocks" {
|
||||||
|
oldClientId = oldClient.Email
|
||||||
|
} else {
|
||||||
|
oldClientId = oldClient.ID
|
||||||
|
}
|
||||||
|
if clientId == oldClientId {
|
||||||
|
oldEmail = oldClient.Email
|
||||||
|
clientIndex = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(clients[0].Email) > 0 && clients[0].Email != oldEmail {
|
||||||
|
existEmail, err := s.checkEmailsExistForClients(clients)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if existEmail != "" {
|
||||||
|
return common.NewError("Duplicate email:", existEmail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldSettings map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
settingsClients := oldSettings["clients"].([]interface{})
|
||||||
|
settingsClients[clientIndex] = inerfaceClients[0]
|
||||||
|
oldSettings["clients"] = settingsClients
|
||||||
|
|
||||||
|
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldInbound.Settings = string(newSettings)
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
if len(clients[index].Email) > 0 {
|
if len(clients[0].Email) > 0 {
|
||||||
if len(oldClients[index].Email) > 0 {
|
if len(oldEmail) > 0 {
|
||||||
s.UpdateClientStat(oldClients[index].Email, &clients[index])
|
err = s.UpdateClientStat(oldEmail, &clients[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
s.AddClientStat(inbound.Id, &clients[index])
|
s.AddClientStat(data.Id, &clients[0])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
s.DelClientStat(db, oldClients[index].Email)
|
err = s.DelClientStat(db, oldEmail)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return db.Save(oldInbound).Error
|
return db.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
|
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error {
|
||||||
if len(traffics) == 0 {
|
if len(traffics) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
db := database.GetDB()
|
// Update traffics in a single transaction
|
||||||
db = db.Model(model.Inbound{})
|
err := database.GetDB().Transaction(func(tx *gorm.DB) error {
|
||||||
tx := db.Begin()
|
for _, traffic := range traffics {
|
||||||
defer func() {
|
if traffic.IsInbound {
|
||||||
if err != nil {
|
update := tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
|
||||||
tx.Rollback()
|
Updates(map[string]interface{}{
|
||||||
} else {
|
"up": gorm.Expr("up + ?", traffic.Up),
|
||||||
tx.Commit()
|
"down": gorm.Expr("down + ?", traffic.Down),
|
||||||
}
|
})
|
||||||
}()
|
if update.Error != nil {
|
||||||
for _, traffic := range traffics {
|
return update.Error
|
||||||
if traffic.IsInbound {
|
}
|
||||||
err = tx.Where("tag = ?", traffic.Tag).
|
|
||||||
UpdateColumns(map[string]interface{}{
|
|
||||||
"up": gorm.Expr("up + ?", traffic.Up),
|
|
||||||
"down": gorm.Expr("down + ?", traffic.Down)}).Error
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return nil
|
||||||
return
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) {
|
func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) {
|
||||||
if len(traffics) == 0 {
|
if len(traffics) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
db := database.GetDB()
|
|
||||||
dbInbound := db.Model(model.Inbound{})
|
|
||||||
|
|
||||||
db = db.Model(xray.ClientTraffic{})
|
db := database.GetDB()
|
||||||
tx := db.Begin()
|
tx := db.Begin()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -372,61 +476,90 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
|||||||
tx.Commit()
|
tx.Commit()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
txInbound := dbInbound.Begin()
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
txInbound.Rollback()
|
|
||||||
} else {
|
|
||||||
txInbound.Commit()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
|
emails := make([]string, 0, len(traffics))
|
||||||
for _, traffic := range traffics {
|
for _, traffic := range traffics {
|
||||||
inbound := &model.Inbound{}
|
emails = append(emails, traffic.Email)
|
||||||
client := &xray.ClientTraffic{}
|
|
||||||
err := tx.Where("email = ?", traffic.Email).First(client).Error
|
|
||||||
if err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
logger.Warning(err, traffic.Email)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = txInbound.Where("id=?", client.InboundId).First(inbound).Error
|
|
||||||
if err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
logger.Warning(err, traffic.Email)
|
|
||||||
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// get settings clients
|
|
||||||
settings := map[string][]model.Client{}
|
|
||||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
|
||||||
clients := settings["clients"]
|
|
||||||
for _, client := range clients {
|
|
||||||
if traffic.Email == client.Email {
|
|
||||||
traffic.ExpiryTime = client.ExpiryTime
|
|
||||||
traffic.Total = client.TotalGB
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tx.Where("inbound_id = ? and email = ?", inbound.Id, traffic.Email).
|
|
||||||
UpdateColumns(map[string]interface{}{
|
|
||||||
"enable": true,
|
|
||||||
"expiry_time": traffic.ExpiryTime,
|
|
||||||
"total": traffic.Total,
|
|
||||||
"up": gorm.Expr("up + ?", traffic.Up),
|
|
||||||
"down": gorm.Expr("down + ?", traffic.Down)}).RowsAffected == 0 {
|
|
||||||
err = tx.Create(traffic).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("AddClientTraffic update data ", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return
|
dbClientTraffics := make([]*xray.ClientTraffic, 0, len(traffics))
|
||||||
|
err = db.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for dbTraffic_index := range dbClientTraffics {
|
||||||
|
for traffic_index := range traffics {
|
||||||
|
if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
|
||||||
|
dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
|
||||||
|
dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Save(dbClientTraffics).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("AddClientTraffic update data ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.ClientTraffic) ([]*xray.ClientTraffic, error) {
|
||||||
|
inboundIds := make([]int, 0, len(dbClientTraffics))
|
||||||
|
for _, dbClientTraffic := range dbClientTraffics {
|
||||||
|
if dbClientTraffic.ExpiryTime < 0 {
|
||||||
|
inboundIds = append(inboundIds, dbClientTraffic.InboundId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(inboundIds) > 0 {
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
err := tx.Model(model.Inbound{}).Where("id IN (?)", inboundIds).Find(&inbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for inbound_index := range inbounds {
|
||||||
|
settings := map[string]interface{}{}
|
||||||
|
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
|
||||||
|
clients, ok := settings["clients"].([]interface{})
|
||||||
|
if ok {
|
||||||
|
var newClients []interface{}
|
||||||
|
for client_index := range clients {
|
||||||
|
c := clients[client_index].(map[string]interface{})
|
||||||
|
for traffic_index := range dbClientTraffics {
|
||||||
|
if dbClientTraffics[traffic_index].ExpiryTime < 0 && c["email"] == dbClientTraffics[traffic_index].Email {
|
||||||
|
oldExpiryTime := c["expiryTime"].(float64)
|
||||||
|
newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime)
|
||||||
|
c["expiryTime"] = newExpiryTime
|
||||||
|
dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newClients = append(newClients, interface{}(c))
|
||||||
|
}
|
||||||
|
settings["clients"] = newClients
|
||||||
|
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
inbounds[inbound_index].Settings = string(modifiedSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = tx.Save(inbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("AddClientTraffic update inbounds ", err)
|
||||||
|
logger.Error(inbounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbClientTraffics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
|
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
|
||||||
@@ -439,6 +572,7 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
|
|||||||
count := result.RowsAffected
|
count := result.RowsAffected
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) DisableInvalidClients() (int64, error) {
|
func (s *InboundService) DisableInvalidClients() (int64, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
now := time.Now().Unix() * 1000
|
now := time.Now().Unix() * 1000
|
||||||
@@ -449,6 +583,19 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
|
|||||||
count := result.RowsAffected
|
count := result.RowsAffected
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) MigrationRemoveOrphanedTraffics() {
|
||||||
|
db := database.GetDB()
|
||||||
|
db.Exec(`
|
||||||
|
DELETE FROM client_traffics
|
||||||
|
WHERE email NOT IN (
|
||||||
|
SELECT JSON_EXTRACT(client.value, '$.email')
|
||||||
|
FROM inbounds,
|
||||||
|
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
|
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
@@ -467,6 +614,7 @@ func (s *InboundService) AddClientStat(inboundId int, client *model.Client) erro
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
|
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
@@ -501,12 +649,174 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.ClientTraffic, err error) {
|
|
||||||
|
func (s *InboundService) ResetAllClientTraffics(id int) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
|
||||||
|
whereText := "inbound_id "
|
||||||
|
if id == -1 {
|
||||||
|
whereText += " > ?"
|
||||||
|
} else {
|
||||||
|
whereText += " = ?"
|
||||||
|
}
|
||||||
|
|
||||||
|
result := db.Model(xray.ClientTraffic{}).
|
||||||
|
Where(whereText, id).
|
||||||
|
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
|
||||||
|
|
||||||
|
err := result.Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) ResetAllTraffics() error {
|
||||||
|
db := database.GetDB()
|
||||||
|
|
||||||
|
result := db.Model(model.Inbound{}).
|
||||||
|
Where("user_id > ?", 0).
|
||||||
|
Updates(map[string]interface{}{"up": 0, "down": 0})
|
||||||
|
|
||||||
|
err := result.Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) DelDepletedClients(id int) (err error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
tx := db.Begin()
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
tx.Commit()
|
||||||
|
} else {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
whereText := "inbound_id "
|
||||||
|
if id < 0 {
|
||||||
|
whereText += "> ?"
|
||||||
|
} else {
|
||||||
|
whereText += "= ?"
|
||||||
|
}
|
||||||
|
|
||||||
|
depletedClients := []xray.ClientTraffic{}
|
||||||
|
err = db.Model(xray.ClientTraffic{}).Where(whereText+" and enable = ?", id, false).Select("inbound_id, GROUP_CONCAT(email) as email").Group("inbound_id").Find(&depletedClients).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, depletedClient := range depletedClients {
|
||||||
|
emails := strings.Split(depletedClient.Email, ",")
|
||||||
|
oldInbound, err := s.GetInbound(depletedClient.InboundId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var oldSettings map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldClients := oldSettings["clients"].([]interface{})
|
||||||
|
var newClients []interface{}
|
||||||
|
for _, client := range oldClients {
|
||||||
|
deplete := false
|
||||||
|
c := client.(map[string]interface{})
|
||||||
|
for _, email := range emails {
|
||||||
|
if email == c["email"].(string) {
|
||||||
|
deplete = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !deplete {
|
||||||
|
newClients = append(newClients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(newClients) > 0 {
|
||||||
|
oldSettings["clients"] = newClients
|
||||||
|
|
||||||
|
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldInbound.Settings = string(newSettings)
|
||||||
|
err = tx.Save(oldInbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Delete inbound if no client remains
|
||||||
|
s.DelInbound(depletedClient.InboundId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Where(whereText+" and enable = ?", id, false).Delete(xray.ClientTraffic{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTraffic, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"tgId": "%s"%%`, tguname)).Find(&inbounds).Error
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var emails []string
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
clients, err := s.getClients(inbound)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Unable to get clients from inbound")
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.TgID == tguname {
|
||||||
|
emails = append(emails, client.Email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var traffics []*xray.ClientTraffic
|
||||||
|
err = db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
logger.Warning(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return traffics, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.ClientTraffic, err error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var traffics []*xray.ClientTraffic
|
||||||
|
|
||||||
|
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(traffics) > 0 {
|
||||||
|
return traffics[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
inbound := &model.Inbound{}
|
inbound := &model.Inbound{}
|
||||||
traffic = &xray.ClientTraffic{}
|
traffic = &xray.ClientTraffic{}
|
||||||
|
|
||||||
err = db.Model(model.Inbound{}).Where("settings like ?", "%"+uuid+"%").First(inbound).Error
|
err = db.Model(model.Inbound{}).Where("settings like ?", "%\""+query+"\"%").First(inbound).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
logger.Warning(err)
|
logger.Warning(err)
|
||||||
@@ -520,9 +830,17 @@ func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.Client
|
|||||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||||
clients := settings["clients"]
|
clients := settings["clients"]
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
if uuid == client.ID {
|
if client.ID == query && client.Email != "" {
|
||||||
traffic.Email = client.Email
|
traffic.Email = client.Email
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
if client.Password == query && client.Email != "" {
|
||||||
|
traffic.Email = client.Email
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if traffic.Email == "" {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error
|
err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -531,3 +849,80 @@ func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.Client
|
|||||||
}
|
}
|
||||||
return traffic, err
|
return traffic, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return inbounds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) MigrationRequirements() {
|
||||||
|
db := database.GetDB()
|
||||||
|
|
||||||
|
// Fix inbounds based problems
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for inbound_index := range inbounds {
|
||||||
|
settings := map[string]interface{}{}
|
||||||
|
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
|
||||||
|
clients, ok := settings["clients"].([]interface{})
|
||||||
|
if ok {
|
||||||
|
// Fix Clinet configuration problems
|
||||||
|
var newClients []interface{}
|
||||||
|
for client_index := range clients {
|
||||||
|
c := clients[client_index].(map[string]interface{})
|
||||||
|
|
||||||
|
// Add email='' if it is not exists
|
||||||
|
if _, ok := c["email"]; !ok {
|
||||||
|
c["email"] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove "flow": "xtls-rprx-direct"
|
||||||
|
if _, ok := c["flow"]; ok {
|
||||||
|
if c["flow"] == "xtls-rprx-direct" {
|
||||||
|
c["flow"] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newClients = append(newClients, interface{}(c))
|
||||||
|
}
|
||||||
|
settings["clients"] = newClients
|
||||||
|
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inbounds[inbound_index].Settings = string(modifiedSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add client traffic row for all clients which has email
|
||||||
|
modelClients, err := s.getClients(inbounds[inbound_index])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, modelClient := range modelClients {
|
||||||
|
if len(modelClient.Email) > 0 {
|
||||||
|
var count int64
|
||||||
|
db.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
|
||||||
|
if count == 0 {
|
||||||
|
s.AddClientStat(inbounds[inbound_index].Id, &modelClient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.Save(inbounds)
|
||||||
|
|
||||||
|
// Remove orphaned traffics
|
||||||
|
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) MigrateDB() {
|
||||||
|
s.MigrationRequirements()
|
||||||
|
s.MigrationRemoveOrphanedTraffics()
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,11 +7,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"x-ui/config"
|
||||||
|
"x-ui/database"
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
|
"x-ui/util/common"
|
||||||
"x-ui/util/sys"
|
"x-ui/util/sys"
|
||||||
"x-ui/xray"
|
"x-ui/xray"
|
||||||
|
|
||||||
@@ -70,7 +76,8 @@ type Release struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ServerService struct {
|
type ServerService struct {
|
||||||
xrayService XrayService
|
xrayService XrayService
|
||||||
|
inboundService InboundService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||||
@@ -191,13 +198,39 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
versions := make([]string, 0, len(releases))
|
var versions []string
|
||||||
for _, release := range releases {
|
for _, release := range releases {
|
||||||
versions = append(versions, release.TagName)
|
if release.TagName >= "v1.8.0" {
|
||||||
|
versions = append(versions, release.TagName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return versions, nil
|
return versions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) StopXrayService() (string error) {
|
||||||
|
|
||||||
|
err := s.xrayService.StopXray()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("stop xray failed:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) RestartXrayService() (string error) {
|
||||||
|
|
||||||
|
s.xrayService.StopXray()
|
||||||
|
defer func() {
|
||||||
|
err := s.xrayService.RestartXray(true)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("start xray failed:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ServerService) downloadXRay(version string) (string, error) {
|
func (s *ServerService) downloadXRay(version string) (string, error) {
|
||||||
osName := runtime.GOOS
|
osName := runtime.GOOS
|
||||||
arch := runtime.GOARCH
|
arch := runtime.GOARCH
|
||||||
@@ -300,3 +333,192 @@ func (s *ServerService) UpdateXray(version string) error {
|
|||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetLogs(count string) ([]string, error) {
|
||||||
|
// Define the journalctl command and its arguments
|
||||||
|
var cmdArgs []string
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count}
|
||||||
|
} else {
|
||||||
|
return []string{"Unsupported operating system"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the command
|
||||||
|
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(out.String(), "\n")
|
||||||
|
|
||||||
|
return lines, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetConfigJson() (interface{}, error) {
|
||||||
|
// Open the file for reading
|
||||||
|
file, err := os.Open(xray.GetConfigPath())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Read the file contents
|
||||||
|
fileContents, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonData interface{}
|
||||||
|
err = json.Unmarshal(fileContents, &jsonData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetDb() ([]byte, error) {
|
||||||
|
// Open the file for reading
|
||||||
|
file, err := os.Open(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Read the file contents
|
||||||
|
fileContents, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileContents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) ImportDB(file multipart.File) error {
|
||||||
|
// Check if the file is a SQLite database
|
||||||
|
isValidDb, err := database.IsSQLiteDB(file)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error checking db file format: %v", err)
|
||||||
|
}
|
||||||
|
if !isValidDb {
|
||||||
|
return common.NewError("Invalid db file format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the file reader to the beginning
|
||||||
|
_, err = file.Seek(0, 0)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error resetting file reader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the file as temporary file
|
||||||
|
tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
|
||||||
|
// Remove the existing fallback file (if any) before creating one
|
||||||
|
_, err = os.Stat(tempPath)
|
||||||
|
if err == nil {
|
||||||
|
errRemove := os.Remove(tempPath)
|
||||||
|
if errRemove != nil {
|
||||||
|
return common.NewErrorf("Error removing existing temporary db file: %v", errRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Create the temporary file
|
||||||
|
tempFile, err := os.Create(tempPath)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error creating temporary db file: %v", err)
|
||||||
|
}
|
||||||
|
defer tempFile.Close()
|
||||||
|
|
||||||
|
// Remove temp file before returning
|
||||||
|
defer os.Remove(tempPath)
|
||||||
|
|
||||||
|
// Save uploaded file to temporary file
|
||||||
|
_, err = io.Copy(tempFile, file)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error saving db: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can init db or not
|
||||||
|
err = database.InitDB(tempPath)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error checking db: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop Xray
|
||||||
|
s.StopXrayService()
|
||||||
|
|
||||||
|
// Backup the current database for fallback
|
||||||
|
fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
|
||||||
|
// Remove the existing fallback file (if any)
|
||||||
|
_, err = os.Stat(fallbackPath)
|
||||||
|
if err == nil {
|
||||||
|
errRemove := os.Remove(fallbackPath)
|
||||||
|
if errRemove != nil {
|
||||||
|
return common.NewErrorf("Error removing existing fallback db file: %v", errRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Move the current database to the fallback location
|
||||||
|
err = os.Rename(config.GetDBPath(), fallbackPath)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error backing up temporary db file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the temporary file before returning
|
||||||
|
defer os.Remove(fallbackPath)
|
||||||
|
|
||||||
|
// Move temp to DB path
|
||||||
|
err = os.Rename(tempPath, config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
errRename := os.Rename(fallbackPath, config.GetDBPath())
|
||||||
|
if errRename != nil {
|
||||||
|
return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename)
|
||||||
|
}
|
||||||
|
return common.NewErrorf("Error moving db file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate DB
|
||||||
|
err = database.InitDB(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
errRename := os.Rename(fallbackPath, config.GetDBPath())
|
||||||
|
if errRename != nil {
|
||||||
|
return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
|
||||||
|
}
|
||||||
|
return common.NewErrorf("Error migrating db: %v", err)
|
||||||
|
}
|
||||||
|
s.inboundService.MigrateDB()
|
||||||
|
|
||||||
|
// Start Xray
|
||||||
|
err = s.RestartXrayService()
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Imported DB but Failed to start Xray: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetNewX25519Cert() (interface{}, error) {
|
||||||
|
// Run the command
|
||||||
|
cmd := exec.Command(xray.GetBinaryPath(), "x25519")
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(out.String(), "\n")
|
||||||
|
|
||||||
|
privateKeyLine := strings.Split(lines[0], ":")
|
||||||
|
publicKeyLine := strings.Split(lines[1], ":")
|
||||||
|
|
||||||
|
privateKey := strings.TrimSpace(privateKeyLine[1])
|
||||||
|
publicKey := strings.TrimSpace(publicKeyLine[1])
|
||||||
|
|
||||||
|
keyPair := map[string]interface{}{
|
||||||
|
"privateKey": privateKey,
|
||||||
|
"publicKey": publicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyPair, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -28,11 +29,16 @@ var defaultValueMap = map[string]string{
|
|||||||
"webKeyFile": "",
|
"webKeyFile": "",
|
||||||
"secret": random.Seq(32),
|
"secret": random.Seq(32),
|
||||||
"webBasePath": "/",
|
"webBasePath": "/",
|
||||||
|
"sessionMaxAge": "0",
|
||||||
|
"expireDiff": "0",
|
||||||
|
"trafficDiff": "0",
|
||||||
"timeLocation": "Asia/Tehran",
|
"timeLocation": "Asia/Tehran",
|
||||||
"tgBotEnable": "false",
|
"tgBotEnable": "false",
|
||||||
"tgBotToken": "",
|
"tgBotToken": "",
|
||||||
"tgBotChatId": "0",
|
"tgBotChatId": "",
|
||||||
"tgRunTime": "",
|
"tgRunTime": "@daily",
|
||||||
|
"tgBotBackup": "false",
|
||||||
|
"tgCpu": "0",
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingService struct {
|
type SettingService struct {
|
||||||
@@ -202,30 +208,38 @@ func (s *SettingService) SetTgBotToken(token string) error {
|
|||||||
return s.setString("tgBotToken", token)
|
return s.setString("tgBotToken", token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetTgBotChatId() (int, error) {
|
func (s *SettingService) GetTgBotChatId() (string, error) {
|
||||||
return s.getInt("tgBotChatId")
|
return s.getString("tgBotChatId")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) SetTgBotChatId(chatId int) error {
|
func (s *SettingService) SetTgBotChatId(chatIds string) error {
|
||||||
return s.setInt("tgBotChatId", chatId)
|
return s.setString("tgBotChatId", chatIds)
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SettingService) SetTgbotenabled(value bool) error {
|
|
||||||
return s.setBool("tgBotEnable", value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetTgbotenabled() (bool, error) {
|
func (s *SettingService) GetTgbotenabled() (bool, error) {
|
||||||
return s.getBool("tgBotEnable")
|
return s.getBool("tgBotEnable")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) SetTgbotRuntime(time string) error {
|
func (s *SettingService) SetTgbotenabled(value bool) error {
|
||||||
return s.setString("tgRunTime", time)
|
return s.setBool("tgBotEnable", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetTgbotRuntime() (string, error) {
|
func (s *SettingService) GetTgbotRuntime() (string, error) {
|
||||||
return s.getString("tgRunTime")
|
return s.getString("tgRunTime")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetTgbotRuntime(time string) error {
|
||||||
|
return s.setString("tgRunTime", time)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetTgBotBackup() (bool, error) {
|
||||||
|
return s.getBool("tgBotBackup")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetTgCpu() (int, error) {
|
||||||
|
return s.getInt("tgCpu")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetPort() (int, error) {
|
func (s *SettingService) GetPort() (int, error) {
|
||||||
return s.getInt("webPort")
|
return s.getInt("webPort")
|
||||||
}
|
}
|
||||||
@@ -242,6 +256,18 @@ func (s *SettingService) GetKeyFile() (string, error) {
|
|||||||
return s.getString("webKeyFile")
|
return s.getString("webKeyFile")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetExpireDiff() (int, error) {
|
||||||
|
return s.getInt("expireDiff")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetTrafficDiff() (int, error) {
|
||||||
|
return s.getInt("trafficDiff")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSessionMaxAge() (int, error) {
|
||||||
|
return s.getInt("sessionMaxAge")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetSecret() ([]byte, error) {
|
func (s *SettingService) GetSecret() ([]byte, error) {
|
||||||
secret, err := s.getString("secret")
|
secret, err := s.getString("secret")
|
||||||
if secret == defaultValueMap["secret"] {
|
if secret == defaultValueMap["secret"] {
|
||||||
@@ -301,3 +327,12 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
|||||||
}
|
}
|
||||||
return common.Combine(errs...)
|
return common.Combine(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetDefaultXrayConfig() (interface{}, error) {
|
||||||
|
var jsonData interface{}
|
||||||
|
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return jsonData, nil
|
||||||
|
}
|
||||||
|
|||||||
635
web/service/sub.go
Normal file
635
web/service/sub.go
Normal file
@@ -0,0 +1,635 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"x-ui/database"
|
||||||
|
"x-ui/database/model"
|
||||||
|
"x-ui/logger"
|
||||||
|
"x-ui/xray"
|
||||||
|
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubService struct {
|
||||||
|
address string
|
||||||
|
inboundService InboundService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) GetSubs(subId string, host string) ([]string, []string, error) {
|
||||||
|
s.address = host
|
||||||
|
var result []string
|
||||||
|
var headers []string
|
||||||
|
var traffic xray.ClientTraffic
|
||||||
|
var clientTraffics []xray.ClientTraffic
|
||||||
|
inbounds, err := s.getInboundsBySubId(subId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
clients, err := s.inboundService.getClients(inbound)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("SubService - GetSub: Unable to get clients from inbound")
|
||||||
|
}
|
||||||
|
if clients == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.Enable && client.SubID == subId {
|
||||||
|
link := s.getLink(inbound, client.Email)
|
||||||
|
result = append(result, link)
|
||||||
|
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index, clientTraffic := range clientTraffics {
|
||||||
|
if index == 0 {
|
||||||
|
traffic.Up = clientTraffic.Up
|
||||||
|
traffic.Down = clientTraffic.Down
|
||||||
|
traffic.Total = clientTraffic.Total
|
||||||
|
if clientTraffic.ExpiryTime > 0 {
|
||||||
|
traffic.ExpiryTime = clientTraffic.ExpiryTime
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
traffic.Up += clientTraffic.Up
|
||||||
|
traffic.Down += clientTraffic.Down
|
||||||
|
if traffic.Total == 0 || clientTraffic.Total == 0 {
|
||||||
|
traffic.Total = 0
|
||||||
|
} else {
|
||||||
|
traffic.Total += clientTraffic.Total
|
||||||
|
}
|
||||||
|
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
|
||||||
|
traffic.ExpiryTime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
|
||||||
|
headers = append(headers, "12")
|
||||||
|
headers = append(headers, subId)
|
||||||
|
return result, headers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return inbounds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {
|
||||||
|
for _, traffic := range traffics {
|
||||||
|
if traffic.Email == email {
|
||||||
|
return traffic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return xray.ClientTraffic{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||||
|
switch inbound.Protocol {
|
||||||
|
case "vmess":
|
||||||
|
return s.genVmessLink(inbound, email)
|
||||||
|
case "vless":
|
||||||
|
return s.genVlessLink(inbound, email)
|
||||||
|
case "trojan":
|
||||||
|
return s.genTrojanLink(inbound, email)
|
||||||
|
case "shadowsocks":
|
||||||
|
return s.genShadowsocksLink(inbound, email)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||||
|
if inbound.Protocol != model.VMess {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
|
||||||
|
obj := map[string]interface{}{
|
||||||
|
"v": "2",
|
||||||
|
"ps": remark,
|
||||||
|
"add": s.address,
|
||||||
|
"port": inbound.Port,
|
||||||
|
"type": "none",
|
||||||
|
}
|
||||||
|
var stream map[string]interface{}
|
||||||
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||||
|
network, _ := stream["network"].(string)
|
||||||
|
obj["net"] = network
|
||||||
|
switch network {
|
||||||
|
case "tcp":
|
||||||
|
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := tcp["header"].(map[string]interface{})
|
||||||
|
typeStr, _ := header["type"].(string)
|
||||||
|
obj["type"] = typeStr
|
||||||
|
if typeStr == "http" {
|
||||||
|
request := header["request"].(map[string]interface{})
|
||||||
|
requestPath, _ := request["path"].([]interface{})
|
||||||
|
obj["path"] = requestPath[0].(string)
|
||||||
|
headers, _ := request["headers"].(map[string]interface{})
|
||||||
|
obj["host"] = searchHost(headers)
|
||||||
|
}
|
||||||
|
case "kcp":
|
||||||
|
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := kcp["header"].(map[string]interface{})
|
||||||
|
obj["type"], _ = header["type"].(string)
|
||||||
|
obj["path"], _ = kcp["seed"].(string)
|
||||||
|
case "ws":
|
||||||
|
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||||
|
obj["path"] = ws["path"].(string)
|
||||||
|
headers, _ := ws["headers"].(map[string]interface{})
|
||||||
|
obj["host"] = searchHost(headers)
|
||||||
|
case "http":
|
||||||
|
obj["net"] = "h2"
|
||||||
|
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||||
|
obj["path"], _ = http["path"].(string)
|
||||||
|
obj["host"] = searchHost(http)
|
||||||
|
case "quic":
|
||||||
|
quic, _ := stream["quicSettings"].(map[string]interface{})
|
||||||
|
header := quic["header"].(map[string]interface{})
|
||||||
|
obj["type"], _ = header["type"].(string)
|
||||||
|
obj["host"], _ = quic["security"].(string)
|
||||||
|
obj["path"], _ = quic["key"].(string)
|
||||||
|
case "grpc":
|
||||||
|
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||||
|
obj["path"] = grpc["serviceName"].(string)
|
||||||
|
if grpc["multiMode"].(bool) {
|
||||||
|
obj["type"] = "multi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
security, _ := stream["security"].(string)
|
||||||
|
obj["tls"] = security
|
||||||
|
if security == "tls" {
|
||||||
|
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||||
|
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||||
|
if len(alpns) > 0 {
|
||||||
|
var alpn []string
|
||||||
|
for _, a := range alpns {
|
||||||
|
alpn = append(alpn, a.(string))
|
||||||
|
}
|
||||||
|
obj["alpn"] = strings.Join(alpn, ",")
|
||||||
|
}
|
||||||
|
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||||
|
if tlsSetting != nil {
|
||||||
|
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||||
|
obj["sni"], _ = sniValue.(string)
|
||||||
|
}
|
||||||
|
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||||
|
obj["fp"], _ = fpValue.(string)
|
||||||
|
}
|
||||||
|
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||||
|
obj["allowInsecure"], _ = insecure.(bool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serverName, _ := tlsSetting["serverName"].(string)
|
||||||
|
if serverName != "" {
|
||||||
|
obj["add"] = serverName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clients, _ := s.inboundService.getClients(inbound)
|
||||||
|
clientIndex := -1
|
||||||
|
for i, client := range clients {
|
||||||
|
if client.Email == email {
|
||||||
|
clientIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
obj["id"] = clients[clientIndex].ID
|
||||||
|
obj["aid"] = clients[clientIndex].AlterIds
|
||||||
|
|
||||||
|
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||||
|
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||||
|
address := s.address
|
||||||
|
if inbound.Protocol != model.VLESS {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var stream map[string]interface{}
|
||||||
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||||
|
clients, _ := s.inboundService.getClients(inbound)
|
||||||
|
clientIndex := -1
|
||||||
|
for i, client := range clients {
|
||||||
|
if client.Email == email {
|
||||||
|
clientIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uuid := clients[clientIndex].ID
|
||||||
|
port := inbound.Port
|
||||||
|
streamNetwork := stream["network"].(string)
|
||||||
|
params := make(map[string]string)
|
||||||
|
params["type"] = streamNetwork
|
||||||
|
|
||||||
|
switch streamNetwork {
|
||||||
|
case "tcp":
|
||||||
|
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := tcp["header"].(map[string]interface{})
|
||||||
|
typeStr, _ := header["type"].(string)
|
||||||
|
if typeStr == "http" {
|
||||||
|
request := header["request"].(map[string]interface{})
|
||||||
|
requestPath, _ := request["path"].([]interface{})
|
||||||
|
params["path"] = requestPath[0].(string)
|
||||||
|
headers, _ := request["headers"].(map[string]interface{})
|
||||||
|
params["host"] = searchHost(headers)
|
||||||
|
params["headerType"] = "http"
|
||||||
|
}
|
||||||
|
case "kcp":
|
||||||
|
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := kcp["header"].(map[string]interface{})
|
||||||
|
params["headerType"] = header["type"].(string)
|
||||||
|
params["seed"] = kcp["seed"].(string)
|
||||||
|
case "ws":
|
||||||
|
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||||
|
params["path"] = ws["path"].(string)
|
||||||
|
headers, _ := ws["headers"].(map[string]interface{})
|
||||||
|
params["host"] = searchHost(headers)
|
||||||
|
case "http":
|
||||||
|
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||||
|
params["path"] = http["path"].(string)
|
||||||
|
params["host"] = searchHost(http)
|
||||||
|
case "quic":
|
||||||
|
quic, _ := stream["quicSettings"].(map[string]interface{})
|
||||||
|
params["quicSecurity"] = quic["security"].(string)
|
||||||
|
params["key"] = quic["key"].(string)
|
||||||
|
header := quic["header"].(map[string]interface{})
|
||||||
|
params["headerType"] = header["type"].(string)
|
||||||
|
case "grpc":
|
||||||
|
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||||
|
params["serviceName"] = grpc["serviceName"].(string)
|
||||||
|
if grpc["multiMode"].(bool) {
|
||||||
|
params["mode"] = "multi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
security, _ := stream["security"].(string)
|
||||||
|
if security == "tls" {
|
||||||
|
params["security"] = "tls"
|
||||||
|
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||||
|
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||||
|
var alpn []string
|
||||||
|
for _, a := range alpns {
|
||||||
|
alpn = append(alpn, a.(string))
|
||||||
|
}
|
||||||
|
if len(alpn) > 0 {
|
||||||
|
params["alpn"] = strings.Join(alpn, ",")
|
||||||
|
}
|
||||||
|
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||||
|
if tlsSetting != nil {
|
||||||
|
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||||
|
params["sni"], _ = sniValue.(string)
|
||||||
|
}
|
||||||
|
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||||
|
params["fp"], _ = fpValue.(string)
|
||||||
|
}
|
||||||
|
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||||
|
if insecure.(bool) {
|
||||||
|
params["allowInsecure"] = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||||
|
params["flow"] = clients[clientIndex].Flow
|
||||||
|
}
|
||||||
|
|
||||||
|
serverName, _ := tlsSetting["serverName"].(string)
|
||||||
|
if serverName != "" {
|
||||||
|
address = serverName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if security == "reality" {
|
||||||
|
params["security"] = "reality"
|
||||||
|
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
|
||||||
|
realitySettings, _ := searchKey(realitySetting, "settings")
|
||||||
|
if realitySetting != nil {
|
||||||
|
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
|
||||||
|
sNames, _ := sniValue.([]interface{})
|
||||||
|
params["sni"], _ = sNames[0].(string)
|
||||||
|
}
|
||||||
|
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
|
||||||
|
params["pbk"], _ = pbkValue.(string)
|
||||||
|
}
|
||||||
|
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
|
||||||
|
shortIds, _ := sidValue.([]interface{})
|
||||||
|
params["sid"], _ = shortIds[0].(string)
|
||||||
|
}
|
||||||
|
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
|
||||||
|
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
|
||||||
|
params["fp"] = fp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
|
||||||
|
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
|
||||||
|
params["spx"] = spx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
|
||||||
|
if sname, ok := serverName.(string); ok && len(sname) > 0 {
|
||||||
|
address = sname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||||
|
params["flow"] = clients[clientIndex].Flow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
|
||||||
|
url, _ := url.Parse(link)
|
||||||
|
q := url.Query()
|
||||||
|
|
||||||
|
for k, v := range params {
|
||||||
|
q.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new query values on the URL
|
||||||
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
|
||||||
|
url.Fragment = remark
|
||||||
|
return url.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
|
||||||
|
address := s.address
|
||||||
|
if inbound.Protocol != model.Trojan {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var stream map[string]interface{}
|
||||||
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||||
|
clients, _ := s.inboundService.getClients(inbound)
|
||||||
|
clientIndex := -1
|
||||||
|
for i, client := range clients {
|
||||||
|
if client.Email == email {
|
||||||
|
clientIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
password := clients[clientIndex].Password
|
||||||
|
port := inbound.Port
|
||||||
|
streamNetwork := stream["network"].(string)
|
||||||
|
params := make(map[string]string)
|
||||||
|
params["type"] = streamNetwork
|
||||||
|
|
||||||
|
switch streamNetwork {
|
||||||
|
case "tcp":
|
||||||
|
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := tcp["header"].(map[string]interface{})
|
||||||
|
typeStr, _ := header["type"].(string)
|
||||||
|
if typeStr == "http" {
|
||||||
|
request := header["request"].(map[string]interface{})
|
||||||
|
requestPath, _ := request["path"].([]interface{})
|
||||||
|
params["path"] = requestPath[0].(string)
|
||||||
|
headers, _ := request["headers"].(map[string]interface{})
|
||||||
|
params["host"] = searchHost(headers)
|
||||||
|
params["headerType"] = "http"
|
||||||
|
}
|
||||||
|
case "kcp":
|
||||||
|
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := kcp["header"].(map[string]interface{})
|
||||||
|
params["headerType"] = header["type"].(string)
|
||||||
|
params["seed"] = kcp["seed"].(string)
|
||||||
|
case "ws":
|
||||||
|
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||||
|
params["path"] = ws["path"].(string)
|
||||||
|
headers, _ := ws["headers"].(map[string]interface{})
|
||||||
|
params["host"] = searchHost(headers)
|
||||||
|
case "http":
|
||||||
|
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||||
|
params["path"] = http["path"].(string)
|
||||||
|
params["host"] = searchHost(http)
|
||||||
|
case "quic":
|
||||||
|
quic, _ := stream["quicSettings"].(map[string]interface{})
|
||||||
|
params["quicSecurity"] = quic["security"].(string)
|
||||||
|
params["key"] = quic["key"].(string)
|
||||||
|
header := quic["header"].(map[string]interface{})
|
||||||
|
params["headerType"] = header["type"].(string)
|
||||||
|
case "grpc":
|
||||||
|
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||||
|
params["serviceName"] = grpc["serviceName"].(string)
|
||||||
|
if grpc["multiMode"].(bool) {
|
||||||
|
params["mode"] = "multi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
security, _ := stream["security"].(string)
|
||||||
|
if security == "tls" {
|
||||||
|
params["security"] = "tls"
|
||||||
|
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||||
|
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||||
|
var alpn []string
|
||||||
|
for _, a := range alpns {
|
||||||
|
alpn = append(alpn, a.(string))
|
||||||
|
}
|
||||||
|
if len(alpn) > 0 {
|
||||||
|
params["alpn"] = strings.Join(alpn, ",")
|
||||||
|
}
|
||||||
|
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||||
|
if tlsSetting != nil {
|
||||||
|
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||||
|
params["sni"], _ = sniValue.(string)
|
||||||
|
}
|
||||||
|
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||||
|
params["fp"], _ = fpValue.(string)
|
||||||
|
}
|
||||||
|
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||||
|
if insecure.(bool) {
|
||||||
|
params["allowInsecure"] = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serverName, _ := tlsSetting["serverName"].(string)
|
||||||
|
if serverName != "" {
|
||||||
|
address = serverName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if security == "reality" {
|
||||||
|
params["security"] = "reality"
|
||||||
|
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
|
||||||
|
realitySettings, _ := searchKey(realitySetting, "settings")
|
||||||
|
if realitySetting != nil {
|
||||||
|
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
|
||||||
|
sNames, _ := sniValue.([]interface{})
|
||||||
|
params["sni"], _ = sNames[0].(string)
|
||||||
|
}
|
||||||
|
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
|
||||||
|
params["pbk"], _ = pbkValue.(string)
|
||||||
|
}
|
||||||
|
if sidValue, ok := searchKey(realitySettings, "shortIds"); ok {
|
||||||
|
shortIds, _ := sidValue.([]interface{})
|
||||||
|
params["sid"], _ = shortIds[0].(string)
|
||||||
|
}
|
||||||
|
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
|
||||||
|
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
|
||||||
|
params["fp"] = fp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
|
||||||
|
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
|
||||||
|
params["spx"] = spx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
|
||||||
|
if sname, ok := serverName.(string); ok && len(sname) > 0 {
|
||||||
|
address = sname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||||
|
params["flow"] = clients[clientIndex].Flow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
|
||||||
|
|
||||||
|
url, _ := url.Parse(link)
|
||||||
|
q := url.Query()
|
||||||
|
|
||||||
|
for k, v := range params {
|
||||||
|
q.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new query values on the URL
|
||||||
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
|
||||||
|
url.Fragment = remark
|
||||||
|
return url.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
|
||||||
|
address := s.address
|
||||||
|
if inbound.Protocol != model.Shadowsocks {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var stream map[string]interface{}
|
||||||
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||||
|
clients, _ := s.inboundService.getClients(inbound)
|
||||||
|
|
||||||
|
var settings map[string]interface{}
|
||||||
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||||
|
inboundPassword := settings["password"].(string)
|
||||||
|
method := settings["method"].(string)
|
||||||
|
clientIndex := -1
|
||||||
|
for i, client := range clients {
|
||||||
|
if client.Email == email {
|
||||||
|
clientIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streamNetwork := stream["network"].(string)
|
||||||
|
params := make(map[string]string)
|
||||||
|
params["type"] = streamNetwork
|
||||||
|
|
||||||
|
switch streamNetwork {
|
||||||
|
case "tcp":
|
||||||
|
tcp, _ := stream["tcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := tcp["header"].(map[string]interface{})
|
||||||
|
typeStr, _ := header["type"].(string)
|
||||||
|
if typeStr == "http" {
|
||||||
|
request := header["request"].(map[string]interface{})
|
||||||
|
requestPath, _ := request["path"].([]interface{})
|
||||||
|
params["path"] = requestPath[0].(string)
|
||||||
|
headers, _ := request["headers"].(map[string]interface{})
|
||||||
|
params["host"] = searchHost(headers)
|
||||||
|
params["headerType"] = "http"
|
||||||
|
}
|
||||||
|
case "kcp":
|
||||||
|
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||||
|
header, _ := kcp["header"].(map[string]interface{})
|
||||||
|
params["headerType"] = header["type"].(string)
|
||||||
|
params["seed"] = kcp["seed"].(string)
|
||||||
|
case "ws":
|
||||||
|
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||||
|
params["path"] = ws["path"].(string)
|
||||||
|
headers, _ := ws["headers"].(map[string]interface{})
|
||||||
|
params["host"] = searchHost(headers)
|
||||||
|
case "http":
|
||||||
|
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||||
|
params["path"] = http["path"].(string)
|
||||||
|
params["host"] = searchHost(http)
|
||||||
|
case "quic":
|
||||||
|
quic, _ := stream["quicSettings"].(map[string]interface{})
|
||||||
|
params["quicSecurity"] = quic["security"].(string)
|
||||||
|
params["key"] = quic["key"].(string)
|
||||||
|
header := quic["header"].(map[string]interface{})
|
||||||
|
params["headerType"] = header["type"].(string)
|
||||||
|
case "grpc":
|
||||||
|
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||||
|
params["serviceName"] = grpc["serviceName"].(string)
|
||||||
|
if grpc["multiMode"].(bool) {
|
||||||
|
params["mode"] = "multi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
|
||||||
|
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
|
||||||
|
url, _ := url.Parse(link)
|
||||||
|
q := url.Query()
|
||||||
|
|
||||||
|
for k, v := range params {
|
||||||
|
q.Add(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new query values on the URL
|
||||||
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email)
|
||||||
|
url.Fragment = remark
|
||||||
|
return url.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchKey(data interface{}, key string) (interface{}, bool) {
|
||||||
|
switch val := data.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
for k, v := range val {
|
||||||
|
if k == key {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
if result, ok := searchKey(v, key); ok {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for _, v := range val {
|
||||||
|
if result, ok := searchKey(v, key); ok {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchHost(headers interface{}) string {
|
||||||
|
data, _ := headers.(map[string]interface{})
|
||||||
|
for k, v := range data {
|
||||||
|
if strings.EqualFold(k, "host") {
|
||||||
|
switch v.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
hosts, _ := v.([]interface{})
|
||||||
|
if len(hosts) > 0 {
|
||||||
|
return hosts[0].(string)
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
case interface{}:
|
||||||
|
return v.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
613
web/service/tgbot.go
Normal file
613
web/service/tgbot.go
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"x-ui/config"
|
||||||
|
"x-ui/database/model"
|
||||||
|
"x-ui/logger"
|
||||||
|
"x-ui/util/common"
|
||||||
|
"x-ui/xray"
|
||||||
|
|
||||||
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var bot *tgbotapi.BotAPI
|
||||||
|
var adminIds []int64
|
||||||
|
var isRunning bool
|
||||||
|
|
||||||
|
type LoginStatus byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
LoginSuccess LoginStatus = 1
|
||||||
|
LoginFail LoginStatus = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tgbot struct {
|
||||||
|
inboundService InboundService
|
||||||
|
settingService SettingService
|
||||||
|
serverService ServerService
|
||||||
|
lastStatus *Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) NewTgbot() *Tgbot {
|
||||||
|
return new(Tgbot)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) Start() error {
|
||||||
|
tgBottoken, err := t.settingService.GetTgBotToken()
|
||||||
|
if err != nil || tgBottoken == "" {
|
||||||
|
logger.Warning("Get TgBotToken failed:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tgBotid, err := t.settingService.GetTgBotChatId()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Get GetTgBotChatId failed:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, adminId := range strings.Split(tgBotid, ",") {
|
||||||
|
id, err := strconv.Atoi(adminId)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Failed to get IDs from GetTgBotChatId:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
adminIds = append(adminIds, int64(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
bot, err = tgbotapi.NewBotAPI(tgBottoken)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Get tgbot's api error:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bot.Debug = false
|
||||||
|
|
||||||
|
// listen for TG bot income messages
|
||||||
|
if !isRunning {
|
||||||
|
logger.Info("Starting Telegram receiver ...")
|
||||||
|
go t.OnReceive()
|
||||||
|
isRunning = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) IsRunnging() bool {
|
||||||
|
return isRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) Stop() {
|
||||||
|
bot.StopReceivingUpdates()
|
||||||
|
logger.Info("Stop Telegram receiver ...")
|
||||||
|
isRunning = false
|
||||||
|
adminIds = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) OnReceive() {
|
||||||
|
u := tgbotapi.NewUpdate(0)
|
||||||
|
u.Timeout = 10
|
||||||
|
|
||||||
|
updates := bot.GetUpdatesChan(u)
|
||||||
|
|
||||||
|
for update := range updates {
|
||||||
|
tgId := update.FromChat().ID
|
||||||
|
chatId := update.FromChat().ChatConfig().ChatID
|
||||||
|
isAdmin := checkAdmin(tgId)
|
||||||
|
if update.Message == nil {
|
||||||
|
if update.CallbackQuery != nil {
|
||||||
|
t.asnwerCallback(update.CallbackQuery, isAdmin)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if update.Message.IsCommand() {
|
||||||
|
t.answerCommand(update.Message, chatId, isAdmin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin bool) {
|
||||||
|
msg := ""
|
||||||
|
// Extract the command from the Message.
|
||||||
|
switch message.Command() {
|
||||||
|
case "help":
|
||||||
|
msg = "This bot is providing you some specefic data from the server.\n\n Please choose:"
|
||||||
|
case "start":
|
||||||
|
msg = "Hello <i>" + message.From.FirstName + "</i> 👋"
|
||||||
|
if isAdmin {
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
msg += "\nWelcome to <b>" + hostname + "</b> management bot"
|
||||||
|
}
|
||||||
|
msg += "\n\nI can do some magics for you, please choose:"
|
||||||
|
case "status":
|
||||||
|
msg = "bot is ok ✅"
|
||||||
|
case "usage":
|
||||||
|
if len(message.CommandArguments()) > 1 {
|
||||||
|
if isAdmin {
|
||||||
|
t.searchClient(chatId, message.CommandArguments())
|
||||||
|
} else {
|
||||||
|
t.searchForClient(chatId, message.CommandArguments())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
msg = "❗Please provide a text for search!"
|
||||||
|
}
|
||||||
|
case "inbound":
|
||||||
|
if isAdmin {
|
||||||
|
t.searchInbound(chatId, message.CommandArguments())
|
||||||
|
} else {
|
||||||
|
msg = "❗ Unknown command"
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
msg = "❗ Unknown command"
|
||||||
|
}
|
||||||
|
t.SendAnswer(chatId, msg, isAdmin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bool) {
|
||||||
|
// Respond to the callback query, telling Telegram to show the user
|
||||||
|
// a message with the data received.
|
||||||
|
callback := tgbotapi.NewCallback(callbackQuery.ID, callbackQuery.Data)
|
||||||
|
if _, err := bot.Request(callback); err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch callbackQuery.Data {
|
||||||
|
case "get_usage":
|
||||||
|
t.SendMsgToTgbot(callbackQuery.From.ID, t.getServerUsage())
|
||||||
|
case "inbounds":
|
||||||
|
t.SendMsgToTgbot(callbackQuery.From.ID, t.getInboundUsages())
|
||||||
|
case "deplete_soon":
|
||||||
|
t.SendMsgToTgbot(callbackQuery.From.ID, t.getExhausted())
|
||||||
|
case "get_backup":
|
||||||
|
t.sendBackup(callbackQuery.From.ID)
|
||||||
|
case "client_traffic":
|
||||||
|
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName)
|
||||||
|
case "client_commands":
|
||||||
|
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Password]</code>\r\n \r\nUse UID for vmess/vless and Password for Trojan.")
|
||||||
|
case "commands":
|
||||||
|
t.SendMsgToTgbot(callbackQuery.From.ID, "Search for a client email:\r\n<code>/usage email</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [remark]</code>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAdmin(tgId int64) bool {
|
||||||
|
for _, adminId := range adminIds {
|
||||||
|
if adminId == tgId {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
||||||
|
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
|
||||||
|
tgbotapi.NewInlineKeyboardRow(
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Server Usage", "get_usage"),
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Get DB Backup", "get_backup"),
|
||||||
|
),
|
||||||
|
tgbotapi.NewInlineKeyboardRow(
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"),
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Deplete soon", "deplete_soon"),
|
||||||
|
),
|
||||||
|
tgbotapi.NewInlineKeyboardRow(
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
var numericKeyboardClient = tgbotapi.NewInlineKeyboardMarkup(
|
||||||
|
tgbotapi.NewInlineKeyboardRow(
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "client_traffic"),
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Commands", "client_commands"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
msgConfig := tgbotapi.NewMessage(chatId, msg)
|
||||||
|
msgConfig.ParseMode = "HTML"
|
||||||
|
if isAdmin {
|
||||||
|
msgConfig.ReplyMarkup = numericKeyboard
|
||||||
|
} else {
|
||||||
|
msgConfig.ReplyMarkup = numericKeyboardClient
|
||||||
|
}
|
||||||
|
_, err := bot.Send(msgConfig)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Error sending telegram message :", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
|
||||||
|
if !isRunning {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var allMessages []string
|
||||||
|
limit := 2000
|
||||||
|
// paging message if it is big
|
||||||
|
if len(msg) > limit {
|
||||||
|
messages := strings.Split(msg, "\r\n \r\n")
|
||||||
|
lastIndex := -1
|
||||||
|
for _, message := range messages {
|
||||||
|
if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) {
|
||||||
|
allMessages = append(allMessages, message)
|
||||||
|
lastIndex++
|
||||||
|
} else {
|
||||||
|
allMessages[lastIndex] += "\r\n \r\n" + message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allMessages = append(allMessages, msg)
|
||||||
|
}
|
||||||
|
for _, message := range allMessages {
|
||||||
|
info := tgbotapi.NewMessage(tgid, message)
|
||||||
|
info.ParseMode = "HTML"
|
||||||
|
_, err := bot.Send(info)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Error sending telegram message :", err)
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) SendMsgToTgbotAdmins(msg string) {
|
||||||
|
for _, adminId := range adminIds {
|
||||||
|
t.SendMsgToTgbot(adminId, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) SendReport() {
|
||||||
|
runTime, err := t.settingService.GetTgbotRuntime()
|
||||||
|
if err == nil && len(runTime) > 0 {
|
||||||
|
t.SendMsgToTgbotAdmins("🕰 Scheduled reports: " + runTime + "\r\nDate-Time: " + time.Now().Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
info := t.getServerUsage()
|
||||||
|
t.SendMsgToTgbotAdmins(info)
|
||||||
|
exhausted := t.getExhausted()
|
||||||
|
t.SendMsgToTgbotAdmins(exhausted)
|
||||||
|
backupEnable, err := t.settingService.GetTgBotBackup()
|
||||||
|
if err == nil && backupEnable {
|
||||||
|
for _, adminId := range adminIds {
|
||||||
|
t.sendBackup(int64(adminId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) getServerUsage() string {
|
||||||
|
var info string
|
||||||
|
//get hostname
|
||||||
|
name, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("get hostname error:", err)
|
||||||
|
name = ""
|
||||||
|
}
|
||||||
|
info = fmt.Sprintf("💻 Hostname: %s\r\n", name)
|
||||||
|
info += fmt.Sprintf("🚀X-UI Version: %s\r\n", config.GetVersion())
|
||||||
|
//get ip address
|
||||||
|
var ip string
|
||||||
|
var ipv6 string
|
||||||
|
netInterfaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("net.Interfaces failed, err:", err.Error())
|
||||||
|
info += "🌐 IP: Unknown\r\n \r\n"
|
||||||
|
} else {
|
||||||
|
for i := 0; i < len(netInterfaces); i++ {
|
||||||
|
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
|
||||||
|
addrs, _ := netInterfaces[i].Addrs()
|
||||||
|
|
||||||
|
for _, address := range addrs {
|
||||||
|
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||||
|
if ipnet.IP.To4() != nil {
|
||||||
|
ip += ipnet.IP.String() + " "
|
||||||
|
} else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() {
|
||||||
|
ipv6 += ipnet.IP.String() + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info += fmt.Sprintf("🌐IP: %s\r\n🌐IPv6: %s\r\n", ip, ipv6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get latest status of server
|
||||||
|
t.lastStatus = t.serverService.GetStatus(t.lastStatus)
|
||||||
|
info += fmt.Sprintf("🔌Server Uptime: %d days\r\n", int(t.lastStatus.Uptime/86400))
|
||||||
|
info += fmt.Sprintf("📈Server Load: %.1f, %.1f, %.1f\r\n", t.lastStatus.Loads[0], t.lastStatus.Loads[1], t.lastStatus.Loads[2])
|
||||||
|
info += fmt.Sprintf("📋Server Memory: %s/%s\r\n", common.FormatTraffic(int64(t.lastStatus.Mem.Current)), common.FormatTraffic(int64(t.lastStatus.Mem.Total)))
|
||||||
|
info += fmt.Sprintf("🔹TcpCount: %d\r\n", t.lastStatus.TcpCount)
|
||||||
|
info += fmt.Sprintf("🔸UdpCount: %d\r\n", t.lastStatus.UdpCount)
|
||||||
|
info += fmt.Sprintf("🚦Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
|
||||||
|
info += fmt.Sprintf("ℹXray status: %s", t.lastStatus.Xray.State)
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
|
||||||
|
if username == "" || ip == "" || time == "" {
|
||||||
|
logger.Warning("UserLoginNotify failed,invalid info")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var msg string
|
||||||
|
// Get hostname
|
||||||
|
name, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("get hostname error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status == LoginSuccess {
|
||||||
|
msg = fmt.Sprintf("✅ Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
|
||||||
|
} else if status == LoginFail {
|
||||||
|
msg = fmt.Sprintf("❗ Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
|
||||||
|
}
|
||||||
|
msg += fmt.Sprintf("⏰ Time:%s\r\n", time)
|
||||||
|
msg += fmt.Sprintf("🆔 Username:%s\r\n", username)
|
||||||
|
msg += fmt.Sprintf("🌐 IP:%s\r\n", ip)
|
||||||
|
t.SendMsgToTgbotAdmins(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) getInboundUsages() string {
|
||||||
|
info := ""
|
||||||
|
// get traffic
|
||||||
|
inbouds, err := t.inboundService.GetAllInbounds()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("GetAllInbounds run failed:", err)
|
||||||
|
info += "❌ Failed to get inbounds"
|
||||||
|
} else {
|
||||||
|
// NOTE:If there no any sessions here,need to notify here
|
||||||
|
// TODO:Sub-node push, automatic conversion format
|
||||||
|
for _, inbound := range inbouds {
|
||||||
|
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
|
||||||
|
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||||
|
if inbound.ExpiryTime == 0 {
|
||||||
|
info += "Expire date: ♾ Unlimited\r\n \r\n"
|
||||||
|
} else {
|
||||||
|
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
|
||||||
|
if len(tgUserName) == 0 {
|
||||||
|
msg := "Your configuration is not found!\nYou should configure your telegram username and ask Admin to add it to your configuration."
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
msg := "❌ Something went wrong!"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(traffics) == 0 {
|
||||||
|
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, traffic := range traffics {
|
||||||
|
expiryTime := ""
|
||||||
|
if traffic.ExpiryTime == 0 {
|
||||||
|
expiryTime = "♾Unlimited"
|
||||||
|
} else if traffic.ExpiryTime < 0 {
|
||||||
|
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||||
|
} else {
|
||||||
|
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
total := ""
|
||||||
|
if traffic.Total == 0 {
|
||||||
|
total = "♾Unlimited"
|
||||||
|
} else {
|
||||||
|
total = common.FormatTraffic((traffic.Total))
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||||
|
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||||
|
total, expiryTime)
|
||||||
|
t.SendMsgToTgbot(chatId, output)
|
||||||
|
}
|
||||||
|
t.SendAnswer(chatId, "Please choose:", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) searchClient(chatId int64, email string) {
|
||||||
|
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
msg := "❌ Something went wrong!"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if traffic == nil {
|
||||||
|
msg := "No result!"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expiryTime := ""
|
||||||
|
if traffic.ExpiryTime == 0 {
|
||||||
|
expiryTime = "♾Unlimited"
|
||||||
|
} else if traffic.ExpiryTime < 0 {
|
||||||
|
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||||
|
} else {
|
||||||
|
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
total := ""
|
||||||
|
if traffic.Total == 0 {
|
||||||
|
total = "♾Unlimited"
|
||||||
|
} else {
|
||||||
|
total = common.FormatTraffic((traffic.Total))
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||||
|
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||||
|
total, expiryTime)
|
||||||
|
t.SendMsgToTgbot(chatId, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
||||||
|
inbouds, err := t.inboundService.SearchInbounds(remark)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
msg := "❌ Something went wrong!"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, inbound := range inbouds {
|
||||||
|
info := ""
|
||||||
|
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
|
||||||
|
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||||
|
if inbound.ExpiryTime == 0 {
|
||||||
|
info += "Expire date: ♾ Unlimited\r\n \r\n"
|
||||||
|
} else {
|
||||||
|
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
t.SendMsgToTgbot(chatId, info)
|
||||||
|
for _, traffic := range inbound.ClientStats {
|
||||||
|
expiryTime := ""
|
||||||
|
if traffic.ExpiryTime == 0 {
|
||||||
|
expiryTime = "♾Unlimited"
|
||||||
|
} else if traffic.ExpiryTime < 0 {
|
||||||
|
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||||
|
} else {
|
||||||
|
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
total := ""
|
||||||
|
if traffic.Total == 0 {
|
||||||
|
total = "♾Unlimited"
|
||||||
|
} else {
|
||||||
|
total = common.FormatTraffic((traffic.Total))
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||||
|
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||||
|
total, expiryTime)
|
||||||
|
t.SendMsgToTgbot(chatId, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) searchForClient(chatId int64, query string) {
|
||||||
|
traffic, err := t.inboundService.SearchClientTraffic(query)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
msg := "❌ Something went wrong!"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if traffic == nil {
|
||||||
|
msg := "No result!"
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expiryTime := ""
|
||||||
|
if traffic.ExpiryTime == 0 {
|
||||||
|
expiryTime = "♾Unlimited"
|
||||||
|
} else if traffic.ExpiryTime < 0 {
|
||||||
|
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||||
|
} else {
|
||||||
|
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
total := ""
|
||||||
|
if traffic.Total == 0 {
|
||||||
|
total = "♾Unlimited"
|
||||||
|
} else {
|
||||||
|
total = common.FormatTraffic((traffic.Total))
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||||
|
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||||
|
total, expiryTime)
|
||||||
|
t.SendMsgToTgbot(chatId, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) getExhausted() string {
|
||||||
|
trDiff := int64(0)
|
||||||
|
exDiff := int64(0)
|
||||||
|
now := time.Now().Unix() * 1000
|
||||||
|
var exhaustedInbounds []model.Inbound
|
||||||
|
var exhaustedClients []xray.ClientTraffic
|
||||||
|
var disabledInbounds []model.Inbound
|
||||||
|
var disabledClients []xray.ClientTraffic
|
||||||
|
output := ""
|
||||||
|
TrafficThreshold, err := t.settingService.GetTrafficDiff()
|
||||||
|
if err == nil && TrafficThreshold > 0 {
|
||||||
|
trDiff = int64(TrafficThreshold) * 1073741824
|
||||||
|
}
|
||||||
|
ExpireThreshold, err := t.settingService.GetExpireDiff()
|
||||||
|
if err == nil && ExpireThreshold > 0 {
|
||||||
|
exDiff = int64(ExpireThreshold) * 86400000
|
||||||
|
}
|
||||||
|
inbounds, err := t.inboundService.GetAllInbounds()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Unable to load Inbounds", err)
|
||||||
|
}
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
if inbound.Enable {
|
||||||
|
if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) ||
|
||||||
|
(inbound.Total > 0 && (inbound.Total-(inbound.Up+inbound.Down) < trDiff)) {
|
||||||
|
exhaustedInbounds = append(exhaustedInbounds, *inbound)
|
||||||
|
}
|
||||||
|
if len(inbound.ClientStats) > 0 {
|
||||||
|
for _, client := range inbound.ClientStats {
|
||||||
|
if client.Enable {
|
||||||
|
if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) ||
|
||||||
|
(client.Total > 0 && (client.Total-(client.Up+client.Down) < trDiff)) {
|
||||||
|
exhaustedClients = append(exhaustedClients, client)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disabledClients = append(disabledClients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disabledInbounds = append(disabledInbounds, *inbound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
|
||||||
|
if len(exhaustedInbounds) > 0 {
|
||||||
|
output += "Exhausted Inbounds:\r\n"
|
||||||
|
for _, inbound := range exhaustedInbounds {
|
||||||
|
output += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\nTraffic: %s (↑%s,↓%s)\r\n", inbound.Remark, inbound.Port, common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||||
|
if inbound.ExpiryTime == 0 {
|
||||||
|
output += "Expire date: ♾Unlimited\r\n \r\n"
|
||||||
|
} else {
|
||||||
|
output += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Exhausted: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
|
||||||
|
if len(exhaustedClients) > 0 {
|
||||||
|
output += "Exhausted Clients:\r\n"
|
||||||
|
for _, traffic := range exhaustedClients {
|
||||||
|
expiryTime := ""
|
||||||
|
if traffic.ExpiryTime == 0 {
|
||||||
|
expiryTime = "♾Unlimited"
|
||||||
|
} else if traffic.ExpiryTime < 0 {
|
||||||
|
expiryTime += fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||||
|
} else {
|
||||||
|
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
total := ""
|
||||||
|
if traffic.Total == 0 {
|
||||||
|
total = "♾Unlimited"
|
||||||
|
} else {
|
||||||
|
total = common.FormatTraffic((traffic.Total))
|
||||||
|
}
|
||||||
|
output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire date: %s\r\n \r\n",
|
||||||
|
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||||
|
total, expiryTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) sendBackup(chatId int64) {
|
||||||
|
sendingTime := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
t.SendMsgToTgbot(chatId, "Backup time: "+sendingTime)
|
||||||
|
file := tgbotapi.FilePath(config.GetDBPath())
|
||||||
|
msg := tgbotapi.NewDocument(chatId, file)
|
||||||
|
_, err := bot.Send(msg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Error in uploading backup: ", err)
|
||||||
|
}
|
||||||
|
file = tgbotapi.FilePath(xray.GetConfigPath())
|
||||||
|
msg = tgbotapi.NewDocument(chatId, file)
|
||||||
|
_, err = bot.Send(msg)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Error in uploading config.json: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,15 +84,16 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
|||||||
clients, ok := settings["clients"].([]interface{})
|
clients, ok := settings["clients"].([]interface{})
|
||||||
if ok {
|
if ok {
|
||||||
// check users active or not
|
// check users active or not
|
||||||
|
|
||||||
clientStats := inbound.ClientStats
|
clientStats := inbound.ClientStats
|
||||||
for _, clientTraffic := range clientStats {
|
for _, clientTraffic := range clientStats {
|
||||||
|
|
||||||
|
indexDecrease := 0
|
||||||
for index, client := range clients {
|
for index, client := range clients {
|
||||||
c := client.(map[string]interface{})
|
c := client.(map[string]interface{})
|
||||||
if c["email"] == clientTraffic.Email {
|
if c["email"] == clientTraffic.Email {
|
||||||
if !clientTraffic.Enable {
|
if !clientTraffic.Enable {
|
||||||
clients = RemoveIndex(clients, index)
|
clients = RemoveIndex(clients, index-indexDecrease)
|
||||||
|
indexDecrease++
|
||||||
logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit")
|
logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit")
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -101,8 +102,31 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
settings["clients"] = clients
|
|
||||||
modifiedSettings, err := json.Marshal(settings)
|
// clear client config for additional parameters
|
||||||
|
var final_clients []interface{}
|
||||||
|
for _, client := range clients {
|
||||||
|
|
||||||
|
c := client.(map[string]interface{})
|
||||||
|
|
||||||
|
if c["enable"] != nil {
|
||||||
|
if enable, ok := c["enable"].(bool); ok && !enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key := range c {
|
||||||
|
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "alterId" {
|
||||||
|
delete(c, key)
|
||||||
|
}
|
||||||
|
if c["flow"] == "xtls-rprx-vision-udp443" {
|
||||||
|
c["flow"] = "xtls-rprx-vision"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final_clients = append(final_clients, interface{}(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
settings["clients"] = final_clients
|
||||||
|
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package session
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"x-ui/database/model"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"x-ui/database/model"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -21,6 +22,15 @@ func SetLoginUser(c *gin.Context, user *model.User) error {
|
|||||||
return s.Save()
|
return s.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetMaxAge(c *gin.Context, maxAge int) error {
|
||||||
|
s := sessions.Default(c)
|
||||||
|
s.Options(sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: maxAge,
|
||||||
|
})
|
||||||
|
return s.Save()
|
||||||
|
}
|
||||||
|
|
||||||
func GetLoginUser(c *gin.Context) *model.User {
|
func GetLoginUser(c *gin.Context) *model.User {
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
obj := s.Get(loginUser)
|
obj := s.Get(loginUser)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"enable" = "Enable"
|
"enable" = "Enable"
|
||||||
"protocol" = "Protocol"
|
"protocol" = "Protocol"
|
||||||
"search" = "Search"
|
"search" = "Search"
|
||||||
|
"filter" = "Filter"
|
||||||
|
|
||||||
"loading" = "Loading"
|
"loading" = "Loading"
|
||||||
"second" = "Second"
|
"second" = "Second"
|
||||||
@@ -22,24 +23,27 @@
|
|||||||
"unlimited" = "Unlimited"
|
"unlimited" = "Unlimited"
|
||||||
"none" = "None"
|
"none" = "None"
|
||||||
"qrCode" = "QR Code"
|
"qrCode" = "QR Code"
|
||||||
"info" = "More information"
|
"info" = "More Information"
|
||||||
"edit" = "Edit"
|
"edit" = "Edit"
|
||||||
"delete" = "Delete"
|
"delete" = "Delete"
|
||||||
"reset" = "Reset"
|
"reset" = "Reset"
|
||||||
"copySuccess" = "Copy successfully"
|
"copySuccess" = "Copied successfully"
|
||||||
"sure" = "Sure"
|
"sure" = "Sure"
|
||||||
"encryption" = "Encryption"
|
"encryption" = "Encryption"
|
||||||
"transmission" = "Transmission"
|
"transmission" = "Transmission"
|
||||||
"host" = "Host"
|
"host" = "Host"
|
||||||
"path" = "Path"
|
"path" = "Path"
|
||||||
"camouflage" = "Camouflage"
|
"camouflage" = "Camouflage"
|
||||||
|
"status" = "Status"
|
||||||
"enabled" = "Enabled"
|
"enabled" = "Enabled"
|
||||||
"disabled" = "Disabled"
|
"disabled" = "Disabled"
|
||||||
|
"depleted" = "Depleted"
|
||||||
|
"depletingSoon" = "Depleting soon"
|
||||||
"domainName" = "Domain name"
|
"domainName" = "Domain name"
|
||||||
"additional" = "Alter"
|
"additional" = "Alter ID"
|
||||||
"monitor" = "Listen IP"
|
"monitor" = "Listen IP"
|
||||||
"certificate" = "Certificat"
|
"certificate" = "Certificate"
|
||||||
"fail" = "Fail"
|
"fail" = " Fail"
|
||||||
"success" = " Success"
|
"success" = " Success"
|
||||||
"getVersion" = "Get version"
|
"getVersion" = "Get version"
|
||||||
"install" = "Install"
|
"install" = "Install"
|
||||||
@@ -49,8 +53,8 @@
|
|||||||
[menu]
|
[menu]
|
||||||
"dashboard" = "System Status"
|
"dashboard" = "System Status"
|
||||||
"inbounds" = "Inbounds"
|
"inbounds" = "Inbounds"
|
||||||
"setting" = "Panel Setting"
|
"settings" = "Panel Settings"
|
||||||
"logout" = "LogOut"
|
"logout" = "Logout"
|
||||||
"link" = "Other"
|
"link" = "Other"
|
||||||
|
|
||||||
[pages.login]
|
[pages.login]
|
||||||
@@ -58,83 +62,122 @@
|
|||||||
"loginAgain" = "The login time limit has expired, please log in again"
|
"loginAgain" = "The login time limit has expired, please log in again"
|
||||||
|
|
||||||
[pages.login.toasts]
|
[pages.login.toasts]
|
||||||
"invalidFormData" = "Input Data Format Is Invalid"
|
"invalidFormData" = "Input data format is invalid."
|
||||||
"emptyUsername" = "Please Enter Username"
|
"emptyUsername" = "Please enter username."
|
||||||
"emptyPassword" = "Please Enter Password"
|
"emptyPassword" = "Please enter password."
|
||||||
"wrongUsernameOrPassword" = "Invalid username or password"
|
"wrongUsernameOrPassword" = "Invalid username or password."
|
||||||
"successLogin" = "Login"
|
"successLogin" = "Login"
|
||||||
|
|
||||||
[pages.index]
|
[pages.index]
|
||||||
"title" = "System status"
|
"title" = "System Status"
|
||||||
"memory" = "Memory"
|
"memory" = "Memory"
|
||||||
"hard" = "Hard disk"
|
"hard" = "Hard Disk"
|
||||||
"xrayStatus" = "xray Status"
|
"xrayStatus" = "Xray Status"
|
||||||
|
"stopXray" = "Stop"
|
||||||
|
"restartXray" = "Restart"
|
||||||
"xraySwitch" = "Switch Version"
|
"xraySwitch" = "Switch Version"
|
||||||
"xraySwitchClick" = "Click on the version you want to switch"
|
"xraySwitchClick" = "Choose the version you want to switch to."
|
||||||
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
|
"xraySwitchClickDesk" = "Choose wisely, as older versions may not be compatible with current configurations."
|
||||||
"operationHours" = "Operation Hours"
|
"operationHours" = "Operation Hours"
|
||||||
"operationHoursDesc" = "The running time of the system since it was started"
|
"operationHoursDesc" = "System uptime: time since startup."
|
||||||
"systemLoad" = "System Load"
|
"systemLoad" = "System Load"
|
||||||
"connectionCount" = "Connection Count"
|
"connectionCount" = "Connection Count"
|
||||||
"connectionCountDesc" = "The total number of connections for all network cards"
|
"connectionCountDesc" = "Total connections across all network cards."
|
||||||
"upSpeed" = "Total upload speed for all network cards"
|
"upSpeed" = "Total upload speed for all network cards."
|
||||||
"downSpeed" = "Total download speed for all network cards"
|
"downSpeed" = "Total download speed for all network cards."
|
||||||
"totalSent" = "Total upload traffic of all network cards since system startup"
|
"totalSent" = "Total upload traffic of all network cards since system startup."
|
||||||
"totalReceive" = "Total download traffic of all network cards since system startup"
|
"totalReceive" = "Total download data across all network cards since system startup."
|
||||||
"xraySwitchVersionDialog" = "Switch xray version"
|
"xraySwitchVersionDialog" = "Switch Xray Version"
|
||||||
"xraySwitchVersionDialogDesc" = "Whether to switch the xray version to"
|
"xraySwitchVersionDialogDesc" = "Are you sure you want to switch the Xray version to"
|
||||||
"dontRefreshh" = "Installation is in progress, please do not refresh this page"
|
"dontRefresh" = "Installation is in progress, please do not refresh this page."
|
||||||
|
"logs" = "Logs"
|
||||||
|
"config" = "Config"
|
||||||
|
"backup" = "Backup & Restore"
|
||||||
|
"backupTitle" = "Backup & Restore Database"
|
||||||
|
"backupDescription" = "Remember to backup before importing a new database."
|
||||||
|
"exportDatabase" = "Download Database"
|
||||||
|
"importDatabase" = "Upload Database"
|
||||||
|
|
||||||
[pages.inbounds]
|
[pages.inbounds]
|
||||||
"title" = "Inbounds"
|
"title" = "Inbounds"
|
||||||
"totalDownUp" = "Total uploads/downloads"
|
"totalDownUp" = "Total Uploads/Downloads"
|
||||||
"totalUsage" = "Total usage"
|
"totalUsage" = "Total Usage"
|
||||||
"inboundCount" = "Number of inbound"
|
"inboundCount" = "Number of Inbounds"
|
||||||
"operate" = "Operate"
|
"operate" = "Menu"
|
||||||
"enable" = "Enable"
|
"enable" = "Enable"
|
||||||
"remark" = "Remark"
|
"remark" = "Remark"
|
||||||
"protocol" = "Protocol"
|
"protocol" = "Protocol"
|
||||||
"port" = "Port"
|
"port" = "Port"
|
||||||
"traffic" = "Traffic"
|
"traffic" = "Traffic"
|
||||||
"details" = "Details"
|
"details" = "Details"
|
||||||
"transportConfig" = "Transport config"
|
"transportConfig" = "Transport Config"
|
||||||
"expireDate" = "Expire date"
|
"expireDate" = "Expire Date"
|
||||||
"resetTraffic" = "Reset traffic"
|
"resetTraffic" = "Reset Traffic"
|
||||||
"addInbound" = "Add Inbound"
|
"addInbound" = "Add Inbound"
|
||||||
"addTo" = "Add To"
|
"generalActions" = "General Actions"
|
||||||
"revise" = "Revise"
|
"create" = "Create"
|
||||||
"modifyInbound" = "Modify InBound"
|
"update" = "Update"
|
||||||
|
"modifyInbound" = "Modify Inbound"
|
||||||
"deleteInbound" = "Delete Inbound"
|
"deleteInbound" = "Delete Inbound"
|
||||||
"deleteInboundContent" = "Are you sure you want to delete inbound?"
|
"deleteInboundContent" = "Are you sure you want to delete inbound?"
|
||||||
"resetTrafficContent" = "Are you sure you want to reset traffic?"
|
"resetTrafficContent" = "Are you sure you want to reset traffic?"
|
||||||
"copyLink" = "Copy Link"
|
"copyLink" = "Copy Link"
|
||||||
"address" = "Address"
|
"address" = "Address"
|
||||||
"network" = "Network"
|
"network" = "Network"
|
||||||
"destinationPort" = "Destination port"
|
"destinationPort" = "Destination Port"
|
||||||
"targetAddress" = "Target address"
|
"targetAddress" = "Target Address"
|
||||||
"disableInsecureEncryption" = "Disable insecure encryption"
|
"disableInsecureEncryption" = "Disable Insecure Encryption"
|
||||||
"monitorDesc" = "Leave blank by default"
|
"monitorDesc" = "Leave blank by default"
|
||||||
"meansNoLimit" = "Means no limit"
|
"meansNoLimit" = "Means No Limit"
|
||||||
"totalFlow" = "Total flow"
|
"totalFlow" = "Total Flow"
|
||||||
"leaveBlankToNeverExpire" = "Leave blank to never expire"
|
"leaveBlankToNeverExpire" = "Leave blank to never expire"
|
||||||
"noRecommendKeepDefault" = "There are no special requirements to keep the default"
|
"noRecommendKeepDefault" = "No special requirements to keep the default"
|
||||||
"certificatePath" = "Certificate file path"
|
"certificatePath" = "Certificate File Path"
|
||||||
"certificateContent" = "Certificate file content"
|
"certificateContent" = "Certificate File Content"
|
||||||
"publicKeyPath" = "Public key file path"
|
"publicKeyPath" = "Public Key Path"
|
||||||
"publicKeyContent" = "Public key content"
|
"publicKeyContent" = "Public Key Content"
|
||||||
"keyPath" = "Key file path"
|
"keyPath" = "Private Key Path"
|
||||||
"keyContent" = "Key content"
|
"keyContent" = "Private Key Content"
|
||||||
"clickOnQRcode" = "Click on QR Code to Copy"
|
"clickOnQRcode" = "Click on QR Code to Copy"
|
||||||
"client" = "Client"
|
"client" = "Client"
|
||||||
"export" = "Export links"
|
"export" = "Export Links"
|
||||||
|
"clone" = "Clone"
|
||||||
|
"cloneInbound" = "Clone"
|
||||||
|
"cloneInboundContent" = "All settings of this inbound, except for Port, Listening IP, and Clients, will be applied to the clone."
|
||||||
|
"cloneInboundOk" = "Clone"
|
||||||
|
"resetAllTraffic" = "Reset All Inbounds Traffic"
|
||||||
|
"resetAllTrafficTitle" = "Reset all inbounds traffic"
|
||||||
|
"resetAllTrafficContent" = "Are you sure you want to reset all inbounds traffic?"
|
||||||
|
"resetInboundClientTraffics" = "Reset Clients Traffic"
|
||||||
|
"resetInboundClientTrafficTitle" = "Reset all clients traffic"
|
||||||
|
"resetInboundClientTrafficContent" = "Are you sure you want to reset all traffic for this inbound's clients?"
|
||||||
|
"resetAllClientTraffics" = "Reset All Clients Traffic"
|
||||||
|
"resetAllClientTrafficTitle" = "Reset all clients traffic"
|
||||||
|
"resetAllClientTrafficContent" = "Are you sure you want to reset all traffics for all clients?"
|
||||||
|
"delDepletedClients" = "Delete Depleted Clients"
|
||||||
|
"delDepletedClientsTitle" = "Delete depleted clients"
|
||||||
|
"delDepletedClientsContent" = "Are you sure you want to delete all depleted clients?"
|
||||||
|
"email" = "Email"
|
||||||
|
"emailDesc" = "Please provide a unique email address."
|
||||||
|
"setDefaultCert" = "Set cert from panel"
|
||||||
|
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot )"
|
||||||
|
"subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
"add" = "Add client"
|
"add" = "Add Client"
|
||||||
"edit" = "Edit client"
|
"edit" = "Edit Client"
|
||||||
"submitAdd" = "Add client"
|
"submitAdd" = "Add Client"
|
||||||
"submitEdit" = "Save changes"
|
"submitEdit" = "Save changes"
|
||||||
"clientCount" = "Number of clients"
|
"clientCount" = "Number of Clients"
|
||||||
"bulk" = "Add bulk"
|
"bulk" = "Add Bulk"
|
||||||
|
"method" = "Method"
|
||||||
|
"first" = "First"
|
||||||
|
"last" = "Last"
|
||||||
|
"prefix" = "Prefix"
|
||||||
|
"postfix" = "Postfix"
|
||||||
|
"delayedStart" = "Start after first use"
|
||||||
|
"expireDays" = "Expire days"
|
||||||
|
"days" = "day(s)"
|
||||||
|
|
||||||
[pages.inbounds.toasts]
|
[pages.inbounds.toasts]
|
||||||
"obtain" = "Obtain"
|
"obtain" = "Obtain"
|
||||||
@@ -156,58 +199,125 @@
|
|||||||
[pages.inbounds.stream.quic]
|
[pages.inbounds.stream.quic]
|
||||||
"encryption" = "Encryption"
|
"encryption" = "Encryption"
|
||||||
|
|
||||||
[pages.setting]
|
[pages.settings]
|
||||||
"title" = "Setting"
|
"title" = "Settings"
|
||||||
"save" = "Save"
|
"save" = "Save"
|
||||||
"restartPanel" = "Restart Panel"
|
"infoDesc" = "Every change made here needs to be saved. Please restart the panel for the changes to take effect."
|
||||||
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information"
|
"restartPanel" = "Restart Panel "
|
||||||
"panelConfig" = "Panel Configuration"
|
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log information on the server."
|
||||||
"userSetting" = "User Setting"
|
"resetDefaultConfig" = "Reset to default config"
|
||||||
"xrayConfiguration" = "xray Configuration"
|
"panelConfig" = "Panel Configurations"
|
||||||
"TGReminder" = "TG Reminder Related Settings"
|
"userSettings" = "User Settings"
|
||||||
"otherSetting" = "Other Setting"
|
"xrayConfiguration" = "Xray Configurations"
|
||||||
"panelListeningIP" = "Panel listening IP"
|
"TGBotSettings" = "Telegram Bot Settings"
|
||||||
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs, restart the panel to take effect"
|
"panelListeningIP" = "Panel Listening IP"
|
||||||
|
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs."
|
||||||
"panelPort" = "Panel Port"
|
"panelPort" = "Panel Port"
|
||||||
"panelPortDesc" = "Restart the panel to take effect"
|
"panelPortDesc" = "Port number for serving the panel."
|
||||||
"publicKeyPath" = "Panel certificate public key file path"
|
"publicKeyPath" = "Panel Certificate Public Key File Path"
|
||||||
"publicKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
|
"publicKeyPathDesc" = "Fill in an absolute path starting with '/'"
|
||||||
"privateKeyPath" = "Panel certificate key file path"
|
"privateKeyPath" = "Panel Certificate Private Key File Path"
|
||||||
"privateKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
|
"privateKeyPathDesc" = "Fill in an absolute path starting with '/'"
|
||||||
"panelUrlPath" = "panel url root path"
|
"panelUrlPath" = "Panel URL Root Path"
|
||||||
"panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect"
|
"panelUrlPathDesc" = "Must start with '/' and end with '/'"
|
||||||
"oldUsername" = "Current Username"
|
"oldUsername" = "Current Username"
|
||||||
"currentPassword" = "Current Password"
|
"currentPassword" = "Current Password"
|
||||||
"newUsername" = "New Username"
|
"newUsername" = "New Username"
|
||||||
"newPassword" = "New Password"
|
"newPassword" = "New Password"
|
||||||
"advancedTemplate" = "Advanced template parts"
|
"telegramBotEnable" = "Enable Telegram bot"
|
||||||
"completeTemplate" = "Complete template of Xray configuration"
|
"telegramBotEnableDesc" = "Your telegram bot will interact with the panel"
|
||||||
"xrayConfigTemplate" = "Xray Configuration Template"
|
|
||||||
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect."
|
|
||||||
"xrayConfigTorrent" = "Ban bittorrent usage"
|
|
||||||
"xrayConfigTorrentDesc" = "Change the configuration temlate to avoid using bittorrent by users, restart the panel to take effect"
|
|
||||||
"xrayConfigPrivateIp" = "Ban private ip range to connect"
|
|
||||||
"xrayConfigPrivateIpDesc" = "Change the configuration temlate to avoid connecting with private IP ranges, restart the panel to take effect"
|
|
||||||
"xrayConfigInbounds" = "Configuration of Inbounds"
|
|
||||||
"xrayConfigInboundsDesc" = "Change the configuration temlate to accept special clients, restart the panel to take effect"
|
|
||||||
"xrayConfigOutbounds" = "Configuration of Outbounds"
|
|
||||||
"xrayConfigOutboundsDesc" = "Change the configuration temlate to define outgoing ways for this server, restart the panel to take effect"
|
|
||||||
"xrayConfigRoutings" = "Configuration of Routing rules"
|
|
||||||
"xrayConfigRoutingsDesc" = "Change the configuration temlate to define Routing rules for this server, restart the panel to take effect"
|
|
||||||
"telegramBotEnable" = "Enable telegram bot"
|
|
||||||
"telegramBotEnableDesc" = "Restart the panel to take effect"
|
|
||||||
"telegramToken" = "Telegram Token"
|
"telegramToken" = "Telegram Token"
|
||||||
"telegramTokenDesc" = "Restart the panel to take effect"
|
"telegramTokenDesc" = "The Token you have got from @BotFather"
|
||||||
"telegramChatId" = "Telegram ChatId"
|
"telegramChatId" = "Telegram Admin ChatIDs"
|
||||||
"telegramChatIdDesc" = "Restart the panel to take effect"
|
"telegramChatIdDesc" = "Multi chatIDs separated by comma."
|
||||||
"telegramNotifyTime" = "Telegram bot notification time"
|
"telegramNotifyTime" = "Telegram bot notification time"
|
||||||
"telegramNotifyTimeDesc" = "Using Crontab timing format, restart the panel to take effect"
|
"telegramNotifyTimeDesc" = "Use Crontab timing format."
|
||||||
"timeZonee" = "Time Zone"
|
"tgNotifyBackup" = "Database Backup"
|
||||||
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"
|
"tgNotifyBackupDesc" = "Send database backup file with report notification"
|
||||||
|
"sessionMaxAge" = "Session maximum age"
|
||||||
|
"sessionMaxAgeDesc" = "The time that you can stay login (unit: minute)"
|
||||||
|
"expireTimeDiff" = "Expiration threshold for notification"
|
||||||
|
"expireTimeDiffDesc" = "Get notified about account expiration before the threshold (unit: day)"
|
||||||
|
"trafficDiff" = "Traffic threshold for notification"
|
||||||
|
"trafficDiffDesc" = "Get notified about traffic exhaustion before reaching the threshold (unit: GB)"
|
||||||
|
"tgNotifyCpu" = "CPU percentage alert threshold"
|
||||||
|
"tgNotifyCpuDesc" = "Receive notification if CPU usage exceeds this threshold (unit: %)"
|
||||||
|
"timeZone" = "Time Zone"
|
||||||
|
"timeZoneDesc" = "Scheduled tasks run according to the time in this time zone."
|
||||||
|
|
||||||
[pages.setting.toasts]
|
[pages.settings.templates]
|
||||||
"modifySetting" = "Modify setting"
|
"title" = "Templates"
|
||||||
"getSetting" = "Get setting"
|
"basicTemplate" = "Basic Template"
|
||||||
"modifyUser" = "Modify user"
|
"advancedTemplate" = "Advanced Template"
|
||||||
"originalUserPassIncorrect" = "The original user name or original password is incorrect"
|
"completeTemplate" = "Complete Template"
|
||||||
|
"generalConfigs" = "General Configs"
|
||||||
|
"generalConfigsDesc" = "These options will provide general adjustments."
|
||||||
|
"blockConfigs" = "Blocking Configs"
|
||||||
|
"blockConfigsDesc" = "These options will prevent users from connecting to specific protocols and websites."
|
||||||
|
"blockCountryConfigs" = "Block Country Configs"
|
||||||
|
"blockCountryConfigsDesc" = "These options will prevent users from connecting to specific country domains."
|
||||||
|
"directCountryConfigs" = "Direct Country Configs"
|
||||||
|
"directCountryConfigsDesc" = "These options will connect users directly to specific country domains."
|
||||||
|
"ipv4Configs" = "IPv4 Configs"
|
||||||
|
"ipv4ConfigsDesc" = "These options will route to target domains only via IPv4."
|
||||||
|
"xrayConfigTemplate" = "Xray Configuration Template"
|
||||||
|
"xrayConfigTemplateDesc" = "Generate the final Xray configuration file based on this template."
|
||||||
|
"xrayConfigFreedomStrategy" = "Configure Strategy for Freedom Protocol"
|
||||||
|
"xrayConfigFreedomStrategyDesc" = "Set the output strategy of the network in the Freedom Protocol."
|
||||||
|
"xrayConfigRoutingStrategy" = "Configure Domains Routing Strategy"
|
||||||
|
"xrayConfigRoutingStrategyDesc" = "Set the overall routing strategy for DNS resolving."
|
||||||
|
"xrayConfigTorrent" = "Ban BitTorrent Usage"
|
||||||
|
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using BitTorrent by users."
|
||||||
|
"xrayConfigPrivateIp" = "Ban Private IP Ranges to Connect"
|
||||||
|
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting to private IP ranges."
|
||||||
|
"xrayConfigAds" = "Block Ads"
|
||||||
|
"xrayConfigAdsDesc" = "Change the configuration template to block ads"
|
||||||
|
"xrayConfigFamily" = "Enable Family-Friendly Configuration"
|
||||||
|
"xrayConfigFamilyDesc" = "Avoid connecting to unsafe websites for family protection."
|
||||||
|
"xrayConfigIRIp" = "Disable connection to Iran IP ranges"
|
||||||
|
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting to Iran IP ranges."
|
||||||
|
"xrayConfigIRDomain" = "Disable connection to Iran domains"
|
||||||
|
"xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting to Iran domains."
|
||||||
|
"xrayConfigChinaIp" = "Disable connection to China IP ranges"
|
||||||
|
"xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting to China IP ranges."
|
||||||
|
"xrayConfigChinaDomain" = "Disable connection to China domains"
|
||||||
|
"xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting to China domains."
|
||||||
|
"xrayConfigRussiaIp" = "Disable connection to Russia IP ranges"
|
||||||
|
"xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting to Russia IP ranges."
|
||||||
|
"xrayConfigRussiaDomain" = "Disable connection to Russia domains"
|
||||||
|
"xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting to Russia domains."
|
||||||
|
"xrayConfigDirectIRIp" = "Direct connection to Iran IP ranges"
|
||||||
|
"xrayConfigDirectIRIpDesc" = "Change the configuration template for direct connecting to Iran IP ranges."
|
||||||
|
"xrayConfigDirectIRDomain" = "Direct connection to Iran domains"
|
||||||
|
"xrayConfigDirectIRDomainDesc" = "Change the configuration template for direct connecting to Iran domains."
|
||||||
|
"xrayConfigDirectChinaIp" = "Direct connection to China IP ranges"
|
||||||
|
"xrayConfigDirectChinaIpDesc" = "Change the configuration template for direct connecting to China IP ranges."
|
||||||
|
"xrayConfigDirectChinaDomain" = "Direct connection to China domains"
|
||||||
|
"xrayConfigDirectChinaDomainDesc" = "Change the configuration template for direct connecting to China domains."
|
||||||
|
"xrayConfigDirectRussiaIp" = "Direct connection to Russia IP ranges"
|
||||||
|
"xrayConfigDirectRussiaIpDesc" = "Change the configuration template for direct connecting to Russia IP ranges."
|
||||||
|
"xrayConfigDirectRussiaDomain" = "Direct connection to Russia domains"
|
||||||
|
"xrayConfigDirectRussiaDomainDesc" = "Change the configuration template for direct connecting to Russia domains."
|
||||||
|
"xrayConfigGoogleIPv4" = "Use IPv4 for Google"
|
||||||
|
"xrayConfigGoogleIPv4Desc" = "Add routing for Google to connect with IPv4."
|
||||||
|
"xrayConfigNetflixIPv4" = "Use IPv4 for Netflix"
|
||||||
|
"xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4."
|
||||||
|
"xrayConfigInbounds" = "Configuration of Inbounds"
|
||||||
|
"xrayConfigInboundsDesc" = "Change the configuration template to accept specific clients."
|
||||||
|
"xrayConfigOutbounds" = "Configuration of Outbounds"
|
||||||
|
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server."
|
||||||
|
"xrayConfigRoutings" = "Configuration of routing rules"
|
||||||
|
"xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server."
|
||||||
|
"manualLists" = "Manual Lists"
|
||||||
|
"manualListsDesc" = "Please use the JSON array format."
|
||||||
|
"manualBlockedIPs" = "List of Blocked IPs"
|
||||||
|
"manualBlockedDomains" = "List of Blocked Domains"
|
||||||
|
"manualDirectIPs" = "List of Direct IPs"
|
||||||
|
"manualDirectDomains" = "List of Direct Domains"
|
||||||
|
|
||||||
|
[pages.settings.toasts]
|
||||||
|
"modifySettings" = "Modify Settings "
|
||||||
|
"getSettings" = "Get Settings "
|
||||||
|
"modifyUser" = "Modify User "
|
||||||
|
"originalUserPassIncorrect" = "Incorrect original username or password"
|
||||||
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"
|
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"
|
||||||
@@ -11,8 +11,9 @@
|
|||||||
"enable" = "فعال"
|
"enable" = "فعال"
|
||||||
"protocol" = "پروتکل"
|
"protocol" = "پروتکل"
|
||||||
"search" = "جستجو"
|
"search" = "جستجو"
|
||||||
|
"filter" = "فیلتر"
|
||||||
|
|
||||||
"loading" = "در حال بروزرسانی..."
|
"loading" = "در حال بروزرسانی.."
|
||||||
"second" = "ثانیه"
|
"second" = "ثانیه"
|
||||||
"minute" = "دقیقه"
|
"minute" = "دقیقه"
|
||||||
"hour" = "ساعت"
|
"hour" = "ساعت"
|
||||||
@@ -33,12 +34,15 @@
|
|||||||
"host" = "آدرس"
|
"host" = "آدرس"
|
||||||
"path" = "مسیر"
|
"path" = "مسیر"
|
||||||
"camouflage" = "استتار"
|
"camouflage" = "استتار"
|
||||||
|
"status" = "وضعیت"
|
||||||
"enabled" = "فعال"
|
"enabled" = "فعال"
|
||||||
"disabled" = "غیرفعال"
|
"disabled" = "غیرفعال"
|
||||||
|
"depleted" = "منقضی"
|
||||||
|
"depletingSoon" = "در حال انقضا"
|
||||||
"domainName" = "آدرس دامنه"
|
"domainName" = "آدرس دامنه"
|
||||||
"additional" = "آی دی جایگزین"
|
"additional" = "آی دی جایگزین"
|
||||||
"monitor" = "آی پی اتصال"
|
"monitor" = "آی پی اتصال"
|
||||||
"certificate" = "سرتیفیکیت"
|
"certificate" = "گواهی دیجیتال"
|
||||||
"fail" = "خطا"
|
"fail" = "خطا"
|
||||||
"success" = " موفق"
|
"success" = " موفق"
|
||||||
"getVersion" = "دریافت ورژن"
|
"getVersion" = "دریافت ورژن"
|
||||||
@@ -49,7 +53,7 @@
|
|||||||
[menu]
|
[menu]
|
||||||
"dashboard" = "وضعیت سیستم"
|
"dashboard" = "وضعیت سیستم"
|
||||||
"inbounds" = "سرویس ها"
|
"inbounds" = "سرویس ها"
|
||||||
"setting" = "تنظیمات پنل"
|
"settings" = "تنظیمات پنل"
|
||||||
"logout" = "خروج"
|
"logout" = "خروج"
|
||||||
"link" = "دیگر"
|
"link" = "دیگر"
|
||||||
|
|
||||||
@@ -69,12 +73,14 @@
|
|||||||
"memory" = "حافظه رم"
|
"memory" = "حافظه رم"
|
||||||
"hard" = "حافظه دیسک"
|
"hard" = "حافظه دیسک"
|
||||||
"xrayStatus" = "وضعیت Xray"
|
"xrayStatus" = "وضعیت Xray"
|
||||||
|
"stopXray" = "توقف"
|
||||||
|
"restartXray" = "شروع مجدد"
|
||||||
"xraySwitch" = "تغییر ورژن"
|
"xraySwitch" = "تغییر ورژن"
|
||||||
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
|
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
|
||||||
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
|
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد "
|
||||||
"operationHours" = "ساعت فعال"
|
"operationHours" = "مدت فعالیت"
|
||||||
"operationHoursDesc" = "ساعت فعال بعد از شروع سیستم"
|
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
|
||||||
"systemLoad" = "سرعت لود سیستم"
|
"systemLoad" = "بار روی سیستم"
|
||||||
"connectionCount" = "تعداد کانکشن ها"
|
"connectionCount" = "تعداد کانکشن ها"
|
||||||
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
||||||
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
|
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
|
||||||
@@ -83,7 +89,14 @@
|
|||||||
"totalReceive" = "جمع کل ترافیک دانلود مصرفی"
|
"totalReceive" = "جمع کل ترافیک دانلود مصرفی"
|
||||||
"xraySwitchVersionDialog" = "تغییر ورژن Xray"
|
"xraySwitchVersionDialog" = "تغییر ورژن Xray"
|
||||||
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
|
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
|
||||||
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید "
|
"dontRefresh" = "در حال نصب ، لطفا رفرش نکنید "
|
||||||
|
"logs" = "گزارش ها"
|
||||||
|
"config" = "تنظیمات"
|
||||||
|
"backup" = "پشتیبان گیری"
|
||||||
|
"backupTitle" = "پشتیبان گیری دیتابیس"
|
||||||
|
"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید"
|
||||||
|
"exportDatabase" = "دانلود دیتابیس"
|
||||||
|
"importDatabase" = "آپلود دیتابیس"
|
||||||
|
|
||||||
[pages.inbounds]
|
[pages.inbounds]
|
||||||
"title" = "کاربران"
|
"title" = "کاربران"
|
||||||
@@ -101,8 +114,9 @@
|
|||||||
"expireDate" = "تاریخ انقضا"
|
"expireDate" = "تاریخ انقضا"
|
||||||
"resetTraffic" = "ریست ترافیک"
|
"resetTraffic" = "ریست ترافیک"
|
||||||
"addInbound" = "اضافه کردن سرویس"
|
"addInbound" = "اضافه کردن سرویس"
|
||||||
"addTo" = "اضافه کردن"
|
"generalActions" = "عملیات کلی"
|
||||||
"revise" = "ویرایش"
|
"create" = "اضافه کردن"
|
||||||
|
"update" = "ویرایش"
|
||||||
"modifyInbound" = "ویرایش سرویس"
|
"modifyInbound" = "ویرایش سرویس"
|
||||||
"deleteInbound" = "حذف سرویس"
|
"deleteInbound" = "حذف سرویس"
|
||||||
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
|
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
|
||||||
@@ -112,7 +126,7 @@
|
|||||||
"network" = "شبکه"
|
"network" = "شبکه"
|
||||||
"destinationPort" = "پورت مقصد"
|
"destinationPort" = "پورت مقصد"
|
||||||
"targetAddress" = "آدرس مقصد"
|
"targetAddress" = "آدرس مقصد"
|
||||||
"disableInsecureEncryption" = "رمزگذاری ناامن را غیرفعال کنید"
|
"disableInsecureEncryption" = "غیرفعال سازی رمزگذاری ناامن"
|
||||||
"monitorDesc" = "به طور پیش فرض خالی بگذارید"
|
"monitorDesc" = "به طور پیش فرض خالی بگذارید"
|
||||||
"meansNoLimit" = "یعنی بدون محدودیت"
|
"meansNoLimit" = "یعنی بدون محدودیت"
|
||||||
"totalFlow" = "کل ترافیک"
|
"totalFlow" = "کل ترافیک"
|
||||||
@@ -120,13 +134,33 @@
|
|||||||
"noRecommendKeepDefault" = "توصیه می شود به عنوان پیش فرض حفظ شود"
|
"noRecommendKeepDefault" = "توصیه می شود به عنوان پیش فرض حفظ شود"
|
||||||
"certificatePath" = "مسیر فایل گواهی"
|
"certificatePath" = "مسیر فایل گواهی"
|
||||||
"certificateContent" = "محتوای فایل گواهی"
|
"certificateContent" = "محتوای فایل گواهی"
|
||||||
"publicKeyPath" = "مسیر فایل Certificate.crt"
|
"publicKeyPath" = "مسیر کلید عمومی"
|
||||||
"publicKeyContent" = "محتوای Certificate.crt"
|
"publicKeyContent" = "محتوای کلید عمومی"
|
||||||
"keyPath" = "مسیر فایل Private.key"
|
"keyPath" = "مسیر کلید خصوصی"
|
||||||
"keyContent" = "محتوای Private.key"
|
"keyContent" = "محتوای کلید خصوصی"
|
||||||
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
|
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
|
||||||
"client" = "کاربر"
|
"client" = "کاربر"
|
||||||
"export" = "استخراج لینکها"
|
"export" = "استخراج لینکها"
|
||||||
|
"clone" = "شبیه سازی"
|
||||||
|
"cloneInbound" = "ایجاد"
|
||||||
|
"cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد"
|
||||||
|
"resetAllTraffic" = "ریست ترافیک کل سرویس ها"
|
||||||
|
"resetAllTrafficTitle" = "ریست ترافیک کل سرویس ها"
|
||||||
|
"resetAllTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک سرویس ها را ریست کنید؟"
|
||||||
|
"resetInboundClientTraffics" = "ریست ترافیک کاربران"
|
||||||
|
"resetInboundClientTrafficTitle" = "ریست ترافیک کل کاربران"
|
||||||
|
"resetInboundClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
|
||||||
|
"resetAllClientTraffics" = "ریست ترافیک کاربران"
|
||||||
|
"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران"
|
||||||
|
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران را ریست کنید؟"
|
||||||
|
"delDepletedClients" = "حذف کاربران منقضی"
|
||||||
|
"delDepletedClientsTitle" = "حذف کاربران منقضی"
|
||||||
|
"delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟"
|
||||||
|
"email" = "ایمیل"
|
||||||
|
"emailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
|
||||||
|
"setDefaultCert" = "استفاده از گواهی پنل"
|
||||||
|
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)"
|
||||||
|
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
"add" = "کاربر جدید"
|
"add" = "کاربر جدید"
|
||||||
@@ -135,6 +169,14 @@
|
|||||||
"submitEdit" = "ذخیره تغییرات"
|
"submitEdit" = "ذخیره تغییرات"
|
||||||
"clientCount" = "تعداد کاربران"
|
"clientCount" = "تعداد کاربران"
|
||||||
"bulk" = "انبوه سازی"
|
"bulk" = "انبوه سازی"
|
||||||
|
"method" = "روش"
|
||||||
|
"first" = "از"
|
||||||
|
"last" = "تا"
|
||||||
|
"prefix" = "پیشوند"
|
||||||
|
"postfix" = "پسوند"
|
||||||
|
"delayedStart" = "شروع بعد از اولین استفاده"
|
||||||
|
"expireDays" = "روزهای اعتبار"
|
||||||
|
"days" = "(روز)"
|
||||||
|
|
||||||
[pages.inbounds.toasts]
|
[pages.inbounds.toasts]
|
||||||
"obtain" = "Obtain"
|
"obtain" = "Obtain"
|
||||||
@@ -156,58 +198,125 @@
|
|||||||
[pages.inbounds.stream.quic]
|
[pages.inbounds.stream.quic]
|
||||||
"encryption" = "رمزنگاری"
|
"encryption" = "رمزنگاری"
|
||||||
|
|
||||||
[pages.setting]
|
[pages.settings]
|
||||||
"title" = "تنظیمات"
|
"title" = "تنظیمات"
|
||||||
"save" = "ذخیره"
|
"save" = "ذخیره"
|
||||||
|
"infoDesc" = "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید"
|
||||||
"restartPanel" = "ریستارت پنل"
|
"restartPanel" = "ریستارت پنل"
|
||||||
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
|
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
|
||||||
|
"resetDefaultConfig" = "برگشت به تنظیمات پیشفرض"
|
||||||
"panelConfig" = "تنظیمات پنل"
|
"panelConfig" = "تنظیمات پنل"
|
||||||
"userSetting" = "تنظیمات مدیر"
|
"userSettings" = "تنظیمات مدیر"
|
||||||
"xrayConfiguration" = "تنظیمات Xray"
|
"xrayConfiguration" = "تنظیمات Xray"
|
||||||
"TGReminder" = "تنظیمات ربات تلگرام"
|
"TGBotSettings" = "تنظیمات ربات تلگرام"
|
||||||
"otherSetting" = "دیگر تنظیمات"
|
|
||||||
"panelListeningIP" = "محدودیت آی پی پنل"
|
"panelListeningIP" = "محدودیت آی پی پنل"
|
||||||
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"panelListeningIPDesc" = "برای استفاده از تمام آیپیها به طور پیش فرض خالی بگذارید"
|
||||||
"panelPort" = "پورت پنل"
|
"panelPort" = "پورت پنل"
|
||||||
"panelPortDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل"
|
||||||
"publicKeyPath" = "مسیر فایل پنل Certificate.crt"
|
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
|
||||||
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
|
||||||
"privateKeyPath" = "مسیر فایل پنل private.key"
|
"privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل"
|
||||||
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
|
||||||
"panelUrlPath" = "آدرس روت پنل"
|
"panelUrlPath" = "آدرس روت پنل"
|
||||||
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود"
|
||||||
"oldUsername" = "نام کاربری فعلی"
|
"oldUsername" = "نام کاربری فعلی"
|
||||||
"currentPassword" = "رمز عبور فعلی"
|
"currentPassword" = "رمز عبور فعلی"
|
||||||
"newUsername" = "نام کاربری جدید"
|
"newUsername" = "نام کاربری جدید"
|
||||||
"newPassword" = "رمز عبور جدید"
|
"newPassword" = "رمز عبور جدید"
|
||||||
"advancedTemplate" = "بخش های پیشرفته الگو"
|
|
||||||
"completeTemplate" = "الگوی کامل تنظیمات ایکس ری"
|
|
||||||
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
|
|
||||||
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
|
||||||
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
|
|
||||||
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
|
||||||
"xrayConfigPrivateIp" = "جلوگیری از اتصال آی پی های نامعتبر"
|
|
||||||
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آی پی های نامعتبر و بسته های سرگردان تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
|
||||||
"xrayConfigInbounds" = "تنظیمات ورودی"
|
|
||||||
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
|
||||||
"xrayConfigOutbounds" = "تنظیمات خروجی"
|
|
||||||
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
|
||||||
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
|
|
||||||
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
|
||||||
"telegramBotEnable" = "فعالسازی ربات تلگرام"
|
"telegramBotEnable" = "فعالسازی ربات تلگرام"
|
||||||
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"telegramBotEnableDesc" = "از طریق بات تلگرام به امکانات ابن پنل متصل شوید"
|
||||||
"telegramToken" = "توکن تلگرام"
|
"telegramToken" = "توکن تلگرام"
|
||||||
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
|
||||||
"telegramChatId" = "آی دی تلگرام مدیریت . از ربات @getidsbot آی دی خود را دریافت کنید"
|
"telegramChatId" = "آی دی تلگرام مدیریت"
|
||||||
"telegramChatIdDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید"
|
||||||
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
|
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
|
||||||
"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
|
||||||
"timeZonee" = "منظقه زمانی"
|
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
|
||||||
|
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
|
||||||
|
"sessionMaxAge" = "بیشینه زمان جلسه وب"
|
||||||
|
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
|
||||||
|
"expireTimeDiff" = "آستانه زمان باقی مانده"
|
||||||
|
"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
|
||||||
|
"trafficDiff" = "آستانه ترافیک باقی مانده"
|
||||||
|
"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
|
||||||
|
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
|
||||||
|
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
|
||||||
|
"timeZone" = "منظقه زمانی"
|
||||||
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
|
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
|
||||||
|
|
||||||
[pages.setting.toasts]
|
[pages.settings.templates]
|
||||||
"modifySetting" = "ویرایش تنظیمات"
|
"title" = "الگوها"
|
||||||
"getSetting" = "دریافت تنظیمات"
|
"basicTemplate" = "بخش الگو پایه"
|
||||||
|
"advancedTemplate" = "بخش الگو پیشرفته"
|
||||||
|
"completeTemplate" = "بخش الگو کامل"
|
||||||
|
"generalConfigs" = "تنظیمات عمومی"
|
||||||
|
"generalConfigsDesc" = "این تنظیمات میتواند ترافیک کلی سرویس را متاثر کند"
|
||||||
|
"blockConfigs" = "مسدود سازی"
|
||||||
|
"blockConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند"
|
||||||
|
"blockCountryConfigs" = "تنظیمات برای مسدودسازی کشورها"
|
||||||
|
"blockCountryConfigsDesc" = "این گزینه اتصال کاربران به دامنه های کشوری خاص را مسدود می کند"
|
||||||
|
"directCountryConfigs" = "تنظیمات برای اتصال مستقیم کشورها"
|
||||||
|
"directCountryConfigsDesc" = "این گزینه کاربران را به دامنه های کشوری خاص را به طور مستقیم، متصل می کند"
|
||||||
|
"ipv4Configs" = "تنظیمات برای IPv4"
|
||||||
|
"ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن ۴ به دامنه های هدف هدایت می شود"
|
||||||
|
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
|
||||||
|
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید!"
|
||||||
|
"xrayConfigFreedomStrategy" = "روش استفاده از شبکه خروجی مستقیم"
|
||||||
|
"xrayConfigFreedomStrategyDesc" = "تعیین روش استفاده از خروجی برای پرتکل مستقیم"
|
||||||
|
"xrayConfigRoutingStrategy" = "پیکربندی استراتژی حل دامنه در مسیریابی"
|
||||||
|
"xrayConfigRoutingStrategyDesc" = "تعیین استراتژی مسیریابی کلی برای پیدا کردن دامنه"
|
||||||
|
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
|
||||||
|
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد"
|
||||||
|
"xrayConfigPrivateIp" = "جلوگیری از اتصال آیپی های خصوصی یا محلی"
|
||||||
|
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد"
|
||||||
|
"xrayConfigAds" = "مسدود کردن تبلیغات"
|
||||||
|
"xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد"
|
||||||
|
"xrayConfigFamily" = "فعال کردن حالت خانواده"
|
||||||
|
"xrayConfigFamilyDesc" = "برای جلوگیری از ارتباط با وبسایت های ناامن"
|
||||||
|
"xrayConfigIRIp" = "جلوگیری از اتصال آیپی های ایران"
|
||||||
|
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد"
|
||||||
|
"xrayConfigIRDomain" = "جلوگیری از اتصال دامنه های ایران"
|
||||||
|
"xrayConfigIRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد"
|
||||||
|
"xrayConfigChinaIp" = "جلوگیری از اتصال آیپی های چین"
|
||||||
|
"xrayConfigChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد"
|
||||||
|
"xrayConfigChinaDomain" = "جلوگیری از اتصال دامنه های چین"
|
||||||
|
"xrayConfigChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد"
|
||||||
|
"xrayConfigRussiaIp" = "جلوگیری از اتصال آیپی های روسیه"
|
||||||
|
"xrayConfigRussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد"
|
||||||
|
"xrayConfigRussiaDomain" = "جلوگیری از اتصال دامنه های روسیه"
|
||||||
|
"xrayConfigRussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد"
|
||||||
|
"xrayConfigDirectIRIp" = "ارتباط مستقیم به آیپی های ایران"
|
||||||
|
"xrayConfigDirectIRIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های ایران تغییر میدهد"
|
||||||
|
"xrayConfigDirectIRDomain" = "ارتباط مستقیم به دامنه های ایران"
|
||||||
|
"xrayConfigDirectIRDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های ایران تغییر میدهد"
|
||||||
|
"xrayConfigDirectChinaIp" = "ارتباط مستقیم به آیپی های چین"
|
||||||
|
"xrayConfigDirectChinaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های چین تغییر میدهد"
|
||||||
|
"xrayConfigDirectChinaDomain" = "ارتباط مستقیم به دامنه های چین"
|
||||||
|
"xrayConfigDirectChinaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های چین تغییر میدهد"
|
||||||
|
"xrayConfigDirectRussiaIp" = "ارتباط مستقیم به آیپی های روسیه"
|
||||||
|
"xrayConfigDirectRussiaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های روسیه تغییر میدهد"
|
||||||
|
"xrayConfigDirectRussiaDomain" = "ارتباط مستقیم به دامنه های روسیه"
|
||||||
|
"xrayConfigDirectRussiaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های روسیه تغییر میدهد"
|
||||||
|
"xrayConfigGoogleIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به گوگل"
|
||||||
|
"xrayConfigGoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند"
|
||||||
|
"xrayConfigNetflixIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به نتفلیکس"
|
||||||
|
"xrayConfigNetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند"
|
||||||
|
"xrayConfigInbounds" = "تنظیمات ورودی"
|
||||||
|
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید"
|
||||||
|
"xrayConfigOutbounds" = "تنظیمات خروجی"
|
||||||
|
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید"
|
||||||
|
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
|
||||||
|
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید"
|
||||||
|
"manualLists" = "لیست های دستی"
|
||||||
|
"manualListsDesc" = "فرمت: JSON Array"
|
||||||
|
"manualBlockedIPs" = "لیست آیپی های مسدود شده"
|
||||||
|
"manualBlockedDomains" = "لیست دامنه های مسدود شده"
|
||||||
|
"manualDirectIPs" = "لیست آیپی های مستقیم"
|
||||||
|
"manualDirectDomains" = "لیست دامنه های مستقیم"
|
||||||
|
|
||||||
|
[pages.settings.toasts]
|
||||||
|
"modifySettings" = "ویرایش تنظیمات"
|
||||||
|
"getSettings" = "دریافت تنظیمات"
|
||||||
"modifyUser" = "ویرایش کاربر"
|
"modifyUser" = "ویرایش کاربر"
|
||||||
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد ."
|
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد "
|
||||||
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد ."
|
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد "
|
||||||
|
|||||||
323
web/translation/translate.ru_RU.toml
Normal file
323
web/translation/translate.ru_RU.toml
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
"username" = "имя пользователя"
|
||||||
|
"password" = "пароль"
|
||||||
|
"login" = "логин"
|
||||||
|
"confirm" = "подтвердить"
|
||||||
|
"cancel" = "отмена"
|
||||||
|
"close" = "закрыть"
|
||||||
|
"copy" = "копировать"
|
||||||
|
"copied" = "скопировано"
|
||||||
|
"download" = "скачать"
|
||||||
|
"remark" = "примечание"
|
||||||
|
"enable" = "включить"
|
||||||
|
"protocol" = "протокол"
|
||||||
|
"search" = "поиск"
|
||||||
|
"filter" = "Фильтр"
|
||||||
|
|
||||||
|
"loading" = "загрузка"
|
||||||
|
"second" = "секунда"
|
||||||
|
"minute" = "минута"
|
||||||
|
"hour" = "час"
|
||||||
|
"day" = "день"
|
||||||
|
"check" = "просмотр"
|
||||||
|
"indefinite" = "бессрочно"
|
||||||
|
"unlimited" = "безлимитно"
|
||||||
|
"none" = "пусто"
|
||||||
|
"qrCode" = "QR-код"
|
||||||
|
"info" = "больше информации"
|
||||||
|
"edit" = "изменить"
|
||||||
|
"delete" = "удалить"
|
||||||
|
"reset" = "обнулить"
|
||||||
|
"copySuccess" = "скопировано"
|
||||||
|
"sure" = "да"
|
||||||
|
"encryption" = "Шифрование"
|
||||||
|
"transmission" = "протокол передачи"
|
||||||
|
"host" = "хост"
|
||||||
|
"path" = "путь"
|
||||||
|
"camouflage" = "маскировка"
|
||||||
|
"status" = "статус"
|
||||||
|
"enabled" = "включено"
|
||||||
|
"disabled" = "отключено"
|
||||||
|
"depleted" = "исчерпано"
|
||||||
|
"depletingSoon" = "почти исчерпано"
|
||||||
|
"domainName" = "домен"
|
||||||
|
"additional" = "допольнительно"
|
||||||
|
"monitor" = "порт IP"
|
||||||
|
"certificate" = "сертификат"
|
||||||
|
"fail" = "неудача"
|
||||||
|
"success" = "успешно"
|
||||||
|
"getVersion" = "узнать версию"
|
||||||
|
"install" = "установка"
|
||||||
|
"clients" = "клиенты"
|
||||||
|
"usage" = "использование"
|
||||||
|
|
||||||
|
[menu]
|
||||||
|
"dashboard" = "статус системы"
|
||||||
|
"inbounds" = "пользователи"
|
||||||
|
"settings" = "настройки"
|
||||||
|
"logout" = "выход"
|
||||||
|
"link" = "другое"
|
||||||
|
|
||||||
|
[pages.login]
|
||||||
|
"title" = "логин"
|
||||||
|
"loginAgain" = "Время пребывания в сети вышло. Пожалуйста, войдите в систему снова"
|
||||||
|
|
||||||
|
[pages.login.toasts]
|
||||||
|
"invalidFormData" = "Недопустимый формат данных"
|
||||||
|
"emptyUsername" = "Введите имя пользователя"
|
||||||
|
"emptyPassword" = "Введите пароль"
|
||||||
|
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль"
|
||||||
|
"successLogin" = "успешный вход"
|
||||||
|
|
||||||
|
[pages.index]
|
||||||
|
"title" = "статус системы"
|
||||||
|
"memory" = "память"
|
||||||
|
"hard" = "жесткий диск"
|
||||||
|
"xrayStatus" = "статус Xray"
|
||||||
|
"stopXray" = "стоп"
|
||||||
|
"restartXray" = "рестарт Xray"
|
||||||
|
"xraySwitch" = "переключить версию"
|
||||||
|
"xraySwitchClick" = "Выберите желаемую версию"
|
||||||
|
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями"
|
||||||
|
"operationHours" = "Часы работы"
|
||||||
|
"operationHoursDesc" = "Аптайм системы: время системы в сети"
|
||||||
|
"systemLoad" = "Системная нагрузка"
|
||||||
|
"connectionCount" = "количество соединений"
|
||||||
|
"connectionCountDesc" = "Всего подключений по всем сетям»"
|
||||||
|
"upSpeed" = "Общая скорость upload"
|
||||||
|
"downSpeed" = "Общая скорость download"
|
||||||
|
"totalSent" = "Общий объем загруженных данных с момента запуска системы"
|
||||||
|
"totalReceive" = "Общий объем полученных данных с момента запуска системы"
|
||||||
|
"xraySwitchVersionDialog" = "переключить версию Xray"
|
||||||
|
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
|
||||||
|
"dontRefresh" = "Установка. Не обновляйте эту страницу"
|
||||||
|
"logs" = "Логи"
|
||||||
|
"config" = "Конфиг"
|
||||||
|
"backup" = "Бекап и восстановление"
|
||||||
|
"backupTitle" = "База данных бекапа и восстановления"
|
||||||
|
"backupDescription" = "Не забудьте сделать резервную копию перед импортом новой базы данных"
|
||||||
|
"exportDatabase" = "Экспорт базы данных"
|
||||||
|
"importDatabase" = "Импорт базы данных"
|
||||||
|
|
||||||
|
[pages.inbounds]
|
||||||
|
"title" = "пользователи"
|
||||||
|
"totalDownUp" = "Всего входящих/исходящих"
|
||||||
|
"totalUsage" = "Всего использовано"
|
||||||
|
"inboundCount" = "Количество пользователей"
|
||||||
|
"operate" = "Меню"
|
||||||
|
"enable" = "Включить"
|
||||||
|
"remark" = "Примечание"
|
||||||
|
"protocol" = "Протокол"
|
||||||
|
"port" = "Порт"
|
||||||
|
"traffic" = "Траффик"
|
||||||
|
"details" = "Подробнее"
|
||||||
|
"transportConfig" = "Перенести"
|
||||||
|
"expireDate" = "Дата окончания"
|
||||||
|
"resetTraffic" = "Обнулить траффик"
|
||||||
|
"addInbound" = "Добавить пользователя"
|
||||||
|
"generalActions" = "Общие действия"
|
||||||
|
"create" = "Создать"
|
||||||
|
"update" = "Обновить"
|
||||||
|
"modifyInbound" = "Изменить данные"
|
||||||
|
"deleteInbound" = "Удалить пользователя"
|
||||||
|
"deleteInboundContent" = "Подтвердите удаление пользователя?"
|
||||||
|
"resetTrafficContent" = "Подтвердите обнуление траффика?"
|
||||||
|
"copyLink" = "Копировать ключ"
|
||||||
|
"address" = "Адрес"
|
||||||
|
"network" = "Сеть"
|
||||||
|
"destinationPort" = "Порт назначения"
|
||||||
|
"targetAddress" = "Целевой адрес"
|
||||||
|
"disableInsecureEncryption" = "Отключить небезопасное шифрование"
|
||||||
|
"monitorDesc" = "Оставьте пустым по умолчанию"
|
||||||
|
"meansNoLimit" = "Значит без ограничений"
|
||||||
|
"totalFlow" = "Общий расход"
|
||||||
|
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы никогда не истекать"
|
||||||
|
"noRecommendKeepDefault" = "Нет требований для сохранения настроек по умолчанию"
|
||||||
|
"certificatePath" = "Путь файла сертификата"
|
||||||
|
"certificateContent" = "Содержимое файла сертификата"
|
||||||
|
"publicKeyPath" = "Путь к публичному ключу"
|
||||||
|
"publicKeyContent" = "Содержимое публичного ключа"
|
||||||
|
"keyPath" = "Путь к приватному ключу"
|
||||||
|
"keyContent" = "Содержимое приватного ключа"
|
||||||
|
"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать"
|
||||||
|
"client" = "Клиент"
|
||||||
|
"export" = "Поделиться ключом"
|
||||||
|
"clone" = "Клонировать"
|
||||||
|
"cloneInbound" = "Клонировать пользователя"
|
||||||
|
"cloneInboundContent" = "Все настройки этого пользователя, кроме порта, порт прослушки и клиентов, будут клонированы"
|
||||||
|
"cloneInboundOk" = "Клонировать"
|
||||||
|
"resetAllTraffic" = "Обнулить весь траффик"
|
||||||
|
"resetAllTrafficTitle" = "Обнуление всего траффика"
|
||||||
|
"resetAllTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
|
||||||
|
"resetInboundClientTraffics" = "Обнулить траффик пользователей"
|
||||||
|
"resetInboundClientTrafficTitle" = "Обнуление траффика пользователей"
|
||||||
|
"resetInboundClientTrafficContent" = "Вы уверены, что хотите обнулить весь трафик для этих пользователей?"
|
||||||
|
"resetAllClientTraffics" = "Обнулить весь траффик пользователей"
|
||||||
|
"resetAllClientTrafficTitle" = "Обнуление всего траффика пользователей"
|
||||||
|
"resetAllClientTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
|
||||||
|
"delDepletedClients" = "Удалить отключенных пользователей"
|
||||||
|
"delDepletedClientsTitle" = "Удаление отключенных пользователей"
|
||||||
|
"delDepletedClientsContent" = "Подтверждаете удаление отключенных пользователей?"
|
||||||
|
"email" = "Email"
|
||||||
|
"emailDesc" = "Пожалуйста, укажите уникальный Email"
|
||||||
|
"setDefaultCert" = "Установить сертификат с панели"
|
||||||
|
"telegramDesc" = "используйте Telegram ID (вы можете получить его у @userinfobot)"
|
||||||
|
"subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов"
|
||||||
|
|
||||||
|
[pages.client]
|
||||||
|
"add" = "Добавить пользователя"
|
||||||
|
"edit" = "Редактировать пользователя"
|
||||||
|
"submitAdd" = "Добавить пользователя"
|
||||||
|
"submitEdit" = "Сохранить изменения"
|
||||||
|
"clientCount" = "Количество пользователей"
|
||||||
|
"bulk" = "Добавить несколько"
|
||||||
|
"method" = "Метод"
|
||||||
|
"first" = "Первый"
|
||||||
|
"last" = "Последний"
|
||||||
|
"prefix" = "Префикс"
|
||||||
|
"postfix" = "Постфикс"
|
||||||
|
"delayedStart" = "Начать со времени первого подключения"
|
||||||
|
"expireDays" = "Срок действия"
|
||||||
|
"days" = "дней"
|
||||||
|
|
||||||
|
[pages.inbounds.toasts]
|
||||||
|
"obtain" = "Получить"
|
||||||
|
|
||||||
|
[pages.inbounds.stream.general]
|
||||||
|
"requestHeader" = "Требуется заголовок"
|
||||||
|
"name" = "Имя"
|
||||||
|
"value" = "Значение"
|
||||||
|
|
||||||
|
[pages.inbounds.stream.tcp]
|
||||||
|
"requestVersion" = "Требуется версия"
|
||||||
|
"requestMethod" = "Требуется метод"
|
||||||
|
"requestPath" = "Требуется путь"
|
||||||
|
"responseVersion" = "Указать версию"
|
||||||
|
"responseStatus" = "Указать статус"
|
||||||
|
"responseStatusDescription" = "Указать примечание статуса"
|
||||||
|
"responseHeader" = "Указать заголовок"
|
||||||
|
|
||||||
|
[pages.inbounds.stream.quic]
|
||||||
|
"encryption" = "Шифрование"
|
||||||
|
|
||||||
|
[pages.settings]
|
||||||
|
"title" = "Настройки"
|
||||||
|
"save" = "Сохранить"
|
||||||
|
"infoDesc" = "Каждое изменение здесь необходимо сохранить и перезапустить панель, чтобы оно вступило в силу"
|
||||||
|
"restartPanel" = "Рестарт панели"
|
||||||
|
"restartPanelDesc" = "Подтвердите рестарт панели? ОК для рестарта панели через 3 сек. Если вы не можете пользоваться панелью после рестарта, пожалуйста, посмотрите лог панели на сервере"
|
||||||
|
"resetDefaultConfig" = "Сбросить всё по-умолчанию"
|
||||||
|
"panelConfig" = "Настройки панели"
|
||||||
|
"userSettings" = "Настройки безопасности"
|
||||||
|
"xrayConfiguration" = "Конфигурация Xray"
|
||||||
|
"TGBotSettings" = "Настройки Телеграм-бота"
|
||||||
|
"panelListeningIP" = "IP-порт панели"
|
||||||
|
"panelListeningIPDesc" = "Оставьте пустым для работы с любого IP. Перезагрузите панель для применения настроек"
|
||||||
|
"panelPort" = "Порт панели"
|
||||||
|
"panelPortDesc" = "Перезагрузите панель для применения настроек"
|
||||||
|
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
|
||||||
|
"publicKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек"
|
||||||
|
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
|
||||||
|
"privateKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек"
|
||||||
|
"panelUrlPath" = "Корневой путь URL-адреса панели"
|
||||||
|
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на «/». Перезагрузите панель для применения настроек"
|
||||||
|
"oldUsername" = "Имя пользователя сейчас"
|
||||||
|
"currentPassword" = "Пароль сейчас"
|
||||||
|
"newUsername" = "Новое имя пользователя"
|
||||||
|
"newPassword" = "Новый пароль"
|
||||||
|
"telegramBotEnable" = "Включить Телеграм-бота"
|
||||||
|
"telegramBotEnableDesc" = "Перезагрузите панель для применения настроек"
|
||||||
|
"telegramToken" = "Токен Телеграм-бота"
|
||||||
|
"telegramTokenDesc" = "Перезагрузите панель для применения настроек"
|
||||||
|
"telegramChatId" = "Телеграм-ID админа бота"
|
||||||
|
"telegramChatIdDesc" = "Если несколько Телеграм-ID, разделить запятой. Используйте @userinfobot, чтобы получить Телеграм-ID. Перезагрузите панель для применения настроек"
|
||||||
|
"telegramNotifyTime" = "Частота уведомлений телеграм-бота"
|
||||||
|
"telegramNotifyTimeDesc" = "Используйте формат Crontab. Перезагрузите панель для применения настроек"
|
||||||
|
"tgNotifyBackup" = "Резервное копирование базы данных"
|
||||||
|
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете. Перезагрузите панель для применения настроек"
|
||||||
|
"sessionMaxAge" = "Продолжительность сессии"
|
||||||
|
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
|
||||||
|
"expireTimeDiff" = "Порог истечения срока сессии для уведомления"
|
||||||
|
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)"
|
||||||
|
"trafficDiff" = "Порог траффика для уведомления"
|
||||||
|
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)"
|
||||||
|
"tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления"
|
||||||
|
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (значение:%)"
|
||||||
|
"timeZone" = "Временная зона"
|
||||||
|
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе. Перезагрузите панель для применения настроек"
|
||||||
|
|
||||||
|
[pages.settings.templates]
|
||||||
|
"title" = "Шаблоны"
|
||||||
|
"basicTemplate" = "Базовые шаблоны"
|
||||||
|
"advancedTemplate" = "Расширенные шаблоны"
|
||||||
|
"completeTemplate" = "Конфигурация шаблона"
|
||||||
|
"generalConfigs" = "Основные настройки"
|
||||||
|
"generalConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
|
||||||
|
"blockConfigs" = "Блокировка конфигураций"
|
||||||
|
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам."
|
||||||
|
"blockCountryConfigs" = "Заблокировать конфигурации страны"
|
||||||
|
"blockCountryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны."
|
||||||
|
"directCountryConfigs" = "Прямые настройки страны"
|
||||||
|
"directCountryConfigsDesc" = "Эти параметры будут подключать пользователей напрямую к доменам определенной страны."
|
||||||
|
"ipv4Configs" = "Настройки IPv4 "
|
||||||
|
"ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4"
|
||||||
|
"xrayConfigTemplate" = "Шаблон конфигурации Xray"
|
||||||
|
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigFreedomStrategy" = "Настроить стратегию протокола Freedom"
|
||||||
|
"xrayConfigFreedomStrategyDesc" = "Установить стратегию вывода сети в протоколе Freedom"
|
||||||
|
"xrayConfigRoutingStrategy" = "Настроить доменную стратегию маршрутизации"
|
||||||
|
"xrayConfigRoutingStrategyDesc" = "Установить общую стратегию маршрутизации разрешения DNS"
|
||||||
|
"xrayConfigTorrent" = "Запретить использование BitTorrent"
|
||||||
|
"xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения"
|
||||||
|
"xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigAds" = "Бокировка рекламы"
|
||||||
|
"xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigFamily" = "Включить семейную конфигурацию"
|
||||||
|
"xrayConfigFamilyDesc" = "Избегайте подключения к небезопасным веб-сайтам для всей семьи"
|
||||||
|
"xrayConfigIRIp" = "Отключить подключение к диапазонам IP-адресов Ирана"
|
||||||
|
"xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigIRDomain" = "Отключить подключение к доменам Ирана"
|
||||||
|
"xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая"
|
||||||
|
"xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigChinaDomain" = "Отключить подключение к доменам Китая"
|
||||||
|
"xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigRussiaIp" = "Отключить подключение к диапазонам IP-адресов России"
|
||||||
|
"xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigRussiaDomain" = "Отключить подключение к доменам России"
|
||||||
|
"xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigDirectIRIp" = "Прямое подключение к диапазонам IP-адресов Ирана"
|
||||||
|
"xrayConfigDirectIRIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Ирана"
|
||||||
|
"xrayConfigDirectIRDomain" = "Прямое подключение к доменам Ирана"
|
||||||
|
"xrayConfigDirectIRDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Ирана"
|
||||||
|
"xrayConfigDirectChinaIp" = "Прямое подключение к диапазонам IP-адресов Китая"
|
||||||
|
"xrayConfigDirectChinaIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Китая"
|
||||||
|
"xrayConfigDirectChinaDomain" = "Прямое подключение к доменам Китая"
|
||||||
|
"xrayConfigDirectChinaDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Китая"
|
||||||
|
"xrayConfigDirectRussiaIp" = "Прямое подключение к диапазонам IP-адресов России"
|
||||||
|
"xrayConfigDirectRussiaIpDesc" = "Изменить шаблон конфигурации для прямого подключения к диапазонам IP-адресов России"
|
||||||
|
"xrayConfigDirectRussiaDomain" = "Прямое подключение к доменам России"
|
||||||
|
"xrayConfigDirectRussiaDomainDesc" = "Изменить шаблон конфигурации для прямого подключения к доменам России"
|
||||||
|
"xrayConfigGoogleIPv4" = "Использовать IPv4 для Google"
|
||||||
|
"xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigNetflixIPv4" = "Использовать IPv4 для Netflix"
|
||||||
|
"xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigInbounds" = "Конфигурация подключений"
|
||||||
|
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigOutbounds" = "Конфигурация исходящих"
|
||||||
|
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера. Перезагрузите панель для применения настроек"
|
||||||
|
"xrayConfigRoutings" = "Настройка правил маршрутизации"
|
||||||
|
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера. Перезагрузите панель для применения настроек"
|
||||||
|
"manualLists" = "ручные списки"
|
||||||
|
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
|
||||||
|
"manualBlockedIPs" = "Список заблокированных IP-адресов"
|
||||||
|
"manualBlockedDomains" = "Список заблокированных доменов"
|
||||||
|
"manualDirectIPs" = "Список прямых IP-адресов"
|
||||||
|
"manualDirectDomains" = "Список прямых доменов"
|
||||||
|
|
||||||
|
[pages.settings.toasts]
|
||||||
|
"modifySettings" = "Изменение настроек"
|
||||||
|
"getSettings" = "Просмотр настроек"
|
||||||
|
"modifyUser" = "Изменение пользователя "
|
||||||
|
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
|
||||||
|
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"enable" = "启用"
|
"enable" = "启用"
|
||||||
"protocol" = "协议"
|
"protocol" = "协议"
|
||||||
"search" = "搜尋"
|
"search" = "搜尋"
|
||||||
|
"filter" = "过滤器"
|
||||||
|
|
||||||
"loading" = "加载中"
|
"loading" = "加载中"
|
||||||
"second" = "秒"
|
"second" = "秒"
|
||||||
@@ -33,10 +34,13 @@
|
|||||||
"host" = "主持人"
|
"host" = "主持人"
|
||||||
"path" = "小路"
|
"path" = "小路"
|
||||||
"camouflage" = "伪装"
|
"camouflage" = "伪装"
|
||||||
|
"status" = "状态"
|
||||||
"enabled" = "开启"
|
"enabled" = "开启"
|
||||||
"disabled" = "关闭"
|
"disabled" = "关闭"
|
||||||
|
"depleted" = "耗尽"
|
||||||
|
"depletingSoon" = "即将耗尽"
|
||||||
"domainName" = "域名"
|
"domainName" = "域名"
|
||||||
"additional" = "额外"
|
"additional" = "额外 ID"
|
||||||
"monitor" = "监听"
|
"monitor" = "监听"
|
||||||
"certificate" = "证书"
|
"certificate" = "证书"
|
||||||
"fail" = "失败"
|
"fail" = "失败"
|
||||||
@@ -49,7 +53,7 @@
|
|||||||
[menu]
|
[menu]
|
||||||
"dashboard" = "系统状态"
|
"dashboard" = "系统状态"
|
||||||
"inbounds" = "入站列表"
|
"inbounds" = "入站列表"
|
||||||
"setting" = "面板设置"
|
"settings" = "面板设置"
|
||||||
"logout" = "退出登录"
|
"logout" = "退出登录"
|
||||||
"link" = "其他"
|
"link" = "其他"
|
||||||
|
|
||||||
@@ -68,7 +72,9 @@
|
|||||||
"title" = "系统状态"
|
"title" = "系统状态"
|
||||||
"memory" = "内存"
|
"memory" = "内存"
|
||||||
"hard" = "硬盘"
|
"hard" = "硬盘"
|
||||||
"xrayStatus" = "xray 状态"
|
"xrayStatus" = "Xray 状态"
|
||||||
|
"stopXray" = "停止"
|
||||||
|
"restartXray" = "重启"
|
||||||
"xraySwitch" = "切换版本"
|
"xraySwitch" = "切换版本"
|
||||||
"xraySwitchClick" = "点击你想切换的版本"
|
"xraySwitchClick" = "点击你想切换的版本"
|
||||||
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
|
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
|
||||||
@@ -81,9 +87,16 @@
|
|||||||
"downSpeed" = "所有网卡的总下载速度"
|
"downSpeed" = "所有网卡的总下载速度"
|
||||||
"totalSent" = "系统启动以来所有网卡的总上传流量"
|
"totalSent" = "系统启动以来所有网卡的总上传流量"
|
||||||
"totalReceive" = "系统启动以来所有网卡的总下载流量"
|
"totalReceive" = "系统启动以来所有网卡的总下载流量"
|
||||||
"xraySwitchVersionDialog" = "切换 xray 版本"
|
"xraySwitchVersionDialog" = "切换 Xray 版本"
|
||||||
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
|
"xraySwitchVersionDialogDesc" = "是否切换 Xray 版本至"
|
||||||
"dontRefreshh" = "安装中,请不要刷新此页面"
|
"dontRefresh" = "安装中,请不要刷新此页面"
|
||||||
|
"logs" = "日志"
|
||||||
|
"config" = "配置"
|
||||||
|
"backup" = "备份"
|
||||||
|
"backupTitle" = "备份数据库"
|
||||||
|
"backupDescription" = "请记住在导入新数据库之前进行备份"
|
||||||
|
"exportDatabase" = "下载数据库"
|
||||||
|
"importDatabase" = "上传数据库"
|
||||||
|
|
||||||
[pages.inbounds]
|
[pages.inbounds]
|
||||||
"title" = "入站列表"
|
"title" = "入站列表"
|
||||||
@@ -101,8 +114,9 @@
|
|||||||
"expireDate" = "到期时间"
|
"expireDate" = "到期时间"
|
||||||
"resetTraffic" = "重置流量"
|
"resetTraffic" = "重置流量"
|
||||||
"addInbound" = "添加入"
|
"addInbound" = "添加入"
|
||||||
"addTo" = "添加"
|
"generalActions" = "通用操作"
|
||||||
"revise" = "修改"
|
"create" = "添加"
|
||||||
|
"update" = "修改"
|
||||||
"modifyInbound" = "修改入站"
|
"modifyInbound" = "修改入站"
|
||||||
"deleteInbound" = "删除入站"
|
"deleteInbound" = "删除入站"
|
||||||
"deleteInboundContent" = "确定要删除入站吗?"
|
"deleteInboundContent" = "确定要删除入站吗?"
|
||||||
@@ -127,6 +141,27 @@
|
|||||||
"clickOnQRcode" = "点击二维码复制"
|
"clickOnQRcode" = "点击二维码复制"
|
||||||
"client" = "客户"
|
"client" = "客户"
|
||||||
"export" = "导出链接"
|
"export" = "导出链接"
|
||||||
|
"clone" = "克隆"
|
||||||
|
"cloneInbound" = "创造"
|
||||||
|
"cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆"
|
||||||
|
"cloneInboundOk" = "创造"
|
||||||
|
"resetAllTraffic" = "重置所有入站流量"
|
||||||
|
"resetAllTrafficTitle" = "重置所有入站流量"
|
||||||
|
"resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
|
||||||
|
"resetInboundClientTraffics" = "重置客户端流量"
|
||||||
|
"resetInboundClientTrafficTitle" = "重置所有客户端流量"
|
||||||
|
"resetInboundClientTrafficContent" = "您确定要重置此入站客户端的所有流量吗?"
|
||||||
|
"resetAllClientTraffics" = "重置所有客户端流量"
|
||||||
|
"resetAllClientTrafficTitle" = "重置所有客户端流量"
|
||||||
|
"resetAllClientTrafficContent" = "你确定要重置所有客户端的所有流量吗?"
|
||||||
|
"delDepletedClients" = "删除耗尽的客户端"
|
||||||
|
"delDepletedClientsTitle" = "删除耗尽的客户"
|
||||||
|
"delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?"
|
||||||
|
"email" = "电子邮件"
|
||||||
|
"emailDesc" = "电子邮件必须完全唯"
|
||||||
|
"setDefaultCert" = "从面板设置证书"
|
||||||
|
"telegramDesc" = "使用不带@的电报 ID 或聊天 ID(您可以在此处获取 @userinfobot)"
|
||||||
|
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
"add" = "添加客户端"
|
"add" = "添加客户端"
|
||||||
@@ -135,6 +170,14 @@
|
|||||||
"submitEdit" = "保存修改"
|
"submitEdit" = "保存修改"
|
||||||
"clientCount" = "客户数量"
|
"clientCount" = "客户数量"
|
||||||
"bulk" = "批量创建"
|
"bulk" = "批量创建"
|
||||||
|
"method" = "方法"
|
||||||
|
"first" = "第一"
|
||||||
|
"last" = "最后"
|
||||||
|
"prefix" = "前缀"
|
||||||
|
"postfix" = "后缀"
|
||||||
|
"delayedStart" = "首次使用后开始"
|
||||||
|
"expireDays" = "过期天数"
|
||||||
|
"days" = "天"
|
||||||
|
|
||||||
[pages.inbounds.toasts]
|
[pages.inbounds.toasts]
|
||||||
"obtain" = "获取"
|
"obtain" = "获取"
|
||||||
@@ -156,58 +199,125 @@
|
|||||||
[pages.inbounds.stream.quic]
|
[pages.inbounds.stream.quic]
|
||||||
"encryption" = "加密"
|
"encryption" = "加密"
|
||||||
|
|
||||||
[pages.setting]
|
[pages.settings]
|
||||||
"title" = "设置"
|
"title" = "设置"
|
||||||
"save" = "保存配置"
|
"save" = "保存配置"
|
||||||
|
"infoDesc" = "此处的所有更改都需要保存并重启面板才能生效"
|
||||||
"restartPanel" = "重启面板"
|
"restartPanel" = "重启面板"
|
||||||
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
|
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
|
||||||
|
"resetDefaultConfig" = "重置为默认配置"
|
||||||
"panelConfig" = "面板配置"
|
"panelConfig" = "面板配置"
|
||||||
"userSetting" = "用户设置"
|
"userSettings" = "用户设置"
|
||||||
"xrayConfiguration" = "xray 相关设置"
|
"xrayConfiguration" = "Xray 相关设置"
|
||||||
"TGReminder" = "TG提醒相关设置"
|
"TGBotSettings" = "TG提醒相关设置"
|
||||||
"otherSetting" = "其他设置"
|
|
||||||
"panelListeningIP" = "面板监听 IP"
|
"panelListeningIP" = "面板监听 IP"
|
||||||
"panelListeningIPDesc" = "默认留空监听所有 IP,重启面板生效"
|
"panelListeningIPDesc" = "默认留空监听所有 IP"
|
||||||
"panelPort" = "面板监听端口"
|
"panelPort" = "面板监听端口"
|
||||||
"panelPortDesc" = "重启面板生效"
|
"panelPortDesc" = "重启面板生效"
|
||||||
"publicKeyPath" = "面板证书公钥文件路径"
|
"publicKeyPath" = "面板证书公钥文件路径"
|
||||||
"publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效"
|
"publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
|
||||||
"privateKeyPath" = "面板证书密钥文件路径"
|
"privateKeyPath" = "面板证书密钥文件路径"
|
||||||
"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效"
|
"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
|
||||||
"panelUrlPath" = "面板 url 根路径"
|
"panelUrlPath" = "面板 url 根路径"
|
||||||
"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾,重启面板生效"
|
"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾"
|
||||||
"oldUsername" = "原用户名"
|
"oldUsername" = "原用户名"
|
||||||
"currentPassword" = "原密码"
|
"currentPassword" = "原密码"
|
||||||
"newUsername" = "新用户名"
|
"newUsername" = "新用户名"
|
||||||
"newPassword" = "新密码"
|
"newPassword" = "新密码"
|
||||||
"advancedTemplate" = "高级模板部件"
|
|
||||||
"completeTemplate" = "Xray 配置的完整模板"
|
|
||||||
"xrayConfigTemplate" = "xray 配置模板"
|
|
||||||
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件,重新启动面板生成效率"
|
|
||||||
"xrayConfigTorrent" = "禁止使用 bittorrent"
|
|
||||||
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent,重启面板生效"
|
|
||||||
"xrayConfigPrivateIp" = "禁止私人 ip 范围连接"
|
|
||||||
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围,重启面板生效"
|
|
||||||
"xrayConfigInbounds" = "入站配置"
|
|
||||||
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端,重启面板生效"
|
|
||||||
"xrayConfigOutbounds" = "出站配置"
|
|
||||||
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效"
|
|
||||||
"xrayConfigRoutings" = "路由规则配置"
|
|
||||||
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效"
|
|
||||||
"telegramBotEnable" = "启用电报机器人"
|
"telegramBotEnable" = "启用电报机器人"
|
||||||
"telegramBotEnableDesc" = "重启面板生效"
|
"telegramBotEnableDesc" = "重启面板生效"
|
||||||
"telegramToken" = "电报机器人TOKEN"
|
"telegramToken" = "电报机器人TOKEN"
|
||||||
"telegramTokenDesc" = "重启面板生效"
|
"telegramTokenDesc" = "重启面板生效"
|
||||||
"telegramChatId" = "电报机器人ChatId"
|
"telegramChatId" = "以逗号分隔的多个 chatID"
|
||||||
"telegramChatIdDesc" = "重启面板生效"
|
"telegramChatIdDesc" = "重启面板生效"
|
||||||
"telegramNotifyTime" = "电报机器人通知时间"
|
"telegramNotifyTime" = "电报机器人通知时间"
|
||||||
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
|
"telegramNotifyTimeDesc" = "采用Crontab定时格式"
|
||||||
"timeZonee" = "时区"
|
"tgNotifyBackup" = "数据库备份"
|
||||||
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
|
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知"
|
||||||
|
"sessionMaxAge" = "会话最大年龄"
|
||||||
|
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
|
||||||
|
"expireTimeDiff" = "耗尽时间阈值"
|
||||||
|
"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
|
||||||
|
"trafficDiff" = "耗尽流量阈值"
|
||||||
|
"trafficDiffDesc" = "完成流量前检测耗尽(单位:GB)"
|
||||||
|
"tgNotifyCpu" = "CPU 百分比警报阈值"
|
||||||
|
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
|
||||||
|
"timeZone" = "时区"
|
||||||
|
"timeZoneDesc" = "定时任务按照该时区的时间运行"
|
||||||
|
|
||||||
[pages.setting.toasts]
|
[pages.settings.templates]
|
||||||
"modifySetting" = "修改设置"
|
"title" = "模板"
|
||||||
"getSetting" = "获取设置"
|
"basicTemplate" = "基本模板"
|
||||||
|
"advancedTemplate" = "高级模板部件"
|
||||||
|
"completeTemplate" = "Xray 配置的完整模板"
|
||||||
|
"generalConfigs" = "通用配置"
|
||||||
|
"generalConfigsDesc" = "这些选项将提供一般调整"
|
||||||
|
"blockConfigs" = "阻塞配置"
|
||||||
|
"blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站"
|
||||||
|
"blockCountryConfigs" = "阻止国家配置"
|
||||||
|
"blockCountryConfigsDesc" = "这些选项将阻止用户连接到特定国家/地区的域。"
|
||||||
|
"directCountryConfigs" = "直接国家配置"
|
||||||
|
"directCountryConfigsDesc" = "这些选项会将用户直接连接到特定国家/地区的域。"
|
||||||
|
"ipv4Configs" = "IPv4 配置"
|
||||||
|
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域"
|
||||||
|
"xrayConfigTemplate" = "Xray 配置模板"
|
||||||
|
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的Xray配置文件,重新启动面板生成效率"
|
||||||
|
"xrayConfigFreedomStrategy" = "配置自由协议的策略"
|
||||||
|
"xrayConfigFreedomStrategyDesc" = "在自由协议中设置网络输出策略"
|
||||||
|
"xrayConfigRoutingStrategy" = "配置路由域策略"
|
||||||
|
"xrayConfigRoutingStrategyDesc" = "设置DNS解析的整体路由策略"
|
||||||
|
"xrayConfigTorrent" = "禁止使用 bittorrent"
|
||||||
|
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent"
|
||||||
|
"xrayConfigPrivateIp" = "禁止私人 IP 范围连接"
|
||||||
|
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围"
|
||||||
|
"xrayConfigAds" = "屏蔽广告"
|
||||||
|
"xrayConfigAdsDesc" = "修改配置模板屏蔽广告"
|
||||||
|
"xrayConfigFamily" = "启用家庭友好配置"
|
||||||
|
"xrayConfigFamilyDesc" = "避免为家人连接到不安全的网站"
|
||||||
|
"xrayConfigIRIp" = "禁止伊朗 IP 范围连接"
|
||||||
|
"xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP段"
|
||||||
|
"xrayConfigIRDomain" = "禁止伊朗域连接"
|
||||||
|
"xrayConfigIRDomainDesc" = "更改配置模板避免连接伊朗域名"
|
||||||
|
"xrayConfigChinaIp" = "禁止中国 IP 范围连接"
|
||||||
|
"xrayConfigChinaIpDesc" = "修改配置模板避免连接中国IP段"
|
||||||
|
"xrayConfigChinaDomain" = "禁止中国域名连接"
|
||||||
|
"xrayConfigChinaDomainDesc" = "更改配置模板避免连接中国域"
|
||||||
|
"xrayConfigRussiaIp" = "禁止俄罗斯 IP 范围连接"
|
||||||
|
"xrayConfigRussiaIpDesc" = "修改配置模板避免连接俄罗斯IP范围"
|
||||||
|
"xrayConfigRussiaDomain" = "禁止俄罗斯域连接"
|
||||||
|
"xrayConfigRussiaDomainDesc" = "更改配置模板避免连接俄罗斯域"
|
||||||
|
"xrayConfigDirectIRIp" = "直接连接到伊朗 IP 范围"
|
||||||
|
"xrayConfigDirectIRIpDesc" = "更改直接连接到伊朗 IP 范围的配置模板"
|
||||||
|
"xrayConfigDirectIRDomain" = "直接连接到伊朗域"
|
||||||
|
"xrayConfigDirectIRDomainDesc" = "更改直接连接到伊朗域的配置模板"
|
||||||
|
"xrayConfigDirectChinaIp" = "直连中国IP范围"
|
||||||
|
"xrayConfigDirectChinaIpDesc" = "更改直连中国 IP 范围的配置模板"
|
||||||
|
"xrayConfigDirectChinaDomain" = "直连中国域名"
|
||||||
|
"xrayConfigDirectChinaDomainDesc" = "修改中国域名直连配置模板"
|
||||||
|
"xrayConfigDirectRussiaIp" = "直接连接到俄罗斯 IP 范围"
|
||||||
|
"xrayConfigDirectRussiaIpDesc" = "更改直接连接到俄罗斯 IP 范围的配置模板"
|
||||||
|
"xrayConfigDirectRussiaDomain" = "直接连接到俄罗斯域"
|
||||||
|
"xrayConfigDirectRussiaDomainDesc" = "更改直接连接到俄罗斯域的配置模板"
|
||||||
|
"xrayConfigGoogleIPv4" = "为谷歌使用 IPv4"
|
||||||
|
"xrayConfigGoogleIPv4Desc" = "添加谷歌连接IPv4的路由"
|
||||||
|
"xrayConfigNetflixIPv4" = "为 Netflix 使用 IPv4"
|
||||||
|
"xrayConfigNetflixIPv4Desc" = "添加Netflix连接IPv4的路由"
|
||||||
|
"xrayConfigInbounds" = "入站配置"
|
||||||
|
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端"
|
||||||
|
"xrayConfigOutbounds" = "出站配置"
|
||||||
|
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式"
|
||||||
|
"xrayConfigRoutings" = "路由规则配置"
|
||||||
|
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则"
|
||||||
|
"manualLists" = "手动列表"
|
||||||
|
"manualListsDesc" = "请使用 JSON 数组格式"
|
||||||
|
"manualBlockedIPs" = "被阻止的 IP 列表"
|
||||||
|
"manualBlockedDomains" = "被阻止的域列表"
|
||||||
|
"manualDirectIPs" = "直接 IP 列表"
|
||||||
|
"manualDirectDomains" = "直接域列表"
|
||||||
|
|
||||||
|
[pages.settings.toasts]
|
||||||
|
"modifySettings" = "修改设置"
|
||||||
|
"getSettings" = "获取设置"
|
||||||
"modifyUser" = "修改用户"
|
"modifyUser" = "修改用户"
|
||||||
"originalUserPassIncorrect" = "原用户名或原密码错误"
|
"originalUserPassIncorrect" = "原用户名或原密码错误"
|
||||||
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空"
|
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空"
|
||||||
28
web/web.go
28
web/web.go
@@ -85,10 +85,11 @@ type Server struct {
|
|||||||
server *controller.ServerController
|
server *controller.ServerController
|
||||||
xui *controller.XUIController
|
xui *controller.XUIController
|
||||||
api *controller.APIController
|
api *controller.APIController
|
||||||
|
sub *controller.SUBController
|
||||||
|
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
inboundService service.InboundService
|
tgbotService service.Tgbot
|
||||||
|
|
||||||
cron *cron.Cron
|
cron *cron.Cron
|
||||||
|
|
||||||
@@ -208,6 +209,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
s.server = controller.NewServerController(g)
|
s.server = controller.NewServerController(g)
|
||||||
s.xui = controller.NewXUIController(g)
|
s.xui = controller.NewXUIController(g)
|
||||||
s.api = controller.NewAPIController(g)
|
s.api = controller.NewAPIController(g)
|
||||||
|
s.sub = controller.NewSUBController(g)
|
||||||
|
|
||||||
return engine, nil
|
return engine, nil
|
||||||
}
|
}
|
||||||
@@ -237,11 +239,11 @@ func (s *Server) initI18n(engine *gin.Engine) error {
|
|||||||
names := make([]string, 0)
|
names := make([]string, 0)
|
||||||
keyLen := len(key)
|
keyLen := len(key)
|
||||||
for i := 0; i < keyLen-1; i++ {
|
for i := 0; i < keyLen-1; i++ {
|
||||||
if key[i:i+2] == "{{" { // 判断开头 "{{"
|
if key[i:i+2] == "{{" {
|
||||||
j := i + 2
|
j := i + 2
|
||||||
isFind := false
|
isFind := false
|
||||||
for ; j < keyLen-1; j++ {
|
for ; j < keyLen-1; j++ {
|
||||||
if key[j:j+2] == "}}" { // 结尾 "}}"
|
if key[j:j+2] == "}}" {
|
||||||
isFind = true
|
isFind = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -274,8 +276,6 @@ func (s *Server) initI18n(engine *gin.Engine) error {
|
|||||||
engine.FuncMap["i18n"] = I18n
|
engine.FuncMap["i18n"] = I18n
|
||||||
|
|
||||||
engine.Use(func(c *gin.Context) {
|
engine.Use(func(c *gin.Context) {
|
||||||
//accept := c.GetHeader("Accept-Language")
|
|
||||||
|
|
||||||
var lang string
|
var lang string
|
||||||
|
|
||||||
if cookie, err := c.Request.Cookie("lang"); err == nil {
|
if cookie, err := c.Request.Cookie("lang"); err == nil {
|
||||||
@@ -325,8 +325,13 @@ func (s *Server) startTask() {
|
|||||||
logger.Warning("Add NewStatsNotifyJob error", err)
|
logger.Warning("Add NewStatsNotifyJob error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// listen for TG bot income messages
|
|
||||||
go job.NewStatsNotifyJob().OnReceive()
|
// Check CPU load and alarm to TgBot if threshold passes
|
||||||
|
cpuThreshold, err := s.settingService.GetTgCpu()
|
||||||
|
if (err == nil) && (cpuThreshold > 0) {
|
||||||
|
s.cron.AddJob("@every 10s", job.NewCheckCpuJob())
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
s.cron.Remove(entry)
|
s.cron.Remove(entry)
|
||||||
}
|
}
|
||||||
@@ -403,6 +408,12 @@ func (s *Server) Start() (err error) {
|
|||||||
s.httpServer.Serve(listener)
|
s.httpServer.Serve(listener)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
isTgbotenabled, err := s.settingService.GetTgbotenabled()
|
||||||
|
if (err == nil) && (isTgbotenabled) {
|
||||||
|
tgBot := s.tgbotService.NewTgbot()
|
||||||
|
tgBot.Start()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,6 +423,9 @@ func (s *Server) Stop() error {
|
|||||||
if s.cron != nil {
|
if s.cron != nil {
|
||||||
s.cron.Stop()
|
s.cron.Stop()
|
||||||
}
|
}
|
||||||
|
if s.tgbotService.IsRunnging() {
|
||||||
|
s.tgbotService.Stop()
|
||||||
|
}
|
||||||
var err1 error
|
var err1 error
|
||||||
var err2 error
|
var err2 error
|
||||||
if s.httpServer != nil {
|
if s.httpServer != nil {
|
||||||
|
|||||||
65
x-ui.sh
65
x-ui.sh
@@ -20,46 +20,41 @@ function LOGI() {
|
|||||||
# check root
|
# check root
|
||||||
[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1
|
[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1
|
||||||
|
|
||||||
# check os
|
# Check OS and set release variable
|
||||||
if [[ -f /etc/redhat-release ]]; then
|
if [[ -f /etc/os-release ]]; then
|
||||||
release="centos"
|
source /etc/os-release
|
||||||
elif cat /etc/issue | grep -Eqi "debian"; then
|
release=$ID
|
||||||
release="debian"
|
elif [[ -f /usr/lib/os-release ]]; then
|
||||||
elif cat /etc/issue | grep -Eqi "ubuntu"; then
|
source /usr/lib/os-release
|
||||||
release="ubuntu"
|
release=$ID
|
||||||
elif cat /etc/issue | grep -Eqi "centos|red hat|redhat"; then
|
|
||||||
release="centos"
|
|
||||||
elif cat /proc/version | grep -Eqi "debian"; then
|
|
||||||
release="debian"
|
|
||||||
elif cat /proc/version | grep -Eqi "ubuntu"; then
|
|
||||||
release="ubuntu"
|
|
||||||
elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then
|
|
||||||
release="centos"
|
|
||||||
else
|
else
|
||||||
LOGE "check system OS failed,please contact with author! \n" && exit 1
|
echo "Failed to check the system OS, please contact the author!" >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "The OS release is: $release"
|
||||||
|
|
||||||
|
|
||||||
os_version=""
|
os_version=""
|
||||||
|
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
|
||||||
|
|
||||||
# os version
|
if [[ "${release}" == "centos" ]]; then
|
||||||
if [[ -f /etc/os-release ]]; then
|
|
||||||
os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release)
|
|
||||||
fi
|
|
||||||
if [[ -z "$os_version" && -f /etc/lsb-release ]]; then
|
|
||||||
os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ x"${release}" == x"centos" ]]; then
|
|
||||||
if [[ ${os_version} -le 6 ]]; then
|
|
||||||
LOGE "please use CentOS 7 or higher version! \n" && exit 1
|
|
||||||
fi
|
|
||||||
elif [[ x"${release}" == x"ubuntu" ]]; then
|
|
||||||
if [[ ${os_version} -lt 16 ]]; then
|
|
||||||
LOGE "please use Ubuntu 16 or higher version!\n" && exit 1
|
|
||||||
fi
|
|
||||||
elif [[ x"${release}" == x"debian" ]]; then
|
|
||||||
if [[ ${os_version} -lt 8 ]]; then
|
if [[ ${os_version} -lt 8 ]]; then
|
||||||
LOGE "please use Debian 8 or higher version!\n" && exit 1
|
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
|
||||||
|
fi
|
||||||
|
elif [[ "${release}" == "ubuntu" ]]; then
|
||||||
|
if [[ ${os_version} -lt 20 ]]; then
|
||||||
|
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "${release}" == "fedora" ]]; then
|
||||||
|
if [[ ${os_version} -lt 36 ]]; then
|
||||||
|
echo -e "${red}please use Fedora 36 or higher version! ${plain}\n" && exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "${release}" == "debian" ]]; then
|
||||||
|
if [[ ${os_version} -lt 10 ]]; then
|
||||||
|
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -525,7 +520,7 @@ show_menu() {
|
|||||||
${green}14.${plain} Cancel x-ui Autostart
|
${green}14.${plain} Cancel x-ui Autostart
|
||||||
————————————————
|
————————————————
|
||||||
${green}15.${plain} 一A key installation bbr (latest kernel)
|
${green}15.${plain} 一A key installation bbr (latest kernel)
|
||||||
${green}16.${plain} 一Apply for an SSL certificate with one click(acme script)
|
${green}16.${plain} 一Apply for a SSL certificate with one click(acme script)
|
||||||
"
|
"
|
||||||
show_status
|
show_status
|
||||||
echo && read -p "Please enter your selection [0-16]: " num
|
echo && read -p "Please enter your selection [0-16]: " num
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type InboundConfig struct {
|
type InboundConfig struct {
|
||||||
Listen json_util.RawMessage `json:"listen"` // listen 不能为空字符串
|
Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `json:"protocol"`
|
||||||
Settings json_util.RawMessage `json:"settings"`
|
Settings json_util.RawMessage `json:"settings"`
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"x-ui/config"
|
||||||
"x-ui/util/common"
|
"x-ui/util/common"
|
||||||
|
|
||||||
"github.com/Workiva/go-datastructures/queue"
|
"github.com/Workiva/go-datastructures/queue"
|
||||||
@@ -29,19 +30,19 @@ func GetBinaryName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetBinaryPath() string {
|
func GetBinaryPath() string {
|
||||||
return "bin/" + GetBinaryName()
|
return config.GetBinFolderPath() + "/" + GetBinaryName()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetConfigPath() string {
|
func GetConfigPath() string {
|
||||||
return "bin/config.json"
|
return config.GetBinFolderPath() + "/config.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetGeositePath() string {
|
func GetGeositePath() string {
|
||||||
return "bin/geosite.dat"
|
return config.GetBinFolderPath() + "/geosite.dat"
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetGeoipPath() string {
|
func GetGeoipPath() string {
|
||||||
return "bin/geoip.dat"
|
return config.GetBinFolderPath() + "/geoip.dat"
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopProcess(p *Process) {
|
func stopProcess(p *Process) {
|
||||||
|
|||||||
Reference in New Issue
Block a user