Compare commits
434 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9adc2054a0 | ||
|
|
5efedd3a5e | ||
|
|
b4e2bba934 | ||
|
|
40e4145263 | ||
|
|
2b8c913be9 | ||
|
|
3d1fd65c3a | ||
|
|
5b306e57c9 | ||
|
|
102f8bab51 | ||
|
|
e4a011b6d9 | ||
|
|
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 | ||
|
|
2f611684a2 | ||
|
|
0471bbbdb1 | ||
|
|
0ced0fcdc3 | ||
|
|
33f354c71c | ||
|
|
f87dad254b | ||
|
|
340b54d45f | ||
|
|
1288427cce | ||
|
|
52d3956203 | ||
|
|
e13540901f | ||
|
|
4f91375cb2 | ||
|
|
9e398aaa65 | ||
|
|
ae45f9fef2 | ||
|
|
698af18d25 | ||
|
|
afd369993a | ||
|
|
40bb9f9864 | ||
|
|
1f669aa61d | ||
|
|
a776bdbb87 | ||
|
|
9a0055d5d5 | ||
|
|
19c13625a1 | ||
|
|
e1428ff0dc | ||
|
|
4843c93608 | ||
|
|
757c2a76f8 | ||
|
|
95c777f0ac | ||
|
|
fc4f1a4fd3 | ||
|
|
c7be137f6d | ||
|
|
8a07474999 | ||
|
|
7ec68574d0 | ||
|
|
414669fa1b | ||
|
|
76f774fd6c | ||
|
|
b82ce31ec1 | ||
|
|
9bbd612327 | ||
|
|
a5922a8620 | ||
|
|
35e4b96e9e | ||
|
|
ceb7a7ac14 | ||
|
|
92dc390e58 | ||
|
|
a6edd1c631 | ||
|
|
146b4d7873 | ||
|
|
f3dac6395a | ||
|
|
b48707923c | ||
|
|
40ac7d4115 | ||
|
|
3e550d7e13 | ||
|
|
2e12cd6f6e | ||
|
|
57f538bf6b | ||
|
|
7d23e8cf9c | ||
|
|
4120024e04 | ||
|
|
fedb0610e0 | ||
|
|
9ed41cf427 | ||
|
|
8a66a42a4e | ||
|
|
ac98fce996 | ||
|
|
2aacd1111c | ||
|
|
bcf3fab524 | ||
|
|
76919a1cc5 | ||
|
|
cef6e13441 | ||
|
|
0c8b58841f | ||
|
|
c8043bf9e2 | ||
|
|
e9b7ba8b11 | ||
|
|
5bee682d39 | ||
|
|
ef0dd10fe7 | ||
|
|
9ea9d4d4dd | ||
|
|
d2e313a588 | ||
|
|
35de896e5e | ||
|
|
1c00da7537 | ||
|
|
1700e85d06 | ||
|
|
931e949442 | ||
|
|
8bca416142 | ||
|
|
5407e2330b | ||
|
|
cb2c2b1f5c | ||
|
|
f131f2512b | ||
|
|
adf2b96f38 | ||
|
|
e47ea983bd | ||
|
|
b62f747e88 | ||
|
|
784e6e24e2 | ||
|
|
4af4aadd14 | ||
|
|
9b3415865a | ||
|
|
8b6ea9e0d5 | ||
|
|
2a6f678b53 | ||
|
|
7225f00c2b | ||
|
|
85db586d95 | ||
|
|
2d7663f971 | ||
|
|
c935014ba9 | ||
|
|
2c61b5a426 | ||
|
|
a5cfe4fc1e | ||
|
|
3cc3b3b04d | ||
|
|
8e550bc308 | ||
|
|
3498dcd50d | ||
|
|
abb8a7084e | ||
|
|
e2be6ce851 | ||
|
|
f30235373a | ||
|
|
9d75925f30 | ||
|
|
fbc8fa7345 | ||
|
|
d971a61afc | ||
|
|
971e8f2351 | ||
|
|
a9d29a4dfc | ||
|
|
6639d69a6d | ||
|
|
8114e3fc17 |
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.
|
||||
57
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,57 +0,0 @@
|
||||
name: Issue Report
|
||||
description: "Create a report to help us improve."
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Welcome
|
||||
options:
|
||||
- label: Yes, I'm using the latest major release. Only such installations are supported.
|
||||
required: true
|
||||
- label: Yes, I'm using the supported system. Only such systems are supported.
|
||||
required: true
|
||||
- label: Yes, I have read all WIKI document,nothing can help me in my problem.
|
||||
required: true
|
||||
- label: Yes, I've searched similar issues on GitHub and didn't find any.
|
||||
required: true
|
||||
- label: Yes, I've included all information below (version, config, log, etc).
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Description of the problem,screencshot would be good
|
||||
placeholder: Your problem description
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: version
|
||||
attributes:
|
||||
label: Version of x-ui
|
||||
value: |-
|
||||
<details>
|
||||
|
||||
```console
|
||||
$ x-ui version
|
||||
# Paste output here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: x-ui log or xray log
|
||||
value: |-
|
||||
<details>
|
||||
|
||||
```console
|
||||
# paste log here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
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
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Question template
|
||||
about: Ask if it is not clear that it is a bug
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
70
.github/workflows/docker.yml
vendored
@@ -1,49 +1,55 @@
|
||||
name: Docker Image CI
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
project:
|
||||
description: 'Project'
|
||||
required: true
|
||||
default:
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
alireza7/x-ui
|
||||
ghcr.io/alireza0/x-ui
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=pep440,pattern={{version}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
|
||||
- name: Docker Hub login
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
run: |
|
||||
echo "${DOCKERHUB_TOKEN}" | docker login --username ${DOCKERHUB_USERNAME} --password-stdin
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
buildx-version: latest
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build Dockerfile
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
DOCKERHUB_REPO: ${{ secrets.DOCKER_HUB_REPOSITORY }}
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform=linux/amd64,linux/arm64 \
|
||||
--output "type=image,push=true" \
|
||||
--file ./Dockerfile ./ \
|
||||
--tag $(echo "${DOCKERHUB_USERNAME}" | tr '[:upper:]' '[:lower:]')/${{ github.event.inputs.project }}:latest
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
91
.github/workflows/release.yml
vendored
@@ -4,32 +4,17 @@ on:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-18.04
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: true
|
||||
prerelease: true
|
||||
linuxamd64build:
|
||||
name: build x-ui amd64 version
|
||||
needs: release
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 'stable'
|
||||
- name: build linux amd64 version
|
||||
run: |
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
|
||||
@@ -41,35 +26,34 @@ jobs:
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/hossinasaadi/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
|
||||
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/geosite.dat
|
||||
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
|
||||
mv xray xray-linux-amd64
|
||||
cd ..
|
||||
cd ..
|
||||
- name: package
|
||||
run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui
|
||||
- name: upload
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
|
||||
uses: svenstaro/upload-release-action@2.5.0
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: x-ui-linux-amd64.tar.gz
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-linux-amd64.tar.gz
|
||||
asset_name: x-ui-linux-amd64.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
prerelease: true
|
||||
linuxarm64build:
|
||||
name: build x-ui arm64 version
|
||||
needs: release
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: '1.20'
|
||||
- name: build linux arm64 version
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -83,35 +67,34 @@ jobs:
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/hossinasaadi/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
|
||||
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/geosite.dat
|
||||
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
|
||||
mv xray xray-linux-arm64
|
||||
cd ..
|
||||
cd ..
|
||||
- name: package
|
||||
run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui
|
||||
- name: upload
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
|
||||
uses: svenstaro/upload-release-action@2.5.0
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: x-ui-linux-arm64.tar.gz
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-linux-arm64.tar.gz
|
||||
asset_name: x-ui-linux-arm64.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
prerelease: true
|
||||
linuxs390xbuild:
|
||||
name: build x-ui s390x version
|
||||
needs: release
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: '1.20'
|
||||
- name: build linux s390x version
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -125,22 +108,22 @@ jobs:
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/hossinasaadi/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
|
||||
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/geosite.dat
|
||||
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
|
||||
mv xray xray-linux-s390x
|
||||
cd ..
|
||||
cd ..
|
||||
- name: package
|
||||
run: tar -zcvf x-ui-linux-s390x.tar.gz x-ui
|
||||
- name: upload
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
|
||||
uses: svenstaro/upload-release-action@2.5.0
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: x-ui-linux-s390x.tar.gz
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-linux-s390x.tar.gz
|
||||
asset_name: x-ui-linux-s390x.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
prerelease: true
|
||||
|
||||
6
.gitignore
vendored
@@ -1,7 +1,8 @@
|
||||
.idea
|
||||
.vscode
|
||||
tmp
|
||||
bin/xray-darwin-arm64
|
||||
bin/config.json
|
||||
backup/
|
||||
bin/
|
||||
dist/
|
||||
x-ui-*.tar.gz
|
||||
/x-ui
|
||||
@@ -10,4 +11,5 @@ x-ui-*.tar.gz
|
||||
main
|
||||
release/
|
||||
access.log
|
||||
error.log
|
||||
.cache
|
||||
|
||||
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 ../../
|
||||
27
Dockerfile
@@ -1,17 +1,18 @@
|
||||
FROM golang:latest AS builder
|
||||
WORKDIR /root
|
||||
FROM golang:1.20-alpine AS builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
RUN apk --no-cache --update add build-base gcc wget unzip
|
||||
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
|
||||
LABEL org.opencontainers.image.authors="alireza7@gmail.com"
|
||||
ENV TZ=Asia/Tehran
|
||||
WORKDIR /app
|
||||
|
||||
FROM debian:11-slim
|
||||
LABEL org.opencontainers.image.authors="hossin.asaadi77@gmail.com"
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends -y ca-certificates \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
ENV TZ=Asia/Shanghai
|
||||
WORKDIR /root
|
||||
COPY --from=builder /root/main /root/x-ui
|
||||
COPY ./bin/. /root/bin/.
|
||||
RUN apk add ca-certificates tzdata
|
||||
|
||||
COPY --from=builder /app/build/ /app/
|
||||
VOLUME [ "/etc/x-ui" ]
|
||||
CMD [ "./x-ui" ]
|
||||
CMD [ "./x-ui" ]
|
||||
313
README.md
@@ -1,96 +1,47 @@
|
||||
# x-ui
|
||||
|
||||

|
||||

|
||||
[](https://goreportcard.com/report/github.com/alireza0/x-ui)
|
||||
[](https://img.shields.io/github/downloads/alireza0/x-ui/total.svg)
|
||||
[](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**
|
||||
|
||||
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
|
||||
|
||||
xray panel supporting multi-protocol, **Multi-lang (English,Chinese)**, **IP Restrication Per Inbound**
|
||||
| Features | Enable? |
|
||||
| ------------------------------------ | :----------------: |
|
||||
| Multi-lang | :heavy_check_mark: |
|
||||
| Dark/Light Theme | :heavy_check_mark: |
|
||||
| Search in deep | :heavy_check_mark: |
|
||||
| Inbound Multi User | :heavy_check_mark: |
|
||||
| Multi User Traffic & Expiration time | :heavy_check_mark: |
|
||||
| REST API | :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: |
|
||||
|
||||
| Features | Enable? |
|
||||
| ------------- |:-------------:|
|
||||
| Multi-lang | :heavy_check_mark: |
|
||||
| [IP Restriction](https://github.com/HexaSoftwareTech/x-ui/#enable-ip-restrictions-per-inbound) | :heavy_check_mark: |
|
||||
| [Inbound Multi User](https://github.com/HexaSoftwareTech/x-ui/#enable-multi-user-traffic--exprire-day) | :heavy_check_mark: |
|
||||
| [Multi User Traffic & expire day](https://github.com/HexaSoftwareTech/x-ui/#enable-multi-user-traffic--exprire-day) | :heavy_check_mark: |
|
||||
| [REST API](https://github.com/HexaSoftwareTech/x-ui/pull/51) | :heavy_check_mark: |
|
||||
| [Telegram BOT](https://github.com/HexaSoftwareTech/x-ui/pull/110) | :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
|
||||
|
||||
- System Status Monitoring
|
||||
- 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
|
||||
|
||||
# Enable IP Restrictions Per Inbound
|
||||
`!!! NO NEED TO DO THIS IF YOU HAVE FRESH INSTALL`
|
||||
|
||||
1 - open panel settings and tab xray related settings find `"api": ` and put bellow code just before it :
|
||||
```json
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"access": "./access.log"
|
||||
},
|
||||
```
|
||||
- change access log path as you want
|
||||
|
||||
2 - add **IP limit and Unique Email** for inbound(vmess & vless)
|
||||
|
||||
# Enable Multi User Traffic & Exprire Day
|
||||

|
||||
|
||||
`!!! NO NEED TO DO THIS IF YOU HAVE FRESH INSTALL`
|
||||
|
||||
**for enable traffic for users you should do :**
|
||||
|
||||
find this in config :
|
||||
``` json
|
||||
"policy": {
|
||||
"system": {
|
||||
```
|
||||
**and add this just after ` "policy": {` :**
|
||||
```json
|
||||
"levels": {
|
||||
"0": {
|
||||
"statsUserUplink": true,
|
||||
"statsUserDownlink": true
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
**the final output is like :**
|
||||
```json
|
||||
"policy": {
|
||||
"levels": {
|
||||
"0": {
|
||||
"statsUserUplink": true,
|
||||
"statsUserDownlink": true
|
||||
}
|
||||
},
|
||||
|
||||
"system": {
|
||||
"statsInboundDownlink": true,
|
||||
"statsInboundUplink": true
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
```
|
||||
restart panel
|
||||
# Install & Upgrade
|
||||
# Install & Upgrade to latest version
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/HexaSoftwareTech/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
|
||||
|
||||
1. First download the latest compressed package from https://github.com/HexaSoftwareTech/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`
|
||||
2. Then upload the compressed package to the server's `/root/` directory and `root` rootlog in to the server with user
|
||||
|
||||
> If your server cpu architecture is not `amd64` replace another architecture
|
||||
@@ -110,8 +61,6 @@ systemctl restart x-ui
|
||||
|
||||
## Install using docker
|
||||
|
||||
> This docker tutorial and docker image are provided by [HexaSoftwareTech](https://github.com/HexaSoftwareTech)
|
||||
|
||||
1. install docker
|
||||
|
||||
```shell
|
||||
@@ -122,11 +71,13 @@ curl -fsSL https://get.docker.com | sh
|
||||
|
||||
```shell
|
||||
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/cert/:/root/cert/ \
|
||||
--name x-ui --restart=unless-stopped \
|
||||
HexaSoftwareTech/x-ui:latest
|
||||
alireza7/x-ui:latest
|
||||
```
|
||||
|
||||
> Build your own image
|
||||
@@ -135,66 +86,132 @@ docker run -itd --network=host \
|
||||
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
|
||||
- Support multi-domain configuration and multi-certificate inbounds
|
||||
- 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
|
||||
|
||||
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
|
||||
<details>
|
||||
<summary>Click for details</summary>
|
||||
|
||||
The script has a built-in SSL certificate application function. To use this script to apply for a certificate, the following conditions must be met:
|
||||
### Certbot
|
||||
|
||||
- Know the Cloudflare registered email
|
||||
- Know the Cloudflare Global API Key
|
||||
- The domain name has been resolved to the current server through cloudflare
|
||||
```bash
|
||||
snap install core; snap refresh core
|
||||
snap install --classic certbot
|
||||
ln -s /snap/bin/certbot /usr/bin/certbot
|
||||
|
||||
How to get the Cloudflare Global API Key:
|
||||

|
||||

|
||||
certbot certonly --standalone --register-unsafely-without-email --non-interactive --agree-tos -d <Your Domain Name>
|
||||
```
|
||||
|
||||
When using, just enter `email`, `domain`, `API KEY` and the schematic diagram is as follows:
|
||||

|
||||
</details>
|
||||
|
||||
Precautions:
|
||||
## Tg robot use
|
||||
|
||||
- The script uses DNS API for certificate request
|
||||
- By default, Let'sEncrypt is used as the CA party
|
||||
- The certificate installation directory is the /root/cert directory
|
||||
- The certificates applied for by this script are all generic domain name certificates
|
||||
|
||||
## Tg robot use (under development, temporarily unavailable)
|
||||
|
||||
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
|
||||
<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)
|
||||
Set the robot-related parameters in the panel background, including:
|
||||
|
||||
- Tg Robot Token
|
||||
- Tg Robot ChatId
|
||||
- Tg robot Token
|
||||
- Tg robot ChatId
|
||||
- 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:
|
||||
|
||||
- 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
|
||||
- @daily // Daily notification (00:00 in the morning)
|
||||
- @every 8h // notify every 8 hours
|
||||
- TG notification content:
|
||||
|
||||
- Node traffic usage
|
||||
- Panel login reminder
|
||||
- Node expiration reminder
|
||||
- Traffic warning reminder
|
||||
### Telegram Bot Features
|
||||
|
||||
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
|
||||
|
||||
- CentOS 7+
|
||||
- Ubuntu 16+
|
||||
- Debian 8+
|
||||
|
||||
# common problem
|
||||
|
||||
<details>
|
||||
<summary>Click for details</summary>
|
||||
## 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`
|
||||
@@ -205,6 +222,56 @@ First install the latest version of x-ui on the server where v2-ui is installed,
|
||||
x-ui v2-ui
|
||||
```
|
||||
|
||||
# T-Shoots:
|
||||
|
||||
**If you upgrade from an old version or other forks, for enable traffic for users you should do :**
|
||||
|
||||
find this in config :
|
||||
|
||||
```json
|
||||
"policy": {
|
||||
"system": {
|
||||
```
|
||||
|
||||
**and add this just after ` "policy": {` :**
|
||||
|
||||
```json
|
||||
"levels": {
|
||||
"0": {
|
||||
"statsUserUplink": true,
|
||||
"statsUserDownlink": true
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
**the final output is like :**
|
||||
|
||||
```json
|
||||
"policy": {
|
||||
"levels": {
|
||||
"0": {
|
||||
"statsUserUplink": true,
|
||||
"statsUserDownlink": true
|
||||
}
|
||||
},
|
||||
|
||||
"system": {
|
||||
"statsInboundDownlink": true,
|
||||
"statsInboundUplink": true
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
```
|
||||
|
||||
restart panel
|
||||
|
||||
</details>
|
||||
|
||||
# a special thanks to
|
||||
|
||||
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
|
||||
- [MHSanaei](https://github.com/MHSanaei)
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://starchart.cc/HexaSoftwareTech/x-ui)
|
||||
[](https://starchart.cc/alireza0/x-ui)
|
||||
|
||||
BIN
bin/geoip.dat
16117
bin/geosite.dat
@@ -45,6 +45,22 @@ func IsDebug() bool {
|
||||
return os.Getenv("XUI_DEBUG") == "true"
|
||||
}
|
||||
|
||||
func GetDBPath() string {
|
||||
return fmt.Sprintf("/etc/%s/%s.db", GetName(), GetName())
|
||||
func GetBinFolderPath() string {
|
||||
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.5.3
|
||||
1.4.0
|
||||
@@ -1,15 +1,18 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"bytes"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"x-ui/config"
|
||||
"x-ui/xray"
|
||||
"x-ui/database/model"
|
||||
"x-ui/xray"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
@@ -41,9 +44,7 @@ func initInbound() error {
|
||||
func initSetting() error {
|
||||
return db.AutoMigrate(&model.Setting{})
|
||||
}
|
||||
func initInboundClientIps() error {
|
||||
return db.AutoMigrate(&model.InboundClientIps{})
|
||||
}
|
||||
|
||||
func initClientTraffic() error {
|
||||
return db.AutoMigrate(&xray.ClientTraffic{})
|
||||
}
|
||||
@@ -83,15 +84,12 @@ func InitDB(dbPath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = initInboundClientIps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = initClientTraffic()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -102,3 +100,13 @@ func GetDB() *gorm.DB {
|
||||
func IsNotFound(err error) bool {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -24,15 +24,15 @@ type User struct {
|
||||
}
|
||||
|
||||
type Inbound struct {
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
UserId int `json:"-"`
|
||||
Up int64 `json:"up" form:"up"`
|
||||
Down int64 `json:"down" form:"down"`
|
||||
Total int64 `json:"total" form:"total"`
|
||||
Remark string `json:"remark" form:"remark"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
|
||||
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
UserId int `json:"-"`
|
||||
Up int64 `json:"up" form:"up"`
|
||||
Down int64 `json:"down" form:"down"`
|
||||
Total int64 `json:"total" form:"total"`
|
||||
Remark string `json:"remark" form:"remark"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
|
||||
|
||||
// config part
|
||||
Listen string `json:"listen" form:"listen"`
|
||||
@@ -43,11 +43,6 @@ type Inbound struct {
|
||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||
Sniffing string `json:"sniffing" form:"sniffing"`
|
||||
}
|
||||
type InboundClientIps struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
||||
Ips string `json:"ips" form:"ips"`
|
||||
}
|
||||
|
||||
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||
listen := i.Listen
|
||||
@@ -70,12 +65,16 @@ type Setting struct {
|
||||
Key string `json:"key" form:"key"`
|
||||
Value string `json:"value" form:"value"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ID string `json:"id"`
|
||||
AlterIds uint16 `json:"alterId"`
|
||||
Email string `json:"email"`
|
||||
LimitIP int `json:"limitIp"`
|
||||
Security string `json:"security"`
|
||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
||||
ID string `json:"id"`
|
||||
Password string `json:"password"`
|
||||
Flow string `json:"flow"`
|
||||
AlterIds uint16 `json:"alterId"`
|
||||
Email string `json:"email"`
|
||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
}
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
TgID string `json:"tgId" form:"tgId"`
|
||||
SubID string `json:"subId" form:"subId"`
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
version: '3.9'
|
||||
---
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
xui:
|
||||
image: hossinasaadi/x-ui
|
||||
image: alireza7/x-ui
|
||||
container_name: x-ui
|
||||
hostname: yourhostname
|
||||
volumes:
|
||||
- $PWD/db/:/etc/x-ui/
|
||||
- $PWD/cert/:/root/cert/
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
XRAY_VMESS_AEAD_FORCED: "false"
|
||||
tty: true
|
||||
network_mode: host
|
||||
restart: unless-stopped
|
||||
|
||||
73
go.mod
@@ -1,27 +1,62 @@
|
||||
module x-ui
|
||||
|
||||
go 1.16
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
|
||||
github.com/Workiva/go-datastructures v1.0.53
|
||||
github.com/gin-contrib/sessions v0.0.3
|
||||
github.com/gin-gonic/gin v1.7.1
|
||||
github.com/go-cmd/cmd v1.4.1 // indirect
|
||||
github.com/go-ole/go-ole v1.2.5 // indirect
|
||||
github.com/Workiva/go-datastructures v1.1.0
|
||||
github.com/gin-contrib/sessions v0.0.4
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/nicksnyder/go-i18n/v2 v2.1.2
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.1
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.0.7
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil v3.21.3+incompatible
|
||||
github.com/tklauser/go-sysconf v0.3.5 // indirect
|
||||
github.com/xtls/xray-core v1.4.2
|
||||
go.uber.org/atomic v1.7.0
|
||||
golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744 // indirect
|
||||
golang.org/x/text v0.3.6
|
||||
google.golang.org/grpc v1.38.0
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gorm.io/driver/sqlite v1.1.4
|
||||
gorm.io/gorm v1.21.9
|
||||
github.com/shirou/gopsutil/v3 v3.23.4
|
||||
github.com/xtls/xray-core v1.8.1
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/text v0.9.0
|
||||
google.golang.org/grpc v1.55.0
|
||||
gorm.io/driver/sqlite v1.5.1
|
||||
gorm.io/gorm v1.25.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1 // indirect
|
||||
github.com/bytedance/sonic v1.8.7 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.12.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||
github.com/leodido/go-urn v1.2.3 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // 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/reflect2 v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // 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/numcpus v0.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.8.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
519
go.sum
@@ -1,437 +1,254 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 h1:5sXbqlSomvdjlRbWyNqkPsJ3Fg+tQZCbgeX1VGljbQY=
|
||||
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
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/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/Workiva/go-datastructures v1.1.0 h1:hu20UpgZneBhQ3ZvwiOGlqJSKIosin2Rd5wAKUHEO/k=
|
||||
github.com/Workiva/go-datastructures v1.1.0/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
||||
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/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/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/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
|
||||
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-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 h1:BS21ZUJ/B5X2UVUbczfmdWH7GapPWAhxcMsDnjJTU1E=
|
||||
github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew=
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI=
|
||||
github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I=
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
|
||||
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/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/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
|
||||
github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8=
|
||||
github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
|
||||
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-cmd/cmd v1.4.1 h1:JUcEIE84v8DSy02XTZpUDeGKExk2oW3DA10hTjbQwmc=
|
||||
github.com/go-cmd/cmd v1.4.1/go.mod h1:tbBenttXtZU4c5djS1o7PWL5pd2xAr5sIqH1kGdNiRc=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
|
||||
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
|
||||
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/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
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/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
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.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
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/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
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-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
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/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/h12w/go-socks5 v0.0.0-20200522160539-76189e178364/go.mod h1:eDJQioIyy4Yn3MVivT7rv/39gAJTrA7lgmYr8EW950c=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
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/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
||||
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
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/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
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.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/lucas-clemente/quic-go v0.20.0 h1:FSU3YN5VnLafHR27Ejs1r1CYMS7XMyIVDzRewkDLNBw=
|
||||
github.com/lucas-clemente/quic-go v0.20.0/go.mod h1:fZq/HUDIM+mW6X6wtzORjC0E/WDBMKe5Hf9bgjISwLk=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
|
||||
github.com/marten-seemann/qtls-go1-15 v0.1.4 h1:RehYMOyRW8hPVEja1KBVsFVNSm35Jj9Mvs5yNoZZ28A=
|
||||
github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.3 h1:XEZ1xGorVy9u+lJq+WXNE+hiqRYLNvJGYmwfwKQN2gU=
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.3/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
|
||||
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/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-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
|
||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.1.2 h1:QHYxcUJnGHBaq7XbvgunmZ2Pn0focXFqTD61CkH146c=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
|
||||
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
|
||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/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/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/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
|
||||
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/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pires/go-proxyproto v0.5.0 h1:A4Jv4ZCaV3AFJeGh5mGwkz4iuWUYMlQ7IoO/GTuSuLo=
|
||||
github.com/pires/go-proxyproto v0.5.0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||
github.com/refraction-networking/utls v0.0.0-20201210053706-2179f286686b h1:lzo71oHzQEz0fKMSjR0BpVzuh2hOHvJTxnN3Rnikmtg=
|
||||
github.com/refraction-networking/utls v0.0.0-20201210053706-2179f286686b/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0=
|
||||
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/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
|
||||
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
|
||||
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
|
||||
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
|
||||
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/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c h1:pqy40B3MQWYrza7YZXOXgl0Nf0QGFqrOC0BKae1UNAA=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shirou/gopsutil v3.21.3+incompatible h1:uenXGGa8ESCQq+dbgtl916dmg6PSAz2cXov0uORQ9v8=
|
||||
github.com/shirou/gopsutil v3.21.3+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw=
|
||||
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/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
|
||||
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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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/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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
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.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/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4=
|
||||
github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=
|
||||
github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA=
|
||||
github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM=
|
||||
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/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
|
||||
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
|
||||
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
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/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499 h1:QHESTXtfgc1ABV+ArlbPVqUx9Ht5I0dDkYhxYoXFxNo=
|
||||
github.com/xtls/go v0.0.0-20201118062508-3632bf3b7499/go.mod h1:5TB2+k58gx4A4g2Nf5miSHNDF6CuAzHKpWBooLAshTs=
|
||||
github.com/xtls/xray-core v1.4.2 h1:D0Le+Qy9L/eY5LbUQfrk7WJ8wbODpQSW/ZRCg+BRe7c=
|
||||
github.com/xtls/xray-core v1.4.2/go.mod h1:DmL/9rOCliev/a6HciWEvSJVEhUF6C0EpD3clW8v0pc=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
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/xtls/reality v0.0.0-20230331223127-176a94313eda h1:psRJD2RrZbnI0OWyHvXfgYCPqlRM5q5SPDcjDoDBWhE=
|
||||
github.com/xtls/xray-core v1.8.1 h1:iSTTqXj82ZdwC1ah+eV331X4JTcnrDz+WuKuB/EB3P4=
|
||||
github.com/xtls/xray-core v1.8.1/go.mod h1:AXxSso0MZwUE4NhRocCfHCg73BtJ+T2dSpQVo1Cg9VM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.starlark.net v0.0.0-20210312235212-74c10e2c17dc h1:pVkptfeOTFfx+zXZo7HEHN3d5LmhatBFvHdm/f2QnpY=
|
||||
go.starlark.net v0.0.0-20210312235212-74c10e2c17dc/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
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/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
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-20190313024323-a1f597ede03a/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-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
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-20210330230544-e57232859fb2 h1:nGCZOty+lVDsc4H2qPFksI5Se296+V+GhMiL/TzmYNk=
|
||||
golang.org/x/net v0.0.0-20210330230544-e57232859fb2/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
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-20190911185100-cd5d95a43a6e/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-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744 h1:yhBbb4IRs2HS9PPlAg6DMC6mUOKexJBNsLf4Z+6En1Q=
|
||||
golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
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-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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
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-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-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
|
||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
|
||||
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
|
||||
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 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
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 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/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.4/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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
|
||||
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
||||
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||
gorm.io/gorm v1.21.9 h1:INieZtn4P2Pw6xPJ8MzT0G4WUOsHq3RhfuDF1M6GW0E=
|
||||
gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
h12.io/socks v1.0.2/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.1 h1:hYyrLkAWE71bcarJDPdZNTLWtr8XrSjOWyjUYI6xdL4=
|
||||
gorm.io/driver/sqlite v1.5.1/go.mod h1:7MZZ2Z8bqyfSQA1gYEV6MagQWj3cpUkJj9Z+d1HEMEQ=
|
||||
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
|
||||
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
|
||||
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
140
install.sh
@@ -8,26 +8,20 @@ plain='\033[0m'
|
||||
cur_dir=$(pwd)
|
||||
|
||||
# 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
|
||||
if [[ -f /etc/redhat-release ]]; then
|
||||
release="centos"
|
||||
elif cat /etc/issue | grep -Eqi "debian"; then
|
||||
release="debian"
|
||||
elif cat /etc/issue | grep -Eqi "ubuntu"; then
|
||||
release="ubuntu"
|
||||
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"
|
||||
# Check OS and set release variable
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
source /etc/os-release
|
||||
release=$ID
|
||||
elif [[ -f /usr/lib/os-release ]]; then
|
||||
source /usr/lib/os-release
|
||||
release=$ID
|
||||
else
|
||||
echo -e "${red} check system OS failed,please contact with author! ${plain}\n" && exit 1
|
||||
echo "Failed to check the system OS, please contact the author!" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "The OS release is: $release"
|
||||
|
||||
arch=$(arch)
|
||||
|
||||
@@ -39,42 +33,44 @@ elif [[ $arch == "s390x" ]]; then
|
||||
arch="s390x"
|
||||
else
|
||||
arch="amd64"
|
||||
echo -e "${red} Fail to check system arch,will use default arch here: ${arch}${plain}"
|
||||
echo -e "${red} Failed to check system arch, will use default arch: ${arch}${plain}"
|
||||
fi
|
||||
|
||||
echo "arch: ${arch}"
|
||||
|
||||
if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then
|
||||
echo "x-ui dosen't support 32bit(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
|
||||
fi
|
||||
|
||||
os_version=""
|
||||
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
|
||||
|
||||
# os version
|
||||
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 [[ "${release}" == "centos" ]]; 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
|
||||
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
|
||||
|
||||
|
||||
install_base() {
|
||||
if [[ x"${release}" == x"centos" ]]; then
|
||||
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]] ; then
|
||||
yum install wget curl tar -y
|
||||
else
|
||||
apt install wget curl tar -y
|
||||
@@ -83,23 +79,37 @@ install_base() {
|
||||
|
||||
#This function will be called when user installed x-ui out of sercurity
|
||||
config_after_install() {
|
||||
echo -e "${yellow} Install/update finished need to modify panel settings out of security ${plain}"
|
||||
read -p "are you continue,if you type n will skip this at this time[y/n]": config_confirm
|
||||
if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then
|
||||
read -p "please set up your username:" config_account
|
||||
echo -e "${yellow}your username will be:${config_account}${plain}"
|
||||
read -p "please set up your password:" config_password
|
||||
echo -e "${yellow}your password will be:${config_password}${plain}"
|
||||
read -p "please set up the panel port:" config_port
|
||||
echo -e "${yellow}your panel port is:${config_port}${plain}"
|
||||
echo -e "${yellow}initializing,wait some time here...${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
|
||||
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
|
||||
read -p "Please set up your username:" config_account
|
||||
echo -e "${yellow}Your username will be:${config_account}${plain}"
|
||||
read -p "Please set up your password:" config_password
|
||||
echo -e "${yellow}Your password will be:${config_password}${plain}"
|
||||
read -p "Please set up the panel port:" config_port
|
||||
echo -e "${yellow}Your panel port is:${config_port}${plain}"
|
||||
echo -e "${yellow}Initializing, please wait...${plain}"
|
||||
/usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password}
|
||||
echo -e "${yellow}account name and password set down!${plain}"
|
||||
echo -e "${yellow}Account name and password set successfully!${plain}"
|
||||
/usr/local/x-ui/x-ui setting -port ${config_port}
|
||||
echo -e "${yellow}panel port set down!${plain}"
|
||||
echo -e "${yellow}Panel port set successfully!${plain}"
|
||||
else
|
||||
echo -e "${red}Canceled, all setting items are 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
|
||||
/usr/local/x-ui/x-ui migrate
|
||||
}
|
||||
|
||||
install_x-ui() {
|
||||
@@ -107,21 +117,21 @@ install_x-ui() {
|
||||
cd /usr/local/
|
||||
|
||||
if [ $# == 0 ]; then
|
||||
last_version=$(curl -Ls "https://api.github.com/repos/hossinasaadi/x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
last_version=$(curl -Ls "https://api.github.com/repos/alireza0/x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$last_version" ]]; then
|
||||
echo -e "${red}refresh x-ui version failed,it may due to Github API restriction,please try it later${plain}"
|
||||
echo -e "${red}Failed to fetch x-ui version, it maybe due to Github API restrictions, please try it later${plain}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "get x-ui latest version succeed: ${last_version}, begin to install..."
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/hossinasaadi/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz
|
||||
echo -e "Got x-ui latest version: ${last_version}, beginning the installation..."
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/alireza0/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}dowanload x-ui failed,please be sure that your server can access Github ${plain}"
|
||||
echo -e "${red}Dowanloading x-ui failed, please be sure that your server can access Github ${plain}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
last_version=$1
|
||||
url="https://github.com/hossinasaadi/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
|
||||
echo -e "begin to install x-ui v$1"
|
||||
url="https://github.com/alireza0/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
|
||||
echo -e "Begining to install x-ui v$1"
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url}
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}dowanload x-ui v$1 failed,please check the verison exists${plain}"
|
||||
@@ -138,20 +148,20 @@ install_x-ui() {
|
||||
cd x-ui
|
||||
chmod +x x-ui bin/xray-linux-${arch}
|
||||
cp -f x-ui.service /etc/systemd/system/
|
||||
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/hossinasaadi/x-ui/main/x-ui.sh
|
||||
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/alireza0/x-ui/main/x-ui.sh
|
||||
chmod +x /usr/local/x-ui/x-ui.sh
|
||||
chmod +x /usr/bin/x-ui
|
||||
config_after_install
|
||||
#echo -e "如果是全新安装,默认网页端口为 ${green}54321${plain},用户名和密码默认都是 ${green}admin${plain}"
|
||||
#echo -e "请自行确保此端口没有被其他程序占用,${yellow}并且确保 54321 端口已放行${plain}"
|
||||
# echo -e "若想将 54321 修改为其它端口,输入 x-ui 命令进行修改,同样也要确保你修改的端口也是放行的"
|
||||
#echo -e "If it is a new installation, the default web port is ${green}54321${plain}, The username and password are ${green}admin${plain} by default"
|
||||
#echo -e "Please make sure that this port is not occupied by other procedures,${yellow} And make sure that port 54321 has been released${plain}"
|
||||
# echo -e "If you want to modify the 54321 to other ports and enter the x-ui command to modify it, you must also ensure that the port you modify is also released"
|
||||
#echo -e ""
|
||||
#echo -e "如果是更新面板,则按你之前的方式访问面板"
|
||||
#echo -e "If it is updated panel, access the panel in your previous way"
|
||||
#echo -e ""
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl start x-ui
|
||||
echo -e "${green}x-ui v${last_version}${plain} install finished,it is working now..."
|
||||
echo -e "${green}x-ui v${last_version}${plain} installation finished, it is up and running now..."
|
||||
echo -e ""
|
||||
echo -e "x-ui control menu usages: "
|
||||
echo -e "----------------------------------------------"
|
||||
@@ -170,6 +180,6 @@ install_x-ui() {
|
||||
echo -e "----------------------------------------------"
|
||||
}
|
||||
|
||||
echo -e "${green}excuting...${plain}"
|
||||
echo -e "${green}Excuting...${plain}"
|
||||
install_base
|
||||
install_x-ui $1
|
||||
|
||||
69
main.go
@@ -11,6 +11,7 @@ import (
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/logger"
|
||||
"x-ui/sub"
|
||||
"x-ui/v2ui"
|
||||
"x-ui/web"
|
||||
"x-ui/web/global"
|
||||
@@ -50,9 +51,19 @@ func runWebServer() {
|
||||
return
|
||||
}
|
||||
|
||||
var subServer *sub.Server
|
||||
subServer = sub.NewServer()
|
||||
global.SetSubServer(subServer)
|
||||
|
||||
err = subServer.Start()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
//信号量捕获处理
|
||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
|
||||
// Trap shutdown signals
|
||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
|
||||
for {
|
||||
sig := <-sigCh
|
||||
|
||||
@@ -62,6 +73,11 @@ func runWebServer() {
|
||||
if err != nil {
|
||||
logger.Warning("stop server err:", err)
|
||||
}
|
||||
err = subServer.Stop()
|
||||
if err != nil {
|
||||
logger.Warning("stop server err:", err)
|
||||
}
|
||||
|
||||
server = web.NewServer()
|
||||
global.SetWebServer(server)
|
||||
err = server.Start()
|
||||
@@ -69,8 +85,18 @@ func runWebServer() {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
subServer = sub.NewServer()
|
||||
global.SetSubServer(subServer)
|
||||
|
||||
err = subServer.Start()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
server.Stop()
|
||||
subServer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -97,7 +123,7 @@ func showSetting(show bool) {
|
||||
settingService := service.SettingService{}
|
||||
port, err := settingService.GetPort()
|
||||
if err != nil {
|
||||
fmt.Println("get current port fialed,error info:", err)
|
||||
fmt.Println("get current port failed,error info:", err)
|
||||
}
|
||||
userService := service.UserService{}
|
||||
userModel, err := userService.GetFirstUser()
|
||||
@@ -109,7 +135,7 @@ func showSetting(show bool) {
|
||||
if (username == "") || (userpasswd == "") {
|
||||
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("userpasswd:", userpasswd)
|
||||
fmt.Println("port:", port)
|
||||
@@ -133,10 +159,9 @@ func updateTgbotEnableSts(status bool) {
|
||||
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())
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -165,7 +190,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string)
|
||||
}
|
||||
}
|
||||
|
||||
if tgBotChatid != 0 {
|
||||
if tgBotChatid != "" {
|
||||
err := settingService.SetTgBotChatId(tgBotChatid)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -204,6 +229,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() {
|
||||
if len(os.Args) < 2 {
|
||||
runWebServer()
|
||||
@@ -217,14 +254,14 @@ func main() {
|
||||
|
||||
v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError)
|
||||
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)
|
||||
var port int
|
||||
var username string
|
||||
var password string
|
||||
var tgbottoken string
|
||||
var tgbotchatid int
|
||||
var tgbotchatid string
|
||||
var enabletgbot bool
|
||||
var tgbotRuntime string
|
||||
var reset bool
|
||||
@@ -234,9 +271,9 @@ func main() {
|
||||
settingCmd.IntVar(&port, "port", 0, "set panel port")
|
||||
settingCmd.StringVar(&username, "username", "", "set login username")
|
||||
settingCmd.StringVar(&password, "password", "", "set login password")
|
||||
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
|
||||
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
|
||||
settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id")
|
||||
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegram bot token")
|
||||
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegram bot cron time")
|
||||
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegram bot chat id")
|
||||
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
|
||||
|
||||
oldUsage := flag.Usage
|
||||
@@ -246,6 +283,7 @@ func main() {
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println(" run run web panel")
|
||||
fmt.Println(" v2-ui migrate form v2-ui")
|
||||
fmt.Println(" migrate migrate form other/old x-ui")
|
||||
fmt.Println(" setting set settings")
|
||||
}
|
||||
|
||||
@@ -263,6 +301,8 @@ func main() {
|
||||
return
|
||||
}
|
||||
runWebServer()
|
||||
case "migrate":
|
||||
migrateDb()
|
||||
case "v2-ui":
|
||||
err := v2uiCmd.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
@@ -287,9 +327,12 @@ func main() {
|
||||
if show {
|
||||
showSetting(show)
|
||||
}
|
||||
if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") {
|
||||
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
|
||||
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
|
||||
}
|
||||
if enabletgbot {
|
||||
updateTgbotEnableSts(enabletgbot)
|
||||
}
|
||||
default:
|
||||
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
|
||||
fmt.Println()
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 23 KiB |
BIN
media/inbounds-dark.png
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
media/inbounds.png
Normal file
|
After Width: | Height: | Size: 316 KiB |
171
sub/sub.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/network"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
|
||||
sub *SUBController
|
||||
settingService service.SettingService
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Server{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
if config.IsDebug() {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
subPath, err := s.settingService.GetSubPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subDomain, err := s.settingService.GetSubDomain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if subDomain != "" {
|
||||
validateDomain := func(c *gin.Context) {
|
||||
host := strings.Split(c.Request.Host, ":")[0]
|
||||
|
||||
if host != subDomain {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
|
||||
engine.Use(validateDomain)
|
||||
}
|
||||
|
||||
g := engine.Group(subPath)
|
||||
|
||||
s.sub = NewSUBController(g)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() (err error) {
|
||||
//This is an anonymous function, no function name
|
||||
defer func() {
|
||||
if err != nil {
|
||||
s.Stop()
|
||||
}
|
||||
}()
|
||||
|
||||
subEnable, err := s.settingService.GetSubEnable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !subEnable {
|
||||
return nil
|
||||
}
|
||||
|
||||
engine, err := s.initRouter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certFile, err := s.settingService.GetSubCertFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyFile, err := s.settingService.GetSubKeyFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listen, err := s.settingService.GetSubListen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
port, err := s.settingService.GetSubPort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
|
||||
listener, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if certFile != "" || keyFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
return err
|
||||
}
|
||||
c := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
listener = network.NewAutoHttpsListener(listener)
|
||||
listener = tls.NewListener(listener, c)
|
||||
}
|
||||
|
||||
if certFile != "" || keyFile != "" {
|
||||
logger.Info("Sub server run https on", listener.Addr())
|
||||
} else {
|
||||
logger.Info("Sub server run http on", listener.Addr())
|
||||
}
|
||||
s.listener = listener
|
||||
|
||||
s.httpServer = &http.Server{
|
||||
Handler: engine,
|
||||
}
|
||||
|
||||
go func() {
|
||||
s.httpServer.Serve(listener)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Stop() error {
|
||||
s.cancel()
|
||||
|
||||
var err1 error
|
||||
var err2 error
|
||||
if s.httpServer != nil {
|
||||
err1 = s.httpServer.Shutdown(s.ctx)
|
||||
}
|
||||
if s.listener != nil {
|
||||
err2 = s.listener.Close()
|
||||
}
|
||||
return common.Combine(err1, err2)
|
||||
}
|
||||
|
||||
func (s *Server) GetCtx() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
45
sub/subController.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SUBController struct {
|
||||
subService SubService
|
||||
}
|
||||
|
||||
func NewSUBController(g *gin.RouterGroup) *SUBController {
|
||||
a := &SUBController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/")
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
694
sub/subService.go
Normal file
@@ -0,0 +1,694 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
type SubService struct {
|
||||
address string
|
||||
inboundService service.InboundService
|
||||
settingServics service.SettingService
|
||||
}
|
||||
|
||||
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))
|
||||
updateInterval, _ := s.settingServics.GetSubUpdates()
|
||||
headers = append(headers, fmt.Sprintf("%d", updateInterval))
|
||||
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)
|
||||
var domains []interface{}
|
||||
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)
|
||||
}
|
||||
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
|
||||
domains, _ = domainSettings.([]interface{})
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
if len(domains) > 0 {
|
||||
links := ""
|
||||
for index, d := range domains {
|
||||
domain := d.(map[string]interface{})
|
||||
obj["ps"] = remark + "-" + domain["remark"].(string)
|
||||
obj["add"] = domain["domain"].(string)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||
links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
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)
|
||||
var domains []interface{}
|
||||
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 domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
|
||||
domains, _ = domainSettings.([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if len(domains) > 0 {
|
||||
links := ""
|
||||
for index, d := range domains {
|
||||
domain := d.(map[string]interface{})
|
||||
url.Fragment = remark + "-" + domain["remark"].(string)
|
||||
url.Host = domain["domain"].(string)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
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)
|
||||
var domains []interface{}
|
||||
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 domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
|
||||
domains, _ = domainSettings.([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if len(domains) > 0 {
|
||||
links := ""
|
||||
for index, d := range domains {
|
||||
domain := d.(map[string]interface{})
|
||||
url.Fragment = remark + "-" + domain["remark"].(string)
|
||||
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
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 ""
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"x-ui/logger"
|
||||
)
|
||||
|
||||
var CtxDone = errors.New("context done")
|
||||
|
||||
func NewErrorf(format string, a ...interface{}) error {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
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
|
||||
|
||||
// MarshalJSON 自定义 json.RawMessage 默认行为
|
||||
// MarshalJSON: Customize json.RawMessage default behavior
|
||||
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||
if len(m) == 0 {
|
||||
return []byte("null"), nil
|
||||
@@ -14,7 +14,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
|
||||
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 {
|
||||
if m == nil {
|
||||
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
|
||||
|
||||
@@ -4,5 +4,5 @@ import (
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
//go:linkname HostProc github.com/shirou/gopsutil/internal/common.HostProc
|
||||
//go:linkname HostProc github.com/shirou/gopsutil/v3/internal/common.HostProc
|
||||
func HostProc(combineWith ...string) string
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package sys
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/net"
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
)
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package sys
|
||||
|
||||
24
util/sys/sys_windows.go
Normal file
@@ -0,0 +1,24 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package sys
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
)
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
stats, err := net.Connections("tcp")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(stats), nil
|
||||
}
|
||||
|
||||
func GetUDPCount() (int, error) {
|
||||
stats, err := net.Connections("udp")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(stats), nil
|
||||
}
|
||||
@@ -1,147 +1,445 @@
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-layout-sider-zero-width-trigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.ant-card-hoverable {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.ant-card+.ant-card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.drawer-handle {
|
||||
position: absolute;
|
||||
top: 72px;
|
||||
width: 41px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
z-index: 0;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
right: -40px;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.drawer-handle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in-enter, .fade-in-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
|
||||
-webkit-transition: opacity .2s linear;
|
||||
transition: opacity .2s linear
|
||||
}
|
||||
|
||||
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
|
||||
-webkit-transition: opacity .2s linear;
|
||||
transition: opacity .2s linear
|
||||
}
|
||||
|
||||
.fade-in-enter-active, .fade-in-leave-active {
|
||||
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
|
||||
transition: all .3s cubic-bezier(.55, 0, .1, 1)
|
||||
}
|
||||
|
||||
.zoom-in-center-enter-active, .zoom-in-center-leave-active {
|
||||
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
|
||||
transition: all .3s cubic-bezier(.55, 0, .1, 1)
|
||||
}
|
||||
|
||||
.zoom-in-center-enter, .zoom-in-center-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scaleX(0);
|
||||
transform: scaleX(0)
|
||||
}
|
||||
|
||||
.zoom-in-top-enter-active, .zoom-in-top-leave-active {
|
||||
opacity: 1;
|
||||
-webkit-transform: scaleY(1);
|
||||
transform: scaleY(1);
|
||||
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
-webkit-transform-origin: center top;
|
||||
transform-origin: center top
|
||||
}
|
||||
|
||||
.zoom-in-top-enter, .zoom-in-top-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scaleY(0);
|
||||
transform: scaleY(0)
|
||||
}
|
||||
|
||||
.zoom-in-bottom-enter-active, .zoom-in-bottom-leave-active {
|
||||
opacity: 1;
|
||||
-webkit-transform: scaleY(1);
|
||||
transform: scaleY(1);
|
||||
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
-webkit-transform-origin: center bottom;
|
||||
transform-origin: center bottom
|
||||
}
|
||||
|
||||
.zoom-in-bottom-enter, .zoom-in-bottom-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scaleY(0);
|
||||
transform: scaleY(0)
|
||||
}
|
||||
|
||||
.zoom-in-left-enter-active, .zoom-in-left-leave-active {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1, 1);
|
||||
transform: scale(1, 1);
|
||||
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
-webkit-transform-origin: top left;
|
||||
transform-origin: top left
|
||||
}
|
||||
|
||||
.zoom-in-left-enter, .zoom-in-left-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(.45, .45);
|
||||
transform: scale(.45, .45)
|
||||
}
|
||||
|
||||
.list-enter-active, .list-leave-active {
|
||||
-webkit-transition: all .3s;
|
||||
transition: all .3s
|
||||
}
|
||||
|
||||
.list-enter, .list-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(-30px);
|
||||
transform: translateY(-30px)
|
||||
}
|
||||
|
||||
.ant-progress-inner {
|
||||
background-color: #EBEEF5;
|
||||
html,
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-layout-sider-zero-width-trigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ant-layout-sider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.ant-card-hoverable {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.ant-card+.ant-card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.drawer-handle {
|
||||
position: absolute;
|
||||
top: 72px;
|
||||
width: 41px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
z-index: 0;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
right: -40px;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.drawer-handle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in-enter, .fade-in-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
|
||||
-webkit-transition: opacity .2s linear;
|
||||
transition: opacity .2s linear
|
||||
}
|
||||
|
||||
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
|
||||
-webkit-transition: opacity .2s linear;
|
||||
transition: opacity .2s linear
|
||||
}
|
||||
|
||||
.fade-in-enter-active, .fade-in-leave-active {
|
||||
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
|
||||
transition: all .3s cubic-bezier(.55, 0, .1, 1)
|
||||
}
|
||||
|
||||
.zoom-in-center-enter-active, .zoom-in-center-leave-active {
|
||||
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
|
||||
transition: all .3s cubic-bezier(.55, 0, .1, 1)
|
||||
}
|
||||
|
||||
.zoom-in-center-enter, .zoom-in-center-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scaleX(0);
|
||||
transform: scaleX(0)
|
||||
}
|
||||
|
||||
.zoom-in-top-enter-active, .zoom-in-top-leave-active {
|
||||
opacity: 1;
|
||||
-webkit-transform: scaleY(1);
|
||||
transform: scaleY(1);
|
||||
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
-webkit-transform-origin: center top;
|
||||
transform-origin: center top
|
||||
}
|
||||
|
||||
.zoom-in-top-enter, .zoom-in-top-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scaleY(0);
|
||||
transform: scaleY(0)
|
||||
}
|
||||
|
||||
.zoom-in-bottom-enter-active, .zoom-in-bottom-leave-active {
|
||||
opacity: 1;
|
||||
-webkit-transform: scaleY(1);
|
||||
transform: scaleY(1);
|
||||
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
-webkit-transform-origin: center bottom;
|
||||
transform-origin: center bottom
|
||||
}
|
||||
|
||||
.zoom-in-bottom-enter, .zoom-in-bottom-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scaleY(0);
|
||||
transform: scaleY(0)
|
||||
}
|
||||
|
||||
.zoom-in-left-enter-active, .zoom-in-left-leave-active {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1, 1);
|
||||
transform: scale(1, 1);
|
||||
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
-webkit-transform-origin: top left;
|
||||
transform-origin: top left
|
||||
}
|
||||
|
||||
.zoom-in-left-enter, .zoom-in-left-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(.45, .45);
|
||||
transform: scale(.45, .45)
|
||||
}
|
||||
|
||||
.list-enter-active, .list-leave-active {
|
||||
-webkit-transition: all .3s;
|
||||
transition: all .3s
|
||||
}
|
||||
|
||||
.list-enter, .list-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(-30px);
|
||||
transform: translateY(-30px)
|
||||
}
|
||||
|
||||
.ant-list-item-meta-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ant-progress-inner {
|
||||
background-color: #EBEEF5;
|
||||
}
|
||||
|
||||
.deactive-client .ant-collapse-header{
|
||||
color:rgb(255, 255, 255) !important;
|
||||
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
|
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.interceptors.request.use(
|
||||
config => {
|
||||
config.data = Qs.stringify(config.data, {
|
||||
arrayFormat: 'repeat'
|
||||
});
|
||||
(config) => {
|
||||
if (config.data instanceof FormData) {
|
||||
config.headers['Content-Type'] = 'multipart/form-data';
|
||||
} else {
|
||||
config.data = Qs.stringify(config.data, {
|
||||
arrayFormat: 'repeat',
|
||||
});
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => Promise.reject(error)
|
||||
);
|
||||
(error) => Promise.reject(error),
|
||||
);
|
||||
|
||||
@@ -1,31 +1,41 @@
|
||||
supportLangs = [
|
||||
const supportLangs = [
|
||||
{
|
||||
name : "English",
|
||||
value : "en-US",
|
||||
icon : "🇺🇸"
|
||||
name: 'English',
|
||||
value: 'en-US',
|
||||
icon: '🇺🇸',
|
||||
},
|
||||
{
|
||||
name : "汉语",
|
||||
value : "zh-Hans",
|
||||
icon : "🇨🇳"
|
||||
name: 'فارسی',
|
||||
value: 'fa_IR',
|
||||
icon: '🇮🇷',
|
||||
},
|
||||
]
|
||||
{
|
||||
name: '汉语',
|
||||
value: 'zh-Hans',
|
||||
icon: '🇨🇳',
|
||||
},
|
||||
{
|
||||
name: 'Русский',
|
||||
value: 'ru_RU',
|
||||
icon: '🇷🇺',
|
||||
},
|
||||
];
|
||||
|
||||
function getLang(){
|
||||
let lang = getCookie('lang')
|
||||
function getLang() {
|
||||
let lang = getCookie('lang');
|
||||
|
||||
if (! lang){
|
||||
if (window.navigator){
|
||||
if (!lang) {
|
||||
if (window.navigator) {
|
||||
lang = window.navigator.language || window.navigator.userLanguage;
|
||||
|
||||
if (isSupportLang(lang)){
|
||||
setCookie('lang' , lang , 150)
|
||||
}else{
|
||||
setCookie('lang' , 'en-US' , 150)
|
||||
if (isSupportLang(lang)) {
|
||||
setCookie('lang', lang, 150);
|
||||
} else {
|
||||
setCookie('lang', 'en-US', 150);
|
||||
window.location.reload();
|
||||
}
|
||||
}else{
|
||||
setCookie('lang' , 'en-US' , 150)
|
||||
} else {
|
||||
setCookie('lang', 'en-US', 150);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
@@ -33,47 +43,21 @@ function getLang(){
|
||||
return lang;
|
||||
}
|
||||
|
||||
function setLang(lang){
|
||||
|
||||
if (!isSupportLang(lang)){
|
||||
function setLang(lang) {
|
||||
if (!isSupportLang(lang)) {
|
||||
lang = 'en-US';
|
||||
}
|
||||
|
||||
setCookie('lang' , lang , 150)
|
||||
setCookie('lang', lang, 150);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function isSupportLang(lang){
|
||||
for (l of supportLangs){
|
||||
if (l.value === lang){
|
||||
function isSupportLang(lang) {
|
||||
for (l of supportLangs) {
|
||||
if (l.value === lang) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
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=/";
|
||||
}
|
||||
@@ -151,9 +151,14 @@ class DBInbound {
|
||||
}
|
||||
}
|
||||
|
||||
genLink() {
|
||||
genLink(address=this.address, remark=this.remark, clientIndex=0) {
|
||||
const inbound = this.toInbound();
|
||||
return inbound.genLink(this.address, this.remark);
|
||||
return inbound.genLink(address, remark, clientIndex);
|
||||
}
|
||||
|
||||
get genInboundLinks() {
|
||||
const inbound = this.toInbound();
|
||||
return inbound.genInboundLinks(this.address, this.remark);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,13 +170,26 @@ class AllSetting {
|
||||
this.webCertFile = "";
|
||||
this.webKeyFile = "";
|
||||
this.webBasePath = "/";
|
||||
this.sessionMaxAge = "";
|
||||
this.expireDiff = "";
|
||||
this.trafficDiff = "";
|
||||
this.tgBotEnable = false;
|
||||
this.tgBotToken = "";
|
||||
this.tgBotChatId = 0;
|
||||
this.tgRunTime = "";
|
||||
this.tgBotChatId = "";
|
||||
this.tgRunTime = "@daily";
|
||||
this.tgBotBackup = false;
|
||||
this.tgCpu = "";
|
||||
this.xrayTemplateConfig = "";
|
||||
this.subEnable = false;
|
||||
this.subListen = "";
|
||||
this.subPort = "2096";
|
||||
this.subPath = "sub/";
|
||||
this.subDomain = "";
|
||||
this.subCertFile = "";
|
||||
this.subKeyFile = "";
|
||||
this.subUpdates = 0;
|
||||
|
||||
this.timeLocation = "Asia/Shanghai";
|
||||
this.timeLocation = "Asia/Tehran";
|
||||
|
||||
if (data == null) {
|
||||
return
|
||||
|
||||
@@ -54,4 +54,63 @@ function addZero(num) {
|
||||
function toFixed(num, n) {
|
||||
n = Math.pow(10, n);
|
||||
return Math.round(num * n) / n;
|
||||
}
|
||||
}
|
||||
|
||||
function debounce(fn, delay) {
|
||||
var timeoutID = null;
|
||||
return function () {
|
||||
clearTimeout(timeoutID);
|
||||
var args = arguments;
|
||||
var that = this;
|
||||
timeoutID = setTimeout(function () {
|
||||
fn.apply(that, args);
|
||||
}, 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 oneHour = oneMinute * 60; // 一小时的毫秒数
|
||||
const oneDay = oneHour * 24; // 一天的毫秒数
|
||||
const oneWeek = oneDay * 7; // 一星期的毫秒数
|
||||
const oneMonth = oneDay * 30; // 一个月的毫秒数
|
||||
const oneMinute = 1000 * 60; // 一The millise times of minutes
|
||||
const oneHour = oneMinute * 60; // 一Hours of millise times
|
||||
const oneDay = oneHour * 24; // 一Day's milliseconds
|
||||
const oneWeek = oneDay * 7; // 一Number of millise times on week
|
||||
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) {
|
||||
return this.minusMillis(oneDay * days);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按天数增加
|
||||
* Increase by day
|
||||
*
|
||||
* @param days 要增加的天数
|
||||
* @param days The number of days to be increased
|
||||
*/
|
||||
Date.prototype.plusDays = function (days) {
|
||||
return this.plusMillis(oneDay * days);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按小时减少
|
||||
* Reduced
|
||||
*
|
||||
* @param hours 要减少的小时数
|
||||
* @param hours The number of hours to be reduced
|
||||
*/
|
||||
Date.prototype.minusHours = function (hours) {
|
||||
return this.minusMillis(oneHour * hours);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按小时增加
|
||||
* Increase
|
||||
*
|
||||
* @param hours 要增加的小时数
|
||||
* @param hours Increase the number of hours
|
||||
*/
|
||||
Date.prototype.plusHours = function (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) {
|
||||
return this.minusMillis(oneMinute * minutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按分钟增加
|
||||
* Increase
|
||||
*
|
||||
* @param minutes 要增加的分钟数
|
||||
* @param minutes The number of minutes to be increased
|
||||
*/
|
||||
Date.prototype.plusMinutes = function (minutes) {
|
||||
return this.plusMillis(oneMinute * minutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按毫秒减少
|
||||
* Decrease by millisecond
|
||||
*
|
||||
* @param millis 要减少的毫秒数
|
||||
* @param millis Number of milliligues to be reduced
|
||||
*/
|
||||
Date.prototype.minusMillis = function(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) {
|
||||
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 () {
|
||||
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 () {
|
||||
this.setHours(23);
|
||||
@@ -105,14 +105,14 @@ Date.prototype.setMaxTime = function () {
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* Formatting date
|
||||
*/
|
||||
Date.prototype.formatDate = function () {
|
||||
return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate());
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* Formatting time
|
||||
*/
|
||||
Date.prototype.formatTime = function () {
|
||||
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 = ' ') {
|
||||
return this.formatDate() + split + this.formatTime();
|
||||
};
|
||||
|
||||
class DateUtil {
|
||||
|
||||
// 字符串转 Date 对象
|
||||
// String string to date object
|
||||
static parseDate(str) {
|
||||
return new Date(str.replace(/-/g, '/'));
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -144,4 +143,4 @@ class DateUtil {
|
||||
date.setMinTime();
|
||||
return date;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,13 +68,11 @@ class HttpUtil {
|
||||
}
|
||||
|
||||
class PromiseUtil {
|
||||
|
||||
static async sleep(timeout) {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, timeout)
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const seq = [
|
||||
@@ -90,7 +88,6 @@ const seq = [
|
||||
];
|
||||
|
||||
class RandomUtil {
|
||||
|
||||
static randomIntRange(min, max) {
|
||||
return parseInt(Math.random() * (max - min) + min, 10);
|
||||
}
|
||||
@@ -136,10 +133,25 @@ class RandomUtil {
|
||||
return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
static randomText(minLen = 6, varLen = 5) {
|
||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||
var string = '';
|
||||
var len = minLen + Math.floor(Math.random() * varLen);
|
||||
for (var ii = 0; ii < len; ii++) {
|
||||
string += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
static randomShadowsocksPassword() {
|
||||
let array = new Uint8Array(32);
|
||||
window.crypto.getRandomValues(array);
|
||||
return btoa(String.fromCharCode.apply(null, array));
|
||||
}
|
||||
}
|
||||
|
||||
class ObjectUtil {
|
||||
|
||||
static getPropIgnoreCase(obj, prop) {
|
||||
for (const name in obj) {
|
||||
if (!obj.hasOwnProperty(name)) {
|
||||
@@ -169,7 +181,7 @@ class ObjectUtil {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return obj.toString().indexOf(key) >= 0;
|
||||
return obj.toString().toLowerCase().indexOf(key.toLowerCase()) >= 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -287,5 +299,4 @@ class ObjectUtil {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
type APIController struct {
|
||||
BaseController
|
||||
|
||||
inboundController *InboundController
|
||||
settingController *SettingController
|
||||
}
|
||||
|
||||
func NewAPIController(g *gin.RouterGroup) *APIController {
|
||||
@@ -22,21 +19,30 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
|
||||
g.GET("/", a.inbounds)
|
||||
g.GET("/get/:id", a.inbound)
|
||||
g.GET("/getClientTraffics/:email", a.getClientTraffics)
|
||||
g.POST("/add", a.addInbound)
|
||||
g.POST("/del/:id", a.delInbound)
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
func (a *APIController) inbounds(c *gin.Context) {
|
||||
a.inboundController.getInbounds(c)
|
||||
}
|
||||
func (a *APIController) inbound(c *gin.Context) {
|
||||
a.inboundController.getInbound(c)
|
||||
}
|
||||
func (a *APIController) getClientTraffics(c *gin.Context) {
|
||||
a.inboundController.getClientTraffics(c)
|
||||
}
|
||||
func (a *APIController) addInbound(c *gin.Context) {
|
||||
a.inboundController.addInbound(c)
|
||||
}
|
||||
@@ -46,3 +52,24 @@ func (a *APIController) delInbound(c *gin.Context) {
|
||||
func (a *APIController) updateInbound(c *gin.Context) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type BaseController struct {
|
||||
func (a *BaseController) checkLogin(c *gin.Context) {
|
||||
if !session.IsLogin(c) {
|
||||
if isAjax(c) {
|
||||
pureJsonMsg(c, false, I18n(c , "pages.login.loginAgain"))
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.loginAgain"))
|
||||
} else {
|
||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
||||
}
|
||||
@@ -22,12 +22,11 @@ func (a *BaseController) checkLogin(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func I18n(c *gin.Context, name string) string {
|
||||
anyfunc, _ := c.Get("I18n")
|
||||
i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
|
||||
|
||||
func I18n(c *gin.Context , name string) string{
|
||||
anyfunc, _ := c.Get("I18n")
|
||||
i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
|
||||
message, _ := i18n(name)
|
||||
|
||||
message, _ := i18n(name)
|
||||
|
||||
return message;
|
||||
return message
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@ package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"strconv"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type InboundController struct {
|
||||
@@ -30,11 +31,13 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/add", a.addInbound)
|
||||
g.POST("/del/:id", a.delInbound)
|
||||
g.POST("/update/:id", a.updateInbound)
|
||||
|
||||
g.POST("/clientIps/:email", a.getClientIps)
|
||||
g.POST("/clearClientIps/:email", a.clearClientIps)
|
||||
g.POST("/resetClientTraffic/:email", a.resetClientTraffic)
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
|
||||
@@ -55,7 +58,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
inbounds, err := a.inboundService.GetInbounds(user.Id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c , "pages.inbounds.toasts.obtain"), err)
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, inbounds, nil)
|
||||
@@ -63,22 +66,31 @@ func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
func (a *InboundController) getInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c , "get"), err)
|
||||
jsonMsg(c, I18n(c, "get"), err)
|
||||
return
|
||||
}
|
||||
inbound, err := a.inboundService.GetInbound(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c , "pages.inbounds.toasts.obtain"), err)
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
|
||||
return
|
||||
}
|
||||
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) {
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c , "pages.inbounds.addTo"), err)
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.create"), err)
|
||||
return
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
@@ -86,7 +98,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
||||
inbound.Enable = true
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
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 {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
@@ -95,11 +107,11 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
||||
func (a *InboundController) delInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c , "delete"), err)
|
||||
jsonMsg(c, I18n(c, "delete"), err)
|
||||
return
|
||||
}
|
||||
err = a.inboundService.DelInbound(id)
|
||||
jsonMsgObj(c, I18n(c , "delete"), id, err)
|
||||
jsonMsgObj(c, I18n(c, "delete"), id, err)
|
||||
if err == nil {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
@@ -108,7 +120,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
|
||||
func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c , "pages.inbounds.revise"), err)
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
inbound := &model.Inbound{
|
||||
@@ -116,42 +128,128 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
||||
}
|
||||
err = c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c , "pages.inbounds.revise"), err)
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
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 {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
func (a *InboundController) getClientIps(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
ips , err := a.inboundService.GetInboundClientIps(email)
|
||||
func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||
data := &model.Inbound{}
|
||||
err := c.ShouldBind(data)
|
||||
if err != nil {
|
||||
jsonObj(c, "No IP Record", nil)
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, ips, nil)
|
||||
}
|
||||
func (a *InboundController) clearClientIps(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
err := a.inboundService.ClearClientIps(email)
|
||||
err = a.inboundService.AddInboundClient(data)
|
||||
if err != nil {
|
||||
jsonMsg(c, "修改", err)
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Log Cleared", nil)
|
||||
jsonMsg(c, "Client(s) added", nil)
|
||||
if err == nil {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
clientId := c.Param("clientId")
|
||||
|
||||
err = a.inboundService.DelInboundClient(id, clientId)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Client deleted", nil)
|
||||
if err == nil {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||
clientId := c.Param("clientId")
|
||||
|
||||
inbound := &model.Inbound{}
|
||||
err := c.ShouldBind(inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
|
||||
err = a.inboundService.UpdateInboundClient(inbound, clientId)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "Client updated", nil)
|
||||
if err == nil {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
|
||||
return
|
||||
}
|
||||
email := c.Param("email")
|
||||
|
||||
err := a.inboundService.ResetClientTraffic(email)
|
||||
err = a.inboundService.ResetClientTraffic(id, email)
|
||||
if err != nil {
|
||||
jsonMsg(c, "something worng!", err)
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "traffic reseted", nil)
|
||||
if err == nil {
|
||||
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"
|
||||
"time"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/job"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
@@ -19,7 +18,9 @@ type LoginForm struct {
|
||||
type IndexController struct {
|
||||
BaseController
|
||||
|
||||
userService service.UserService
|
||||
settingService service.SettingService
|
||||
userService service.UserService
|
||||
tgbot service.Tgbot
|
||||
}
|
||||
|
||||
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
||||
@@ -46,7 +47,7 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
var form LoginForm
|
||||
err := c.ShouldBind(&form)
|
||||
if err != nil {
|
||||
pureJsonMsg(c, false, I18n(c , "pages.login.toasts.invalidFormData"))
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.invalidFormData"))
|
||||
return
|
||||
}
|
||||
if form.Username == "" {
|
||||
@@ -54,24 +55,36 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if form.Password == "" {
|
||||
pureJsonMsg(c, false, I18n(c , "pages.login.toasts.emptyPassword"))
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword"))
|
||||
return
|
||||
}
|
||||
user := a.userService.CheckUser(form.Username, form.Password)
|
||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
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)
|
||||
pureJsonMsg(c, false, I18n(c , "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
return
|
||||
} else {
|
||||
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)
|
||||
logger.Info("user", user.Id, "login success")
|
||||
jsonMsg(c, I18n(c , "pages.login.toasts.successLogin"), err)
|
||||
jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err)
|
||||
}
|
||||
|
||||
func (a *IndexController) logout(c *gin.Context) {
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
|
||||
|
||||
type ServerController struct {
|
||||
BaseController
|
||||
|
||||
@@ -34,7 +40,14 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||
g.Use(a.checkLogin)
|
||||
g.POST("/status", a.status)
|
||||
g.POST("/getXrayVersion", a.getXrayVersion)
|
||||
g.POST("/stopXrayService", a.stopXrayService)
|
||||
g.POST("/restartXrayService", a.restartXrayService)
|
||||
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() {
|
||||
@@ -68,7 +81,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
|
||||
versions, err := a.serverService.GetXrayVersions()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c , "getVersion"), err)
|
||||
jsonMsg(c, I18n(c, "getVersion"), err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -81,5 +94,96 @@ func (a *ServerController) getXrayVersion(c *gin.Context) {
|
||||
func (a *ServerController) installXray(c *gin.Context) {
|
||||
version := c.Param("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 (
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"time"
|
||||
"x-ui/web/entity"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type updateUserForm struct {
|
||||
@@ -32,45 +33,122 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/setting")
|
||||
|
||||
g.POST("/all", a.getAllSetting)
|
||||
g.POST("/defaultSettings", a.getDefaultSettings)
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.POST("/updateUser", a.updateUser)
|
||||
g.POST("/restartPanel", a.restartPanel)
|
||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||
}
|
||||
|
||||
func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||
allSetting, err := a.settingService.GetAllSetting()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c , "pages.setting.toasts.getSetting"), err)
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
tgBotEnable, err := a.settingService.GetTgbotenabled()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subEnable, err := a.settingService.GetSubEnable()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subPort, err := a.settingService.GetSubPort()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subPath, err := a.settingService.GetSubPath()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subDomain, err := a.settingService.GetSubDomain()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subKeyFile, err := a.settingService.GetSubKeyFile()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subCertFile, err := a.settingService.GetSubCertFile()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
subTLS := false
|
||||
if subKeyFile != "" || subCertFile != "" {
|
||||
subTLS = true
|
||||
}
|
||||
result := map[string]interface{}{
|
||||
"expireDiff": expireDiff,
|
||||
"trafficDiff": trafficDiff,
|
||||
"defaultCert": defaultCert,
|
||||
"defaultKey": defaultKey,
|
||||
"tgBotEnable": tgBotEnable,
|
||||
"subEnable": subEnable,
|
||||
"subPort": subPort,
|
||||
"subPath": subPath,
|
||||
"subDomain": subDomain,
|
||||
"subTLS": subTLS,
|
||||
}
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
func (a *SettingController) updateSetting(c *gin.Context) {
|
||||
allSetting := &entity.AllSetting{}
|
||||
err := c.ShouldBind(allSetting)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
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) {
|
||||
form := &updateUserForm{}
|
||||
err := c.ShouldBind(form)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c , "pages.setting.toasts.modifySetting"), err)
|
||||
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
|
||||
return
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
|
||||
@@ -79,10 +157,19 @@ func (a *SettingController) updateUser(c *gin.Context) {
|
||||
user.Password = form.NewPassword
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"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 {
|
||||
value := c.GetHeader("X-Forwarded-For")
|
||||
if value != "" {
|
||||
@@ -46,12 +38,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
|
||||
if err == nil {
|
||||
m.Success = true
|
||||
if msg != "" {
|
||||
m.Msg = msg + I18n(c , "success")
|
||||
m.Msg = msg + I18n(c, "success")
|
||||
}
|
||||
} else {
|
||||
m.Success = false
|
||||
m.Msg = msg + I18n(c , "fail") + ": " + err.Error()
|
||||
logger.Warning(msg + I18n(c , "fail") + ": ", err)
|
||||
m.Msg = msg + I18n(c, "fail") + ": " + err.Error()
|
||||
logger.Warning(msg+I18n(c, "fail")+": ", err)
|
||||
}
|
||||
c.JSON(http.StatusOK, m)
|
||||
}
|
||||
@@ -75,6 +67,7 @@ func html(c *gin.Context, name string, title string, data gin.H) {
|
||||
data = gin.H{}
|
||||
}
|
||||
data["title"] = title
|
||||
data["host"] = strings.Split(c.Request.Host, ":")[0]
|
||||
data["request_uri"] = c.Request.RequestURI
|
||||
data["base_path"] = c.GetString("base_path")
|
||||
c.HTML(http.StatusOK, name, getContext(data))
|
||||
@@ -84,10 +77,8 @@ func getContext(h gin.H) gin.H {
|
||||
a := gin.H{
|
||||
"cur_ver": config.GetVersion(),
|
||||
}
|
||||
if h != nil {
|
||||
for key, value := range h {
|
||||
a[key] = value
|
||||
}
|
||||
for key, value := range h {
|
||||
a[key] = value
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||
|
||||
g.GET("/", a.index)
|
||||
g.GET("/inbounds", a.inbounds)
|
||||
g.GET("/setting", a.setting)
|
||||
g.GET("/settings", a.settings)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
a.settingController = NewSettingController(g)
|
||||
@@ -37,6 +37,6 @@ func (a *XUIController) inbounds(c *gin.Context) {
|
||||
html(c, "inbounds.html", "pages.inbounds.title", nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) setting(c *gin.Context) {
|
||||
html(c, "setting.html", "pages.setting.title", nil)
|
||||
func (a *XUIController) settings(c *gin.Context) {
|
||||
html(c, "settings.html", "pages.settings.title", nil)
|
||||
}
|
||||
|
||||
@@ -32,13 +32,25 @@ type AllSetting struct {
|
||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
||||
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"`
|
||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
||||
TgBotChatId int `json:"tgBotChatId" form:"tgBotChatId"`
|
||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
||||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
||||
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
||||
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||
SubListen string `json:"subListen" form:"subListen"`
|
||||
SubPort int `json:"subPort" form:"subPort"`
|
||||
SubPath string `json:"subPath" form:"subPath"`
|
||||
SubDomain string `json:"subDomain" form:"subDomain"`
|
||||
SubCertFile string `json:"subCertFile" form:"subCertFile"`
|
||||
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
|
||||
SubUpdates int `json:"subUpdates" form:"subUpdates"`
|
||||
}
|
||||
|
||||
func (s *AllSetting) CheckValid() error {
|
||||
@@ -49,10 +61,25 @@ func (s *AllSetting) CheckValid() error {
|
||||
}
|
||||
}
|
||||
|
||||
if s.SubListen != "" {
|
||||
ip := net.ParseIP(s.SubListen)
|
||||
if ip == nil {
|
||||
return common.NewError("Sub listen is not valid ip:", s.SubListen)
|
||||
}
|
||||
}
|
||||
|
||||
if s.WebPort <= 0 || s.WebPort > 65535 {
|
||||
return common.NewError("web port is not a valid port:", s.WebPort)
|
||||
}
|
||||
|
||||
if s.SubPort <= 0 || s.SubPort > 65535 {
|
||||
return common.NewError("Sub port is not a valid port:", s.SubPort)
|
||||
}
|
||||
|
||||
if s.SubPort == s.WebPort {
|
||||
return common.NewError("Sub and Web could not use same port:", s.SubPort)
|
||||
}
|
||||
|
||||
if s.WebCertFile != "" || s.WebKeyFile != "" {
|
||||
_, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)
|
||||
if err != nil {
|
||||
@@ -60,6 +87,13 @@ func (s *AllSetting) CheckValid() error {
|
||||
}
|
||||
}
|
||||
|
||||
if s.SubCertFile != "" || s.SubKeyFile != "" {
|
||||
_, err := tls.LoadX509KeyPair(s.SubCertFile, s.SubKeyFile)
|
||||
if err != nil {
|
||||
return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.SubCertFile, s.SubKeyFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(s.WebBasePath, "/") {
|
||||
s.WebBasePath = "/" + s.WebBasePath
|
||||
}
|
||||
|
||||
@@ -2,17 +2,23 @@ package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/robfig/cron/v3"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
var webServer WebServer
|
||||
var subServer SubServer
|
||||
|
||||
type WebServer interface {
|
||||
GetCron() *cron.Cron
|
||||
GetCtx() context.Context
|
||||
}
|
||||
|
||||
type SubServer interface {
|
||||
GetCtx() context.Context
|
||||
}
|
||||
|
||||
func SetWebServer(s WebServer) {
|
||||
webServer = s
|
||||
}
|
||||
@@ -20,3 +26,11 @@ func SetWebServer(s WebServer) {
|
||||
func GetWebServer() WebServer {
|
||||
return webServer
|
||||
}
|
||||
|
||||
func SetSubServer(s SubServer) {
|
||||
subServer = s
|
||||
}
|
||||
|
||||
func GetSubServer() SubServer {
|
||||
return subServer
|
||||
}
|
||||
|
||||
@@ -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/element-ui@2.15.0/theme-chalk/display.css">
|
||||
<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>
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<title>{{ i18n .title}}</title>
|
||||
<title>{{ .host }}-{{ i18n .title}}</title>
|
||||
</head>
|
||||
{{end}}
|
||||
@@ -1,6 +1,7 @@
|
||||
{{define "promptModal"}}
|
||||
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
|
||||
:closable="true" @ok="promptModal.ok" :mask-closable="false"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
|
||||
<a-input id="prompt-modal-input" :type="promptModal.type"
|
||||
v-model="promptModal.value"
|
||||
@@ -35,11 +36,11 @@
|
||||
},
|
||||
confirm() {},
|
||||
open({
|
||||
title='',
|
||||
type='text',
|
||||
value='',
|
||||
okText='{{ i18n "sure"}}',
|
||||
confirm=() => {},
|
||||
title = '',
|
||||
type = 'text',
|
||||
value = '',
|
||||
okText = '{{ i18n "sure"}}',
|
||||
confirm = () => {},
|
||||
}) {
|
||||
this.title = title;
|
||||
this.type = type;
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
{{define "qrcodeModal"}}
|
||||
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
|
||||
:closable="true" width="300px" :ok-text="qrModal.okText"
|
||||
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
|
||||
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >click on QR Code to Copy</a-tag>
|
||||
<canvas v-if="qrModal.inbound.protocol != Protocols.VMESS && qrModal.inbound.protocol != Protocols.VLESS" id="qrCode" style="width: 100%; height: 100%;"></canvas>
|
||||
|
||||
<template v-if="qrModal.inbound.protocol === Protocols.VMESS" v-for="(vmess, index) in qrModal.inbound.settings.vmesses">
|
||||
<a-tag color="red" style="margin-bottom: 10px;display: block;text-align: center;" v-text="vmess.email"></a-tag>
|
||||
<canvas @click="copyTextToClipboard(`qrCode-vmess-${vmess.id}`,index)" :id="`qrCode-vmess-${vmess.id}`" style="width: 100%; height: 100%;"></canvas>
|
||||
<a-divider style="height: 2px; background-color: #7e7e7e" />
|
||||
:closable="true"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:footer="null"
|
||||
width="300px">
|
||||
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
|
||||
<template v-if="app.subSettings.enable && qrModal.subId">
|
||||
<a-divider>Subscription</a-divider>
|
||||
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas>
|
||||
</template>
|
||||
|
||||
<template v-if="qrModal.inbound.protocol === Protocols.VLESS" v-for="(vless, index) in qrModal.inbound.settings.vlesses">
|
||||
<a-tag color="red" style="margin-bottom: 10px;display: block;text-align: center;" v-text="vless.email"></a-tag>
|
||||
<canvas @click="copyTextToClipboard(`qrCode-vless-${vless.id}`,index)" :id="`qrCode-vless-${vless.id}`" style="width: 100%; height: 100%;"></canvas>
|
||||
<a-divider style="height: 2px; background-color: #7e7e7e" />
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<template v-for="(row, index) in qrModal.qrcodes">
|
||||
<a-tag color="orange" style="margin-top: 10px;display: block;text-align: center;">[[ row.remark ]]</a-tag>
|
||||
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
@@ -22,43 +20,38 @@
|
||||
|
||||
const qrModal = {
|
||||
title: '',
|
||||
content: '',
|
||||
clientIndex: 0,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
okText: '',
|
||||
copyText: '',
|
||||
qrcode: null,
|
||||
client: null,
|
||||
qrcodes: [],
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
show: function (title='', content='', dbInbound=new DBInbound(),okText='{{ i18n "copy" }}', copyText='') {
|
||||
subId: '',
|
||||
show: function (title = '', dbInbound = new DBInbound(), clientIndex = 0) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.clientIndex = clientIndex;
|
||||
this.dbInbound = dbInbound;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.okText = okText;
|
||||
if (ObjectUtil.isEmpty(copyText)) {
|
||||
this.copyText = content;
|
||||
settings = JSON.parse(this.inbound.settings);
|
||||
this.client = settings.clients[clientIndex];
|
||||
remark = this.dbInbound.remark + "-" + this.client.email;
|
||||
address = this.dbInbound.address;
|
||||
this.qrcodes = [];
|
||||
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
|
||||
this.inbound.stream.tls.settings.domains.forEach((domain) => {
|
||||
this.qrcodes.push({
|
||||
remark: remark + "-" + domain.remark,
|
||||
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, clientIndex)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.copyText = copyText;
|
||||
this.qrcodes.push({
|
||||
remark: remark,
|
||||
link: this.inbound.genLink(address, remark, clientIndex)
|
||||
});
|
||||
}
|
||||
this.visible = true;
|
||||
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) {
|
||||
this.qrcode = new QRious({
|
||||
element: document.querySelector('#qrCode'),
|
||||
size: 260,
|
||||
value: content,
|
||||
});
|
||||
} else {
|
||||
this.qrcode.value = content;
|
||||
}
|
||||
});
|
||||
},
|
||||
close: function () {
|
||||
this.visible = false;
|
||||
@@ -66,53 +59,45 @@
|
||||
};
|
||||
|
||||
const qrModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#qrcode-modal',
|
||||
data: {
|
||||
qrModal: qrModal,
|
||||
},
|
||||
methods: {
|
||||
setQrCode(elmentId,index) {
|
||||
content = qrModal.inbound.genLink(qrModal.dbInbound.address,qrModal.dbInbound.remark,index)
|
||||
|
||||
copyToClipboard(elmentId,content) {
|
||||
this.qrModal.clipboard = new ClipboardJS('#'+elmentId, {
|
||||
text: () => content,
|
||||
});
|
||||
this.qrModal.clipboard.on('success', () => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
this.qrModal.clipboard.destroy();
|
||||
});
|
||||
},
|
||||
setQrCode(elmentId,content) {
|
||||
new QRious({
|
||||
element: document.querySelector('#'+elmentId),
|
||||
size: 260,
|
||||
value: content,
|
||||
});
|
||||
},
|
||||
copyTextToClipboard(elmentId,index) {
|
||||
link = qrModal.inbound.genLink(qrModal.dbInbound.address,qrModal.dbInbound.remark,index)
|
||||
this.qrModal.copyText = link
|
||||
|
||||
this.qrModal.clipboard = new ClipboardJS('#' + elmentId, {
|
||||
text: () => link,
|
||||
});
|
||||
this.qrModal.clipboard.on('success', () => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
this.qrModal.clipboard.destroy();
|
||||
});
|
||||
|
||||
|
||||
genSubLink(subID) {
|
||||
protocol = app.subSettings.tls ? "https://" : "http://";
|
||||
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
|
||||
subPort = app.subSettings.port;
|
||||
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
|
||||
subPath = app.subSettings.path;
|
||||
return protocol + hostName + port + subPath + subID;
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
switch (qrModal.inbound.protocol) {
|
||||
case Protocols.VMESS:
|
||||
vmesses = qrModal.inbound.settings.vmesses
|
||||
for (const index in vmesses) {
|
||||
this.setQrCode("qrCode-vmess-" + vmesses[index].id ,index)
|
||||
}
|
||||
break;
|
||||
case Protocols.VLESS:
|
||||
vlesses = qrModal.inbound.settings.vlesses
|
||||
|
||||
for (const index in vlesses) {
|
||||
this.setQrCode("qrCode-vless-" + vlesses[index].id ,index)
|
||||
}
|
||||
break;
|
||||
default: return null;
|
||||
updated() {
|
||||
if (qrModal.client.subId){
|
||||
qrModal.subId = qrModal.client.subId;
|
||||
this.setQrCode("qrCode-sub",this.genSubLink(this.subId));
|
||||
}
|
||||
|
||||
qrModal.qrcodes.forEach((element,index) => {
|
||||
this.setQrCode("qrCode-"+index, element.link);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{{define "textModal"}}
|
||||
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
|
||||
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
|
||||
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
|
||||
@click="downloader.download(txtModal.fileName, txtModal.content)">
|
||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
|
||||
:download="txtModal.fileName">
|
||||
{{ i18n "download" }} [[ txtModal.fileName ]]
|
||||
</a-button>
|
||||
<a-input type="textarea" v-model="txtModal.content"
|
||||
@@ -19,7 +21,7 @@
|
||||
qrcode: null,
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
show: function (title='', content='', fileName='') {
|
||||
show: function (title = '', content = '', fileName = '') {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.fileName = fileName;
|
||||
@@ -31,15 +33,6 @@
|
||||
});
|
||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
||||
}
|
||||
if (this.qrcode === null) {
|
||||
this.qrcode = new QRious({
|
||||
element: document.querySelector('#qrCode'),
|
||||
size: 260,
|
||||
value: content,
|
||||
});
|
||||
} else {
|
||||
this.qrcode.value = content;
|
||||
}
|
||||
});
|
||||
},
|
||||
close: function () {
|
||||
@@ -48,6 +41,7 @@
|
||||
};
|
||||
|
||||
const textModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#text-modal',
|
||||
data: {
|
||||
txtModal: txtModal,
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
margin: 20px 0 50px 0;
|
||||
}
|
||||
|
||||
@@ -18,6 +17,12 @@
|
||||
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 {
|
||||
left: 23px;
|
||||
}
|
||||
@@ -26,20 +31,26 @@
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.selectLang{
|
||||
.centered {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.darkCardClass">
|
||||
<transition name="list" appear>
|
||||
<a-layout-content>
|
||||
<a-row type="flex" justify="center">
|
||||
<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-row>
|
||||
<a-row type="flex" justify="center">
|
||||
@@ -48,40 +59,38 @@
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
|
||||
@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-form-item>
|
||||
<a-form-item>
|
||||
<a-input type="password" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
|
||||
<a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
|
||||
</a-input>
|
||||
<password-input icon="lock" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
|
||||
</password-input>
|
||||
</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-row justify="center" class="selectLang">
|
||||
<a-col :span="4"><span>Language : </span></a-col>
|
||||
|
||||
<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" >
|
||||
<a-row justify="center" class="centered">
|
||||
<a-col :span="12">
|
||||
<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">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
|
||||
</a-row>
|
||||
|
||||
|
||||
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<theme-switch />
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-col>
|
||||
@@ -90,22 +99,21 @@
|
||||
</transition>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "component/password" .}}
|
||||
<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({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
loading: false,
|
||||
user: new User(),
|
||||
lang : ""
|
||||
lang: ""
|
||||
},
|
||||
created(){
|
||||
this.lang = getLang();
|
||||
created() {
|
||||
this.updateBackground();
|
||||
this.lang = getLang();
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
@@ -115,8 +123,16 @@
|
||||
if (msg.success) {
|
||||
location.href = basePath + 'xui/';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateBackground() {
|
||||
document.querySelector('#app').style.background = colors[this.themeSwitcher.currentTheme].bg;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'themeSwitcher.isDarkTheme'(newVal, oldVal) {
|
||||
this.updateBackground();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
257
web/html/xui/client_bulk_modal.html
Normal file
@@ -0,0 +1,257 @@
|
||||
{{define "clientsBulkModal"}}
|
||||
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
|
||||
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
<a-form layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.client.method" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 250px"
|
||||
:dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option :value="0">Random</a-select-option>
|
||||
<a-select-option :value="1">Random+Prefix</a-select-option>
|
||||
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
|
||||
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
|
||||
<a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.emailMethod>1">
|
||||
<td>{{ i18n "pages.client.first" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.emailMethod>1">
|
||||
<td>{{ i18n "pages.client.last" }}</td>
|
||||
<td>
|
||||
<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 v-if="app.subSettings.enable">
|
||||
<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 v-if="app.tgBotEnable">
|
||||
<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-modal>
|
||||
<script>
|
||||
|
||||
const clientsBulkModal = {
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
title: '',
|
||||
okText: '',
|
||||
confirm: null,
|
||||
dbInbound: new DBInbound(),
|
||||
inbound: new Inbound(),
|
||||
quantity: 1,
|
||||
totalGB: 0,
|
||||
expiryTime: '',
|
||||
emailMethod: 0,
|
||||
firstNum: 1,
|
||||
lastNum: 1,
|
||||
emailPrefix: "",
|
||||
emailPostfix: "",
|
||||
subId: "",
|
||||
tgId: "",
|
||||
flow: "",
|
||||
delayedStart: false,
|
||||
ok() {
|
||||
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);
|
||||
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._expiryTime = clientsBulkModal.expiryTime;
|
||||
if(clientsBulkModal.inbound.canEnableTlsFlow()){
|
||||
newClient.flow = clientsBulkModal.flow;
|
||||
}
|
||||
clients.push(newClient);
|
||||
}
|
||||
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
|
||||
},
|
||||
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
|
||||
this.visible = true;
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.quantity = 1;
|
||||
this.totalGB = 0;
|
||||
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.inbound = dbInbound.toInbound();
|
||||
this.delayedStart = false;
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
newClient(protocol) {
|
||||
switch (protocol) {
|
||||
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
|
||||
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
|
||||
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
|
||||
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks();
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
close() {
|
||||
clientsBulkModal.visible = false;
|
||||
clientsBulkModal.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
clientsBulkModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
const clientsBulkModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#client-bulk-modal',
|
||||
data: {
|
||||
clientsBulkModal,
|
||||
get 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>
|
||||
{{end}}
|
||||
152
web/html/xui/client_modal.html
Normal file
@@ -0,0 +1,152 @@
|
||||
{{define "clientsModal"}}
|
||||
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
|
||||
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
{{template "form/client"}}
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const clientModal = {
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
title: '',
|
||||
okText: '',
|
||||
isEdit: false,
|
||||
dbInbound: new DBInbound(),
|
||||
inbound: new Inbound(),
|
||||
clients: [],
|
||||
clientStats: [],
|
||||
oldClientId: "",
|
||||
index: null,
|
||||
delayedStart: false,
|
||||
ok() {
|
||||
if(clientModal.isEdit){
|
||||
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
|
||||
} else {
|
||||
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
|
||||
}
|
||||
},
|
||||
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=()=>{}, isEdit=false }) {
|
||||
this.visible = true;
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.isEdit = isEdit;
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
||||
this.index = index === null ? this.clients.length : index;
|
||||
this.delayedStart = false;
|
||||
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.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
||||
this.confirm = confirm;
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
getClientId(protocol, client) {
|
||||
switch(protocol){
|
||||
case Protocols.TROJAN: return client.password;
|
||||
case Protocols.SHADOWSOCKS: return client.email;
|
||||
default: return client.id;
|
||||
}
|
||||
},
|
||||
addClient(protocol, clients) {
|
||||
switch (protocol) {
|
||||
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
|
||||
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
||||
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
||||
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks());
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
close() {
|
||||
clientModal.visible = false;
|
||||
clientModal.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
clientModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
const clientModalApp = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#client-modal',
|
||||
data: {
|
||||
clientModal,
|
||||
get inbound() {
|
||||
return this.clientModal.inbound;
|
||||
},
|
||||
get client() {
|
||||
return this.clientModal.clients[this.clientModal.index];
|
||||
},
|
||||
get clientStats() {
|
||||
return this.clientModal.clientStats;
|
||||
},
|
||||
get isEdit() {
|
||||
return this.clientModal.isEdit;
|
||||
},
|
||||
get isTrafficExhausted() {
|
||||
if(!clientStats) return false
|
||||
if(clientStats.total <= 0) return false
|
||||
if(clientStats.up + clientStats.down < clientStats.total) return false
|
||||
return true
|
||||
},
|
||||
get isExpiry() {
|
||||
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
|
||||
},
|
||||
get statsColor() {
|
||||
return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
|
||||
},
|
||||
get delayedExpireDays() {
|
||||
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
|
||||
},
|
||||
set delayedExpireDays(days){
|
||||
this.client.expiryTime = -86400000 * days;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getNewEmail(client) {
|
||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||
var string = '';
|
||||
var len = 6 + Math.floor(Math.random() * 5);
|
||||
for(var ii=0; ii<len; ii++){
|
||||
string += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
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>
|
||||
{{end}}
|
||||
@@ -7,24 +7,10 @@
|
||||
<a-icon type="user"></a-icon>
|
||||
<span>{{ i18n "menu.inbounds"}}</span>
|
||||
</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>
|
||||
<span>{{ i18n "menu.setting"}}</span>
|
||||
<span>{{ i18n "menu.settings"}}</span>
|
||||
</a-menu-item>
|
||||
<!--<a-menu-item key="{{ .base_path }}xui/clients">-->
|
||||
<!-- <a-icon type="laptop"></a-icon>-->
|
||||
<!-- <span>客户端</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/hossinasaadi/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-icon type="logout"></a-icon>
|
||||
<span>{{ i18n "menu.logout"}}</span>
|
||||
@@ -33,25 +19,38 @@
|
||||
|
||||
|
||||
{{define "commonSider"}}
|
||||
<a-layout-sider id="sider" collapsible breakpoint="md" collapsed-width="0">
|
||||
<a-menu theme="dark" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
<a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0">
|
||||
<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">
|
||||
{{template "menuItems" .}}
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
<a-drawer id="sider-drawer" placement="left" :closable="false"
|
||||
@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">
|
||||
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
|
||||
</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">
|
||||
{{template "menuItems" .}}
|
||||
</a-menu>
|
||||
</a-drawer>
|
||||
<script>
|
||||
|
||||
const siderDrawer = {
|
||||
visible: false,
|
||||
show() {
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
{{define "inboundInfoStream"}}
|
||||
<p>{{ i18n "transmission" }}: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
|
||||
|
||||
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
|
||||
<p v-if="inbound.host">host: <a-tag color="green">[[ inbound.host ]]</a-tag></p>
|
||||
<p v-else>{{ i18n "host" }}: <a-tag color="orange">{{ i18n "none" }}</a-tag></p>
|
||||
|
||||
<p v-if="inbound.path">path: <a-tag color="green">[[ inbound.path ]]</a-tag></p>
|
||||
<p v-else>{{ i18n "path" }}: <a-tag color="orange">{{ i18n "none" }}</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isQuic">
|
||||
<p>quic {{ i18n "encryption" }}: <a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></p>
|
||||
<p>quic {{ i18n "password" }}: <a-tag color="green">[[ inbound.quicKey ]]</a-tag></p>
|
||||
<p>quic {{ i18n "camouflage" }}: <a-tag color="green">[[ inbound.quicType ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isKcp">
|
||||
<p>kcp {{ i18n "encryption" }}: <a-tag color="green">[[ inbound.kcpType ]]</a-tag></p>
|
||||
<p>kcp {{ i18n "password" }}: <a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isGrpc">
|
||||
<p>grpc serviceName: <a-tag color="green">[[ inbound.serviceName ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.tls || inbound.xtls">
|
||||
<p v-if="inbound.tls">tls: <a-tag color="green">{{ i18n "turnOn" }}</a-tag></p>
|
||||
<p v-if="inbound.xtls">xtls: <a-tag color="green">{{ i18n "turnOn" }}</a-tag></p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>tls: <a-tag color="red">{{ i18n "closure" }}</a-tag></p>
|
||||
</template>
|
||||
<p v-if="inbound.tls">
|
||||
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
</p>
|
||||
<p v-if="inbound.xtls">
|
||||
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
</p>
|
||||
{{end}}
|
||||
|
||||
|
||||
{{define "component/inboundInfoComponent"}}
|
||||
<div>
|
||||
<p>{{ i18n "protocol"}}: <a-tag color="green">[[ dbInbound.protocol ]]</a-tag></p>
|
||||
<p>{{ i18n "pages.inbounds.address"}}: <a-tag color="blue">[[ dbInbound.address ]]</a-tag></p>
|
||||
<p>{{ i18n "pages.inbounds.port"}}: <a-tag color="green">[[ dbInbound.port ]]</a-tag></p>
|
||||
|
||||
<template v-if="dbInbound.isVMess" v-for="(vmess, index) in inbound.settings.vmesses">
|
||||
<p>uuid: <a-tag color="green">[[ vmess.id ]]</a-tag></p>
|
||||
<p>alterId: <a-tag color="green">[[ vmess.alterId ]]</a-tag></p>
|
||||
<a-divider style="height: 2px; background-color: #7e7e7e" />
|
||||
</template>
|
||||
|
||||
<template v-if="dbInbound.isVLess" v-for="(vless, index) in inbound.settings.vlesses">
|
||||
<p>uuid: <a-tag color="green">[[ vless.id ]]</a-tag></p>
|
||||
<p v-if="inbound.isXTls">flow: <a-tag color="green">[[ vless.flow ]]</a-tag></p>
|
||||
<a-divider style="height: 2px; background-color: #7e7e7e" />
|
||||
</template>
|
||||
|
||||
<template v-if="dbInbound.isTrojan">
|
||||
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="dbInbound.isSS">
|
||||
<p>{{ i18n "encryption"}}: <a-tag color="green">[[ inbound.method ]]</a-tag></p>
|
||||
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="dbInbound.isSocks">
|
||||
<p>{{ i18n "username"}}: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
|
||||
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="dbInbound.isHTTP">
|
||||
<p>{{ i18n "username"}}: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
|
||||
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
|
||||
</template>
|
||||
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
{{template "inboundInfoStream"}}
|
||||
</template>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "component/inboundInfo"}}
|
||||
<script>
|
||||
Vue.component('inbound-info', {
|
||||
delimiters: ['[[', ']]'],
|
||||
props: ["dbInbound", "inbound"],
|
||||
template: `{{template "component/inboundInfoComponent"}}`,
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
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"}}
|
||||
<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-list-item-meta :title="title" :description="desc"/>
|
||||
</a-col>
|
||||
@@ -9,10 +15,7 @@
|
||||
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
|
||||
</template>
|
||||
<template v-else-if="type === 'number'">
|
||||
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)"></a-input>
|
||||
</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>
|
||||
<a-input-number :value="value" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number>
|
||||
</template>
|
||||
<template v-else-if="type === 'switch'">
|
||||
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
|
||||
@@ -25,7 +28,7 @@
|
||||
{{define "component/setting"}}
|
||||
<script>
|
||||
Vue.component('setting-list-item', {
|
||||
props: ["type", "title", "desc", "value"],
|
||||
props: ["type", "title", "desc", "value", "min"],
|
||||
template: `{{template "component/settingListItem"}}`,
|
||||
});
|
||||
</script>
|
||||
|
||||
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}}
|
||||
151
web/html/xui/form/client.html
Normal file
@@ -0,0 +1,151 @@
|
||||
{{define "form/client"}}
|
||||
<a-form layout="inline" v-if="client">
|
||||
<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>
|
||||
</template>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.enable" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="client.enable"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.email" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
|
||||
<td>password
|
||||
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.password" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
|
||||
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.id" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.protocol === Protocols.VMESS">
|
||||
<td>{{ i18n "additional" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="client.alterId"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.subSettings.enable">
|
||||
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.subId" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.tgId" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</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>
|
||||
{{end}}
|
||||
@@ -1,57 +1,91 @@
|
||||
{{define "form/inbound"}}
|
||||
<!-- base -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "remark" }}'>
|
||||
<a-input v-model.trim="dbInbound.remark"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "enable" }}'>
|
||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="inbound.protocol" style="width: 160px;">
|
||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
{{ i18n "monitor" }}
|
||||
<a-tooltip>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "enable" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "remark" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="dbInbound.remark" style="width: 250px;"></a-input>
|
||||
</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">
|
||||
<span>{{ i18n "pages.inbounds.monitorDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="inbound.listen"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input type="number" v-model.number="inbound.port"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<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>
|
||||
</span>
|
||||
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<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>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.listen" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.port" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.port"></a-input-number>
|
||||
</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="dbInbound.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</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>
|
||||
|
||||
<!-- vmess settings -->
|
||||
|
||||
@@ -1,17 +1,42 @@
|
||||
{{define "form/dokodemo"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
|
||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
|
||||
<a-input type="number" v-model.number="inbound.settings.port"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
|
||||
<a-select v-model="inbound.settings.network" style="width: 100px;">
|
||||
<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>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.targetAddress"}}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.destinationPort"}}</td>
|
||||
<td>
|
||||
<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>
|
||||
{{end}}
|
||||
@@ -1,10 +1,22 @@
|
||||
{{define "form/http"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "username"}}'>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "username"}}</td>
|
||||
<td>
|
||||
<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>
|
||||
{{end}}
|
||||
@@ -1,19 +1,148 @@
|
||||
{{define "form/shadowsocks"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "encryption" }}'>
|
||||
<a-select v-model="inbound.settings.method" style="width: 165px;">
|
||||
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.settings.password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
||||
<a-select v-model="inbound.settings.network" style="width: 100px;">
|
||||
<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>
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<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 v-if="client.email && app.subSettings.enable">
|
||||
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<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>
|
||||
{{end}}
|
||||
@@ -1,24 +1,49 @@
|
||||
{{define "form/socks"}}
|
||||
<a-form layout="inline">
|
||||
<!-- <a-form-item label="密码认证">-->
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-switch :checked="inbound.settings.auth === 'password'"
|
||||
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<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'">
|
||||
<a-form-item label='{{ i18n "username" }}'>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
|
||||
</a-form-item>
|
||||
<tr>
|
||||
<td>{{ i18n "username" }}</td>
|
||||
<td>
|
||||
<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>
|
||||
</template>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.settings.udp"
|
||||
label="IP">
|
||||
<a-input v-model.trim="inbound.settings.ip"></a-input>
|
||||
</a-form-item>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.enable" }} udp</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
</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>
|
||||
{{end}}
|
||||
@@ -1,49 +1,148 @@
|
||||
{{define "form/trojan"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.settings.clients[0].password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="inbound.settings.clients[0].flow" style="width: 150px">
|
||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<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" }}'>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>password</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.password" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.subSettings.enable">
|
||||
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<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.trojans.length">
|
||||
<table width="100%">
|
||||
<tr class="client-table-header">
|
||||
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
|
||||
</tr>
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp && inbound.tls">
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-row>
|
||||
<a-button type="primary" size="small"
|
||||
@click="inbound.settings.addTrojanFallback()">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="fallbacks">
|
||||
<a-row>
|
||||
<a-button type="primary" size="small"
|
||||
@click="inbound.settings.addTrojanFallback()">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- trojan fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
||||
<a-divider>
|
||||
fallback[[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
</a-divider>
|
||||
<a-form-item label="name">
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="alpn">
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="path">
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="dest">
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="xver">
|
||||
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
||||
</a-form-item>
|
||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||
</a-form>
|
||||
<!-- trojan fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
||||
<a-divider>
|
||||
fallback[[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
</a-divider>
|
||||
<a-form-item label="name">
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="alpn">
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="path">
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="dest">
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="xver">
|
||||
<a-input-number v-model.number="fallback.xver"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||
</a-form>
|
||||
</template>
|
||||
{{end}}
|
||||
@@ -1,181 +1,159 @@
|
||||
{{define "form/vless"}}
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(vless, index) in inbound.settings.vlesses"
|
||||
:key="`vless-${index}`">
|
||||
|
||||
<a-collapse-panel :header="getHeaderText(vless.email)">
|
||||
<a-tag v-if="isExpiry(index) || ((getUpStats(vless.email) + getDownStats(vless.email)) > vless.totalGB && vless.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<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" }}">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
<!--Renew Svg Icon-->
|
||||
<svg
|
||||
@click="getNewEmail(vless)"
|
||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
|
||||
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="vless.email"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
IP Count Limit
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.id" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.canEnableTlsFlow()">
|
||||
<td>flow</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :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 v-if="client.email && app.subSettings.enable">
|
||||
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<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">
|
||||
disable inbound if more than entered count (0 for disable limit ip)
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
|
||||
<a-input type="number" v-model.number="vless.limitIp" min="0" ></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="vless.email && vless.limitIp > 0 && isEdit">
|
||||
<span slot="label">
|
||||
IP log
|
||||
</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">
|
||||
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
clear the log
|
||||
</template>
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete" @click="clearDBClientIps(vless.email,$event)"></a-icon>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-form layout="block">
|
||||
|
||||
<a-textarea readonly @click="getDBClientIps(vless.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 0.5, maxRows: 10 }">
|
||||
</a-textarea>
|
||||
</a-form>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form-item label="id">
|
||||
<a-input v-model.trim="vless.id"></a-input>
|
||||
</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.vlesses.length">
|
||||
<table width="100%">
|
||||
<tr class="client-table-header">
|
||||
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
|
||||
</tr>
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp && inbound.tls">
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-row>
|
||||
<a-button type="primary" size="small"
|
||||
@click="inbound.settings.addFallback()">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="vless.flow" style="width: 150px">
|
||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<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>
|
||||
</span>
|
||||
<a-input-number v-model="vless._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<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>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="vless._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
<a-form layout="inline">
|
||||
<a-tooltip v-if="vless._totalGB > 0">
|
||||
<template slot="title">
|
||||
reset traffic
|
||||
</template>
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete" @click="resetClientTraffic(vless,$event)"></a-icon>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<a-tag color="blue">[[ sizeFormat(getUpStats(vless.email)) ]] / [[ sizeFormat(getDownStats(vless.email)) ]]</a-tag>
|
||||
<a-tag v-if="vless._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vless.email) + getDownStats(vless.email)) ]]</a-tag>
|
||||
<a-tag>
|
||||
<svg
|
||||
|
||||
@click="addClient(inbound.protocol,vless, inbound.settings.vlesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 22 22"
|
||||
width="22"
|
||||
height="22"
|
||||
class="mt-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="green"
|
||||
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
||||
/>
|
||||
</svg>
|
||||
</a-tag>
|
||||
<a-tag v-show="inbound.settings.vlesses.length > 1">
|
||||
<svg
|
||||
@click="removeClient(index, inbound.settings.vlesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 22 22"
|
||||
width="22"
|
||||
height="22"
|
||||
class="mt-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="#EC4899"
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
||||
/>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</a-form>
|
||||
|
||||
|
||||
</a-form>
|
||||
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="fallbacks">
|
||||
<a-row>
|
||||
<a-button type="primary" size="small"
|
||||
@click="inbound.settings.addFallback()">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vless fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
||||
<a-divider>
|
||||
fallback[[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
</a-divider>
|
||||
<a-form-item label="name">
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="alpn">
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="path">
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="dest">
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="xver">
|
||||
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
||||
</a-form-item>
|
||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||
</a-form>
|
||||
<!-- vless fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
||||
<a-divider>
|
||||
fallback[[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
</a-divider>
|
||||
<a-form-item label="name">
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="alpn">
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="path">
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="dest">
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="xver">
|
||||
<a-input-number v-model.number="fallback.xver"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||
</a-form>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
@@ -1,155 +1,124 @@
|
||||
{{define "form/vmess"}}
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses"
|
||||
:key="`vmess-${index}`">
|
||||
<a-collapse-panel :header="getHeaderText(vmess.email)">
|
||||
<a-tag v-if="isExpiry(index) || ((getUpStats(vmess.email) + getDownStats(vmess.email)) > vmess.totalGB && vmess.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<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" }}">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
<!--Renew Svg Icon-->
|
||||
<svg
|
||||
@click="getNewEmail(vmess)"
|
||||
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
|
||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="vmess.email"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
IP Count Limit
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.id" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "additional" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="client.alterId"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.subSettings.enable">
|
||||
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<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">
|
||||
disable inbound if more than entered count (0 for disable limit ip)
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
|
||||
<a-input type="number" v-model.number="vmess.limitIp" min="0" ></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="vmess.email && vmess.limitIp > 0 && isEdit">
|
||||
<span slot="label">
|
||||
IP Log
|
||||
</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">
|
||||
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
clear the log
|
||||
</template>
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete" @click="clearDBClientIps(vmess.email,$event)"></a-icon>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
|
||||
<a-textarea readonly @click="getDBClientIps(vmess.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 0.5, maxRows: 10 }">
|
||||
</a-textarea>
|
||||
</a-form-item>
|
||||
|
||||
</a-form>
|
||||
<a-form-item label="id">
|
||||
<a-input v-model.trim="vmess.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "additional" }} ID'>
|
||||
<a-input type="number" v-model.number="vmess.alterId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<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>
|
||||
</span>
|
||||
<a-input-number v-model="vmess._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<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>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="vmess._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
<a-form layout="inline">
|
||||
<a-tooltip v-if="vmess._totalGB > 0">
|
||||
<template slot="title">
|
||||
reset traffic
|
||||
</template>
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete" @click="resetClientTraffic(vmess,$event)"></a-icon>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag>
|
||||
<a-tag v-if="vmess._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]]</a-tag>
|
||||
<a-tag>
|
||||
<!--Add Svg Icon-->
|
||||
<svg
|
||||
|
||||
@click="addClient(inbound.protocol,vmess, inbound.settings.vmesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 22 22"
|
||||
width="22"
|
||||
height="22"
|
||||
class="mt-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="green"
|
||||
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
||||
/>
|
||||
</svg>
|
||||
</a-tag>
|
||||
<a-tag v-show="inbound.settings.vmesses.length > 1">
|
||||
|
||||
<!--Remove Svg Icon-->
|
||||
<svg
|
||||
@click="removeClient(index, inbound.settings.vmesses)"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 22 22"
|
||||
width="22"
|
||||
height="22"
|
||||
class="mt-2 cursor-pointer"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="#EC4899"
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
||||
/>
|
||||
</svg>
|
||||
</a-tag>
|
||||
</a-form>
|
||||
|
||||
|
||||
</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.vmesses.length">
|
||||
<table width="100%">
|
||||
<tr class="client-table-header">
|
||||
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
|
||||
</tr>
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
|
||||
</a-collapse>
|
||||
|
||||
|
||||
</a-form>
|
||||
</a-form>
|
||||
<a-form layout="inline">
|
||||
<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>
|
||||
|
||||
{{end}}
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
sniffing
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
|
||||
<span>{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
|
||||
</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>
|
||||
{{end}}
|
||||
@@ -1,7 +1,22 @@
|
||||
{{define "form/streamGRPC"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="serviceName">
|
||||
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<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>
|
||||
{{end}}
|
||||
@@ -1,12 +1,24 @@
|
||||
{{define "form/streamHTTP"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.http.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="host">
|
||||
<a-row v-for="(host, index) in inbound.stream.http.host">
|
||||
<a-input v-model.trim="inbound.stream.http.host[index]"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "path" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.http.path" style="width: 250px;"></a-input>
|
||||
</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>
|
||||
{{end}}
|
||||
@@ -1,38 +1,85 @@
|
||||
{{define "form/streamKCP"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;">
|
||||
<a-select-option value="none">none(not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
|
||||
<a-select-option value="utp">utp(camouflage BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video(camouflage WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls(camouflage DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard(camouflage wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.number="inbound.stream.kcp.seed"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="mtu">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.mtu"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="tti (ms)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.tti"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uplink capacity (MB/S)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.upCap"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="downlink capacity (MB/S)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.downCap"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="congestion">
|
||||
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="read buffer size (MB)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.readBuffer"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="write buffer size (MB)">
|
||||
<a-input type="number" v-model.number="inbound.stream.kcp.writeBuffer"></a-input>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "camouflage" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.kcp.type" style="width: 250px;" :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>
|
||||
<tr>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model="inbound.stream.kcp.seed" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>mtu</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.mtu"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
{{end}}
|
||||
@@ -1,24 +1,41 @@
|
||||
{{define "form/streamQUIC"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
||||
<a-select v-model="inbound.stream.quic.security" style="width: 165px;">
|
||||
<a-select-option value="none">none</a-select-option>
|
||||
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
||||
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="inbound.stream.quic.type" style="width: 280px;">
|
||||
<a-select-option value="none">none(not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
|
||||
<a-select-option value="utp">utp(camouflage BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video(camouflage WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls(camouflage DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard(camouflage wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.quic.encryption" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.quic.security" style="width: 200px;" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="none">none</a-select-option>
|
||||
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
||||
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.quic.key" style="width: 200px;"></a-input>
|
||||
</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>
|
||||
{{end}}
|
||||
@@ -1,8 +1,9 @@
|
||||
{{define "form/streamSettings"}}
|
||||
<!-- select stream network -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="inbound.stream.network" @change="streamNetworkChange">
|
||||
<a-form-item label="{{ i18n "transmission" }}">
|
||||
<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="kcp">kcp</a-select-option>
|
||||
<a-select-option value="ws">ws</a-select-option>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<a-form-item label="acceptProxyProtocol">
|
||||
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="http 伪装">
|
||||
<a-form-item label="http {{ i18n "camouflage" }}">
|
||||
<a-switch
|
||||
:checked="inbound.stream.tcp.type === 'http'"
|
||||
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
|
||||
@@ -13,74 +13,109 @@
|
||||
</a-form>
|
||||
|
||||
<!-- tcp request -->
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'"
|
||||
layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestPath" }}'>
|
||||
<a-row v-for="(path, index) in inbound.stream.tcp.request.path">
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.path[index]"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.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.request.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tcp response -->
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'"
|
||||
layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
|
||||
</a-form-item>
|
||||
<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>
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.version" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.method" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.requestPath" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-row v-for="(path, index) in inbound.stream.tcp.request.path">
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.path[index]" style="width: 200px;"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.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.request.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- tcp response -->
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.version" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.status" style="width: 200px;"></a-input>
|
||||
</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>
|
||||
{{end}}
|
||||
@@ -1,33 +1,47 @@
|
||||
{{define "form/streamWS"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="acceptProxyProtocol">
|
||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.ws.addHeader('Host', '')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.ws.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.ws.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>acceptProxyProtocol</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "path" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.ws.path" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<a-row>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.ws.addHeader('Host', '')">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-input-group v-for="(header, index) in inbound.stream.ws.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.ws.removeHeader(index)">
|
||||
-
|
||||
</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,53 +1,267 @@
|
||||
{{define "form/tlsSettings"}}
|
||||
<!-- tls enable -->
|
||||
<a-form layout="inline" v-if="inbound.canSetTls()">
|
||||
<a-form-item label="tls">
|
||||
<a-form v-if="inbound.canSetTls()" layout="inline">
|
||||
<a-form-item label="TLS">
|
||||
<a-switch v-model="inbound.tls">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.canEnableXTls()" label="xtls">
|
||||
<a-switch v-model="inbound.xtls"></a-switch>
|
||||
<a-form-item v-if="inbound.canEnableReality()" label="Reality">
|
||||
<a-switch v-model="inbound.reality"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tls settings -->
|
||||
<a-form v-if="inbound.tls || inbound.xtls"
|
||||
layout="inline">
|
||||
<a-form-item label='{{ i18n "domainName" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="alpn" placeholder="http/1.1,h2">
|
||||
<a-input v-model.trim="inbound.stream.tls.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="inbound.stream.tls.certs[0].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-form-item>
|
||||
<template v-if="inbound.stream.tls.certs[0].useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||
<a-input type="textarea" :rows="2"
|
||||
v-model="inbound.stream.tls.certs[0].cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
|
||||
<a-input type="textarea" :rows="2"
|
||||
v-model="inbound.stream.tls.certs[0].key"></a-input>
|
||||
</a-form-item>
|
||||
<a-form v-if="inbound.tls" layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>SNI</td>
|
||||
<td>
|
||||
<a-form-item placeholder="Server Name Indication" v-if="inbound.tls">
|
||||
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CipherSuites</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option value="">auto</a-select-option>
|
||||
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MinVersion</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MaxVersion</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.darkCardClass">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>uTLS</td>
|
||||
<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>Multi Domain</td>
|
||||
<td>
|
||||
<a-switch v-model="multiDomain"></a-switch>
|
||||
<a-button v-if="multiDomain" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})">+</a-button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="multiDomain">
|
||||
<td colspan="2">
|
||||
<a-form-item>
|
||||
<a-input-group v-for="(row, index) in inbound.stream.tls.settings.domains">
|
||||
<a-input style="width: 40%" v-model.trim="row.remark" addon-before='{{ i18n "remark" }}'></a-input>
|
||||
<a-input style="width: 60%" v-model.trim="row.domain" addon-before='{{ i18n "host" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small" style="margin: 0px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<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>
|
||||
</table>
|
||||
</a-form>
|
||||
<a-form layout="inline" v-else-if = "inbound.stream.network === 'tcp' ">
|
||||
<a-form-item label="tcp-acceptProxyProtocol">
|
||||
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 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>
|
||||
{{end}}
|
||||
47
web/html/xui/inbound_client_table.html
Normal file
@@ -0,0 +1,47 @@
|
||||
{{define "client_table"}}
|
||||
<template slot="actions" slot-scope="text, client, index">
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "qrCode" }}</template>
|
||||
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index,'',client.email);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
||||
<a-icon style="font-size: 24px;" type="edit" @click="openEditClient(record.id,client);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "info" }}</template>
|
||||
<a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||
<a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
<template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template>
|
||||
<a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
|
||||
</a-tooltip>
|
||||
</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">
|
||||
[[ client.email ]]
|
||||
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, client">
|
||||
<a-tag :color="statsColor(record, client.email)">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
|
||||
<template v-if="client._totalGB > 0">
|
||||
<a-tag :color="statsColor(record, client.email)">[[client._totalGB]]GB</a-tag>
|
||||
</template>
|
||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, client, index">
|
||||
<template v-if="client.expiryTime > 0">
|
||||
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</a-tag>
|
||||
</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>
|
||||
</template>
|
||||
{{end}}
|
||||
@@ -1,46 +1,251 @@
|
||||
{{define "inboundInfoModal"}}
|
||||
{{template "component/inboundInfo"}}
|
||||
<a-modal id="inbound-info-modal" v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' @ok="infoModal.ok"
|
||||
:closable="true" :mask-closable="true"
|
||||
ok-text='{{ i18n "pages.inbounds.copyLink"}}' cancel-text='{{ i18n "close" }}' :ok-button-props="infoModal.okBtnPros">
|
||||
<inbound-info :db-inbound="dbInbound" :inbound="inbound"></inbound-info>
|
||||
<a-modal id="inbound-info-modal"
|
||||
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
|
||||
:closable="true"
|
||||
:mask-closable="true"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:footer="null"
|
||||
width="600px"
|
||||
>
|
||||
<table style="margin-bottom: 10px; width: 100%;">
|
||||
<tr><td>
|
||||
<table>
|
||||
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr>
|
||||
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr>
|
||||
<tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag color="green">[[ dbInbound.port ]]</a-tag></td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
<table>
|
||||
<tr>
|
||||
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
|
||||
</tr>
|
||||
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
|
||||
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
|
||||
<tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
|
||||
|
||||
<tr v-if="inbound.path"><td>{{ i18n "path" }}</td><td><a-tag color="green">[[ inbound.path ]]</a-tag></td></tr>
|
||||
<tr v-else><td>{{ i18n "path" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isQuic">
|
||||
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></td></tr>
|
||||
<tr><td>quic {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.quicKey ]]</a-tag></td></tr>
|
||||
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag color="green">[[ inbound.quicType ]]</a-tag></td></tr>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isKcp">
|
||||
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.kcpType ]]</a-tag></td></tr>
|
||||
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></td></tr>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isGrpc">
|
||||
<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>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr colspan="2" v-if="dbInbound.hasLink()">
|
||||
<td v-if="inbound.tls">
|
||||
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>
|
||||
</td>
|
||||
<td v-else-if="inbound.reality">
|
||||
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
||||
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
|
||||
</td>
|
||||
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="infoModal.clientSettings">
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<table style="margin-bottom: 10px;">
|
||||
<tr v-for="col,index in Object.keys(infoModal.clientSettings).slice(0, 3)">
|
||||
<td>[[ col ]]</td>
|
||||
<td><a-tag color="green">[[ infoModal.clientSettings[col] ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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 style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<th>{{ i18n "usage" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
|
||||
<tr>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientStats" color="green">
|
||||
[[ sizeFormat(infoModal.clientStats['up']) ]] /
|
||||
[[ sizeFormat(infoModal.clientStats['down']) ]]
|
||||
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
|
||||
</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">[[ sizeFormat(infoModal.clientSettings.totalGB) ]]</a-tag>
|
||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings.expiryTime > 0">
|
||||
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
|
||||
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
|
||||
</a-tag>
|
||||
</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>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
|
||||
<a-divider>Subscription link</a-divider>
|
||||
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
|
||||
<a-icon id="copy-sub-link" type="snippets" @click="copyToClipboard('copy-sub-link', infoModal.subLink)"></a-icon>
|
||||
</template>
|
||||
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
|
||||
<a-divider>Telegram Username</a-divider>
|
||||
<a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a>
|
||||
<a-icon id="copy-tg-link" type="snippets" @click="copyToClipboard('copy-tg-link', '@' + infoModal.clientSettings.tgId)"></a-icon>
|
||||
</template>
|
||||
</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()">
|
||||
<a-divider>URL</a-divider>
|
||||
<a-row v-for="(link,index) in infoModal.links">
|
||||
<a-col :span="21"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
|
||||
<a-col :span="3" style="text-align: right;">
|
||||
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
|
||||
<a-icon type="snippets"></a-icon>{{ i18n "copy" }}
|
||||
</button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const infoModal = {
|
||||
visible: false,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
settings: null,
|
||||
clientSettings: null,
|
||||
clientStats: [],
|
||||
upStats: 0,
|
||||
downStats: 0,
|
||||
clipboard: null,
|
||||
okBtnPros: {
|
||||
attrs: {
|
||||
id: "inbound-info-modal-ok-btn",
|
||||
style: "",
|
||||
},
|
||||
},
|
||||
show(dbInbound) {
|
||||
links: [],
|
||||
index: null,
|
||||
isExpired: false,
|
||||
subLink: '',
|
||||
tgLink: '',
|
||||
show(dbInbound, index) {
|
||||
this.index = index;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.visible = true;
|
||||
|
||||
if (dbInbound.hasLink()) {
|
||||
this.okBtnPros.attrs.style = "";
|
||||
} else {
|
||||
this.okBtnPros.attrs.style = "display: none";
|
||||
}
|
||||
|
||||
if (this.clipboard == null) {
|
||||
infoModalApp.$nextTick(() => {
|
||||
this.clipboard = new ClipboardJS(`#${this.okBtnPros.attrs.id}`, {
|
||||
text: () => this.dbInbound.genLink(),
|
||||
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.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
||||
remark = this.dbInbound.remark + "-" + this.clientSettings.email;
|
||||
address = this.dbInbound.address;
|
||||
this.links = [];
|
||||
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
|
||||
this.inbound.stream.tls.settings.domains.forEach((domain) => {
|
||||
this.links.push({
|
||||
remark: remark + "-" + domain.remark,
|
||||
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, index)
|
||||
});
|
||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copySuccess" }}'));
|
||||
});
|
||||
} else {
|
||||
this.links.push({
|
||||
remark: remark,
|
||||
link: this.inbound.genLink(address, remark, index)
|
||||
});
|
||||
}
|
||||
if (this.clientSettings) {
|
||||
if (this.clientSettings.subId) {
|
||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||
}
|
||||
if (this.clientSettings.tgId) {
|
||||
this.tgLink = "https://t.me/" + this.clientSettings.tgId;
|
||||
}
|
||||
}
|
||||
this.visible = true;
|
||||
},
|
||||
close() {
|
||||
infoModal.visible = false;
|
||||
},
|
||||
genSubLink(subID) {
|
||||
protocol = app.subSettings.tls ? "https://" : "http://";
|
||||
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
|
||||
subPort = app.subSettings.port;
|
||||
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
|
||||
subPath = app.subSettings.path;
|
||||
return protocol + hostName + port + subPath + subID;
|
||||
}
|
||||
};
|
||||
|
||||
const infoModalApp = new Vue({
|
||||
@@ -53,8 +258,35 @@
|
||||
},
|
||||
get inbound() {
|
||||
return this.infoModal.inbound;
|
||||
},
|
||||
get isActive() {
|
||||
if(infoModal.clientStats){
|
||||
return infoModal.clientStats.enable;
|
||||
}
|
||||
return infoModal.dbInbound.isEnable;
|
||||
},
|
||||
get isEnable() {
|
||||
if(infoModal.clientSettings){
|
||||
return infoModal.clientSettings.enable;
|
||||
}
|
||||
return infoModal.dbInbound.isEnable;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(elmentId,content) {
|
||||
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
|
||||
text: () => content,
|
||||
});
|
||||
this.infoModal.clipboard.on('success', () => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
this.infoModal.clipboard.destroy();
|
||||
});
|
||||
},
|
||||
statsColor(stats) {
|
||||
return usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{{define "inboundModal"}}
|
||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
|
||||
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
{{template "form/inbound"}}
|
||||
</a-modal>
|
||||
@@ -42,6 +43,15 @@
|
||||
loading(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 = {
|
||||
@@ -61,6 +71,7 @@
|
||||
inModal: inModal,
|
||||
Protocols: protocols,
|
||||
SSMethods: SSMethods,
|
||||
delayedStart: false,
|
||||
get inbound() {
|
||||
return inModal.inbound;
|
||||
},
|
||||
@@ -69,126 +80,60 @@
|
||||
},
|
||||
get 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;
|
||||
},
|
||||
get multiDomain() {
|
||||
return this.inbound.stream.tls.settings.domains.length > 0;
|
||||
},
|
||||
set multiDomain(value) {
|
||||
if (value) {
|
||||
inModal.inbound.stream.tls.server = "";
|
||||
inModal.inbound.stream.tls.settings.domains = [{remark: "", domain: window.location.host.split(":")[0]}];
|
||||
} else {
|
||||
inModal.inbound.stream.tls.server = "";
|
||||
inModal.inbound.stream.tls.settings.domains = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
streamNetworkChange(oldValue) {
|
||||
if (oldValue === 'kcp') {
|
||||
this.inModal.inbound.tls = false;
|
||||
streamNetworkChange() {
|
||||
if (!inModal.inbound.canSetTls()) {
|
||||
this.inModal.inbound.stream.security = 'none';
|
||||
}
|
||||
if (!inModal.inbound.canEnableReality()) {
|
||||
this.inModal.inbound.reality = false;
|
||||
}
|
||||
},
|
||||
addClient(protocol,value, clients) {
|
||||
switch (protocol) {
|
||||
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
|
||||
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
||||
default: return null;
|
||||
}
|
||||
setDefaultCertData(index){
|
||||
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
|
||||
inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;
|
||||
},
|
||||
removeClient(index, clients) {
|
||||
clients.splice(index, 1);
|
||||
},
|
||||
|
||||
async getDBClientIps(email,event) {
|
||||
|
||||
const msg = await HttpUtil.post('/xui/inbound/clientIps/'+ email);
|
||||
async getNewX25519Cert(){
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.post('/server/getNewX25519Cert');
|
||||
inModal.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ips = JSON.parse(msg.obj)
|
||||
ips = ips.join(",")
|
||||
event.target.value = ips
|
||||
} catch (error) {
|
||||
// text
|
||||
event.target.value = msg.obj
|
||||
|
||||
}
|
||||
|
||||
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
|
||||
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
|
||||
},
|
||||
async clearDBClientIps(email,event) {
|
||||
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
event.target.value = ""
|
||||
},
|
||||
async resetClientTraffic(client,event) {
|
||||
const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
clientStats = this.inbound.clientStats
|
||||
if(clientStats.length > 0)
|
||||
{
|
||||
for (const key in clientStats) {
|
||||
if (Object.hasOwnProperty.call(clientStats, key)) {
|
||||
if(clientStats[key]['email'] == client.email){
|
||||
clientStats[key]['up'] = 0
|
||||
clientStats[key]['down'] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
isExpiry(index) {
|
||||
return this.inbound.isExpiry(index)
|
||||
},
|
||||
getUpStats(email) {
|
||||
clientStats = this.inbound.clientStats
|
||||
if(clientStats.length > 0)
|
||||
{
|
||||
for (const key in clientStats) {
|
||||
if (Object.hasOwnProperty.call(clientStats, key)) {
|
||||
if(clientStats[key]['email'] == email)
|
||||
return clientStats[key]['up']
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
getDownStats(email) {
|
||||
clientStats = this.inbound.clientStats
|
||||
if(clientStats.length > 0)
|
||||
{
|
||||
for (const key in clientStats) {
|
||||
if (Object.hasOwnProperty.call(clientStats, key)) {
|
||||
if(clientStats[key]['email'] == email)
|
||||
return clientStats[key]['down']
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
isClientEnable(email) {
|
||||
clientStats = this.inbound.clientStats
|
||||
if(clientStats.length > 0)
|
||||
{
|
||||
for (const key in clientStats) {
|
||||
if (Object.hasOwnProperty.call(clientStats, key)) {
|
||||
if(clientStats[key]['email'] == email)
|
||||
return clientStats[key]['enable']
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getHeaderText(email) {
|
||||
if(email == "")
|
||||
return "Add Client"
|
||||
|
||||
return email + (this.isClientEnable(email) == true ? ' Active' : ' Deactive')
|
||||
},
|
||||
|
||||
getNewEmail(client) {
|
||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||
var string = '';
|
||||
var len = 6 + Math.floor(Math.random() * 5)
|
||||
var len = 6 + Math.floor(Math.random() * 5);
|
||||
for(var ii=0; ii<len; ii++){
|
||||
string += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
client.email = string + "@gmail.com"
|
||||
client.email = string;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout">
|
||||
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip="loading">
|
||||
<transition name="list" appear>
|
||||
@@ -24,50 +25,144 @@
|
||||
</a-tag>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable style="margin-bottom: 20px;">
|
||||
<a-card hoverable style="margin-bottom: 20px;" :class="themeSwitcher.darkCardClass">
|
||||
<a-row>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "pages.inbounds.totalDownUp" }}:
|
||||
{{ i18n "pages.inbounds.totalDownUp" }}:
|
||||
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "pages.inbounds.totalUsage" }}:
|
||||
{{ i18n "pages.inbounds.totalUsage" }}:
|
||||
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "pages.inbounds.inboundCount" }}:
|
||||
{{ i18n "pages.inbounds.inboundCount" }}:
|
||||
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "clients" }}:
|
||||
<a-tag color="green">[[ total.clients ]]</a-tag>
|
||||
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
|
||||
<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-row>
|
||||
</a-card>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable>
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
<div slot="title">
|
||||
<a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
|
||||
<a-row>
|
||||
<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>
|
||||
<!-- <a-input v-model="searchKey" placeholder="搜索" autofocus style="max-width: 300px"></a-input>-->
|
||||
<a-switch v-model="enableFilter"
|
||||
checked-children="{{ i18n "search" }}" un-checked-children="{{ i18n "filter" }}"
|
||||
@change="toggleFilter">
|
||||
</a-switch>
|
||||
<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-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||
:data-source="dbInbounds"
|
||||
:loading="spinning" :scroll="{ x: 1500 }"
|
||||
:data-source="searchedInbounds"
|
||||
:loading="spinning" :scroll="{ x: 1300 }"
|
||||
:pagination="false"
|
||||
style="margin-top: 20px"
|
||||
@change="() => getDBInbounds()">
|
||||
<template slot="action" slot-scope="text, dbInbound">
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
|
||||
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)">
|
||||
<a-menu-item v-if="dbInbound.hasLink()" key="qrcode">
|
||||
<a-icon type="qrcode"></a-icon>
|
||||
{{ i18n "qrCode" }}
|
||||
</a-menu-item>
|
||||
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon>
|
||||
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="edit">
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS">
|
||||
<a-menu-item key="addClient">
|
||||
<a-icon type="user-add"></a-icon>
|
||||
{{ i18n "pages.client.add"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="addBulkClient">
|
||||
<a-icon type="usergroup-add"></a-icon>
|
||||
{{ i18n "pages.client.bulk"}}
|
||||
</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-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export"}}
|
||||
</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>
|
||||
<a-menu-item key="resetTraffic">
|
||||
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
|
||||
</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">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
@@ -77,7 +172,35 @@
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<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 slot="traffic" slot-scope="text, dbInbound">
|
||||
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
|
||||
@@ -87,19 +210,8 @@
|
||||
</template>
|
||||
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
|
||||
</template>
|
||||
<template slot="settings" slot-scope="text, dbInbound">
|
||||
<a-button type="link" @click="showInfo(dbInbound)">{{ i18n "check" }}</a-button>
|
||||
</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">
|
||||
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch>
|
||||
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, dbInbound">
|
||||
<template v-if="dbInbound.expiryTime > 0">
|
||||
@@ -110,7 +222,27 @@
|
||||
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-tag v-else color="green">{{ i18n "indefinitely" }}</a-tag>
|
||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||
</template>
|
||||
<template slot="expandedRowRender" slot-scope="record">
|
||||
<a-table
|
||||
v-if="(record.protocol === Protocols.VLESS) || (record.protocol === Protocols.VMESS)"
|
||||
:row-key="client => client.id"
|
||||
:columns="innerColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination="false"
|
||||
>
|
||||
{{template "client_table"}}
|
||||
</a-table>
|
||||
<a-table
|
||||
v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS"
|
||||
:row-key="client => client.id"
|
||||
:columns="innerTrojanColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination="false"
|
||||
>
|
||||
{{template "client_table"}}
|
||||
</a-table>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
@@ -120,12 +252,12 @@
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
<script>
|
||||
|
||||
const columns = [{
|
||||
title: '{{ i18n "pages.inbounds.operate" }}',
|
||||
align: 'center',
|
||||
width: 30,
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'action' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.enable" }}',
|
||||
@@ -133,40 +265,35 @@
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'enable' },
|
||||
}, {
|
||||
title: "Id",
|
||||
title: "ID",
|
||||
align: 'center',
|
||||
dataIndex: "id",
|
||||
width: 30,
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.remark" }}',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
width: 50,
|
||||
dataIndex: "remark",
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.protocol" }}',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'protocol' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.port" }}',
|
||||
align: 'center',
|
||||
dataIndex: "port",
|
||||
width: 60,
|
||||
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" }}↑|↓',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
width: 120,
|
||||
scopedSlots: { customRender: 'traffic' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.details" }}',
|
||||
align: 'center',
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'settings' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.transportConfig" }}',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'stream' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
||||
align: 'center',
|
||||
@@ -174,37 +301,139 @@
|
||||
scopedSlots: { customRender: 'expiryTime' },
|
||||
}];
|
||||
|
||||
const innerColumns = [
|
||||
{ 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.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: 'UID', width: 120, dataIndex: "id" },
|
||||
];
|
||||
|
||||
const innerTrojanColumns = [
|
||||
{ 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.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: 'Password', width: 120, dataIndex: "password" },
|
||||
];
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
siderDrawer,
|
||||
themeSwitcher,
|
||||
spinning: false,
|
||||
inbounds: [],
|
||||
dbInbounds: [],
|
||||
searchKey: '',
|
||||
enableFilter: false,
|
||||
filterBy: '',
|
||||
searchedInbounds: [],
|
||||
expireDiff: 0,
|
||||
trafficDiff: 0,
|
||||
defaultCert: '',
|
||||
defaultKey: '',
|
||||
clientCount: {},
|
||||
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
||||
refreshing: false,
|
||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||
subSettings: {
|
||||
enable : false,
|
||||
port: 0,
|
||||
path: '',
|
||||
domain: '',
|
||||
tls: false
|
||||
},
|
||||
tgBotEnable: false
|
||||
},
|
||||
methods: {
|
||||
loading(spinning=true) {
|
||||
loading(spinning = true) {
|
||||
this.spinning = spinning;
|
||||
},
|
||||
async getDBInbounds() {
|
||||
this.loading();
|
||||
this.refreshing = true;
|
||||
const msg = await HttpUtil.post('/xui/inbound/list');
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
this.setInbounds(msg.obj);
|
||||
setTimeout(() => {
|
||||
this.refreshing = false;
|
||||
}, 500);
|
||||
},
|
||||
async getDefaultSettings() {
|
||||
const msg = await HttpUtil.post('/xui/setting/defaultSettings');
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
with(msg.obj){
|
||||
this.expireDiff = expireDiff * 86400000;
|
||||
this.trafficDiff = trafficDiff * 1073741824;
|
||||
this.defaultCert = defaultCert;
|
||||
this.defaultKey = defaultKey;
|
||||
this.tgBotEnable = tgBotEnable;
|
||||
this.subSettings = {
|
||||
enable : subEnable,
|
||||
port: subPort,
|
||||
path: subPath,
|
||||
domain: subDomain,
|
||||
tls: subTLS
|
||||
};
|
||||
}
|
||||
},
|
||||
setInbounds(dbInbounds) {
|
||||
this.inbounds.splice(0);
|
||||
this.dbInbounds.splice(0);
|
||||
for (const inbound of dbInbounds) {
|
||||
const dbInbound = new DBInbound(inbound);
|
||||
this.inbounds.push(dbInbound.toInbound());
|
||||
to_inbound = dbInbound.toInbound()
|
||||
this.inbounds.push(to_inbound);
|
||||
this.dbInbounds.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) {
|
||||
if (ObjectUtil.isEmpty(key)) {
|
||||
@@ -213,31 +442,111 @@
|
||||
this.searchedInbounds.splice(0, this.searchedInbounds.length);
|
||||
this.dbInbounds.forEach(inbound => {
|
||||
if (ObjectUtil.deepSearch(inbound, key)) {
|
||||
this.searchedInbounds.push(inbound);
|
||||
const newInbound = new DBInbound(inbound);
|
||||
const inboundSettings = JSON.parse(inbound.settings);
|
||||
if (inboundSettings.hasOwnProperty('clients')) {
|
||||
const searchedSettings = { "clients": [] };
|
||||
inboundSettings.clients.forEach(client => {
|
||||
if (ObjectUtil.deepSearch(client, key)) {
|
||||
searchedSettings.clients.push(client);
|
||||
}
|
||||
});
|
||||
newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, searchedSettings);
|
||||
}
|
||||
this.searchedInbounds.push(newInbound);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
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) {
|
||||
switch (action.key) {
|
||||
case "qrcode":
|
||||
this.showQrcode(dbInbound);
|
||||
break;
|
||||
case "showInfo":
|
||||
this.showInfo(dbInbound);
|
||||
break;
|
||||
case "edit":
|
||||
this.openEditInbound(dbInbound);
|
||||
this.openEditInbound(dbInbound.id);
|
||||
break;
|
||||
case "addClient":
|
||||
this.openAddClient(dbInbound.id)
|
||||
break;
|
||||
case "addBulkClient":
|
||||
this.openAddBulkClient(dbInbound.id)
|
||||
break;
|
||||
case "export":
|
||||
this.inboundLinks(dbInbound.id);
|
||||
break;
|
||||
case "resetTraffic":
|
||||
this.resetTraffic(dbInbound);
|
||||
this.resetTraffic(dbInbound.id);
|
||||
break;
|
||||
case "resetClients":
|
||||
this.resetAllClientTraffics(dbInbound.id);
|
||||
break;
|
||||
case "clone":
|
||||
this.openCloneInbound(dbInbound);
|
||||
break;
|
||||
case "delete":
|
||||
this.delInbound(dbInbound);
|
||||
this.delInbound(dbInbound.id);
|
||||
break;
|
||||
case "delDepletedClients":
|
||||
this.delDepletedClients(dbInbound.id)
|
||||
break;
|
||||
}
|
||||
},
|
||||
openAddInbound() {
|
||||
inModal.show({
|
||||
title: '{{ i18n "pages.inbounds.addInbound"}}',
|
||||
okText: '{{ i18n "pages.inbounds.addTo"}}',
|
||||
okText: '{{ i18n "pages.inbounds.create"}}',
|
||||
cancelText: '{{ i18n "close" }}',
|
||||
confirm: async (inbound, dbInbound) => {
|
||||
inModal.loading();
|
||||
@@ -247,11 +556,12 @@
|
||||
isEdit: false
|
||||
});
|
||||
},
|
||||
openEditInbound(dbInbound) {
|
||||
openEditInbound(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
const inbound = dbInbound.toInbound();
|
||||
inModal.show({
|
||||
title: '{{ i18n "pages.inbounds.modifyInbound"}}',
|
||||
okText: '{{ i18n "pages.inbounds.revise"}}',
|
||||
okText: '{{ i18n "pages.inbounds.update"}}',
|
||||
cancelText: '{{ i18n "close" }}',
|
||||
inbound: inbound,
|
||||
dbInbound: dbInbound,
|
||||
@@ -262,6 +572,38 @@
|
||||
},
|
||||
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) {
|
||||
const data = {
|
||||
@@ -276,9 +618,10 @@
|
||||
port: inbound.port,
|
||||
protocol: inbound.protocol,
|
||||
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);
|
||||
},
|
||||
async updateInbound(inbound, dbInbound) {
|
||||
@@ -294,15 +637,80 @@
|
||||
port: inbound.port,
|
||||
protocol: inbound.protocol,
|
||||
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);
|
||||
},
|
||||
resetTraffic(dbInbound) {
|
||||
openAddClient(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
clientModal.show({
|
||||
title: '{{ i18n "pages.client.add"}}',
|
||||
okText: '{{ i18n "pages.client.submitAdd"}}',
|
||||
dbInbound: dbInbound,
|
||||
confirm: async (clients, dbInboundId) => {
|
||||
clientModal.loading();
|
||||
await this.addClient(clients, dbInboundId);
|
||||
clientModal.close();
|
||||
},
|
||||
isEdit: false
|
||||
});
|
||||
},
|
||||
openAddBulkClient(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
clientsBulkModal.show({
|
||||
title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
|
||||
okText: '{{ i18n "pages.client.bulk"}}',
|
||||
dbInbound: dbInbound,
|
||||
confirm: async (clients, dbInboundId) => {
|
||||
clientsBulkModal.loading();
|
||||
await this.addClient(clients, dbInboundId);
|
||||
clientsBulkModal.close();
|
||||
},
|
||||
});
|
||||
},
|
||||
openEditClient(dbInboundId, client) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
clients = this.getInboundClients(dbInbound);
|
||||
index = this.findIndexOfClient(clients, client);
|
||||
clientModal.show({
|
||||
title: '{{ i18n "pages.client.edit"}}',
|
||||
okText: '{{ i18n "pages.client.submitEdit"}}',
|
||||
dbInbound: dbInbound,
|
||||
index: index,
|
||||
confirm: async (client, dbInboundId, clientId) => {
|
||||
clientModal.loading();
|
||||
await this.updateClient(client, dbInboundId, clientId);
|
||||
clientModal.close();
|
||||
},
|
||||
isEdit: true
|
||||
});
|
||||
},
|
||||
findIndexOfClient(clients, client) {
|
||||
firstKey = Object.keys(client)[0];
|
||||
return clients.findIndex(c => c[firstKey] === client[firstKey]);
|
||||
},
|
||||
async addClient(clients, dbInboundId) {
|
||||
const data = {
|
||||
id: dbInboundId,
|
||||
settings: '{"clients": [' + clients.toString() + ']}',
|
||||
};
|
||||
await this.submit(`/xui/inbound/addClient`, data);
|
||||
},
|
||||
async updateClient(client, dbInboundId, clientId) {
|
||||
const data = {
|
||||
id: dbInboundId,
|
||||
settings: '{"clients": [' + client.toString() + ']}',
|
||||
};
|
||||
await this.submit(`/xui/inbound/updateClient/${clientId}`, data);
|
||||
},
|
||||
resetTraffic(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => {
|
||||
@@ -313,60 +721,234 @@
|
||||
},
|
||||
});
|
||||
},
|
||||
delInbound(dbInbound) {
|
||||
delInbound(dbInboundId) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/xui/inbound/del/' + dbInbound.id),
|
||||
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
|
||||
});
|
||||
},
|
||||
showQrcode(dbInbound) {
|
||||
const link = dbInbound.genLink();
|
||||
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
|
||||
delClient(dbInboundId, client) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
clientId = this.getClientId(dbInbound.protocol, client);
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`),
|
||||
});
|
||||
},
|
||||
showInfo(dbInbound) {
|
||||
infoModal.show(dbInbound);
|
||||
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;
|
||||
}
|
||||
},
|
||||
switchEnable(dbInbound) {
|
||||
this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound);
|
||||
getClientId(protocol, client) {
|
||||
switch (protocol) {
|
||||
case Protocols.TROJAN: return client.password;
|
||||
case Protocols.SHADOWSOCKS: return client.email;
|
||||
default: return client.id;
|
||||
}
|
||||
},
|
||||
async submit(url, data, modal) {
|
||||
const msg = await HttpUtil.postWithModal(url, data, modal);
|
||||
showQrcode(dbInbound, clientIndex) {
|
||||
qrModal.show('{{ i18n "qrCode"}}', dbInbound, clientIndex);
|
||||
},
|
||||
showInfo(dbInbound, index) {
|
||||
infoModal.show(dbInbound, index);
|
||||
},
|
||||
switchEnable(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
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) {
|
||||
const msg = await HttpUtil.postWithModal(url, data);
|
||||
if (msg.success) {
|
||||
await this.getDBInbounds();
|
||||
}
|
||||
},
|
||||
getInboundClients(dbInbound) {
|
||||
if (dbInbound.protocol == Protocols.VLESS) {
|
||||
return dbInbound.toInbound().settings.vlesses;
|
||||
} else if (dbInbound.protocol == Protocols.VMESS) {
|
||||
return dbInbound.toInbound().settings.vmesses;
|
||||
} else if (dbInbound.protocol == Protocols.TROJAN) {
|
||||
return dbInbound.toInbound().settings.trojans;
|
||||
} else if (dbInbound.protocol == Protocols.SHADOWSOCKS) {
|
||||
return dbInbound.toInbound().settings.shadowsockses;
|
||||
}
|
||||
},
|
||||
resetClientTraffic(client, dbInboundId) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
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) {
|
||||
return dbInbound.toInbound().isExpiry(index)
|
||||
},
|
||||
getUpStats(dbInbound, email) {
|
||||
if (email.length == 0) return 0
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
||||
return clientStats ? clientStats.up : 0
|
||||
},
|
||||
getDownStats(dbInbound, email) {
|
||||
if (email.length == 0) return 0
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
||||
return clientStats ? clientStats.down : 0
|
||||
},
|
||||
statsColor(dbInbound, email) {
|
||||
if (email.length == 0) return 'blue';
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
||||
return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
|
||||
},
|
||||
isClientEnabled(dbInbound, email) {
|
||||
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
|
||||
return clientStats ? clientStats['enable'] : true
|
||||
},
|
||||
isRemovable(dbInbound_id) {
|
||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
|
||||
},
|
||||
inboundLinks(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}', dbInbound.genInboundLinks, dbInbound.remark);
|
||||
},
|
||||
exportAllLinks() {
|
||||
let copyText = '';
|
||||
for (const dbInbound of this.dbInbounds) {
|
||||
copyText += dbInbound.genInboundLinks
|
||||
}
|
||||
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: {
|
||||
searchKey(value) {
|
||||
this.searchInbounds(value);
|
||||
}
|
||||
searchKey: debounce(function (newVal) {
|
||||
this.searchInbounds(newVal);
|
||||
}, 500)
|
||||
},
|
||||
mounted() {
|
||||
this.getDBInbounds();
|
||||
this.loading();
|
||||
this.getDefaultSettings();
|
||||
if (this.isRefreshEnabled) {
|
||||
this.startDataRefreshLoop();
|
||||
}
|
||||
else {
|
||||
this.getDBInbounds();
|
||||
}
|
||||
this.loading(false);
|
||||
},
|
||||
computed: {
|
||||
total() {
|
||||
let down = 0, up = 0;
|
||||
for (let i = 0; i < this.dbInbounds.length; ++i) {
|
||||
down += this.dbInbounds[i].down;
|
||||
up += this.dbInbounds[i].up;
|
||||
}
|
||||
let clients = 0, deactive = [], depleted = [], expiring = [];
|
||||
this.dbInbounds.forEach(dbInbound => {
|
||||
down += dbInbound.down;
|
||||
up += dbInbound.up;
|
||||
if (this.clientCount[dbInbound.id]) {
|
||||
clients += this.clientCount[dbInbound.id].clients;
|
||||
deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
|
||||
depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);
|
||||
expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);
|
||||
}
|
||||
});
|
||||
return {
|
||||
down: down,
|
||||
up: up,
|
||||
clients: clients,
|
||||
deactive: deactive,
|
||||
depleted: depleted,
|
||||
expiring: expiring,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{{template "inboundModal"}}
|
||||
{{template "promptModal"}}
|
||||
{{template "qrcodeModal"}}
|
||||
{{template "textModal"}}
|
||||
{{template "inboundInfoModal"}}
|
||||
{{template "clientsModal"}}
|
||||
{{template "clientsBulkModal"}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,28 +11,34 @@
|
||||
.ant-col-sm-24 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ant-card-dark h2 {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout">
|
||||
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
|
||||
<transition name="list" appear>
|
||||
<a-row>
|
||||
<a-card hoverable>
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
<a-row>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-row>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.cpu.color"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:percent="status.cpu.percent"></a-progress>
|
||||
<div>CPU</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.mem.color"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:percent="status.mem.percent"></a-progress>
|
||||
<div>
|
||||
{{ 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-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.swap.color"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:percent="status.swap.percent"></a-progress>
|
||||
<div>
|
||||
swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
|
||||
@@ -53,6 +60,7 @@
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.disk.color"
|
||||
:class="themeSwitcher.darkCardClass"
|
||||
:percent="status.disk.percent"></a-progress>
|
||||
<div>
|
||||
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
|
||||
@@ -67,23 +75,15 @@
|
||||
<transition name="list" appear>
|
||||
<a-row>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
{{ 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="green" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
||||
<a-tag color="blue" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch"}}</a-tag>
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
x-ui: <a href="https://github.com/alireza0/x-ui/releases" target="_blank"><a-tag color="green">{{ .cur_ver }}</a-tag></a>
|
||||
xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
{{ i18n "pages.index.operationHours" }}:
|
||||
<a-tag color="#87d068">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.operationHoursDesc" }}
|
||||
@@ -93,12 +93,35 @@
|
||||
</a-card>
|
||||
</a-col>
|
||||
<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] ]]
|
||||
</a-card>
|
||||
</a-col>
|
||||
<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 ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
@@ -109,7 +132,7 @@
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-up"></a-icon>
|
||||
@@ -135,7 +158,7 @@
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
<a-card hoverable :class="themeSwitcher.darkCardClass">
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="cloud-upload"></a-icon>
|
||||
@@ -164,9 +187,11 @@
|
||||
</transition>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
|
||||
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
||||
: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.xraySwitchClickDesk"}}</h2>
|
||||
<template v-for="version, index in versionModal.versions">
|
||||
@@ -176,8 +201,59 @@
|
||||
</a-tag>
|
||||
</template>
|
||||
</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>
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "textModal"}}
|
||||
<script>
|
||||
|
||||
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({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
siderDrawer,
|
||||
themeSwitcher,
|
||||
status: new Status(),
|
||||
versionModal,
|
||||
logModal,
|
||||
backupModal,
|
||||
spinning: false,
|
||||
loadingTip: '{{ i18n "loading"}}',
|
||||
},
|
||||
@@ -307,15 +423,93 @@
|
||||
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
|
||||
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
|
||||
okText: '{{ i18n "confirm"}}',
|
||||
class: themeSwitcher.darkCardClass,
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: async () => {
|
||||
versionModal.hide();
|
||||
this.loading(true, '{{ i18n "pages.index.dontRefreshh"}}');
|
||||
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
||||
await HttpUtil.post(`/server/installXray/${version}`);
|
||||
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() {
|
||||
while (true) {
|
||||
|
||||
@@ -1,196 +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">修改</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="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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
845
web/html/xui/settings.html
Normal file
@@ -0,0 +1,845 @@
|
||||
<!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-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }}'>
|
||||
<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.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></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",
|
||||
"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>
|
||||