Compare commits

...

66 Commits
0.4.0 ... 0.4.3

Author SHA1 Message Date
Alireza Ahmadi
8f0a701d9e v0.4.3 2023-03-26 13:38:32 +02:00
Alireza Ahmadi
921a72b5cf v0.4.3 2023-03-24 12:52:00 +01:00
Alireza Ahmadi
9e902fc82f Add version and log 2023-03-24 12:24:12 +01:00
Alireza Ahmadi
6ed8ff8e05 [infoModal] better display 2023-03-24 12:23:23 +01:00
Alireza Ahmadi
f333ad23d9 Merge pull request #92 from alireza0/dependabot/go_modules/google.golang.org/grpc-1.54.0
Bump google.golang.org/grpc from 1.53.0 to 1.54.0
2023-03-24 12:06:58 +01:00
Alireza Ahmadi
3f49aa5541 Merge pull request #89 from alireza0/TLS-enhancements
TLS-enhancements
2023-03-24 12:06:33 +01:00
Alireza Ahmadi
0f487dc10f [tgbot] fix admins input 2023-03-24 08:43:09 +01:00
dependabot[bot]
f3c0edac07 Bump google.golang.org/grpc from 1.53.0 to 1.54.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.53.0 to 1.54.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.53.0...v1.54.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-23 22:07:30 +00:00
Alireza Ahmadi
394c857bb6 [tgbot] fix exhausted report 2023-03-23 22:55:23 +01:00
Alireza Ahmadi
6080709fc7 Enable fallback in xtls as well 2023-03-23 20:13:21 +01:00
Alireza Ahmadi
17c47f0711 Remove ugly-bugy qrCode footer 2023-03-23 18:29:12 +01:00
Alireza Ahmadi
dac234357a Fix input-group dark theme 2023-03-23 12:53:36 +01:00
Alireza Ahmadi
2d15073a90 Fix vmess link 2023-03-23 11:16:48 +01:00
Alireza Ahmadi
1b2e165a65 Fix TLS and ALPN 2023-03-23 10:03:09 +01:00
Alireza Ahmadi
e1f8e8efd2 Add allowInsecure option 2023-03-23 10:02:18 +01:00
Alireza Ahmadi
738c7064a9 Default TLS version change to 1.0-1.2 2023-03-23 10:01:28 +01:00
Alireza Ahmadi
c24483ad44 [tgbot] fix client search #85 2023-03-23 09:59:34 +01:00
Alireza Ahmadi
046f071378 [bug] Fix Transport column in search table 2023-03-21 15:03:26 +01:00
Alireza Ahmadi
d40a16e29d Update README 2023-03-20 13:50:39 +01:00
Alireza Ahmadi
2f1f28cc0b v0.4.2 2023-03-20 12:03:52 +01:00
Alireza Ahmadi
70ff51d629 [Bulk client] add option: without random email #72 2023-03-20 11:50:44 +01:00
Alireza Ahmadi
7472c31c34 [TGBOT] add seach inbound #75 2023-03-20 11:18:47 +01:00
Alireza Ahmadi
04a8d5c177 [DarkMode] better dark theme based on antdv2 2023-03-20 10:50:31 +01:00
Alireza Ahmadi
7b2bd4247d TGBOT: Add xray config to backup #75 2023-03-20 10:22:41 +01:00
Alireza Ahmadi
5aee1c886b Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-19 20:19:04 +01:00
Alireza Ahmadi
7a9a30a038 Dark-Mode: Better contrast 2023-03-19 19:59:35 +01:00
Alireza Ahmadi
eb17cec289 Remove chat answer 2023-03-19 19:07:40 +01:00
Alireza Ahmadi
22e107c746 Update install.sh
Fix centos7+ message
2023-03-19 11:56:42 +01:00
Alireza Ahmadi
36a93e2959 Merge pull request #77 from forkeer/main
Fedora OS Support
2023-03-19 11:56:13 +01:00
Alireza Ahmadi
073247f054 Fix Link bug 2023-03-19 11:51:08 +01:00
Leonardo
96c8932961 Merge branch 'alireza0:main' into main 2023-03-19 00:14:23 +03:30
Alireza Ahmadi
3d4b87d72f v0.4.1 2023-03-18 17:26:39 +01:00
Leonardo
5fac7df174 Merge branch 'alireza0:main' into main 2023-03-18 13:45:41 +03:30
Leonardo
8bba9f3c25 Update install.sh
Fix s390x
2023-03-18 13:43:54 +03:30
Leonardo
268e8c7eb1 Update x-ui.sh
Fix Debian 8 Support
2023-03-18 13:39:54 +03:30
Leonardo
14e8f6cbfa Update x-ui.sh
Fix OS Version
2023-03-18 13:38:37 +03:30
Leonardo
7f0ba495db Update install.sh
Fix OS Version
2023-03-18 13:36:01 +03:30
Alireza Ahmadi
af07a9bf15 Fix error handling in client update 2023-03-17 18:57:20 +01:00
Alireza Ahmadi
6b8ca0c321 v0.4.1 2023-03-17 15:15:06 +01:00
Alireza Ahmadi
48554ab497 Dark-Mode: Media css fix 2023-03-17 12:36:55 +01:00
Alireza Ahmadi
dddebf02ef Add translation xray functions 2023-03-17 11:09:04 +01:00
Alireza Ahmadi
0c469848dc Merge pull request #63 from lk29/main-add-1
add Xray stop & Xray restart into web
2023-03-17 10:58:02 +01:00
Alireza Ahmadi
cb85d4f83f Merge pull request #70 from alireza0/dependabot/github_actions/actions/setup-go-4
Bump actions/setup-go from 3 to 4
2023-03-17 10:57:30 +01:00
Alireza Ahmadi
df29570dc1 Dark-Mode: CSS fixes #50 2023-03-17 10:49:44 +01:00
Alireza Ahmadi
7e9c336a17 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-16 20:48:29 +01:00
Alireza Ahmadi
585f43498c TGBot: Add usage search for user #67 2023-03-16 20:48:21 +01:00
Alireza Ahmadi
2a7a09d12f Merge pull request #71 from alireza0:Dark-Mode
Add Dark-Mode Theme
2023-03-16 16:12:17 +01:00
Alireza Ahmadi
98759e964c Add theme to modal forms #50 2023-03-16 15:31:05 +01:00
Alireza Ahmadi
9ae3d62afb Remove access.log in default config 2023-03-16 11:22:28 +01:00
Alireza Ahmadi
24b718dca3 Init dark mode for main pages #50 2023-03-16 11:06:04 +01:00
dependabot[bot]
d0c4d0a941 Bump actions/setup-go from 3 to 4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-15 22:07:17 +00:00
Leonardo
f829213dd1 go-version
go-version update to stable
2023-03-14 14:17:52 +03:30
Leonardo
4d5a73512e Update README.md
Fedora 36+
2023-03-14 14:01:52 +03:30
Leonardo
6dccc5b891 Update install.sh
Fedora OS Fix
2023-03-14 14:00:14 +03:30
Leonardo
f642830dc8 Update x-ui.sh
Fix Fedora
2023-03-14 13:56:56 +03:30
root
073b2ee8a8 add Xray stop & Xray restart 2023-03-13 22:32:24 +03:00
Alireza Ahmadi
8bcfa0b101 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-13 13:19:56 +01:00
Alireza Ahmadi
cd9e903ec3 Delete db directory 2023-03-13 13:19:45 +01:00
Alireza Ahmadi
b9fecaa918 Merge branch 'main' of https://github.com/alireza0/x-ui 2023-03-13 13:19:18 +01:00
Alireza Ahmadi
eb516575ec Remove default docker db 2023-03-13 13:19:12 +01:00
Alireza Ahmadi
aeb5c9acaa Merge pull request #62 from alireza0/revert-59-main
Revert "Added Fedora OS Support"
2023-03-13 11:36:55 +01:00
Alireza Ahmadi
68a7b38117 Revert "Added Fedora OS Support" 2023-03-13 11:36:37 +01:00
Alireza Ahmadi
e084518f4f Merge pull request #59 from forkeer/main
Added Fedora OS Support
2023-03-13 11:23:49 +01:00
Leonardo
e6460d1d00 Update README.md
Added Fedora OS
2023-03-11 15:29:43 +03:30
Leonardo
0218af8b4f Update install.sh
Fix Fedora OS
2023-03-11 15:28:56 +03:30
Leonardo
552bc6c1f6 Update x-ui.sh
Fix Fedora OS
2023-03-11 15:27:26 +03:30
43 changed files with 995 additions and 396 deletions

View File

@@ -12,9 +12,9 @@ jobs:
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
@@ -50,7 +50,7 @@ jobs:
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
@@ -90,7 +90,7 @@ jobs:
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

View File

@@ -11,6 +11,7 @@ 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: |
@@ -24,6 +25,7 @@ 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
@@ -36,13 +38,20 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
# Screenshot from Inbouds page
![inbounds](./media/inbounds.png)
![Dark inbounds](./media/inbounds-dark.png)
# Install & Upgrade
# 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 +74,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 +84,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 \
@@ -113,8 +122,8 @@ certbot certonly --standalone --register-unsafely-without-email --non-interactiv
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
@@ -135,8 +144,9 @@ Reference syntax:
- 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 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
@@ -149,6 +159,7 @@ Reference syntax:
- CentOS 7+
- Ubuntu 16+
- Debian 8+
- Fedora 36+
# common problem

View File

@@ -1 +1 @@
0.4.0
0.4.3

Binary file not shown.

2
go.mod
View File

@@ -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
View File

@@ -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=

View File

@@ -10,24 +10,18 @@ cur_dir=$(pwd)
# check root
[[ $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 [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 7 ]]; then
echo -e "${red} Please use CentOS 7 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 18 ]]; then
echo -e "${red}please use Ubuntu 18 or higher version${plain}\n" && exit 1
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
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 [[ 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
elif [[ "${release}" == "debian" ]]; 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 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 107 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,152 +1,339 @@
#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-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;
}
.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 {
background-color: #004488;
}
.ant-card-dark tbody .ant-table-expanded-row {
color: hsla(0,0%,100%,.65);
background-color: #1a212a;
}
.ant-card-dark .ant-input,
.ant-card-dark .ant-input-number,
.ant-card-dark .ant-calendar-input,
.ant-card-dark .ant-select-dropdown-menu-item-selected,
.ant-card-dark .ant-select-selection {
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-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-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(0,0,0,.12);
box-shadow: 0 2px 0 rgba(0,0,0,.045);
}

View File

@@ -92,6 +92,11 @@ const UTLS_FINGERPRINT = {
UTLS_RANDOMIZED: "randomized",
};
const ALPN_OPTION = {
H2: "h2",
HTTP1: "http/1.1",
};
Object.freeze(Protocols);
Object.freeze(VmessMethods);
Object.freeze(SSMethods);
@@ -101,6 +106,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 +473,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 +486,7 @@ class TlsStreamSettings extends XrayCommonClass {
this.cipherSuites = cipherSuites;
this.certs = certificates;
this.alpn = alpn;
this.settings = settings;
}
addCert(cert) {
@@ -491,17 +499,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 +526,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 +573,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',
@@ -920,7 +958,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 +1080,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 +1095,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 +1140,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 +1199,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 +1244,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) {

View File

@@ -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", a.getLogs)
}
func (a *ServerController) refreshStatus() {
@@ -83,3 +87,32 @@ 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) {
logs, err := a.serverService.GetLogs()
if err != nil {
jsonMsg(c, I18n(c, "getLogs"), err)
return
}
jsonObj(c, logs, nil)
}

View File

@@ -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"

View File

@@ -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'),

View File

@@ -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">

View File

@@ -1,15 +1,17 @@
{{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">Random+Prefix+Num@Telegram Username</a-select-option>
<a-select-option :value="5">Prefix+Num+Postfix [ BE CAREFUL! ]</a-select-option>
</a-select>
</a-form-item><br />
<a-form-item v-if="clientsBulkModal.emailMethod>1">
@@ -57,7 +59,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>
@@ -89,11 +92,12 @@
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 : "";
for (let i = start; i < end; i++) {
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
if(method==5) newClient.email = "";
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
newClient._totalGB = clientsBulkModal.totalGB;
newClient._expiryTime = clientsBulkModal.expiryTime;

View File

@@ -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>

View File

@@ -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';
}
};

View File

@@ -25,7 +25,7 @@
<a-input type="number" v-model.number="client.alterId"></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 +48,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) ]]
@@ -67,7 +67,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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option>

View File

@@ -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">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,12 +59,10 @@
</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>
</tr>
<tr>
<td v-for="col in Object.values(infoModal.clientSettings).slice(0, 3)"><a-tag color="green">[[ col ]]</a-tag></td>
<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>
</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>
@@ -211,7 +210,7 @@
return this.infoModal.inbound;
},
get isEnable() {
if(infoModal.clientStats.length){
if(infoModal.clientStats){
return infoModal.clientStats.enable;
}
return infoModal.dbInbound.isEnable;

View File

@@ -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>

View File

@@ -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" }}:
@@ -48,7 +48,7 @@
</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>
@@ -63,7 +63,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" style="border: 1px solid rgba(255, 255, 255, 0.65);">
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
@@ -114,11 +114,11 @@
</template>
<a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
</template>
<template slot="stream" slot-scope="text, dbInbound, index">
<template slot="stream" slot-scope="text, dbInbound">
<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>
<a-tag color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag v-if="dbInbound.toInbound().stream.isTls" color="blue">tls</a-tag>
<a-tag v-if="dbInbound.toInbound().stream.isXTls" color="blue">xtls</a-tag>
</template>
<template v-else>{{ i18n "none" }}</template>
</template>
@@ -195,11 +195,11 @@
title: '{{ i18n "pages.inbounds.port" }}',
align: 'center',
dataIndex: "port",
width: 30,
width: 50,
}, {
title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
align: 'center',
width: 150,
width: 120,
scopedSlots: { customRender: 'traffic' },
}, {
title: '{{ i18n "pages.inbounds.transportConfig" }}',
@@ -214,19 +214,19 @@
}];
const innerColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 60, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 80, scopedSlots: { customRender: 'actions' } },
{ 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: 80, scopedSlots: { customRender: 'actions' } },
{ 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({
@@ -451,6 +451,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 +466,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 +486,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),
@@ -527,6 +530,7 @@
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),

View File

@@ -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">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,17 @@
</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="">
<table style="margin: 0px; width: 100%; background-color: black; color: hsla(0,0%,100%,.65);">
<tr v-for="log , index in logModal.logs">
<td style="vertical-align: top;">[[ index ]]</td><td>[[ log ]]</td>
</tr>
</table>
</a-modal>
</a-layout>
{{template "js" .}}
<script>
@@ -269,6 +293,18 @@
},
};
const logModal = {
visible: false,
logs: '',
show(logs) {
this.visible = true;
this.logs = logs;
},
hide() {
this.visible = false;
},
};
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
@@ -276,6 +312,7 @@
siderDrawer,
status: new Status(),
versionModal,
logModal,
spinning: false,
loadingTip: '{{ i18n "loading"}}',
},
@@ -307,6 +344,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 +354,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(){
this.loading(true);
const msg = await HttpUtil.post('server/logs');
this.loading(false);
if (!msg.success) {
return;
}
logModal.show(msg.obj);
}
},
async mounted() {
while (true) {

View File

@@ -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,17 @@
<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>
<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 +56,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>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
@@ -71,7 +72,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 +94,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,10 +114,10 @@
</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>
@@ -125,7 +126,7 @@
</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>

View File

@@ -1,7 +1,6 @@
{
"log": {
"loglevel": "warning",
"access": "./access.log"
"loglevel": "warning"
},
"api": {

View File

@@ -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
}
@@ -396,7 +402,6 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err, traffic.Email)
}
continue
}
@@ -424,7 +429,6 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
logger.Warning("AddClientTraffic update data ", err)
continue
}
}
return
}
@@ -528,3 +532,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
}

View File

@@ -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() ([]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", "100"}
} 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
}

View File

@@ -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.
@@ -162,8 +166,10 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
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 and 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>")
}
}
@@ -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
@@ -420,6 +428,76 @@ 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 {
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 {
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)
@@ -435,7 +513,7 @@ func (t *Tgbot) getExhausted() string {
}
ExpireThreshold, err := t.settingService.GetTgExpireDiff()
if err == nil && ExpireThreshold > 0 {
exDiff = int64(ExpireThreshold) * 84600
exDiff = int64(ExpireThreshold) * 84600000
}
inbounds, err := t.inboundService.GetAllInbounds()
if err != nil {
@@ -443,14 +521,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)
}
@@ -464,7 +542,7 @@ func (t *Tgbot) getExhausted() string {
}
}
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 {
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))
@@ -476,7 +554,7 @@ 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 {
if len(exhaustedClients) > 0 {
output += "Exhausted Clients:\r\n"
for _, traffic := range exhaustedClients {
expiryTime := ""
@@ -491,7 +569,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 +587,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)
}
}

View File

@@ -69,6 +69,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"

View File

@@ -69,11 +69,13 @@
"memory" = "حافظه رم"
"hard" = "حافظه دیسک"
"xrayStatus" = "وضعیت Xray"
"stopXray" = "توقف"
"restartXray" = "شروع مجدد"
"xraySwitch" = "تغییر ورژن"
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
"operationHours" = "ساعت فعال"
"operationHoursDesc" = "ساعت فعال بعد از شروع سیستم"
"operationHours" = "مدت فعالیت"
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
"systemLoad" = "بار روی سیستم"
"connectionCount" = "تعداد کانکشن ها"
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"

View File

@@ -69,6 +69,8 @@
"memory" = "内存"
"hard" = "硬盘"
"xrayStatus" = "xray 状态"
"stopXray" = "停止"
"restartXray" = "重启"
"xraySwitch" = "切换版本"
"xraySwitchClick" = "点击你想切换的版本"
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"

59
x-ui.sh
View File

@@ -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 [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 7 ]]; then
echo -e "${red} Please use CentOS 7 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 18 ]]; then
echo -e "${red}please use Ubuntu 18 or higher version${plain}\n" && exit 1
fi
if [[ x"${release}" == x"centos" ]]; then
if [[ ${os_version} -le 6 ]]; then
LOGE "please use CentOS 7 or higher version! \n" && exit 1
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 [[ 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
elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 8 ]]; then
LOGE "please use Debian 8 or higher version\n" && exit 1
echo -e "${red} Please use Debian 8 or higher ${plain}\n" && exit 1
fi
fi