mirror of
https://github.com/alireza0/x-ui.git
synced 2026-03-14 05:23:09 +00:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02a4f0c7d4 | ||
|
|
e0a64a8257 | ||
|
|
38db94f507 | ||
|
|
d0ce67a8f0 | ||
|
|
fcc3f67181 | ||
|
|
357630b077 | ||
|
|
1246b54ffa | ||
|
|
2e3119ab9b | ||
|
|
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 |
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -8,13 +8,13 @@ on:
|
||||
jobs:
|
||||
linuxamd64build:
|
||||
name: build x-ui amd64 version
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.20'
|
||||
go-version: 'stable'
|
||||
- name: build linux amd64 version
|
||||
run: |
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
|
||||
@@ -46,11 +46,11 @@ jobs:
|
||||
prerelease: true
|
||||
linuxarm64build:
|
||||
name: build x-ui arm64 version
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.20'
|
||||
- name: build linux arm64 version
|
||||
@@ -86,11 +86,11 @@ jobs:
|
||||
prerelease: true
|
||||
linuxs390xbuild:
|
||||
name: build x-ui s390x version
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.20'
|
||||
- name: build linux s390x version
|
||||
@@ -123,4 +123,4 @@ jobs:
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-linux-s390x.tar.gz
|
||||
asset_name: x-ui-linux-s390x.tar.gz
|
||||
prerelease: true
|
||||
prerelease: true
|
||||
|
||||
80
README.md
80
README.md
@@ -11,12 +11,15 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
|
||||
| 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 | :heavy_check_mark: |
|
||||
| Calculate expire date on first usage | :heavy_check_mark: |
|
||||
|
||||
**If you think this project is helpful to you, you may wish to give a** :star2:
|
||||
|
||||
@@ -24,25 +27,61 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
|
||||
|
||||
- System Status Monitoring
|
||||
- Search within all inbounds and clients
|
||||
- Support Dark/Light theme UI
|
||||
- Support multi-user multi-protocol, web page visualization operation
|
||||
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
|
||||
- Support for configuring more transport configurations
|
||||
- Traffic statistics, limit traffic, limit expiration time
|
||||
- Customizable xray configuration templates
|
||||
- Support subscription ( multi ) link
|
||||
- Detect users which are expiring or exceed traffic limit soon
|
||||
- Support https access panel (self-provided domain name + ssl certificate)
|
||||
- Support one-click SSL certificate application and automatic renewal
|
||||
- For more advanced configuration items, please refer to the panel
|
||||
|
||||
## 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 | "/delClient/:email" | Delete Client |
|
||||
| POST | "/updateClient/:index" | Update Client |
|
||||
| POST | "/:id/resetClientTraffic/:email" | Reset Client's Traffic |
|
||||
| POST | "/resetAllTraffics" | Reset traffics of all inbounds |
|
||||
| POST | "/resetAllClientTraffics/:id" | Reset traffics of all clients in an inbound |
|
||||
|
||||
# Screenshot from Inbouds page
|
||||
|
||||

|
||||

|
||||
|
||||
# Install & Upgrade
|
||||
## suggestion system
|
||||
|
||||
- CentOS 8+
|
||||
- Ubuntu 20+
|
||||
- Debian 8+
|
||||
- Fedora 36+
|
||||
|
||||
# Install & Upgrade to latest version
|
||||
|
||||
```
|
||||
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.4.0`:
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.4.0
|
||||
```
|
||||
|
||||
## Manual install & upgrade
|
||||
|
||||
1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases , generally choose Architecture `amd64`
|
||||
@@ -65,8 +104,6 @@ systemctl restart x-ui
|
||||
|
||||
## Install using docker
|
||||
|
||||
> This docker tutorial and docker image are provided by [alireza0](https://github.com/alireza0)
|
||||
|
||||
1. install docker
|
||||
|
||||
```shell
|
||||
@@ -77,7 +114,9 @@ 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 \
|
||||
@@ -91,6 +130,8 @@ docker build -t x-ui .
|
||||
```
|
||||
|
||||
## SSL certificate application
|
||||
<details>
|
||||
<summary>Click for details</summary>
|
||||
|
||||
### Cloudflare
|
||||
|
||||
@@ -105,16 +146,19 @@ ln -s /snap/bin/certbot /usr/bin/certbot
|
||||
|
||||
certbot certonly --standalone --register-unsafely-without-email --non-interactive --agree-tos -d <Your Domain Name>
|
||||
```
|
||||
</details>
|
||||
|
||||
## Tg robot use
|
||||
<details>
|
||||
<summary>Click for details</summary>
|
||||
|
||||
> This feature and tutorial are provided by [FranzKafkaYu](https://github.com/FranzKafkaYu)
|
||||
|
||||
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
|
||||
@@ -124,6 +168,7 @@ Set the robot-related parameters in the panel background, including:
|
||||
Reference syntax:
|
||||
|
||||
- 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
|
||||
@@ -134,24 +179,19 @@ Reference syntax:
|
||||
- Login notification
|
||||
- CPU threshold notification
|
||||
- Threshold for Expiration time and Traffic to report in advance
|
||||
- Support client report if client's telegram username is added to the end of `email` like 'test123@telegram_username'
|
||||
- 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
|
||||
- Search client by email ( only admin )
|
||||
- Check all inbounds
|
||||
- Check server status
|
||||
- Check Exhausted users
|
||||
- Check depleted users
|
||||
- Receive backup by request and in periodic reports
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
## suggestion system
|
||||
|
||||
- CentOS 7+
|
||||
- Ubuntu 16+
|
||||
- Debian 8+
|
||||
|
||||
# common problem
|
||||
|
||||
# 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`
|
||||
@@ -181,7 +221,6 @@ find this in config :
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
**the final output is like :**
|
||||
```json
|
||||
"policy": {
|
||||
@@ -200,6 +239,7 @@ find this in config :
|
||||
"routing": {
|
||||
```
|
||||
restart panel
|
||||
</details>
|
||||
|
||||
# a special thanks to
|
||||
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.4.0
|
||||
0.5.0
|
||||
@@ -74,4 +74,7 @@ type Client struct {
|
||||
Email string `json:"email"`
|
||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
Enable bool `json:"enable" from:"enable"`
|
||||
TgID string `json:"tgId" from:"tgId"`
|
||||
SubID string `json:"subId" from:"subId"`
|
||||
}
|
||||
|
||||
BIN
db/x-ui.db
BIN
db/x-ui.db
Binary file not shown.
2
go.mod
2
go.mod
@@ -15,7 +15,7 @@ require (
|
||||
github.com/xtls/xray-core v1.8.0
|
||||
go.uber.org/atomic v1.10.0
|
||||
golang.org/x/text v0.8.0
|
||||
google.golang.org/grpc v1.53.0
|
||||
google.golang.org/grpc v1.54.0
|
||||
gorm.io/driver/sqlite v1.4.4
|
||||
gorm.io/gorm v1.24.6
|
||||
)
|
||||
|
||||
4
go.sum
4
go.sum
@@ -229,8 +229,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
|
||||
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
|
||||
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
|
||||
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
|
||||
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
|
||||
|
||||
72
install.sh
72
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 the 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: ${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 32-bit(x86) system, please use 64 bit operating system(x86_64) instead,if there is something wrong, plz let me know"
|
||||
echo "x-ui dosen't support 32-bit(x86) system, please use 64 bit operating system(x86_64) instead, if there is something wrong, please get in touch with me!"
|
||||
exit -1
|
||||
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 8 ]]; then
|
||||
echo -e "${red} Please use Debian 8 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" ]]; then
|
||||
yum install wget curl tar -y
|
||||
else
|
||||
apt install wget curl tar -y
|
||||
|
||||
BIN
media/inbounds-dark.png
Normal file
BIN
media/inbounds-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 316 KiB |
2
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
2
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,152 +1,386 @@
|
||||
#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;
|
||||
}
|
||||
|
||||
.deactive-client .ant-collapse-header{
|
||||
color:rgb(255, 255, 255) !important;
|
||||
background-color: rgb(255, 127, 127);
|
||||
#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;
|
||||
}
|
||||
|
||||
.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-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;
|
||||
}
|
||||
|
||||
.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-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: #004488;
|
||||
}
|
||||
|
||||
.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-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: #2e3b52;
|
||||
}
|
||||
|
||||
.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,
|
||||
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #222a37;
|
||||
}
|
||||
|
||||
.client-table-header {
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
.client-table-odd-row {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.ant-card-dark .client-table-header {
|
||||
background-color: #1a212a;
|
||||
color: hsla(0,0%,100%,.65);
|
||||
}
|
||||
|
||||
.ant-card-dark .client-table-odd-row {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #242c3a;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-calendar-last-month-cell .ant-calendar-date,
|
||||
.ant-card-dark .ant-calendar-next-month-btn-day .ant-calendar-date {
|
||||
color: hsla(0,0%,100%,.30);
|
||||
}
|
||||
|
||||
.ant-drawer-dark {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
}
|
||||
|
||||
.ant-drawer-dark .ant-drawer-wrapper-body,
|
||||
.ant-drawer-dark .drawer-handle {
|
||||
background-color: #1a212a;
|
||||
border: 1px solid hsla(0,0%,100%,.30);
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background: rgba(255,255,255,.04);
|
||||
border-color: #434343;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag-blue {
|
||||
color: #3c9ae8;
|
||||
background: #111d2c;
|
||||
border-color: #15395b;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag-green {
|
||||
color: #6abe39;
|
||||
background: #162312;
|
||||
border-color: #274916;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag-cyan {
|
||||
color: #33bcb7;
|
||||
background: #112123;
|
||||
border-color: #144848;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag-red {
|
||||
color: #e84749;
|
||||
background: #2a1215;
|
||||
border-color: #58181c;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-tag-orange {
|
||||
color: #e89a3c;
|
||||
background: #2b1d11;
|
||||
border-color: #593815;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-table-row-expand-icon,
|
||||
.ant-card-dark .ant-checkbox-inner {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-switch-checked {
|
||||
background-color: #0c61b0;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-btn,
|
||||
.ant-card-dark .ant-radio-button-wrapper {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background: none;
|
||||
border: 1px solid hsla(0,0%,100%,.65);
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-radio-button-wrapper:hover {
|
||||
color: #177ddc;
|
||||
}
|
||||
|
||||
.ant-card-dark .ant-btn-primary {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
background-color: #073763;
|
||||
border-color: #1890ff;
|
||||
text-shadow: 0 -1px 0 rgba(255,255,255,.12);
|
||||
box-shadow: 0 2px 0 rgba(255,255,255,.045);
|
||||
}
|
||||
.ant-card-dark .ant-btn-primary:hover {
|
||||
background-color: #40a9ff;
|
||||
border-color: #40a9ff;
|
||||
}
|
||||
|
||||
.ant-dark .ant-popover-content {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(255,255,255,.15);
|
||||
}
|
||||
|
||||
.ant-dark .ant-popover-inner {
|
||||
background: #222a37;
|
||||
}
|
||||
|
||||
.ant-dark .ant-popover-title,
|
||||
.ant-dark .ant-popover-inner-content {
|
||||
color: hsla(0,0%,100%,.65);
|
||||
}
|
||||
|
||||
.ant-dark .ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow {
|
||||
border-color: transparent #2e3b52 #2e3b52 transparent;
|
||||
}
|
||||
BIN
web/assets/favicon.ico
Normal file
BIN
web/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
@@ -170,13 +170,13 @@ class AllSetting {
|
||||
this.webCertFile = "";
|
||||
this.webKeyFile = "";
|
||||
this.webBasePath = "/";
|
||||
this.expireDiff = "";
|
||||
this.trafficDiff = "";
|
||||
this.tgBotEnable = false;
|
||||
this.tgBotToken = "";
|
||||
this.tgBotChatId = "";
|
||||
this.tgRunTime = "@daily";
|
||||
this.tgBotBackup = false;
|
||||
this.tgExpireDiff = "";
|
||||
this.tgTrafficDiff = "";
|
||||
this.tgCpu = "";
|
||||
this.xrayTemplateConfig = "";
|
||||
|
||||
|
||||
@@ -92,6 +92,12 @@ const UTLS_FINGERPRINT = {
|
||||
UTLS_RANDOMIZED: "randomized",
|
||||
};
|
||||
|
||||
const ALPN_OPTION = {
|
||||
H3: "h3",
|
||||
H2: "h2",
|
||||
HTTP1: "http/1.1",
|
||||
};
|
||||
|
||||
Object.freeze(Protocols);
|
||||
Object.freeze(VmessMethods);
|
||||
Object.freeze(SSMethods);
|
||||
@@ -101,6 +107,7 @@ Object.freeze(XTLS_FLOW_CONTROL);
|
||||
Object.freeze(TLS_FLOW_CONTROL);
|
||||
Object.freeze(TLS_VERSION_OPTION);
|
||||
Object.freeze(TLS_CIPHER_OPTION);
|
||||
Object.freeze(ALPN_OPTION);
|
||||
|
||||
class XrayCommonClass {
|
||||
|
||||
@@ -467,11 +474,12 @@ class GrpcStreamSettings extends XrayCommonClass {
|
||||
|
||||
class TlsStreamSettings extends XrayCommonClass {
|
||||
constructor(serverName='',
|
||||
minVersion = TLS_VERSION_OPTION.TLS12,
|
||||
maxVersion = TLS_VERSION_OPTION.TLS13,
|
||||
minVersion = TLS_VERSION_OPTION.TLS10,
|
||||
maxVersion = TLS_VERSION_OPTION.TLS12,
|
||||
cipherSuites = '',
|
||||
certificates=[new TlsStreamSettings.Cert()],
|
||||
alpn=["h2", "http/1.1"]) {
|
||||
alpn=[''],
|
||||
settings=[new TlsStreamSettings.Settings()]) {
|
||||
super();
|
||||
this.server = serverName;
|
||||
this.minVersion = minVersion;
|
||||
@@ -479,6 +487,7 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
this.cipherSuites = cipherSuites;
|
||||
this.certs = certificates;
|
||||
this.alpn = alpn;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
addCert(cert) {
|
||||
@@ -491,17 +500,23 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
|
||||
static fromJson(json={}) {
|
||||
let certs;
|
||||
let settings;
|
||||
if (!ObjectUtil.isEmpty(json.certificates)) {
|
||||
certs = json.certificates.map(cert => TlsStreamSettings.Cert.fromJson(cert));
|
||||
}
|
||||
|
||||
if (!ObjectUtil.isEmpty(json.settings)) {
|
||||
let values = json.settings[0];
|
||||
settings = [new TlsStreamSettings.Settings(values.allowInsecure , values.fingerprint, values.serverName)];
|
||||
}
|
||||
return new TlsStreamSettings(
|
||||
json.serverName,
|
||||
json.minVersion,
|
||||
json.maxVersion,
|
||||
json.cipherSuites,
|
||||
certs,
|
||||
json.alpn
|
||||
json.alpn,
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -512,7 +527,8 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
maxVersion: this.maxVersion,
|
||||
cipherSuites: this.cipherSuites,
|
||||
certificates: TlsStreamSettings.toJsonArray(this.certs),
|
||||
alpn: this.alpn
|
||||
alpn: this.alpn,
|
||||
settings: TlsStreamSettings.toJsonArray(this.settings),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -558,6 +574,29 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
}
|
||||
};
|
||||
|
||||
TlsStreamSettings.Settings = class extends XrayCommonClass {
|
||||
constructor(allowInsecure = false, fingerprint = '', serverName = '') {
|
||||
super();
|
||||
this.allowInsecure = allowInsecure;
|
||||
this.fingerprint = fingerprint;
|
||||
this.serverName = serverName;
|
||||
}
|
||||
static fromJson(json = {}) {
|
||||
return new TlsStreamSettings.Settings(
|
||||
json.allowInsecure,
|
||||
json.fingerprint,
|
||||
json.servername,
|
||||
);
|
||||
}
|
||||
toJson() {
|
||||
return {
|
||||
allowInsecure: this.allowInsecure,
|
||||
fingerprint: this.fingerprint,
|
||||
serverName: this.serverName,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
class StreamSettings extends XrayCommonClass {
|
||||
constructor(network='tcp',
|
||||
security='none',
|
||||
@@ -881,16 +920,16 @@ class Inbound extends XrayCommonClass {
|
||||
isExpiry(index) {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
if(this.settings.vmesses[index]._expiryTime != null)
|
||||
return this.settings.vmesses[index]._expiryTime < new Date().getTime();
|
||||
if(this.settings.vmesses[index].expiryTime > 0)
|
||||
return this.settings.vmesses[index].expiryTime < new Date().getTime();
|
||||
return false
|
||||
case Protocols.VLESS:
|
||||
if(this.settings.vlesses[index]._expiryTime != null)
|
||||
return this.settings.vlesses[index]._expiryTime < new Date().getTime();
|
||||
if(this.settings.vlesses[index].expiryTime > 0)
|
||||
return this.settings.vlesses[index].expiryTime < new Date().getTime();
|
||||
return false
|
||||
case Protocols.TROJAN:
|
||||
if(this.settings.trojans[index]._expiryTime != null)
|
||||
return this.settings.trojans[index]._expiryTime < new Date().getTime();
|
||||
if(this.settings.trojans[index].expiryTime > 0)
|
||||
return this.settings.trojans[index].expiryTime < new Date().getTime();
|
||||
return false
|
||||
default:
|
||||
return false;
|
||||
@@ -920,7 +959,7 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
//this is used for xtls-rprx-vison
|
||||
//this is used for xtls-rprx-vision
|
||||
canEnableTlsFlow() {
|
||||
if ((this.stream.security === 'tls') && (this.network === "tcp")) {
|
||||
switch (this.protocol) {
|
||||
@@ -1042,6 +1081,10 @@ class Inbound extends XrayCommonClass {
|
||||
host: host,
|
||||
path: path,
|
||||
tls: this.stream.security,
|
||||
sni: this.stream.tls.settings[0]['serverName'],
|
||||
fp: this.stream.tls.settings[0]['fingerprint'],
|
||||
alpn: this.stream.tls.alpn.join(','),
|
||||
allowInsecure: this.stream.tls.settings[0].allowInsecure,
|
||||
};
|
||||
return 'vmess://' + base64(JSON.stringify(obj, null, 2));
|
||||
}
|
||||
@@ -1053,12 +1096,6 @@ class Inbound extends XrayCommonClass {
|
||||
const type = this.stream.network;
|
||||
const params = new Map();
|
||||
params.set("type", this.stream.network);
|
||||
if (this.xtls) {
|
||||
params.set("security", "xtls");
|
||||
address = this.stream.tls.server;
|
||||
} else {
|
||||
params.set("security", this.stream.security);
|
||||
}
|
||||
switch (type) {
|
||||
case "tcp":
|
||||
const tcp = this.stream.tcp;
|
||||
@@ -1104,15 +1141,33 @@ class Inbound extends XrayCommonClass {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.stream.security === 'tls') {
|
||||
if (this.tls) {
|
||||
params.set("security", "tls");
|
||||
params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings[0].allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
address = this.stream.tls.server;
|
||||
params.set("sni", address);
|
||||
}
|
||||
if (this.stream.tls.settings[0]['serverName'] !== ''){
|
||||
params.set("sni", this.stream.tls.settings[0]['serverName']);
|
||||
}
|
||||
if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) {
|
||||
params.set("flow", this.settings.vlesses[clientIndex].flow);
|
||||
}
|
||||
params.set("flow", this.settings.vlesses[clientIndex].flow);
|
||||
}
|
||||
|
||||
if (this.xtls) {
|
||||
params.set("security", "xtls");
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings[0].allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
address = this.stream.tls.server;
|
||||
}
|
||||
params.set("flow", this.settings.vlesses[clientIndex].flow);
|
||||
}
|
||||
|
||||
@@ -1145,11 +1200,6 @@ class Inbound extends XrayCommonClass {
|
||||
const type = this.stream.network;
|
||||
const params = new Map();
|
||||
params.set("type", this.stream.network);
|
||||
if (this.xtls) {
|
||||
params.set("security", "xtls");
|
||||
} else {
|
||||
params.set("security", this.stream.security);
|
||||
}
|
||||
switch (type) {
|
||||
case "tcp":
|
||||
const tcp = this.stream.tcp;
|
||||
@@ -1195,16 +1245,33 @@ class Inbound extends XrayCommonClass {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.stream.security === 'tls') {
|
||||
if (this.tls) {
|
||||
params.set("security", "tls");
|
||||
params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings[0].allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
address = this.stream.tls.server;
|
||||
params.set("sni", address);
|
||||
}
|
||||
if (this.stream.tls.settings[0]['serverName'] !== ''){
|
||||
params.set("sni", this.stream.tls.settings[0]['serverName']);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.xtls) {
|
||||
params.set("security", "xtls");
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings[0].allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
address = this.stream.tls.server;
|
||||
}
|
||||
params.set("flow", this.settings.trojans[clientIndex].flow);
|
||||
}
|
||||
if (this.xtls) {
|
||||
params.set("flow", this.settings.trojans[clientIndex].flow);
|
||||
}
|
||||
|
||||
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
|
||||
const url = new URL(link);
|
||||
for (const [key, value] of params) {
|
||||
@@ -1365,13 +1432,16 @@ Inbound.VmessSettings = class extends Inbound.Settings {
|
||||
}
|
||||
};
|
||||
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
||||
constructor(id=RandomUtil.randomUUID(), alterId=0, email=RandomUtil.randomText(), totalGB=0, expiryTime='') {
|
||||
constructor(id=RandomUtil.randomUUID(), alterId=0, email=RandomUtil.randomText(), totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
|
||||
super();
|
||||
this.id = id;
|
||||
this.alterId = alterId;
|
||||
this.email = email;
|
||||
this.totalGB = totalGB;
|
||||
this.expiryTime = expiryTime;
|
||||
this.enable = enable;
|
||||
this.tgId = tgId;
|
||||
this.subId = subId;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
@@ -1381,13 +1451,18 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
||||
json.email,
|
||||
json.totalGB,
|
||||
json.expiryTime,
|
||||
|
||||
json.enable,
|
||||
json.tgId,
|
||||
json.subId,
|
||||
);
|
||||
}
|
||||
get _expiryTime() {
|
||||
if (this.expiryTime === 0 || this.expiryTime === "") {
|
||||
return null;
|
||||
}
|
||||
if (this.expiryTime < 0){
|
||||
return this.expiryTime / -86400000;
|
||||
}
|
||||
return moment(this.expiryTime);
|
||||
}
|
||||
|
||||
@@ -1447,15 +1522,16 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
|
||||
};
|
||||
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
|
||||
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(), totalGB=0, expiryTime='') {
|
||||
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(), totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
|
||||
super();
|
||||
this.id = id;
|
||||
this.flow = flow;
|
||||
this.email = email;
|
||||
this.totalGB = totalGB;
|
||||
this.expiryTime = expiryTime;
|
||||
|
||||
this.enable = enable;
|
||||
this.tgId = tgId;
|
||||
this.subId = subId;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
@@ -1465,6 +1541,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
json.email,
|
||||
json.totalGB,
|
||||
json.expiryTime,
|
||||
json.enable,
|
||||
json.tgId,
|
||||
json.subId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1472,6 +1551,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
if (this.expiryTime === 0 || this.expiryTime === "") {
|
||||
return null;
|
||||
}
|
||||
if (this.expiryTime < 0){
|
||||
return this.expiryTime / -86400000;
|
||||
}
|
||||
return moment(this.expiryTime);
|
||||
}
|
||||
|
||||
@@ -1561,13 +1643,16 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
|
||||
}
|
||||
};
|
||||
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomText(), totalGB=0, expiryTime='') {
|
||||
constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomText(), totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
|
||||
super();
|
||||
this.password = password;
|
||||
this.flow = flow;
|
||||
this.email = email;
|
||||
this.totalGB = totalGB;
|
||||
this.expiryTime = expiryTime;
|
||||
this.enable = enable;
|
||||
this.tgId = tgId;
|
||||
this.subId = subId;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
@@ -1577,6 +1662,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
email: this.email,
|
||||
totalGB: this.totalGB,
|
||||
expiryTime: this.expiryTime,
|
||||
enable: this.enable,
|
||||
tgId: this.tgId,
|
||||
subId: this.subId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1587,7 +1675,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
json.email,
|
||||
json.totalGB,
|
||||
json.expiryTime,
|
||||
|
||||
json.enable,
|
||||
json.tgId,
|
||||
json.subId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1595,6 +1685,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
if (this.expiryTime === 0 || this.expiryTime === "") {
|
||||
return null;
|
||||
}
|
||||
if (this.expiryTime < 0){
|
||||
return this.expiryTime / -86400000;
|
||||
}
|
||||
return moment(this.expiryTime);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +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 {
|
||||
@@ -26,6 +22,12 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/add", a.addInbound)
|
||||
g.POST("/del/:id", a.delInbound)
|
||||
g.POST("/update/:id", a.updateInbound)
|
||||
g.POST("/addClient/", a.addInboundClient)
|
||||
g.POST("/delClient/:email", a.delInboundClient)
|
||||
g.POST("/updateClient/:index", a.updateInboundClient)
|
||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
}
|
||||
@@ -45,3 +47,21 @@ 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)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/delClient/:email", a.delInboundClient)
|
||||
g.POST("/updateClient/:index", a.updateInboundClient)
|
||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
|
||||
}
|
||||
|
||||
@@ -208,3 +210,27 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) resetAllTraffics(c *gin.Context) {
|
||||
err := a.inboundService.ResetAllTraffics()
|
||||
if err != nil {
|
||||
jsonMsg(c, "something worng!", 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.revise"), err)
|
||||
return
|
||||
}
|
||||
|
||||
err = a.inboundService.ResetAllClientTraffics(id)
|
||||
if err != nil {
|
||||
jsonMsg(c, "something worng!", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(c, "All traffics of client reseted", nil)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"time"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ServerController struct {
|
||||
@@ -34,7 +35,10 @@ 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)
|
||||
}
|
||||
|
||||
func (a *ServerController) refreshStatus() {
|
||||
@@ -83,3 +87,33 @@ func (a *ServerController) installXray(c *gin.Context) {
|
||||
err := a.serverService.UpdateXray(version)
|
||||
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, I18n(c, "getLogs"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, logs, nil)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/setting")
|
||||
|
||||
g.POST("/all", a.getAllSetting)
|
||||
g.POST("/thresholds", a.getThresholds)
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.POST("/updateUser", a.updateUser)
|
||||
g.POST("/restartPanel", a.restartPanel)
|
||||
@@ -47,6 +48,24 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||
jsonObj(c, allSetting, nil)
|
||||
}
|
||||
|
||||
func (a *SettingController) getThresholds(c *gin.Context) {
|
||||
expireDiff, err := a.settingService.GetExpireDiff()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
|
||||
return
|
||||
}
|
||||
trafficDiff, err := a.settingService.GetTrafficDiff()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
|
||||
return
|
||||
}
|
||||
result := map[string]interface{}{
|
||||
"expireDiff": expireDiff,
|
||||
"trafficDiff": trafficDiff,
|
||||
}
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
func (a *SettingController) updateSetting(c *gin.Context) {
|
||||
allSetting := &entity.AllSetting{}
|
||||
err := c.ShouldBind(allSetting)
|
||||
|
||||
42
web/controller/sub.go
Normal file
42
web/controller/sub.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SUBController struct {
|
||||
BaseController
|
||||
|
||||
subService service.SubService
|
||||
}
|
||||
|
||||
func NewSUBController(g *gin.RouterGroup) *SUBController {
|
||||
a := &SUBController{}
|
||||
a.initRouter(g)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/sub")
|
||||
|
||||
g.GET("/:subid", a.subs)
|
||||
}
|
||||
|
||||
func (a *SUBController) subs(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
host := strings.Split(c.Request.Host, ":")[0]
|
||||
subs, err := a.subService.GetSubs(subId, host)
|
||||
if err != nil {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
result := ""
|
||||
for _, sub := range subs {
|
||||
result += sub + "\n"
|
||||
}
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
}
|
||||
}
|
||||
@@ -32,13 +32,13 @@ type AllSetting struct {
|
||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
||||
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 string `json:"tgBotChatId" form:"tgBotChatId"`
|
||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
||||
TgExpireDiff int `json:"tgExpireDiff" form:"tgExpireDiff"`
|
||||
TgTrafficDiff int `json:"tgTrafficDiff" form:"tgTrafficDiff"`
|
||||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
||||
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<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="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<style>
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
|
||||
@@ -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="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
|
||||
<a-input id="prompt-modal-input" :type="promptModal.type"
|
||||
v-model="promptModal.value"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{{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;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
|
||||
:closable="true"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:footer="null"
|
||||
width="300px">
|
||||
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
|
||||
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas>
|
||||
</a-modal>
|
||||
|
||||
@@ -13,17 +15,15 @@
|
||||
content: '',
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
okText: '',
|
||||
copyText: '',
|
||||
qrcode: null,
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
show: function (title='', content='', dbInbound=new DBInbound(),okText='{{ i18n "copy" }}', copyText='') {
|
||||
show: function (title='', content='', dbInbound=new DBInbound(), copyText='') {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.dbInbound = dbInbound;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.okText = okText;
|
||||
if (ObjectUtil.isEmpty(copyText)) {
|
||||
this.copyText = content;
|
||||
} else {
|
||||
@@ -31,15 +31,6 @@
|
||||
}
|
||||
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" }}')
|
||||
this.clipboard.destroy();
|
||||
});
|
||||
}
|
||||
if (this.qrcode === null) {
|
||||
this.qrcode = new QRious({
|
||||
element: document.querySelector('#qrCode'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{{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="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
|
||||
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
|
||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName">
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
{{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="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "pages.client.method" }}'>
|
||||
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px">
|
||||
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<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">Random_Prefix+Num@Telegram Username</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 [ BE CAREFUL! ]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item><br />
|
||||
<a-form-item v-if="clientsBulkModal.emailMethod>1">
|
||||
@@ -25,15 +26,19 @@
|
||||
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 120px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="clientsBulkModal.emailMethod>2">
|
||||
<span slot="label" v-if="clientsBulkModal.emailMethod == 4">tg_uname</span>
|
||||
<span slot="label" v-else>{{ i18n "pages.client.postfix" }}</span>
|
||||
<span slot="label">{{ i18n "pages.client.postfix" }}</span>
|
||||
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 120px"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="clientsBulkModal.emailMethod < 2">
|
||||
<span slot="label">{{ i18n "pages.client.clientCount" }}</span>
|
||||
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Subscription">
|
||||
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Telegram ID">
|
||||
<a-input v-model.trim="clientsBulkModal.tgId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
@@ -46,7 +51,13 @@
|
||||
</span>
|
||||
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-form-item label="{{ i18n "pages.client.delayedStart" }}">
|
||||
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="{{ i18n "pages.client.expireDays" }}" v-if="clientsBulkModal.delayedStart">
|
||||
<a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-else>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
@@ -57,7 +68,8 @@
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
@@ -80,6 +92,9 @@
|
||||
lastNum: 1,
|
||||
emailPrefix: "",
|
||||
emailPostfix: "",
|
||||
subId: "",
|
||||
tgId: "",
|
||||
delayedStart: false,
|
||||
ok() {
|
||||
method=clientsBulkModal.emailMethod;
|
||||
if(method>1){
|
||||
@@ -89,12 +104,15 @@
|
||||
start=0;
|
||||
end=clientsBulkModal.quantity;
|
||||
}
|
||||
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? "_" + clientsBulkModal.emailPrefix : "";
|
||||
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : "";
|
||||
useNum=(method>1);
|
||||
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? (method == 4 ? "@" : "") + clientsBulkModal.emailPostfix : "";
|
||||
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;
|
||||
clientsBulkModal.clients.push(newClient);
|
||||
@@ -108,16 +126,18 @@
|
||||
this.confirm = confirm;
|
||||
this.quantity = 1;
|
||||
this.totalGB = 0;
|
||||
this.expiryTime = '';
|
||||
this.expiryTime = 0;
|
||||
this.emailMethod= 0;
|
||||
this.firstNum= 1;
|
||||
this.lastNum= 1;
|
||||
this.emailPrefix= "";
|
||||
this.emailPostfix= "";
|
||||
|
||||
this.subId= "";
|
||||
this.tgId= "";
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
||||
this.delayedStart = false;
|
||||
},
|
||||
getClients(protocol, clientSettings) {
|
||||
switch(protocol){
|
||||
@@ -152,6 +172,12 @@
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{{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="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
{{template "form/client"}}
|
||||
</a-modal>
|
||||
@@ -17,6 +18,7 @@
|
||||
clientStats: [],
|
||||
index: null,
|
||||
isExpired: false,
|
||||
delayedStart: false,
|
||||
ok() {
|
||||
ObjectUtil.execute(clientModal.confirm, clientModal.inbound, clientModal.dbInbound, clientModal.index);
|
||||
},
|
||||
@@ -30,8 +32,13 @@
|
||||
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
||||
this.index = index === null ? this.clients.length : index;
|
||||
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
|
||||
this.delayedStart = false;
|
||||
if (!isEdit){
|
||||
this.addClient(this.inbound.protocol, this.clients);
|
||||
} else {
|
||||
if (this.clients[index].expiryTime < 0){
|
||||
this.delayedStart = true;
|
||||
}
|
||||
}
|
||||
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
||||
this.confirm = confirm;
|
||||
@@ -80,7 +87,7 @@
|
||||
},
|
||||
get isTrafficExhausted() {
|
||||
if(!clientStats) return false
|
||||
if(clientStats.total == 0) return false
|
||||
if(clientStats.total <= 0) return false
|
||||
if(clientStats.up + clientStats.down < clientStats.total) return false
|
||||
return true
|
||||
},
|
||||
@@ -89,10 +96,16 @@
|
||||
},
|
||||
get statsColor() {
|
||||
if(!clientStats) return 'blue'
|
||||
if(clientStats.total === 0) return 'blue'
|
||||
if(clientStats.total <= 0) return 'blue'
|
||||
else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan'
|
||||
else return 'red'
|
||||
}
|
||||
},
|
||||
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) {
|
||||
|
||||
@@ -33,27 +33,50 @@
|
||||
|
||||
|
||||
{{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="siderDrawer.theme" id="sider" collapsible breakpoint="md" collapsed-width="0">
|
||||
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
|
||||
<a-menu-item mode="inline">
|
||||
<a-icon type="bg-colors"></a-icon>
|
||||
<a-switch :default-checked="siderDrawer.isDarkTheme"
|
||||
checked-children="☀"
|
||||
un-checked-children="🌙"
|
||||
@change="siderDrawer.changeTheme()"></a-switch>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<a-menu :theme="siderDrawer.theme" 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="siderDrawer.isDarkTheme ? 'ant-drawer-dark' : ''"
|
||||
: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="siderDrawer.theme" mode="inline" selected-keys="">
|
||||
<a-menu-item mode="inline">
|
||||
<a-icon type="bg-colors"></a-icon>
|
||||
<a-switch :default-checked="siderDrawer.isDarkTheme"
|
||||
checked-children="☀"
|
||||
un-checked-children="🌙"
|
||||
@change="siderDrawer.changeTheme()"></a-switch>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<a-menu :theme="siderDrawer.theme" 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 darkClass = "ant-card-dark";
|
||||
const bgDarkStyle = "background-color: #242c3a";
|
||||
const siderDrawer = {
|
||||
visible: false,
|
||||
collapsed: false,
|
||||
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
|
||||
show() {
|
||||
this.visible = true;
|
||||
},
|
||||
@@ -62,6 +85,16 @@
|
||||
},
|
||||
change() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
toggleCollapsed() {
|
||||
this.collapsed = !this.collapsed;
|
||||
},
|
||||
changeTheme() {
|
||||
this.isDarkTheme = ! this.isDarkTheme;
|
||||
localStorage.setItem("dark-mode", this.isDarkTheme);
|
||||
},
|
||||
get theme() {
|
||||
return this.isDarkTheme ? 'dark' : 'light';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,16 +5,19 @@
|
||||
</template>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<span>{{ i18n "pages.inbounds.Email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-input v-model.trim="client.email"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="{{ i18n "pages.inbounds.enable" }}">
|
||||
<a-switch v-model="client.enable"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="password" v-if="inbound.protocol === Protocols.TROJAN">
|
||||
<a-input v-model.trim="client.password"></a-input>
|
||||
</a-form-item>
|
||||
@@ -24,8 +27,14 @@
|
||||
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
|
||||
<a-input type="number" v-model.number="client.alterId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Subscription" v-if="client.email">
|
||||
<a-input v-model.trim="client.subId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Telegram Username" v-if="client.email">
|
||||
<a-input v-model.trim="client.tgId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="client.flow" style="width: 150px">
|
||||
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
@@ -48,7 +57,7 @@
|
||||
</span>
|
||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||
<template v-if="isEdit && clientStats">
|
||||
{{ i18n "usage" }}:
|
||||
<span>{{ i18n "usage" }}:</span>
|
||||
<a-tag :color="statsColor">
|
||||
[[ sizeFormat(clientStats.up) ]] /
|
||||
[[ sizeFormat(clientStats.down) ]]
|
||||
@@ -56,7 +65,13 @@
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-form-item label="{{ i18n "pages.client.delayedStart" }}">
|
||||
<a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="{{ i18n "pages.client.expireDays" }}" v-if="clientModal.delayedStart">
|
||||
<a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-else>
|
||||
<span slot="label">
|
||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
@@ -67,7 +82,8 @@
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<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;" :disabled="isEdit">
|
||||
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
@@ -50,6 +50,7 @@
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<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 v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{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 v-model="inbound.settings.method" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
@@ -9,7 +9,7 @@
|
||||
<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 v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<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>
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<span>{{ i18n "pages.inbounds.Email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
@@ -20,7 +20,7 @@
|
||||
<a-input v-model.trim="client.password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="client.flow" style="width: 150px">
|
||||
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
@@ -48,23 +48,24 @@
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</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 style="background-color: lightgrey;">
|
||||
<tr class="client-table-header">
|
||||
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
|
||||
</tr>
|
||||
<tr v-for="(client, index) in inbound.settings.trojans" :style="[ index % 2 == 1 ? {'background-color': '#fafafa'} : {}]">
|
||||
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp && inbound.tls">
|
||||
<template v-if="inbound.isTcp && (inbound.tls || inbound.xtls)">
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-row>
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<span>{{ i18n "pages.inbounds.Email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||
</a-tooltip>
|
||||
@@ -20,13 +20,13 @@
|
||||
<a-input v-model.trim="client.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
|
||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="flow" layout="inline">
|
||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
|
||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<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>
|
||||
@@ -54,6 +54,7 @@
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</a-collapse-panel>
|
||||
@@ -61,16 +62,16 @@
|
||||
<a-collapse v-else>
|
||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
|
||||
<table width="100%">
|
||||
<tr style="background-color: lightgrey;">
|
||||
<tr class="client-table-header">
|
||||
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
|
||||
</tr>
|
||||
<tr v-for="(client, index) in inbound.settings.vlesses" :style="[ index % 2 == 1 ? {'background-color': '#fafafa'} : {}]">
|
||||
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp && inbound.tls">
|
||||
<template v-if="inbound.isTcp && (inbound.tls || inbound.xtls)">
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-row>
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<a-form layout="inline">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
Email
|
||||
<span>{{ i18n "pages.inbounds.Email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
The email must be completely unique
|
||||
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
||||
</a-tooltip>
|
||||
@@ -45,6 +45,7 @@
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
v-model="client._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</a-collapse-panel>
|
||||
@@ -52,10 +53,10 @@
|
||||
<a-collapse v-else>
|
||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vmesses.length">
|
||||
<table width="100%">
|
||||
<tr style="background-color: lightgrey;">
|
||||
<tr class="client-table-header">
|
||||
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
|
||||
</tr>
|
||||
<tr v-for="(client, index) in inbound.settings.vmesses" :style="[ index % 2 == 1 ? {'background-color': '#fafafa'} : {}]">
|
||||
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{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 v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{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 v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<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>
|
||||
@@ -11,7 +11,7 @@
|
||||
<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 v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<!-- select stream network -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="inbound.stream.network" @change="streamNetworkChange">
|
||||
<a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<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>
|
||||
|
||||
@@ -12,15 +12,8 @@
|
||||
|
||||
<!-- tls settings -->
|
||||
<a-form v-if="inbound.tls || inbound.xtls" layout="inline">
|
||||
<a-form-item label="MinVersion">
|
||||
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="MaxVersion">
|
||||
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication" v-if="inbound.tls">
|
||||
<a-input v-model.trim="inbound.stream.tls.settings[0].serverName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="CipherSuites">
|
||||
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px">
|
||||
@@ -28,11 +21,32 @@
|
||||
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="MinVersion">
|
||||
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="MaxVersion">
|
||||
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS" v-if="inbound.tls" >
|
||||
<a-select v-model="inbound.stream.tls.settings[0].fingerprint" style="width: 135px">
|
||||
<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>
|
||||
<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 label="Alpn">
|
||||
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
|
||||
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow insecure">
|
||||
<a-switch v-model="inbound.stream.tls.settings[0].allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
|
||||
@@ -42,18 +56,18 @@
|
||||
</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-input v-model.trim="inbound.stream.tls.certs[0].certFile" style="width:300px;"></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-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></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-input type="textarea" :rows="3" style="width:300px;" 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-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
|
||||
@@ -21,9 +21,12 @@
|
||||
<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 "disabled" }}</a-tag>
|
||||
<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="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
|
||||
@@ -34,11 +37,12 @@
|
||||
<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">
|
||||
<template v-if="client.expiryTime > 0">
|
||||
<a-tag :color="isExpiry(record, index)? 'red' : 'blue'">
|
||||
[[ 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}}
|
||||
@@ -3,6 +3,7 @@
|
||||
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
|
||||
:closable="true"
|
||||
:mask-closable="true"
|
||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:footer="null"
|
||||
width="600px"
|
||||
>
|
||||
@@ -58,15 +59,25 @@
|
||||
</table>
|
||||
<template v-if="infoModal.clientSettings">
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<table style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<th v-for="col in Object.keys(infoModal.clientSettings).slice(0, 3)">[[ col ]]</th>
|
||||
<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 v-for="col in Object.values(infoModal.clientSettings).slice(0, 3)"><a-tag color="green">[[ col ]]</a-tag></td>
|
||||
<td>{{ i18n "status" }}</td>
|
||||
<td>
|
||||
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
|
||||
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
|
||||
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table style="margin-bottom: 10px; width: 100%;">
|
||||
<tr><th>{{ i18n "usage" }}</th><th>{{ i18n "pages.inbounds.totalFlow" }}</th><th>{{ i18n "pages.inbounds.expireDate" }}</th><th>{{ i18n "enable" }}</th></tr>
|
||||
<tr>
|
||||
<th>{{ i18n "usage" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
|
||||
<tr>
|
||||
<td>
|
||||
<a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)">
|
||||
@@ -85,12 +96,19 @@
|
||||
[[ 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>
|
||||
<td>
|
||||
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
|
||||
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-if="infoModal.clientSettings.subId + infoModal.clientSettings.tgId" style="margin-bottom: 10px;">
|
||||
<tr v-if="infoModal.clientSettings.subId">
|
||||
<td>Subscription link</td>
|
||||
<td><a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a></td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.tgId">
|
||||
<td>Telegram Username</td>
|
||||
<td><a :href="[[ tgBase + infoModal.clientSettings.tgId ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
@@ -161,13 +179,12 @@
|
||||
</div>
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const infoModal = {
|
||||
visible: false,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
settings: null,
|
||||
clientSettings: new Inbound.Settings(),
|
||||
clientSettings: null,
|
||||
clientStats: [],
|
||||
upStats: 0,
|
||||
downStats: 0,
|
||||
@@ -210,12 +227,24 @@
|
||||
get inbound() {
|
||||
return this.infoModal.inbound;
|
||||
},
|
||||
get isEnable() {
|
||||
if(infoModal.clientStats.length){
|
||||
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;
|
||||
},
|
||||
get subBase() {
|
||||
return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port:"") + "/sub/";
|
||||
},
|
||||
get tgBase() {
|
||||
return "https://t.me/"
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copyTextToClipboard(elmentId,content) {
|
||||
|
||||
@@ -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="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
{{template "form/inbound"}}
|
||||
</a-modal>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout">
|
||||
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip="loading">
|
||||
<transition name="list" appear>
|
||||
@@ -24,7 +24,7 @@
|
||||
</a-tag>
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-card hoverable style="margin-bottom: 20px;">
|
||||
<a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-row>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "pages.inbounds.totalDownUp" }}:
|
||||
@@ -41,17 +41,34 @@
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
{{ i18n "clients" }}:
|
||||
<a-tag color="green">[[ total.clients ]]</a-tag>
|
||||
<a-tag color="blue">{{ i18n "enabled" }} [[ total.active ]]</a-tag>
|
||||
<a-tag color="red">{{ i18n "disabled" }} [[ total.deactive ]]</a-tag>
|
||||
<a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
|
||||
<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="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
|
||||
<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="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
|
||||
<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="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<div slot="title">
|
||||
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
|
||||
<a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
|
||||
<a-button type="primary" icon="reload" @click="resetAllTraffic">{{ i18n "pages.inbounds.resetAllTraffic" }}</a-button>
|
||||
</div>
|
||||
<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
|
||||
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||
@@ -63,7 +80,7 @@
|
||||
<template slot="action" slot-scope="text, dbInbound">
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon>
|
||||
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)">
|
||||
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme">
|
||||
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
|
||||
<a-icon type="qrcode"></a-icon>
|
||||
{{ i18n "qrCode" }}
|
||||
@@ -74,13 +91,17 @@
|
||||
</a-menu-item>
|
||||
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
|
||||
<a-menu-item key="addClient">
|
||||
<a-icon type="user"></a-icon>
|
||||
<a-icon type="user-add"></a-icon>
|
||||
{{ i18n "pages.client.add"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="addBulkClient">
|
||||
<a-icon type="team"></a-icon>
|
||||
<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.resetAllClientTraffics"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="export">
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export"}}
|
||||
@@ -95,6 +116,9 @@
|
||||
<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"}}
|
||||
@@ -104,7 +128,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.isXTls" color="cyan">xtls</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="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
|
||||
<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="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
|
||||
<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="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
|
||||
<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>
|
||||
@@ -114,14 +166,6 @@
|
||||
</template>
|
||||
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
|
||||
</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.id)"></a-switch>
|
||||
</template>
|
||||
@@ -184,28 +228,28 @@
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.remark" }}',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
width: 50,
|
||||
dataIndex: "remark",
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.protocol" }}',
|
||||
align: 'center',
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'protocol' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.port" }}',
|
||||
align: 'center',
|
||||
dataIndex: "port",
|
||||
width: 30,
|
||||
width: 40,
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.protocol" }}',
|
||||
align: 'left',
|
||||
width: 70,
|
||||
scopedSlots: { customRender: 'protocol' },
|
||||
}, {
|
||||
title: '{{ i18n "clients" }}',
|
||||
align: 'left',
|
||||
width: 50,
|
||||
scopedSlots: { customRender: 'clients' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
width: 120,
|
||||
scopedSlots: { customRender: 'traffic' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.transportConfig" }}',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'stream' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
||||
align: 'center',
|
||||
@@ -214,19 +258,21 @@
|
||||
}];
|
||||
|
||||
const innerColumns = [
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 60, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 80, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: 'UID', width: 150, dataIndex: "id" },
|
||||
{ title: 'UID', width: 120, dataIndex: "id" },
|
||||
];
|
||||
|
||||
const innerTrojanColumns = [
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 60, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 80, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: 'Password', width: 150, dataIndex: "password" },
|
||||
{ title: 'Password', width: 120, dataIndex: "password" },
|
||||
];
|
||||
|
||||
const app = new Vue({
|
||||
@@ -239,6 +285,9 @@
|
||||
dbInbounds: [],
|
||||
searchKey: '',
|
||||
searchedInbounds: [],
|
||||
expireDiff: 0,
|
||||
trafficDiff: 0,
|
||||
clientCount: {},
|
||||
},
|
||||
methods: {
|
||||
loading(spinning=true) {
|
||||
@@ -254,17 +303,64 @@
|
||||
this.setInbounds(msg.obj);
|
||||
this.searchKey = '';
|
||||
},
|
||||
async getThresholds() {
|
||||
this.loading();
|
||||
const msg = await HttpUtil.post('/xui/setting/thresholds');
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
this.expireDiff = msg.obj.expireDiff * 86400000;
|
||||
this.trafficDiff = msg.obj.trafficDiff * 1073741824;
|
||||
},
|
||||
setInbounds(dbInbounds) {
|
||||
this.inbounds.splice(0);
|
||||
this.dbInbounds.splice(0);
|
||||
this.searchedInbounds.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);
|
||||
this.searchedInbounds.push(dbInbound);
|
||||
if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){
|
||||
this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound);
|
||||
}
|
||||
}
|
||||
},
|
||||
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)) {
|
||||
this.searchedInbounds = this.dbInbounds.slice();
|
||||
@@ -311,6 +407,12 @@
|
||||
case "resetTraffic":
|
||||
this.resetTraffic(dbInbound.id);
|
||||
break;
|
||||
case "resetClients":
|
||||
this.resetAllClientTraffics(dbInbound.id);
|
||||
break;
|
||||
case "clone":
|
||||
this.openCloneInbound(dbInbound);
|
||||
break;
|
||||
case "delete":
|
||||
this.delInbound(dbInbound.id);
|
||||
break;
|
||||
@@ -345,6 +447,39 @@
|
||||
},
|
||||
isEdit: true
|
||||
});
|
||||
},
|
||||
openCloneInbound(dbInbound) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.cloneInbound"}} ' + dbInbound.remark,
|
||||
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
|
||||
okText: '{{ i18n "pages.inbounds.revise"}}',
|
||||
cancelText: '{{ i18n "cancel" }}',
|
||||
onOk: () => {
|
||||
const baseInbound = dbInbound.toInbound();
|
||||
dbInbound.up = 0;
|
||||
dbInbound.down = 0;
|
||||
this.cloneInbound(baseInbound, dbInbound);
|
||||
},
|
||||
});
|
||||
},
|
||||
async cloneInbound(baseInbound, dbInbound) {
|
||||
const inbound = new Inbound();
|
||||
const data = {
|
||||
up: dbInbound.up,
|
||||
down: dbInbound.down,
|
||||
total: dbInbound.total,
|
||||
remark: dbInbound.remark + " - Cloned",
|
||||
enable: dbInbound.enable,
|
||||
expiryTime: dbInbound.expiryTime,
|
||||
|
||||
listen: inbound.listen,
|
||||
port: inbound.port,
|
||||
protocol: baseInbound.protocol,
|
||||
settings: inbound.settings.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 = {
|
||||
@@ -451,6 +586,7 @@
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => {
|
||||
@@ -465,6 +601,7 @@
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||
okText: '{{ i18n "delete"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
|
||||
@@ -484,6 +621,7 @@
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||
okText: '{{ i18n "delete"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/xui/inbound/delClient/' + client.email, data),
|
||||
@@ -508,6 +646,16 @@
|
||||
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
|
||||
await this.updateClient(inbound, dbInbound, index);
|
||||
this.loading(false);
|
||||
},
|
||||
async submit(url, data) {
|
||||
const msg = await HttpUtil.postWithModal(url, data);
|
||||
if (msg.success) {
|
||||
@@ -527,11 +675,32 @@
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||
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: siderDrawer.isDarkTheme ? darkClass : '',
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/xui/inbound/resetAllTraffics'),
|
||||
});
|
||||
},
|
||||
resetAllClientTraffics(dbInboundId) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
|
||||
content: '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
|
||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
|
||||
})
|
||||
},
|
||||
isExpiry(dbInbound, index) {
|
||||
return dbInbound.toInbound().isExpiry(index)
|
||||
},
|
||||
@@ -575,37 +744,30 @@
|
||||
}, 500)
|
||||
},
|
||||
mounted() {
|
||||
this.getThresholds();
|
||||
this.getDBInbounds();
|
||||
},
|
||||
computed: {
|
||||
total() {
|
||||
let down = 0, up = 0;
|
||||
let clients = 0, active = 0, deactive = 0;
|
||||
let clients = 0, deactive = [], depleted = [], expiring = [];
|
||||
this.dbInbounds.forEach(dbInbound => {
|
||||
down += dbInbound.down;
|
||||
up += dbInbound.up;
|
||||
inbound = dbInbound.toInbound();
|
||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||
if(clients){
|
||||
if(dbInbound.enable){
|
||||
isClientEnable = false;
|
||||
clients.forEach(client => {
|
||||
isClientEnable = client.email == "" ? true: this.isClientEnabled(dbInbound,client.email);
|
||||
isClientEnable ? active++ : deactive++;
|
||||
});
|
||||
} else {
|
||||
deactive += clients.length;
|
||||
}
|
||||
} else {
|
||||
dbInbound.enable ? active++ : deactive++;
|
||||
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: active + deactive,
|
||||
active: active,
|
||||
clients: clients,
|
||||
deactive: deactive,
|
||||
depleted: depleted,
|
||||
expiring: expiring,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
|
||||
<transition name="list" appear>
|
||||
<a-row>
|
||||
<a-card hoverable>
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<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="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
: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="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
: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="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
: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="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
:percent="status.disk.percent"></a-progress>
|
||||
<div>
|
||||
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
|
||||
@@ -67,7 +75,7 @@
|
||||
<transition name="list" appear>
|
||||
<a-row>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
{{ i18n "pages.index.xrayStatus" }}:
|
||||
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
|
||||
<a-tooltip v-if="status.xray.state === State.Error">
|
||||
@@ -76,14 +84,18 @@
|
||||
</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-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
||||
<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>
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
x-ui: <a-tag color="green">{{ .cur_ver }}</a-tag>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">Logs</a-tag>
|
||||
{{ 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 +105,12 @@
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
{{ 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="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
tcp / udp {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
@@ -109,7 +121,7 @@
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-up"></a-icon>
|
||||
@@ -135,7 +147,7 @@
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="cloud-upload"></a-icon>
|
||||
@@ -166,7 +178,8 @@
|
||||
</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="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
footer="">
|
||||
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
|
||||
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
|
||||
<template v-for="version, index in versionModal.versions">
|
||||
@@ -176,6 +189,36 @@
|
||||
</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="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||
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="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
||||
<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-layout>
|
||||
{{template "js" .}}
|
||||
<script>
|
||||
@@ -269,6 +312,20 @@
|
||||
},
|
||||
};
|
||||
|
||||
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 app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
@@ -276,6 +333,7 @@
|
||||
siderDrawer,
|
||||
status: new Status(),
|
||||
versionModal,
|
||||
logModal,
|
||||
spinning: false,
|
||||
loadingTip: '{{ i18n "loading"}}',
|
||||
},
|
||||
@@ -307,6 +365,7 @@
|
||||
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
|
||||
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
|
||||
okText: '{{ i18n "confirm"}}',
|
||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: async () => {
|
||||
versionModal.hide();
|
||||
@@ -316,6 +375,33 @@
|
||||
},
|
||||
});
|
||||
},
|
||||
//here add stop xray function
|
||||
async stopXrayService() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('server/stopXrayService');
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
//here add restart xray function
|
||||
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 mounted() {
|
||||
while (true) {
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-tabs-top-bar {
|
||||
:not(.ant-card-dark)>.ant-tabs-top-bar {
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout">
|
||||
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip="loading">
|
||||
<a-space direction="vertical">
|
||||
@@ -35,17 +35,19 @@
|
||||
<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-tabs default-active-key="1" :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||
<a-tab-pane key="1" tab='{{ i18n "pages.setting.panelConfig"}}'>
|
||||
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': '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>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.expireTimeDiff" }}' desc='{{ i18n "pages.setting.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.trafficDiff" }}' desc='{{ i18n "pages.setting.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Language"/>
|
||||
</a-col>
|
||||
@@ -56,9 +58,10 @@
|
||||
ref="selectLang"
|
||||
v-model="lang"
|
||||
@change="setLang(lang)"
|
||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" >
|
||||
<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>
|
||||
@@ -71,7 +74,7 @@
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab='{{ i18n "pages.setting.userSetting"}}'>
|
||||
<a-form style="background: white; padding: 20px">
|
||||
<a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': '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>
|
||||
@@ -93,7 +96,7 @@
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigTorrent"}}' desc='{{ i18n "pages.setting.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.setting.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
|
||||
<a-divider>{{ i18n "pages.setting.advancedTemplate"}}</a-divider>
|
||||
@@ -113,19 +116,17 @@
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab='{{ i18n "pages.setting.TGReminder"}}'>
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': '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.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model="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>
|
||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.tgNotifyBackup" }}' desc='{{ i18n "pages.setting.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyExpireTimeDiff" }}' desc='{{ i18n "pages.setting.tgNotifyExpireTimeDiffDesc" }}' v-model="allSetting.tgExpireDiff" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyTrafficDiff" }}' desc='{{ i18n "pages.setting.tgNotifyTrafficDiffDesc" }}' v-model="allSetting.tgTrafficDiff" :min="0"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyCpu" }}' desc='{{ i18n "pages.setting.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.setting.otherSetting"}}'>
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': '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>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"access": "./access.log"
|
||||
"loglevel": "warning"
|
||||
},
|
||||
|
||||
"api": {
|
||||
|
||||
@@ -319,12 +319,18 @@ func (s *InboundService) UpdateInboundClient(inbound *model.Inbound, index int)
|
||||
|
||||
if len(clients[index].Email) > 0 {
|
||||
if len(oldClients[index].Email) > 0 {
|
||||
s.UpdateClientStat(oldClients[index].Email, &clients[index])
|
||||
err = s.UpdateClientStat(oldClients[index].Email, &clients[index])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
s.AddClientStat(inbound.Id, &clients[index])
|
||||
}
|
||||
} else {
|
||||
s.DelClientStat(db, oldClients[index].Email)
|
||||
err = s.DelClientStat(db, oldClients[index].Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return db.Save(oldInbound).Error
|
||||
}
|
||||
@@ -360,11 +366,16 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
||||
if len(traffics) == 0 {
|
||||
return nil
|
||||
}
|
||||
db := database.GetDB()
|
||||
dbInbound := db.Model(model.Inbound{})
|
||||
|
||||
traffics, err = s.adjustTraffics(traffics)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db := database.GetDB()
|
||||
db = db.Model(xray.ClientTraffic{})
|
||||
tx := db.Begin()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
@@ -372,7 +383,20 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
||||
tx.Commit()
|
||||
}
|
||||
}()
|
||||
|
||||
err = tx.Save(traffics).Error
|
||||
if err != nil {
|
||||
logger.Warning("AddClientTraffic update data ", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) adjustTraffics(traffics []*xray.ClientTraffic) (full_traffics []*xray.ClientTraffic, err error) {
|
||||
db := database.GetDB()
|
||||
dbInbound := db.Model(model.Inbound{})
|
||||
txInbound := dbInbound.Begin()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
txInbound.Rollback()
|
||||
@@ -381,22 +405,23 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
||||
}
|
||||
}()
|
||||
|
||||
for _, traffic := range traffics {
|
||||
for traffic_index, traffic := range traffics {
|
||||
inbound := &model.Inbound{}
|
||||
client := &xray.ClientTraffic{}
|
||||
err := tx.Where("email = ?", traffic.Email).First(client).Error
|
||||
client_traffic := &xray.ClientTraffic{}
|
||||
err := db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(client_traffic).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
logger.Warning(err, traffic.Email)
|
||||
}
|
||||
continue
|
||||
}
|
||||
client_traffic.Up += traffic.Up
|
||||
client_traffic.Down += traffic.Down
|
||||
|
||||
err = txInbound.Where("id=?", client.InboundId).First(inbound).Error
|
||||
err = txInbound.Where("id=?", client_traffic.InboundId).First(inbound).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
logger.Warning(err, traffic.Email)
|
||||
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -404,29 +429,35 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
||||
settings := map[string][]model.Client{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
clients := settings["clients"]
|
||||
for _, client := range clients {
|
||||
needUpdate := false
|
||||
for client_index, client := range clients {
|
||||
if traffic.Email == client.Email {
|
||||
traffic.ExpiryTime = client.ExpiryTime
|
||||
traffic.Total = client.TotalGB
|
||||
if client.ExpiryTime < 0 {
|
||||
clients[client_index].ExpiryTime = (time.Now().Unix() * 1000) - client.ExpiryTime
|
||||
needUpdate = true
|
||||
}
|
||||
client_traffic.ExpiryTime = client.ExpiryTime
|
||||
client_traffic.Total = client.TotalGB
|
||||
break
|
||||
}
|
||||
}
|
||||
if tx.Where("inbound_id = ? and email = ?", inbound.Id, traffic.Email).
|
||||
UpdateColumns(map[string]interface{}{
|
||||
"enable": true,
|
||||
"expiry_time": traffic.ExpiryTime,
|
||||
"total": traffic.Total,
|
||||
"up": gorm.Expr("up + ?", traffic.Up),
|
||||
"down": gorm.Expr("down + ?", traffic.Down)}).RowsAffected == 0 {
|
||||
err = tx.Create(traffic).Error
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Warning("AddClientTraffic update data ", err)
|
||||
continue
|
||||
|
||||
if needUpdate {
|
||||
settings["clients"] = clients
|
||||
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = txInbound.Where("id=?", inbound.Id).Update("settings", string(modifiedSettings)).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
traffics[traffic_index] = client_traffic
|
||||
}
|
||||
return
|
||||
return traffics, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
|
||||
@@ -501,11 +532,57 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *InboundService) GetClientTrafficTgBot(tguname string) (traffic []*xray.ClientTraffic, err error) {
|
||||
db := database.GetDB()
|
||||
var traffics []*xray.ClientTraffic
|
||||
|
||||
err = db.Model(xray.ClientTraffic{}).Where("email like ?", "%@"+tguname).Find(&traffics).Error
|
||||
func (s *InboundService) ResetAllClientTraffics(id int) error {
|
||||
db := database.GetDB()
|
||||
|
||||
result := db.Model(xray.ClientTraffic{}).
|
||||
Where("inbound_id = ?", id).
|
||||
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
|
||||
|
||||
err := result.Error
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) ResetAllTraffics() error {
|
||||
db := database.GetDB()
|
||||
|
||||
result := db.Model(model.Inbound{}).
|
||||
Updates(map[string]interface{}{"up": 0, "down": 0})
|
||||
|
||||
err := result.Error
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTraffic, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"tgId": "%s"%%`, tguname)).Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
var emails []string
|
||||
for _, inbound := range inbounds {
|
||||
clients, err := s.getClients(inbound)
|
||||
if err != nil {
|
||||
logger.Error("Unable to get clients from inbound")
|
||||
}
|
||||
for _, client := range clients {
|
||||
if client.TgID == tguname {
|
||||
emails = append(emails, client.Email)
|
||||
}
|
||||
}
|
||||
}
|
||||
var traffics []*xray.ClientTraffic
|
||||
err = db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
logger.Warning(err)
|
||||
@@ -528,3 +605,52 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic []*xray.
|
||||
}
|
||||
return traffics, err
|
||||
}
|
||||
|
||||
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
|
||||
db := database.GetDB()
|
||||
inbound := &model.Inbound{}
|
||||
traffic = &xray.ClientTraffic{}
|
||||
|
||||
err = db.Model(model.Inbound{}).Where("settings like ?", "%\""+query+"\"%").First(inbound).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
logger.Warning(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
traffic.InboundId = inbound.Id
|
||||
|
||||
// get settings clients
|
||||
settings := map[string][]model.Client{}
|
||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||
clients := settings["clients"]
|
||||
for _, client := range clients {
|
||||
if client.ID == query && client.Email != "" {
|
||||
traffic.Email = client.Email
|
||||
break
|
||||
}
|
||||
if client.Password == query && client.Email != "" {
|
||||
traffic.Email = client.Email
|
||||
break
|
||||
}
|
||||
}
|
||||
if traffic.Email == "" {
|
||||
return nil, err
|
||||
}
|
||||
err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error
|
||||
if err != nil {
|
||||
logger.Warning(err)
|
||||
return nil, err
|
||||
}
|
||||
return traffic, err
|
||||
}
|
||||
|
||||
func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
return inbounds, nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/sys"
|
||||
@@ -198,6 +200,30 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
func (s *ServerService) StopXrayService() (string error) {
|
||||
|
||||
err := s.xrayService.StopXray()
|
||||
if err != nil {
|
||||
logger.Error("stop xray failed:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerService) RestartXrayService() (string error) {
|
||||
|
||||
s.xrayService.StopXray()
|
||||
defer func() {
|
||||
err := s.xrayService.RestartXray(true)
|
||||
if err != nil {
|
||||
logger.Error("start xray failed:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerService) downloadXRay(version string) (string, error) {
|
||||
osName := runtime.GOOS
|
||||
arch := runtime.GOARCH
|
||||
@@ -300,3 +326,26 @@ func (s *ServerService) UpdateXray(version string) error {
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (s *ServerService) GetLogs(count string) ([]string, error) {
|
||||
// Define the journalctl command and its arguments
|
||||
var cmdArgs []string
|
||||
if runtime.GOOS == "linux" {
|
||||
cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count}
|
||||
} else {
|
||||
return []string{"Unsupported operating system"}, nil
|
||||
}
|
||||
|
||||
// Run the command
|
||||
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(out.String(), "\n")
|
||||
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
@@ -28,14 +28,14 @@ var defaultValueMap = map[string]string{
|
||||
"webKeyFile": "",
|
||||
"secret": random.Seq(32),
|
||||
"webBasePath": "/",
|
||||
"expireDiff": "0",
|
||||
"trafficDiff": "0",
|
||||
"timeLocation": "Asia/Tehran",
|
||||
"tgBotEnable": "false",
|
||||
"tgBotToken": "",
|
||||
"tgBotChatId": "",
|
||||
"tgRunTime": "@daily",
|
||||
"tgBotBackup": "false",
|
||||
"tgExpireDiff": "0",
|
||||
"tgTrafficDiff": "0",
|
||||
"tgCpu": "0",
|
||||
}
|
||||
|
||||
@@ -238,22 +238,6 @@ func (s *SettingService) SetTgBotBackup(value bool) error {
|
||||
return s.setBool("tgBotBackup", value)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgExpireDiff() (int, error) {
|
||||
return s.getInt("tgExpireDiff")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetTgExpireDiff(value int) error {
|
||||
return s.setInt("tgExpireDiff", value)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgTrafficDiff() (int, error) {
|
||||
return s.getInt("tgTrafficDiff")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetTgTrafficDiff(value int) error {
|
||||
return s.setInt("tgTrafficDiff", value)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgCpu() (int, error) {
|
||||
return s.getInt("tgCpu")
|
||||
}
|
||||
@@ -278,6 +262,22 @@ func (s *SettingService) GetKeyFile() (string, error) {
|
||||
return s.getString("webKeyFile")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetExpireDiff() (int, error) {
|
||||
return s.getInt("expireDiff")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetExpireDiff(value int) error {
|
||||
return s.setInt("expireDiff", value)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTrafficDiff() (int, error) {
|
||||
return s.getInt("trafficDiff")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetgetTrafficDiff(value int) error {
|
||||
return s.setInt("trafficDiff", value)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSecret() ([]byte, error) {
|
||||
secret, err := s.getString("secret")
|
||||
if secret == defaultValueMap["secret"] {
|
||||
|
||||
505
web/service/sub.go
Normal file
505
web/service/sub.go
Normal file
@@ -0,0 +1,505 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SubService struct {
|
||||
address string
|
||||
inboundService InboundService
|
||||
}
|
||||
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, error) {
|
||||
s.address = host
|
||||
var result []string
|
||||
inbounds, err := s.getInboundsBySubId(subId)
|
||||
if err != nil {
|
||||
return 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.SubID == subId {
|
||||
link := s.getLink(inbound, client.Email)
|
||||
result = append(result, link)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
return inbounds, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
if inbound.Protocol != model.VMess {
|
||||
return ""
|
||||
}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
network, _ := stream["network"].(string)
|
||||
typeStr := "none"
|
||||
host := ""
|
||||
path := ""
|
||||
sni := ""
|
||||
fp := ""
|
||||
var alpn []string
|
||||
allowInsecure := false
|
||||
switch network {
|
||||
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{})
|
||||
path = requestPath[0].(string)
|
||||
headers, _ := request["headers"].(map[string]interface{})
|
||||
host = searchHost(headers)
|
||||
}
|
||||
case "kcp":
|
||||
kcp, _ := stream["kcpSettings"].(map[string]interface{})
|
||||
header, _ := kcp["header"].(map[string]interface{})
|
||||
typeStr, _ = header["type"].(string)
|
||||
path, _ = kcp["seed"].(string)
|
||||
case "ws":
|
||||
ws, _ := stream["wsSettings"].(map[string]interface{})
|
||||
path = ws["path"].(string)
|
||||
headers, _ := ws["headers"].(map[string]interface{})
|
||||
host = searchHost(headers)
|
||||
case "http":
|
||||
network = "h2"
|
||||
http, _ := stream["httpSettings"].(map[string]interface{})
|
||||
path, _ = http["path"].(string)
|
||||
host = searchHost(http)
|
||||
case "quic":
|
||||
quic, _ := stream["quicSettings"].(map[string]interface{})
|
||||
header := quic["header"].(map[string]interface{})
|
||||
typeStr, _ = header["type"].(string)
|
||||
host, _ = quic["security"].(string)
|
||||
path, _ = quic["key"].(string)
|
||||
case "grpc":
|
||||
grpc, _ := stream["grpcSettings"].(map[string]interface{})
|
||||
path = grpc["serviceName"].(string)
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
if security == "tls" {
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||
sni, _ = sniValue.(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
fp, _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
allowInsecure, _ = insecure.(bool)
|
||||
}
|
||||
}
|
||||
serverName, _ := tlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
address = serverName
|
||||
}
|
||||
}
|
||||
|
||||
clients, _ := s.inboundService.getClients(inbound)
|
||||
clientIndex := -1
|
||||
for i, client := range clients {
|
||||
if client.Email == email {
|
||||
clientIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
obj := map[string]interface{}{
|
||||
"v": "2",
|
||||
"ps": email,
|
||||
"add": address,
|
||||
"port": inbound.Port,
|
||||
"id": clients[clientIndex].ID,
|
||||
"aid": clients[clientIndex].AlterIds,
|
||||
"net": network,
|
||||
"type": typeStr,
|
||||
"host": host,
|
||||
"path": path,
|
||||
"tls": security,
|
||||
"sni": sni,
|
||||
"fp": fp,
|
||||
"alpn": strings.Join(alpn, ","),
|
||||
"allowInsecure": allowInsecure,
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
if security == "tls" {
|
||||
params["security"] = "tls"
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
if len(alpn) > 0 {
|
||||
params["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
if insecure.(bool) {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
params["flow"] = clients[clientIndex].Flow
|
||||
}
|
||||
|
||||
serverName, _ := tlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
address = serverName
|
||||
}
|
||||
}
|
||||
|
||||
if security == "xtls" {
|
||||
params["security"] = "xtls"
|
||||
xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
|
||||
alpns, _ := xtlsSetting["alpn"].([]interface{})
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
if len(alpn) > 0 {
|
||||
params["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
|
||||
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
|
||||
if xtlsSetting != nil {
|
||||
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
|
||||
if insecure.(bool) {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
params["flow"] = clients[clientIndex].Flow
|
||||
}
|
||||
|
||||
serverName, _ := xtlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
address = serverName
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
url.Fragment = email
|
||||
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)
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
if security == "tls" {
|
||||
params["security"] = "tls"
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
if len(alpn) > 0 {
|
||||
params["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
if insecure.(bool) {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serverName, _ := tlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
address = serverName
|
||||
}
|
||||
}
|
||||
|
||||
if security == "xtls" {
|
||||
params["security"] = "xtls"
|
||||
xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
|
||||
alpns, _ := xtlsSetting["alpn"].([]interface{})
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
if len(alpn) > 0 {
|
||||
params["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
|
||||
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
|
||||
if xtlsSetting != nil {
|
||||
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
|
||||
if insecure.(bool) {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
params["flow"] = clients[clientIndex].Flow
|
||||
}
|
||||
|
||||
serverName, _ := xtlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
address = serverName
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
url.Fragment = email
|
||||
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{})
|
||||
return hosts[0].(string)
|
||||
case interface{}:
|
||||
return v.(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -105,8 +105,6 @@ func (t *Tgbot) OnReceive() {
|
||||
} else {
|
||||
if update.Message.IsCommand() {
|
||||
t.answerCommand(update.Message, chatId, isAdmin)
|
||||
} else {
|
||||
t.aswerChat(update.Message.Text, chatId, isAdmin)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,10 +126,20 @@ func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin b
|
||||
case "status":
|
||||
msg = "bot is ok ✅"
|
||||
case "usage":
|
||||
if isAdmin {
|
||||
t.searchClient(chatId, message.CommandArguments())
|
||||
if len(message.CommandArguments()) > 1 {
|
||||
if isAdmin {
|
||||
t.searchClient(chatId, message.CommandArguments())
|
||||
} else {
|
||||
t.searchForClient(chatId, message.CommandArguments())
|
||||
}
|
||||
} else {
|
||||
msg = "🚫 Insufficient privilege"
|
||||
msg = "❗Please provide a text for search!"
|
||||
}
|
||||
case "inbound":
|
||||
if isAdmin {
|
||||
t.searchInbound(chatId, message.CommandArguments())
|
||||
} else {
|
||||
msg = "❗ Unknown command"
|
||||
}
|
||||
default:
|
||||
msg = "❗ Unknown command"
|
||||
@@ -139,10 +147,6 @@ func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin b
|
||||
t.SendAnswer(chatId, msg, isAdmin)
|
||||
}
|
||||
|
||||
func (t *Tgbot) aswerChat(message string, chatId int64, isAdmin bool) {
|
||||
t.SendAnswer(chatId, "❗ Unknown message", isAdmin)
|
||||
}
|
||||
|
||||
func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bool) {
|
||||
// Respond to the callback query, telling Telegram to show the user
|
||||
// a message with the data received.
|
||||
@@ -156,14 +160,16 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, t.getServerUsage())
|
||||
case "inbounds":
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, t.getInboundUsages())
|
||||
case "exhausted_soon":
|
||||
case "deplete_soon":
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, t.getExhausted())
|
||||
case "get_backup":
|
||||
t.sendBackup(callbackQuery.From.ID)
|
||||
case "client_traffic":
|
||||
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName)
|
||||
case "client_commands":
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Passowrd]</code>\r\n \r\nUse UID for vmess/vless and Password for Trojan.")
|
||||
case "commands":
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for a client email, just use folowing command:\r\n \r\n<code>/usage email</code>")
|
||||
t.SendMsgToTgbot(callbackQuery.From.ID, "Search for a client email:\r\n<code>/usage email</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [remark]</code>")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +190,7 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Exhausted soon", "exhausted_soon"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Deplete soon", "deplete_soon"),
|
||||
),
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"),
|
||||
@@ -193,6 +199,7 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
||||
var numericKeyboardClient = tgbotapi.NewInlineKeyboardMarkup(
|
||||
tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "client_traffic"),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Commands", "client_commands"),
|
||||
),
|
||||
)
|
||||
msgConfig := tgbotapi.NewMessage(chatId, msg)
|
||||
@@ -269,6 +276,7 @@ func (t *Tgbot) getServerUsage() string {
|
||||
name = ""
|
||||
}
|
||||
info = fmt.Sprintf("💻 Hostname: %s\r\n", name)
|
||||
info += fmt.Sprintf("🚀X-UI Version: %s\r\n", config.GetVersion())
|
||||
//get ip address
|
||||
var ip string
|
||||
var ipv6 string
|
||||
@@ -370,6 +378,8 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
|
||||
expiryTime := ""
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = "♾Unlimited"
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -404,6 +414,8 @@ func (t *Tgbot) searchClient(chatId int64, email string) {
|
||||
expiryTime := ""
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = "♾Unlimited"
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -420,6 +432,80 @@ func (t *Tgbot) searchClient(chatId int64, email string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
||||
inbouds, err := t.inboundService.SearchInbounds(remark)
|
||||
if err != nil {
|
||||
logger.Warning(err)
|
||||
msg := "❌ Something went wrong!"
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
for _, inbound := range inbouds {
|
||||
info := ""
|
||||
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
|
||||
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||
if inbound.ExpiryTime == 0 {
|
||||
info += "Expire date: ♾ Unlimited\r\n \r\n"
|
||||
} else {
|
||||
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
t.SendMsgToTgbot(chatId, info)
|
||||
for _, traffic := range inbound.ClientStats {
|
||||
expiryTime := ""
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = "♾Unlimited"
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
total := ""
|
||||
if traffic.Total == 0 {
|
||||
total = "♾Unlimited"
|
||||
} else {
|
||||
total = common.FormatTraffic((traffic.Total))
|
||||
}
|
||||
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||
total, expiryTime)
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tgbot) searchForClient(chatId int64, query string) {
|
||||
traffic, err := t.inboundService.SearchClientTraffic(query)
|
||||
if err != nil {
|
||||
logger.Warning(err)
|
||||
msg := "❌ Something went wrong!"
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
if traffic == nil {
|
||||
msg := "No result!"
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
expiryTime := ""
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = "♾Unlimited"
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
total := ""
|
||||
if traffic.Total == 0 {
|
||||
total = "♾Unlimited"
|
||||
} else {
|
||||
total = common.FormatTraffic((traffic.Total))
|
||||
}
|
||||
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||
total, expiryTime)
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
}
|
||||
|
||||
func (t *Tgbot) getExhausted() string {
|
||||
trDiff := int64(0)
|
||||
exDiff := int64(0)
|
||||
@@ -429,13 +515,13 @@ func (t *Tgbot) getExhausted() string {
|
||||
var disabledInbounds []model.Inbound
|
||||
var disabledClients []xray.ClientTraffic
|
||||
output := ""
|
||||
TrafficThreshold, err := t.settingService.GetTgTrafficDiff()
|
||||
TrafficThreshold, err := t.settingService.GetTrafficDiff()
|
||||
if err == nil && TrafficThreshold > 0 {
|
||||
trDiff = int64(TrafficThreshold) * 1073741824
|
||||
}
|
||||
ExpireThreshold, err := t.settingService.GetTgExpireDiff()
|
||||
ExpireThreshold, err := t.settingService.GetExpireDiff()
|
||||
if err == nil && ExpireThreshold > 0 {
|
||||
exDiff = int64(ExpireThreshold) * 84600
|
||||
exDiff = int64(ExpireThreshold) * 86400000
|
||||
}
|
||||
inbounds, err := t.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
@@ -443,14 +529,14 @@ func (t *Tgbot) getExhausted() string {
|
||||
}
|
||||
for _, inbound := range inbounds {
|
||||
if inbound.Enable {
|
||||
if (inbound.ExpiryTime > 0 && (now-inbound.ExpiryTime < exDiff)) ||
|
||||
if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) ||
|
||||
(inbound.Total > 0 && (inbound.Total-inbound.Up+inbound.Down < trDiff)) {
|
||||
exhaustedInbounds = append(exhaustedInbounds, *inbound)
|
||||
}
|
||||
if len(inbound.ClientStats) > 0 {
|
||||
for _, client := range inbound.ClientStats {
|
||||
if client.Enable {
|
||||
if (client.ExpiryTime > 0 && (now-client.ExpiryTime < exDiff)) ||
|
||||
if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) ||
|
||||
(client.Total > 0 && (client.Total-client.Up+client.Down < trDiff)) {
|
||||
exhaustedClients = append(exhaustedClients, client)
|
||||
}
|
||||
@@ -463,8 +549,8 @@ func (t *Tgbot) getExhausted() string {
|
||||
disabledInbounds = append(disabledInbounds, *inbound)
|
||||
}
|
||||
}
|
||||
output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
|
||||
if len(disabledInbounds)+len(exhaustedInbounds) > 0 {
|
||||
output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
|
||||
if len(exhaustedInbounds) > 0 {
|
||||
output += "Exhausted Inbounds:\r\n"
|
||||
for _, inbound := range exhaustedInbounds {
|
||||
output += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\nTraffic: %s (↑%s,↓%s)\r\n", inbound.Remark, inbound.Port, common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
||||
@@ -475,13 +561,15 @@ func (t *Tgbot) getExhausted() string {
|
||||
}
|
||||
}
|
||||
}
|
||||
output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
|
||||
if len(disabledClients)+len(exhaustedClients) > 0 {
|
||||
output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Exhausted: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
|
||||
if len(exhaustedClients) > 0 {
|
||||
output += "Exhausted Clients:\r\n"
|
||||
for _, traffic := range exhaustedClients {
|
||||
expiryTime := ""
|
||||
if traffic.ExpiryTime == 0 {
|
||||
expiryTime = "♾Unlimited"
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime += fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
|
||||
} else {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
@@ -491,7 +579,7 @@ func (t *Tgbot) getExhausted() string {
|
||||
} else {
|
||||
total = common.FormatTraffic((traffic.Total))
|
||||
}
|
||||
output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||
output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire date: %s\r\n \r\n",
|
||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||
total, expiryTime)
|
||||
}
|
||||
@@ -509,4 +597,10 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
||||
if err != nil {
|
||||
logger.Warning("Error in uploading backup: ", err)
|
||||
}
|
||||
file = tgbotapi.FilePath(xray.GetConfigPath())
|
||||
msg = tgbotapi.NewDocument(chatId, file)
|
||||
_, err = bot.Send(msg)
|
||||
if err != nil {
|
||||
logger.Warning("Error in uploading config.json: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,15 +84,16 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
clients, ok := settings["clients"].([]interface{})
|
||||
if ok {
|
||||
// check users active or not
|
||||
|
||||
clientStats := inbound.ClientStats
|
||||
for _, clientTraffic := range clientStats {
|
||||
|
||||
indexDecrease := 0
|
||||
for index, client := range clients {
|
||||
c := client.(map[string]interface{})
|
||||
if c["email"] == clientTraffic.Email {
|
||||
if !clientTraffic.Enable {
|
||||
clients = RemoveIndex(clients, index)
|
||||
clients = RemoveIndex(clients, index-indexDecrease)
|
||||
indexDecrease++
|
||||
logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit")
|
||||
|
||||
}
|
||||
@@ -101,6 +102,29 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// clear client config for additional parameters
|
||||
indexDecrease := 0
|
||||
for index, client := range clients {
|
||||
|
||||
c := client.(map[string]interface{})
|
||||
|
||||
// remove disabled clients
|
||||
if c["enable"] != nil {
|
||||
if enable, ok := c["enable"].(bool); ok && !enable {
|
||||
clients = RemoveIndex(clients, index-indexDecrease)
|
||||
indexDecrease++
|
||||
continue
|
||||
}
|
||||
}
|
||||
for key := range c {
|
||||
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "alterId" {
|
||||
delete(c, key)
|
||||
}
|
||||
}
|
||||
clients[index-indexDecrease] = interface{}(c)
|
||||
}
|
||||
|
||||
settings["clients"] = clients
|
||||
modifiedSettings, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
|
||||
@@ -33,8 +33,11 @@
|
||||
"host" = "Host"
|
||||
"path" = "Path"
|
||||
"camouflage" = "Camouflage"
|
||||
"status" = "Status"
|
||||
"enabled" = "Enabled"
|
||||
"disabled" = "Disabled"
|
||||
"depleted" = "Depleted"
|
||||
"depletingSoon" = "Depleting soon"
|
||||
"domainName" = "Domain name"
|
||||
"additional" = "Alter"
|
||||
"monitor" = "Listen IP"
|
||||
@@ -69,6 +72,8 @@
|
||||
"memory" = "Memory"
|
||||
"hard" = "Hard disk"
|
||||
"xrayStatus" = "xray Status"
|
||||
"stopXray" = "Stop"
|
||||
"restartXray" = "Restart"
|
||||
"xraySwitch" = "Switch Version"
|
||||
"xraySwitchClick" = "Click on the version you want to switch"
|
||||
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
|
||||
@@ -126,7 +131,18 @@
|
||||
"keyContent" = "Key content"
|
||||
"clickOnQRcode" = "Click on QR Code to Copy"
|
||||
"client" = "Client"
|
||||
"export" = "Export links"
|
||||
"export" = "Export Links"
|
||||
"Clone" = "Clone"
|
||||
"cloneInbound" = "Create"
|
||||
"cloneInboundContent" = "All items of this inbound except Port, Listening IP, Clients will be applied to the clone"
|
||||
"resetAllTraffic" = "Reset All Inbounds Traffic"
|
||||
"resetAllTrafficTitle" = "Reset all inbounds traffic"
|
||||
"resetAllTrafficContent" = "Are you sure to reset all inbounds traffic ?"
|
||||
"resetAllClientTraffics" = "Reset Clients Traffic"
|
||||
"resetAllClientTrafficTitle" = "Reset all clients traffic"
|
||||
"resetAllClientTrafficContent" = "Are you sure to reset all traffics of this inbound's clients ?"
|
||||
"Email" = "Email"
|
||||
"EmailDesc" = "The Email Must Be Completely Unique"
|
||||
|
||||
[pages.client]
|
||||
"add" = "Add client"
|
||||
@@ -140,6 +156,9 @@
|
||||
"last" = "Last"
|
||||
"prefix" = "Prefix"
|
||||
"postfix" = "postfix"
|
||||
"delayedStart" = "Start after first use"
|
||||
"expireDays" = "Expire days"
|
||||
"days" = "day(s)"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "Obtain"
|
||||
@@ -177,7 +196,7 @@
|
||||
"panelPortDesc" = "Restart the panel to take effect"
|
||||
"publicKeyPath" = "Panel certificate public key file path"
|
||||
"publicKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
|
||||
"privateKeyPath" = "Panel certificate key file path"
|
||||
"privateKeyPath" = "Panel certificate private key file path"
|
||||
"privateKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
|
||||
"panelUrlPath" = "panel url root path"
|
||||
"panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect"
|
||||
@@ -209,10 +228,10 @@
|
||||
"telegramNotifyTimeDesc" = "Using Crontab timing format. Restart the panel to take effect"
|
||||
"tgNotifyBackup" = "Database backup"
|
||||
"tgNotifyBackupDesc" = "Sending database backup file with report notification. Restart the panel to take effect"
|
||||
"tgNotifyExpireTimeDiff" = "Remained time threshold"
|
||||
"tgNotifyExpireTimeDiffDesc" = "This telegram bot will send you a notification before expiration (unit:day)"
|
||||
"tgNotifyTrafficDiff" = "Remained traffic threshold"
|
||||
"tgNotifyTrafficDiffDesc" = "This telegram bot will send you a notification before finishing traffic (unit:GB)"
|
||||
"expireTimeDiff" = "Exhaustion time threshold"
|
||||
"expireTimeDiffDesc" = "Detect exhaustion before expiration (unit:day)"
|
||||
"trafficDiff" = "Exhaustion traffic threshold"
|
||||
"trafficDiffDesc" = "Detect exhaustion before finishing traffic (unit:GB)"
|
||||
"tgNotifyCpu" = "CPU percentage alert threshold"
|
||||
"tgNotifyCpuDesc" = "This telegram bot will send you a notification if CPU usage is more than this percentage (unit:%)"
|
||||
"timeZonee" = "Time Zone"
|
||||
|
||||
@@ -33,12 +33,15 @@
|
||||
"host" = "آدرس"
|
||||
"path" = "مسیر"
|
||||
"camouflage" = "استتار"
|
||||
"status" = "وضعیت"
|
||||
"enabled" = "فعال"
|
||||
"disabled" = "غیرفعال"
|
||||
"depleted" = "منقضی"
|
||||
"depletingSoon" = "در حال انقضا"
|
||||
"domainName" = "آدرس دامنه"
|
||||
"additional" = "آی دی جایگزین"
|
||||
"monitor" = "آی پی اتصال"
|
||||
"certificate" = "سرتیفیکیت"
|
||||
"certificate" = "گواهی دیجیتال"
|
||||
"fail" = "خطا"
|
||||
"success" = " موفق"
|
||||
"getVersion" = "دریافت ورژن"
|
||||
@@ -69,11 +72,13 @@
|
||||
"memory" = "حافظه رم"
|
||||
"hard" = "حافظه دیسک"
|
||||
"xrayStatus" = "وضعیت Xray"
|
||||
"stopXray" = "توقف"
|
||||
"restartXray" = "شروع مجدد"
|
||||
"xraySwitch" = "تغییر ورژن"
|
||||
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
|
||||
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
|
||||
"operationHours" = "ساعت فعال"
|
||||
"operationHoursDesc" = "ساعت فعال بعد از شروع سیستم"
|
||||
"operationHours" = "مدت فعالیت"
|
||||
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
|
||||
"systemLoad" = "بار روی سیستم"
|
||||
"connectionCount" = "تعداد کانکشن ها"
|
||||
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
||||
@@ -120,13 +125,24 @@
|
||||
"noRecommendKeepDefault" = "توصیه می شود به عنوان پیش فرض حفظ شود"
|
||||
"certificatePath" = "مسیر فایل گواهی"
|
||||
"certificateContent" = "محتوای فایل گواهی"
|
||||
"publicKeyPath" = "مسیر فایل Certificate.crt"
|
||||
"publicKeyContent" = "محتوای Certificate.crt"
|
||||
"keyPath" = "مسیر فایل Private.key"
|
||||
"keyContent" = "محتوای Private.key"
|
||||
"publicKeyPath" = "مسیر کلید عمومی"
|
||||
"publicKeyContent" = "محتوای کلید عمومی"
|
||||
"keyPath" = "مسیر کلید خصوصی"
|
||||
"keyContent" = "محتوای کلید خصوصی"
|
||||
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
|
||||
"client" = "کاربر"
|
||||
"export" = "استخراج لینکها"
|
||||
"Clone" = "شبیه سازی"
|
||||
"cloneInbound" = "ایجاد"
|
||||
"cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد"
|
||||
"resetAllTraffic" = "ریست ترافیک کل سرویس ها"
|
||||
"resetAllTrafficTitle" = "ریست ترافیک کل سرویس ها"
|
||||
"resetAllTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک سرویس ها را ریست کنید؟"
|
||||
"resetAllClientTraffics" = "ریست ترافیک کاربران"
|
||||
"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران"
|
||||
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
|
||||
"Email" = "ایمیل"
|
||||
"EmailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
|
||||
|
||||
[pages.client]
|
||||
"add" = "کاربر جدید"
|
||||
@@ -140,6 +156,9 @@
|
||||
"last" = "تا"
|
||||
"prefix" = "پیشوند"
|
||||
"postfix" = "پسوند"
|
||||
"delayedStart" = "شروع بعد از اولین استفاده"
|
||||
"expireDays" = "روزهای اعتبار"
|
||||
"days" = "(روز)"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "Obtain"
|
||||
@@ -175,9 +194,9 @@
|
||||
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"panelPort" = "پورت پنل"
|
||||
"panelPortDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"publicKeyPath" = "مسیر فایل پنل Certificate.crt"
|
||||
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
|
||||
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"privateKeyPath" = "مسیر فایل پنل private.key"
|
||||
"privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل"
|
||||
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"panelUrlPath" = "آدرس روت پنل"
|
||||
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
@@ -206,13 +225,13 @@
|
||||
"telegramChatId" = "آی دی تلگرام مدیریت"
|
||||
"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
|
||||
"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
|
||||
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
|
||||
"tgNotifyExpireTimeDiff" = "آستانه زمان باقی مانده"
|
||||
"tgNotifyExpireTimeDiffDesc" = "این ربات تلگرام قبل از انقضا برای شما پیام ارسال می کند (واحد: روز)"
|
||||
"tgNotifyTrafficDiff" = "آستانه ترافیک باقی مانده"
|
||||
"tgNotifyTrafficDiffDesc" = "این ربات تلگرام قبل از اتمام ترافیک برای شما پیام ارسال می کند (واحد: گیگابایت)"
|
||||
"expireTimeDiff" = "آستانه زمان باقی مانده"
|
||||
"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
|
||||
"trafficDiff" = "آستانه ترافیک باقی مانده"
|
||||
"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
|
||||
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
|
||||
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
|
||||
"timeZonee" = "منظقه زمانی"
|
||||
|
||||
@@ -33,8 +33,11 @@
|
||||
"host" = "主持人"
|
||||
"path" = "小路"
|
||||
"camouflage" = "伪装"
|
||||
"status" = "状态"
|
||||
"enabled" = "开启"
|
||||
"disabled" = "关闭"
|
||||
"depleted" = "耗尽"
|
||||
"depletingSoon" = "即将耗尽"
|
||||
"domainName" = "域名"
|
||||
"additional" = "额外"
|
||||
"monitor" = "监听"
|
||||
@@ -69,6 +72,8 @@
|
||||
"memory" = "内存"
|
||||
"hard" = "硬盘"
|
||||
"xrayStatus" = "xray 状态"
|
||||
"stopXray" = "停止"
|
||||
"restartXray" = "重启"
|
||||
"xraySwitch" = "切换版本"
|
||||
"xraySwitchClick" = "点击你想切换的版本"
|
||||
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
|
||||
@@ -127,6 +132,17 @@
|
||||
"clickOnQRcode" = "点击二维码复制"
|
||||
"client" = "客户"
|
||||
"export" = "导出链接"
|
||||
"Clone" = "克隆"
|
||||
"cloneInbound" = "创造"
|
||||
"cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆"
|
||||
"resetAllTraffic" = "重置所有入站流量"
|
||||
"resetAllTrafficTitle" = "重置所有入站流量"
|
||||
"resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
|
||||
"resetAllClientTraffics" = "重置客户端流量"
|
||||
"resetAllClientTrafficTitle" = "重置所有客户端流量"
|
||||
"resetAllClientTrafficContent" = "您确定要重置此入站客户端的所有流量吗?"
|
||||
"Email" = "电子邮件"
|
||||
"EmailDesc" = "电子邮件必须完全唯"
|
||||
|
||||
[pages.client]
|
||||
"add" = "添加客户端"
|
||||
@@ -140,6 +156,9 @@
|
||||
"last" = "最后"
|
||||
"prefix" = "前缀"
|
||||
"postfix" = "后缀"
|
||||
"delayedStart" = "首次使用后开始"
|
||||
"expireDays" = "过期天数"
|
||||
"days" = "天"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "获取"
|
||||
@@ -209,10 +228,10 @@
|
||||
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
|
||||
"tgNotifyBackup" = "数据库备份"
|
||||
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
|
||||
"tgNotifyExpireTimeDiff" = "剩余时间阈值"
|
||||
"tgNotifyExpireTimeDiffDesc" = "这个 talegram bot 会在到期前给你发送通知(单位:天)"
|
||||
"tgNotifyTrafficDiff" = "剩余流量阈值"
|
||||
"tgNotifyTrafficDiffDesc" = "这个 talegram bot 会在流量结束前给你发送通知(单位:GB)"
|
||||
"expireTimeDiff" = "耗尽时间阈值"
|
||||
"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
|
||||
"trafficDiff" = "耗尽流量阈值"
|
||||
"trafficDiffDesc" = "完成流量前检测耗尽(单位:GB)"
|
||||
"tgNotifyCpu" = "CPU 百分比警报阈值"
|
||||
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
|
||||
"timeZonee" = "时区"
|
||||
|
||||
10
web/web.go
10
web/web.go
@@ -33,6 +33,9 @@ import (
|
||||
//go:embed assets/*
|
||||
var assetsFS embed.FS
|
||||
|
||||
//go:embed assets/favicon.ico
|
||||
var favicon []byte
|
||||
|
||||
//go:embed html/*
|
||||
var htmlFS embed.FS
|
||||
|
||||
@@ -85,6 +88,7 @@ type Server struct {
|
||||
server *controller.ServerController
|
||||
xui *controller.XUIController
|
||||
api *controller.APIController
|
||||
sub *controller.SUBController
|
||||
|
||||
xrayService service.XrayService
|
||||
settingService service.SettingService
|
||||
@@ -157,6 +161,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
// Add favicon
|
||||
engine.GET("/favicon.ico", func(c *gin.Context) {
|
||||
c.Data(200, "image/x-icon", favicon)
|
||||
})
|
||||
|
||||
secret, err := s.settingService.GetSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -208,6 +217,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
s.server = controller.NewServerController(g)
|
||||
s.xui = controller.NewXUIController(g)
|
||||
s.api = controller.NewAPIController(g)
|
||||
s.sub = controller.NewSUBController(g)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
63
x-ui.sh
63
x-ui.sh
@@ -20,46 +20,41 @@ function LOGI() {
|
||||
# check root
|
||||
[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \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
|
||||
LOGE "check system OS failed,please contact with author! \n" && exit 1
|
||||
echo "Failed to check the system OS, please contact the author!" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "The OS release is: $release"
|
||||
|
||||
|
||||
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
|
||||
LOGE "please use CentOS 7 or higher version! \n" && exit 1
|
||||
fi
|
||||
elif [[ x"${release}" == x"ubuntu" ]]; then
|
||||
if [[ ${os_version} -lt 16 ]]; then
|
||||
LOGE "please use Ubuntu 16 or higher version!\n" && exit 1
|
||||
fi
|
||||
elif [[ x"${release}" == x"debian" ]]; then
|
||||
if [[ "${release}" == "centos" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
LOGE "please use Debian 8 or higher version!\n" && exit 1
|
||||
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
elif [[ "${release}" == "ubuntu" ]]; then
|
||||
if [[ ${os_version} -lt 20 ]]; then
|
||||
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "fedora" ]]; then
|
||||
if [[ ${os_version} -lt 36 ]]; then
|
||||
echo -e "${red}please use Fedora 36 or higher version! ${plain}\n" && exit 1
|
||||
fi
|
||||
|
||||
elif [[ "${release}" == "debian" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red} Please use Debian 8 or higher ${plain}\n" && exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user