Compare commits

...

80 Commits
1.2.0 ... 1.3.0

Author SHA1 Message Date
Alireza Ahmadi
7c37319438 v1.3.0 2023-05-16 14:39:07 +02:00
Alireza Ahmadi
22605edb20 [sub] add more headers for hiddifyNG
References
https://github.com/hiddify/HiddifyNG/wiki
2023-05-16 14:35:30 +02:00
Alireza Ahmadi
cb4c843dbf [sub] more text length for random subID 2023-05-16 13:43:17 +02:00
Alireza Ahmadi
c2541d65f9 Merge pull request #311 from hamid-gh98/main
[FIX] themeSwitch class + [ADD] password component + ...
2023-05-16 11:50:01 +02:00
Hamidreza Ghavami
a99035bd3d Revert to doAllItemsExist 2023-05-16 14:16:23 +04:30
Hamidreza Ghavami
57e297d6cf Update translations 2023-05-16 06:14:45 +04:30
Hamidreza Ghavami
97b603c4a7 [Update] split country configs in settings page 2023-05-16 06:14:19 +04:30
Hamidreza Ghavami
8216de011e fix login ui + input border style 2023-05-16 03:40:06 +04:30
Hamidreza Ghavami
f3cdc35452 fix inbounds.html 2023-05-16 03:39:35 +04:30
Hamidreza Ghavami
a118bae974 Update translations 2023-05-16 02:49:09 +04:30
Hamidreza Ghavami
e01a3ac893 FIX settings.html 2023-05-16 02:35:24 +04:30
Hamidreza Ghavami
f9e2d9f61e Merge branch 'main' of https://github.com/hamid-gh98/x-ui into main 2023-05-16 02:22:02 +04:30
Hamidreza Ghavami
28233884bb Update translations 2023-05-16 02:21:24 +04:30
Hamidreza Ghavami
a0a4d7571d rename doAllItemsExist function 2023-05-16 02:20:53 +04:30
Hamidreza
7dde506862 Merge branch 'main' into main 2023-05-16 00:24:01 +03:30
Hamidreza Ghavami
1f5a785806 fix conflict 2023-05-16 01:22:37 +04:30
Hamidreza Ghavami
df13bf1a97 Merge branch 'main' of https://github.com/hamid-gh98/x-ui into main 2023-05-16 01:21:25 +04:30
Hamidreza Ghavami
4f289fbbad update htmls 2023-05-16 01:17:47 +04:30
Alireza Ahmadi
ef0a48de23 [feature] multi cert per inbound #290 2023-05-16 01:15:41 +04:30
Alireza Ahmadi
545d7a73c8 security issue - CVE-2023-29401
Gin Web Framework does not properly sanitize filename parameter of Context.FileAttachment function

References
gin-gonic/gin#3555
gin-gonic/gin#3556
https://pkg.go.dev/vuln/GO-2023-1737

Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2023-05-16 01:15:41 +04:30
Alireza Ahmadi
58474373d9 [darkmode] fix dropdown list theme 2023-05-16 01:15:39 +04:30
Hamidreza Ghavami
eb7fda21be fix styles 2023-05-16 01:13:13 +04:30
Hamidreza Ghavami
68b0cb1312 update common.js 2023-05-16 01:11:31 +04:30
Alireza Ahmadi
520ce4b557 update readme/clean utils 2023-05-15 21:46:15 +02:00
Hamidreza
63eda1ed27 Merge branch 'alireza0:main' into main 2023-05-15 23:13:31 +03:30
Alireza Ahmadi
74595b2785 [feature] multi cert per inbound #290 2023-05-15 21:26:08 +02:00
Hamidreza Ghavami
b79ff596a2 Merge branch 'main' of https://github.com/hamid-gh98/x-ui into main 2023-05-15 23:20:23 +04:30
Hamidreza
e5f0cf9b76 Merge branch 'alireza0:main' into main 2023-05-15 22:20:13 +03:30
Hamidreza Ghavami
0248b981d7 fix styles 2023-05-15 23:12:52 +04:30
Alireza Ahmadi
bcb90ac14a security issue - CVE-2023-29401
Gin Web Framework does not properly sanitize filename parameter of Context.FileAttachment function

References
gin-gonic/gin#3555
gin-gonic/gin#3556
https://pkg.go.dev/vuln/GO-2023-1737

Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2023-05-15 20:41:24 +02:00
Hamidreza
58ae8fadad Merge branch 'main' into main 2023-05-15 22:08:28 +03:30
Alireza Ahmadi
33e41f1bda [darkmode] fix dropdown list theme 2023-05-15 20:37:16 +02:00
Hamidreza Ghavami
b87705f50b Update htmls 2023-05-15 23:02:21 +04:30
Hamidreza Ghavami
2349c9fdbe FIX themeSwitcher classes 2023-05-15 23:02:10 +04:30
Hamidreza Ghavami
b00d33830c Add password component 2023-05-15 23:01:44 +04:30
Hamidreza Ghavami
d14c5f4f67 Update sub remarks 2023-05-15 23:01:07 +04:30
Hamidreza Ghavami
c538301d42 Update docker-compose 2023-05-15 22:59:23 +04:30
Hamidreza Ghavami
161c4c950b Update translation 2023-05-15 22:58:57 +04:30
Alireza Ahmadi
243273defd new xray settings configuration #202 2023-05-15 19:47:48 +02:00
Alireza Ahmadi
ceb0e0837f add copy subID 2023-05-15 19:39:09 +02:00
Alireza Ahmadi
710c2280a8 random UUID/SubID 2023-05-15 19:35:41 +02:00
Alireza Ahmadi
8a9b3eddfe show email in QrCode 2023-05-15 19:33:58 +02:00
Alireza Ahmadi
0c4bf6fac8 small fixes 2023-05-15 19:32:02 +02:00
Alireza Ahmadi
5e8556be33 Add Russian language 2023-05-15 19:26:47 +02:00
Alireza Ahmadi
d86c75b925 translation 2023-05-15 19:26:11 +02:00
Alireza Ahmadi
c8c0bbc455 add email to qrcode modal
Co-authored-by: Hamidreza Ghavami <hamid.r.gh.1998@gmail.com>
2023-05-15 15:32:45 +02:00
Alireza Ahmadi
6d453fa91b fix expiry sign in unlimited clients 2023-05-15 15:26:08 +02:00
Alireza Ahmadi
abc82cef4c [darkmode] re-design theme-switch
Co-authored-by: Hamidreza Ghavami <hamid.r.gh.1998@gmail.com>
2023-05-15 15:24:08 +02:00
Alireza Ahmadi
14270caa16 better JS-CSS 2023-05-15 14:57:40 +02:00
Alireza Ahmadi
aa74f05c52 Add iran data file to build automation #113 2023-05-15 14:27:41 +02:00
Alireza Ahmadi
953a3ae315 fix conflict random credentials 2023-05-15 14:07:17 +02:00
Alireza Ahmadi
a097733ccc [bug] fix cloned inbound settings #305 2023-05-12 11:34:46 +02:00
Alireza Ahmadi
7edaf89596 [bug] fix login failure when tgbot is not active 2023-05-11 10:47:51 +02:00
Alireza Ahmadi
f8fe396ed5 [feature] interactive deplete soon 2023-05-10 21:18:49 +02:00
Alireza Ahmadi
128e027dac pruning some codes 2023-05-10 19:17:27 +02:00
Alireza Ahmadi
7a36eda6b4 Set session maxage to default if defined zero 2023-05-10 18:10:29 +02:00
Alireza Ahmadi
00746c7864 Merge pull request #303 from alireza0/dependabot/go_modules/gorm.io/gorm-1.25.1
Bump gorm.io/gorm from 1.25.0 to 1.25.1
2023-05-09 09:28:46 +02:00
dependabot[bot]
c1fb8712d6 Bump gorm.io/gorm from 1.25.0 to 1.25.1
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.25.0 to 1.25.1.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.25.0...v1.25.1)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-08 21:14:53 +00:00
Alireza Ahmadi
0bd44a64ea Merge pull request #299 from hamid-gh98/main
[Feature] import/export database in the panel
2023-05-08 12:58:53 +02:00
Alireza Ahmadi
d532eb6bd8 Merge pull request #296 from alireza0/dependabot/go_modules/google.golang.org/grpc-1.55.0
Bump google.golang.org/grpc from 1.54.0 to 1.55.0
2023-05-08 12:56:30 +02:00
Alireza Ahmadi
8960e5450a spin only in reload time 2023-05-08 12:43:52 +02:00
Alireza Ahmadi
d1d7ee7f7c remove duplicate remark assignments 2023-05-08 12:42:48 +02:00
Alireza Ahmadi
8ddf7963c7 [feature] add netwrk stream to shadowsocks #294 2023-05-08 12:33:24 +02:00
Alireza Ahmadi
5bb0372aee Init with random credentials 2023-05-08 12:23:49 +02:00
Hamidreza Ghavami
788a1b9d6b update ImportDB and enhancement 2023-05-06 04:47:34 +04:30
Hamidreza Ghavami
9d1aa0d45f fix import db and always restart xray 2023-05-06 02:22:38 +04:30
Hamidreza Ghavami
b125f1835c add MigrateDB func for a single source of truth 2023-05-06 00:22:21 +04:30
Hamidreza Ghavami
5e3c0d6ecc update README.md 2023-05-05 22:58:57 +04:30
Hamidreza Ghavami
c197165da7 update translation 2023-05-05 22:58:40 +04:30
Hamidreza Ghavami
ec1efabcaa add modal and button for import/export db 2023-05-05 22:58:22 +04:30
Hamidreza Ghavami
3ee57fd51d update axios-init and db.go 2023-05-05 22:57:47 +04:30
Hamidreza Ghavami
26b748338a add import db api route 2023-05-05 22:55:40 +04:30
dependabot[bot]
af432828f1 Bump google.golang.org/grpc from 1.54.0 to 1.55.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.54.0 to 1.55.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.54.0...v1.55.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-05-04 22:03:15 +00:00
Alireza Ahmadi
cb14d298c0 Merge pull request #292 from alireza0/dependabot/go_modules/go.uber.org/atomic-1.11.0
Bump go.uber.org/atomic from 1.10.0 to 1.11.0
2023-05-04 10:07:42 +02:00
dependabot[bot]
994a9f4907 Bump go.uber.org/atomic from 1.10.0 to 1.11.0
Bumps [go.uber.org/atomic](https://github.com/uber-go/atomic) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/uber-go/atomic/releases)
- [Changelog](https://github.com/uber-go/atomic/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uber-go/atomic/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: go.uber.org/atomic
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-03 22:03:58 +00:00
Alireza Ahmadi
01bfd36316 Merge pull request #284 from alireza0/dependabot/go_modules/github.com/shirou/gopsutil/v3-3.23.4
Bump github.com/shirou/gopsutil/v3 from 3.23.3 to 3.23.4
2023-05-03 13:44:56 +02:00
Alireza Ahmadi
4a332c6723 Spinning on Refreshing 2023-05-03 13:41:13 +02:00
Alireza Ahmadi
c3d1824ea2 v1.2.1 2023-05-03 11:31:02 +02:00
Alireza Ahmadi
57a32ab524 [bug] fix delete/disable client 2023-05-03 11:30:31 +02:00
dependabot[bot]
c89dbed88e Bump github.com/shirou/gopsutil/v3 from 3.23.3 to 3.23.4
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.23.3 to 3.23.4.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.23.3...v3.23.4)

---
updated-dependencies:
- dependency-name: github.com/shirou/gopsutil/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 22:08:22 +00:00
71 changed files with 2738 additions and 1211 deletions

View File

@@ -28,9 +28,10 @@ jobs:
cd bin cd bin
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-64.zip wget https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-64.zip
unzip Xray-linux-64.zip unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip geoip.dat geosite.dat rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
mv xray xray-linux-amd64 mv xray xray-linux-amd64
cd .. cd ..
cd .. cd ..
@@ -68,9 +69,10 @@ jobs:
cd bin cd bin
wget https://github.com/xtls/xray-core/releases/download/v1.8.1/Xray-linux-arm64-v8a.zip wget https://github.com/xtls/xray-core/releases/download/v1.8.1/Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
mv xray xray-linux-arm64 mv xray xray-linux-arm64
cd .. cd ..
cd .. cd ..
@@ -108,9 +110,10 @@ jobs:
cd bin cd bin
wget https://github.com/xtls/xray-core/releases/download/v1.8.1/Xray-linux-s390x.zip wget https://github.com/xtls/xray-core/releases/download/v1.8.1/Xray-linux-s390x.zip
unzip Xray-linux-s390x.zip unzip Xray-linux-s390x.zip
rm -f Xray-linux-s390x.zip geoip.dat geosite.dat rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
mv xray xray-linux-s390x mv xray xray-linux-s390x
cd .. cd ..
cd .. cd ..

2
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.idea .idea
.vscode .vscode
tmp tmp
backup/
bin/ bin/
dist/ dist/
x-ui-*.tar.gz x-ui-*.tar.gz
@@ -10,4 +11,5 @@ x-ui-*.tar.gz
main main
release/ release/
access.log access.log
error.log
.cache .cache

View File

@@ -13,8 +13,9 @@ mkdir -p build/bin
cd build/bin cd build/bin
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-${ARCH}.zip" wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.1/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip" unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat iran.dat
mv xray "xray-linux-${FNAME}" mv xray "xray-linux-${FNAME}"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
wget "https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat"
cd ../../ cd ../../

130
README.md
View File

@@ -8,7 +8,7 @@
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment** > **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)** xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
| Features | Enable? | | Features | Enable? |
| ------------------------------------ | :----------------: | | ------------------------------------ | :----------------: |
@@ -25,68 +25,6 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
**If you think this project is helpful to you, you may wish to give a** :star2: **If you think this project is helpful to you, you may wish to give a** :star2:
# Features
- System Status Monitoring
- Search within all inbounds and clients
- Support Dark/Light theme UI
- Support multi-user multi-protocol, web page visualization operation
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
- Support for configuring more transport configurations
- Traffic statistics, limit traffic, limit expiration time
- Customizable xray configuration templates
- Support subscription ( multi ) link
- Detect users which are expiring or exceed traffic limit soon
- Support https access panel (self-provided domain name + ssl certificate)
- Support one-click SSL certificate application and automatic renewal
- For more advanced configuration items, please refer to the panel
## API routes
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
- `/xui/API/inbounds` base for following actions:
| Method | Path | Action |
| :----: | --------------------------------- | ------------------------------------------- |
| `GET` | `"/"` | Get all inbounds |
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
| `POST` | `"/add"` | Add inbound |
| `POST` | `"/del/:id"` | Delete Inbound |
| `POST` | `"/update/:id"` | Update Inbound |
| `POST` | `"/addClient/"` | Add Client to inbound |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId* |
| `POST` | `"/updateClient/:clientId"` | Update Client by clientId* |
| `POST` | `"/getClientTraffics/:email"` | Get Client's Traffic |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset inbound clients traffics (-1: all) |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
*- The field `clientId` should be filled by:
- `client.id` for VMESS and VLESS
- `client.password` for TROJAN
- `client.email` for Shadowsocks
# Environment Variables
| Variable | Type | Default |
| -------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
# Screenshot from Inbouds page
![inbounds](./media/inbounds.png)
![Dark inbounds](./media/inbounds-dark.png)
## suggestion system
- CentOS 8+
- Ubuntu 20+
- Debian 10+
- Fedora 36+
# Install & Upgrade to latest version # Install & Upgrade to latest version
``` ```
@@ -94,7 +32,9 @@ bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.s
``` ```
## Install custom version ## Install custom version
To install your desired version you can add the version to the end of install command. Example for ver `0.5.2`: To install your desired version you can add the version to the end of install command. Example for ver `0.5.2`:
``` ```
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.5.2 bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.5.2
``` ```
@@ -146,6 +86,70 @@ docker run -itd \
docker build -t x-ui . docker build -t x-ui .
``` ```
# Features
- System Status Monitoring
- Search within all inbounds and clients
- Support Dark/Light theme UI
- Support multi-user multi-protocol, web page visualization operation
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
- Support for configuring more transport configurations
- Traffic statistics, limit traffic, limit expiration time
- Customizable xray configuration templates
- Support subscription ( multi ) link
- Detect users which are expiring or exceed traffic limit soon
- Support https access panel (self-provided domain name + ssl certificate)
- Support one-click SSL certificate application and automatic renewal
- For more advanced configuration items, please refer to the panel
- Support export/import database from panel
## suggestion system
- CentOS 8+
- Ubuntu 20+
- Debian 10+
- Fedora 36+
## API routes
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
- `/xui/API/inbounds` base for following actions:
| Method | Path | Action |
| :----: | ------------------------------- | ----------------------------------------- |
| `GET` | `"/"` | Get all inbounds |
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
| `POST` | `"/add"` | Add inbound |
| `POST` | `"/del/:id"` | Delete Inbound |
| `POST` | `"/update/:id"` | Update Inbound |
| `POST` | `"/addClient/"` | Add Client to inbound |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId\* |
| `POST` | `"/updateClient/:clientId"` | Update Client by clientId\* |
| `POST` | `"/getClientTraffics/:email"` | Get Client's Traffic |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset inbound clients traffics (-1: all) |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
\*- The field `clientId` should be filled by:
- `client.id` for VMESS and VLESS
- `client.password` for TROJAN
- `client.email` for Shadowsocks
# Environment Variables
| Variable | Type | Default |
| -------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
# Screenshot from Inbouds page
![inbounds](./media/inbounds.png)
![Dark inbounds](./media/inbounds-dark.png)
## SSL certificate application ## SSL certificate application
<details> <details>

View File

@@ -1 +1 @@
1.2.0 1.3.0

View File

@@ -1,6 +1,8 @@
package database package database
import ( import (
"bytes"
"io"
"io/fs" "io/fs"
"os" "os"
"path" "path"
@@ -98,3 +100,13 @@ func GetDB() *gorm.DB {
func IsNotFound(err error) bool { func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound return err == gorm.ErrRecordNotFound
} }
func IsSQLiteDB(file io.Reader) (bool, error) {
signature := []byte("SQLite format 3\x00")
buf := make([]byte, len(signature))
_, err := file.Read(buf)
if err != nil {
return false, err
}
return bytes.Equal(buf, signature), nil
}

View File

@@ -1,12 +1,16 @@
version: '3.9' ---
version: "3.9"
services: services:
xui: xui:
image: alireza7/x-ui image: alireza7/x-ui
container_name: x-ui container_name: x-ui
hostname: yourhostname
volumes: volumes:
- $PWD/db/:/etc/x-ui/ - $PWD/db/:/etc/x-ui/
- $PWD/cert/:/root/cert/ - $PWD/cert/:/root/cert/
environment:
XRAY_VMESS_AEAD_FORCED: "false"
tty: true
network_mode: host
restart: unless-stopped restart: unless-stopped
ports:
- 54321:54321
- 443:443

8
go.mod
View File

@@ -12,13 +12,13 @@ require (
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.0.7 github.com/pelletier/go-toml/v2 v2.0.7
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.23.3 github.com/shirou/gopsutil/v3 v3.23.4
github.com/xtls/xray-core v1.8.1 github.com/xtls/xray-core v1.8.1
go.uber.org/atomic v1.10.0 go.uber.org/atomic v1.11.0
golang.org/x/text v0.9.0 golang.org/x/text v0.9.0
google.golang.org/grpc v1.54.0 google.golang.org/grpc v1.55.0
gorm.io/driver/sqlite v1.5.0 gorm.io/driver/sqlite v1.5.0
gorm.io/gorm v1.25.0 gorm.io/gorm v1.25.1
) )
require ( require (

41
go.sum
View File

@@ -9,8 +9,6 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ= github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
@@ -25,8 +23,6 @@ github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EIT
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo= github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo= github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
@@ -44,8 +40,6 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
@@ -87,28 +81,19 @@ github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/d
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA= github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik= github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw= github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -131,7 +116,6 @@ github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
@@ -143,15 +127,12 @@ github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KP
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk= github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk=
github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw= github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw=
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo= github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U= github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE= github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU= github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8=
github.com/shoenig/go-m1cpu v0.1.4 h1:SZPIgRM2sEF9NJy50mRHu9PKGwxyyTTJIWvCtgVbozs=
github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ= github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ=
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ= github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c= github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
@@ -161,7 +142,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -178,8 +158,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
@@ -191,9 +169,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg= go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
@@ -229,7 +206,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
@@ -255,15 +231,14 @@ 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= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -274,8 +249,8 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c= gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU= gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4= gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -79,10 +79,9 @@ install_base() {
#This function will be called when user installed x-ui out of sercurity #This function will be called when user installed x-ui out of sercurity
config_after_install() { config_after_install() {
/usr/local/x-ui/x-ui migrate
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}" echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
read -p "Do you want to continue with the modification [y/n]? ": config_confirm read -p "Do you want to continue with the modification [y/n]? ": config_confirm
if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
read -p "Please set up your username:" config_account read -p "Please set up your username:" config_account
echo -e "${yellow}Your username will be:${config_account}${plain}" echo -e "${yellow}Your username will be:${config_account}${plain}"
read -p "Please set up your password:" config_password read -p "Please set up your password:" config_password
@@ -95,8 +94,22 @@ config_after_install() {
/usr/local/x-ui/x-ui setting -port ${config_port} /usr/local/x-ui/x-ui setting -port ${config_port}
echo -e "${yellow}Panel port set successfully!${plain}" echo -e "${yellow}Panel port set successfully!${plain}"
else else
echo -e "${red}Canceled, will use the default settings.${plain}" echo -e "${red}cancel...${plain}"
if [[ ! -f "/etc/x-ui/x-ui.db" ]]; then
local usernameTemp=$(head -c 6 /dev/urandom | base64)
local passwordTemp=$(head -c 6 /dev/urandom | base64)
/usr/local/x-ui/x-ui setting -username ${usernameTemp} -password ${passwordTemp}
echo -e "this is a fresh installation,will generate random login info for security concerns:"
echo -e "###############################################"
echo -e "${green}username:${usernameTemp}${plain}"
echo -e "${green}password:${passwordTemp}${plain}"
echo -e "###############################################"
echo -e "${red}if you forgot your login info,you can type x-ui and then type 7 to check after installation${plain}"
else
echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 7 to check${plain}"
fi
fi fi
/usr/local/x-ui/x-ui migrate
} }
install_x-ui() { install_x-ui() {

View File

@@ -211,8 +211,7 @@ func migrateDb() {
log.Fatal(err) log.Fatal(err)
} }
fmt.Println("Start migrating database...") fmt.Println("Start migrating database...")
inboundService.MigrationRequirements() inboundService.MigrateDB()
inboundService.RemoveOrphanedTraffics()
fmt.Println("Migration done!") fmt.Println("Migration done!")
} }

View File

@@ -6,8 +6,6 @@ import (
"x-ui/logger" "x-ui/logger"
) )
var CtxDone = errors.New("context done")
func NewErrorf(format string, a ...interface{}) error { func NewErrorf(format string, a ...interface{}) error {
msg := fmt.Sprintf(format, a...) msg := fmt.Sprintf(format, a...)
return errors.New(msg) return errors.New(msg)

View File

@@ -1,9 +0,0 @@
package common
import "sort"
func IsSubString(target string, str_array []string) bool {
sort.Strings(str_array)
index := sort.SearchStrings(str_array, target)
return index < len(str_array) && str_array[index] == target
}

View File

@@ -1,12 +0,0 @@
package util
import "context"
func IsDone(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}

View File

@@ -6,7 +6,7 @@ import (
type RawMessage []byte type RawMessage []byte
// MarshalJSON 自定义 json.RawMessage 默认行为 // MarshalJSON: Customize json.RawMessage default behavior
func (m RawMessage) MarshalJSON() ([]byte, error) { func (m RawMessage) MarshalJSON() ([]byte, error) {
if len(m) == 0 { if len(m) == 0 {
return []byte("null"), nil return []byte("null"), nil
@@ -14,7 +14,7 @@ func (m RawMessage) MarshalJSON() ([]byte, error) {
return m, nil return m, nil
} }
// UnmarshalJSON sets *m to a copy of data. // UnmarshalJSON: sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error { func (m *RawMessage) UnmarshalJSON(data []byte) error {
if m == nil { if m == nil {
return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")

View File

View File

@@ -1,5 +1,23 @@
html,
body {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
overflow: hidden;
}
#app { #app {
height: 100%; height: 100%;
min-height: 100vh;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
padding: 0;
overflow: auto;
} }
.ant-space { .ant-space {
@@ -10,6 +28,12 @@
display: none; display: none;
} }
@media (max-width: 768px) {
.ant-layout-sider {
display: none;
}
}
.ant-card { .ant-card {
border-radius: 30px; border-radius: 30px;
} }
@@ -142,6 +166,11 @@
transform: translateY(-30px) transform: translateY(-30px)
} }
.ant-list-item-meta-title {
font-weight: bold;
font-size: 16px;
}
.ant-progress-inner { .ant-progress-inner {
background-color: #EBEEF5; background-color: #EBEEF5;
} }
@@ -183,6 +212,18 @@
box-shadow: 0 2px 8px rgba(255,255,255,.15); box-shadow: 0 2px 8px rgba(255,255,255,.15);
} }
.ant-setting-textarea {
margin-top: 1.5rem;
}
.ant-card-dark-box-nohover{
padding: 0 20px 20px !important;
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
.ant-card-dark-box-nohover:hover{
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
.ant-card-dark .ant-table-thead th { .ant-card-dark .ant-table-thead th {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #161b22; background-color: #161b22;
@@ -199,6 +240,7 @@
.ant-card-dark .ant-input-group-addon { .ant-card-dark .ant-input-group-addon {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #262f3d; background-color: #262f3d;
border: 1px solid rgb(0 150 112 / 0%);
} }
.ant-card-dark .ant-list-item-meta-title, .ant-card-dark .ant-list-item-meta-title,
@@ -236,6 +278,10 @@
background-color: #1a212a; background-color: #1a212a;
} }
.ant-input-number {
min-width: 100px;
}
.ant-card-dark .ant-input, .ant-card-dark .ant-input,
.ant-card-dark .ant-input-number, .ant-card-dark .ant-input-number,
.ant-card-dark .ant-input-number-handler-wrap, .ant-card-dark .ant-input-number-handler-wrap,
@@ -245,6 +291,7 @@
.ant-card-dark .ant-calendar-picker-clear { .ant-card-dark .ant-calendar-picker-clear {
color: hsla(0,0%,100%,.65); color: hsla(0,0%,100%,.65);
background-color: #193752; background-color: #193752;
border: 1px solid rgba(0, 65, 150, 0);
} }
.ant-card-dark .ant-select-disabled .ant-select-selection { .ant-card-dark .ant-select-disabled .ant-select-selection {

View File

@@ -2,11 +2,15 @@ axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.interceptors.request.use( axios.interceptors.request.use(
config => { (config) => {
config.data = Qs.stringify(config.data, { if (config.data instanceof FormData) {
arrayFormat: 'repeat' config.headers['Content-Type'] = 'multipart/form-data';
}); } else {
config.data = Qs.stringify(config.data, {
arrayFormat: 'repeat',
});
}
return config; return config;
}, },
error => Promise.reject(error) (error) => Promise.reject(error),
); );

View File

@@ -1,36 +1,41 @@
supportLangs = [ const supportLangs = [
{ {
name : "English", name: 'English',
value : "en-US", value: 'en-US',
icon : "🇺🇸" icon: '🇺🇸',
}, },
{ {
name : "Farsi", name: 'فارسی',
value : "fa_IR", value: 'fa_IR',
icon : "🇮🇷" icon: '🇮🇷',
}, },
{ {
name : "汉语", name: '汉语',
value : "zh-Hans", value: 'zh-Hans',
icon : "🇨🇳" icon: '🇨🇳',
}, },
] {
name: 'Русский',
value: 'ru_RU',
icon: '🇷🇺',
},
];
function getLang(){ function getLang() {
let lang = getCookie('lang') let lang = getCookie('lang');
if (! lang){ if (!lang) {
if (window.navigator){ if (window.navigator) {
lang = window.navigator.language || window.navigator.userLanguage; lang = window.navigator.language || window.navigator.userLanguage;
if (isSupportLang(lang)){ if (isSupportLang(lang)) {
setCookie('lang' , lang , 150) setCookie('lang', lang, 150);
}else{ } else {
setCookie('lang' , 'en-US' , 150) setCookie('lang', 'en-US', 150);
window.location.reload(); window.location.reload();
} }
}else{ } else {
setCookie('lang' , 'en-US' , 150) setCookie('lang', 'en-US', 150);
window.location.reload(); window.location.reload();
} }
} }
@@ -38,47 +43,21 @@ function getLang(){
return lang; return lang;
} }
function setLang(lang){ function setLang(lang) {
if (!isSupportLang(lang)) {
if (!isSupportLang(lang)){
lang = 'en-US'; lang = 'en-US';
} }
setCookie('lang' , lang , 150) setCookie('lang', lang, 150);
window.location.reload(); window.location.reload();
} }
function isSupportLang(lang){ function isSupportLang(lang) {
for (l of supportLangs){ for (l of supportLangs) {
if (l.value === lang){ if (l.value === lang) {
return true; return true;
} }
} }
return false; return false;
}
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
let expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
} }

View File

@@ -478,8 +478,8 @@ class TlsStreamSettings extends XrayCommonClass {
this.settings = settings; this.settings = settings;
} }
addCert(cert) { addCert() {
this.certs.push(cert); this.certs.push(new TlsStreamSettings.Cert());
} }
removeCert(index) { removeCert(index) {
@@ -1009,6 +1009,7 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS: case Protocols.VMESS:
case Protocols.VLESS: case Protocols.VLESS:
case Protocols.TROJAN: case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true; return true;
default: default:
return false; return false;
@@ -1217,16 +1218,64 @@ class Inbound extends XrayCommonClass {
genSSLink(address='', remark='', clientIndex = 0) { genSSLink(address='', remark='', clientIndex = 0) {
let settings = this.settings; let settings = this.settings;
const port = this.port; const port = this.port;
const type = this.stream.network;
const params = new Map();
params.set("type", this.stream.network);
switch (type) {
case "tcp":
const tcp = this.stream.tcp;
if (tcp.type === 'http') {
const request = tcp.request;
params.set("path", request.path.join(','));
const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = request.headers[index].value;
params.set("host", host);
}
params.set("headerType", 'http');
}
break;
case "kcp":
const kcp = this.stream.kcp;
params.set("headerType", kcp.type);
params.set("seed", kcp.seed);
break;
case "ws":
const ws = this.stream.ws;
params.set("path", ws.path);
const index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
const host = ws.headers[index].value;
params.set("host", host);
}
break;
case "http":
const http = this.stream.http;
params.set("path", http.path);
params.set("host", http.host);
break;
case "quic":
const quic = this.stream.quic;
params.set("quicSecurity", quic.security);
params.set("key", quic.key);
params.set("headerType", quic.type);
break;
case "grpc":
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
if(grpc.multiMode){
params.set("mode", "multi");
}
break;
}
return 'ss://' + safeBase64(settings.method + ':' + settings.password + ':' +settings.shadowsockses[clientIndex].password) + '@' + address + ':' + this.port + '#' + encodeURIComponent(remark); let link = `ss://${safeBase64(settings.method + ':' + settings.password + ':' +settings.shadowsockses[clientIndex].password)}@${address}:${this.port}`;
const url = new URL(link);
for (const [key, value] of params) {
// if (settings.method == SSMethods.BLAKE3_AES_128_GCM || settings.method == SSMethods.BLAKE3_AES_256_GCM || settings.method == SSMethods.BLAKE3_CHACHA20_POLY1305) { url.searchParams.set(key, value)
// return `ss://${settings.method}:${settings.password}@${address}:${this.port}#${encodeURIComponent(remark)}`; }
// } else { url.hash = encodeURIComponent(remark);
// return 'ss://' + safeBase64(settings.method + ':' + settings.password + '@' + address + ':' + this.port) return url.toString();
// + '#' + encodeURIComponent(remark);
// }
} }
genTrojanLink(address = '', remark = '', clientIndex = 0) { genTrojanLink(address = '', remark = '', clientIndex = 0) {
@@ -1319,7 +1368,7 @@ class Inbound extends XrayCommonClass {
} }
} }
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`; const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`;
const url = new URL(link); const url = new URL(link);
for (const [key, value] of params) { for (const [key, value] of params) {
url.searchParams.set(key, value) url.searchParams.set(key, value)

View File

@@ -56,14 +56,61 @@ function toFixed(num, n) {
return Math.round(num * n) / n; return Math.round(num * n) / n;
} }
function debounce (fn, delay) { function debounce(fn, delay) {
var timeoutID = null var timeoutID = null;
return function () { return function () {
clearTimeout(timeoutID) clearTimeout(timeoutID);
var args = arguments var args = arguments;
var that = this var that = this;
timeoutID = setTimeout(function () { timeoutID = setTimeout(function () {
fn.apply(that, args) fn.apply(that, args);
}, delay) }, delay);
};
}
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == " ") {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
} }
} return "";
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
let expires = "expires=" + d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}
function usageColor(data, threshold, total) {
switch (true) {
case data === null:
return "blue";
case total <= 0:
return "blue";
case data < total - threshold:
return "cyan";
case data < total:
return "orange";
default:
return "red";
}
}
function doAllItemsExist(array1, array2) {
for (let i = 0; i < array1.length; i++) {
if (!array2.includes(array1[i])) {
return false;
}
}
return true;
}

View File

@@ -1,67 +1,67 @@
const oneMinute = 1000 * 60; // 一分钟的毫秒数 const oneMinute = 1000 * 60; // 一The millise times of minutes
const oneHour = oneMinute * 60; // 一小时的毫秒数 const oneHour = oneMinute * 60; // 一Hours of millise times
const oneDay = oneHour * 24; // 一天的毫秒数 const oneDay = oneHour * 24; // 一Day's milliseconds
const oneWeek = oneDay * 7; // 一星期的毫秒数 const oneWeek = oneDay * 7; // 一Number of millise times on week
const oneMonth = oneDay * 30; // 一个月的毫秒数 const oneMonth = oneDay * 30; // 一Number of millise times a month
/** /**
* 按天数减少 * Decrease by day
* *
* @param days 要减少的天数 * @param days A few days to reduce
*/ */
Date.prototype.minusDays = function (days) { Date.prototype.minusDays = function (days) {
return this.minusMillis(oneDay * days); return this.minusMillis(oneDay * days);
}; };
/** /**
* 按天数增加 * Increase by day
* *
* @param days 要增加的天数 * @param days The number of days to be increased
*/ */
Date.prototype.plusDays = function (days) { Date.prototype.plusDays = function (days) {
return this.plusMillis(oneDay * days); return this.plusMillis(oneDay * days);
}; };
/** /**
* 按小时减少 * Reduced
* *
* @param hours 要减少的小时数 * @param hours The number of hours to be reduced
*/ */
Date.prototype.minusHours = function (hours) { Date.prototype.minusHours = function (hours) {
return this.minusMillis(oneHour * hours); return this.minusMillis(oneHour * hours);
}; };
/** /**
* 按小时增加 * Increase
* *
* @param hours 要增加的小时数 * @param hours Increase the number of hours
*/ */
Date.prototype.plusHours = function (hours) { Date.prototype.plusHours = function (hours) {
return this.plusMillis(oneHour * hours); return this.plusMillis(oneHour * hours);
}; };
/** /**
* 按分钟减少 * Decrease by minute
* *
* @param minutes 要减少的分钟数 * @param minutes The number of minutes to be reduced
*/ */
Date.prototype.minusMinutes = function (minutes) { Date.prototype.minusMinutes = function (minutes) {
return this.minusMillis(oneMinute * minutes); return this.minusMillis(oneMinute * minutes);
}; };
/** /**
* 按分钟增加 * Increase
* *
* @param minutes 要增加的分钟数 * @param minutes The number of minutes to be increased
*/ */
Date.prototype.plusMinutes = function (minutes) { Date.prototype.plusMinutes = function (minutes) {
return this.plusMillis(oneMinute * minutes); return this.plusMillis(oneMinute * minutes);
}; };
/** /**
* 按毫秒减少 * Decrease by millisecond
* *
* @param millis 要减少的毫秒数 * @param millis Number of milliligues to be reduced
*/ */
Date.prototype.minusMillis = function(millis) { Date.prototype.minusMillis = function(millis) {
let time = this.getTime() - millis; let time = this.getTime() - millis;
@@ -71,9 +71,9 @@ Date.prototype.minusMillis = function(millis) {
}; };
/** /**
* 按毫秒增加 * Add in milliseconds
* *
* @param millis 要增加的毫秒数 * @param millis To increase the millimeter number
*/ */
Date.prototype.plusMillis = function(millis) { Date.prototype.plusMillis = function(millis) {
let time = this.getTime() + millis; let time = this.getTime() + millis;
@@ -83,7 +83,7 @@ Date.prototype.plusMillis = function(millis) {
}; };
/** /**
* 设置时间为当天的 00:00:00.000 * Setting time is the day 00:00:00.000
*/ */
Date.prototype.setMinTime = function () { Date.prototype.setMinTime = function () {
this.setHours(0); this.setHours(0);
@@ -94,7 +94,7 @@ Date.prototype.setMinTime = function () {
}; };
/** /**
* 设置时间为当天的 23:59:59.999 * Setting time is the day 23:59:59.999
*/ */
Date.prototype.setMaxTime = function () { Date.prototype.setMaxTime = function () {
this.setHours(23); this.setHours(23);
@@ -105,14 +105,14 @@ Date.prototype.setMaxTime = function () {
}; };
/** /**
* 格式化日期 * Formatting date
*/ */
Date.prototype.formatDate = function () { Date.prototype.formatDate = function () {
return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate()); return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate());
}; };
/** /**
* 格式化时间 * Formatting time
*/ */
Date.prototype.formatTime = function () { Date.prototype.formatTime = function () {
return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds()); return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds());
@@ -121,21 +121,20 @@ Date.prototype.formatTime = function () {
/** /**
* 格式化日期加时间 * 格式化日期加时间
* *
* @param split 日期和时间之间的分隔符,默认是一个空格 * @param split Division between date and time, the default is a space
*/ */
Date.prototype.formatDateTime = function (split = ' ') { Date.prototype.formatDateTime = function (split = ' ') {
return this.formatDate() + split + this.formatTime(); return this.formatDate() + split + this.formatTime();
}; };
class DateUtil { class DateUtil {
// String string to date object
// 字符串转 Date 对象
static parseDate(str) { static parseDate(str) {
return new Date(str.replace(/-/g, '/')); return new Date(str.replace(/-/g, '/'));
} }
static formatMillis(millis) { static formatMillis(millis) {
return moment(millis).format('YYYY-M-D H:m:s') return moment(millis).format('YYYY-M-D H:m:s');
} }
static firstDayOfMonth() { static firstDayOfMonth() {
@@ -144,4 +143,4 @@ class DateUtil {
date.setMinTime(); date.setMinTime();
return date; return date;
} }
} }

View File

@@ -68,13 +68,11 @@ class HttpUtil {
} }
class PromiseUtil { class PromiseUtil {
static async sleep(timeout) { static async sleep(timeout) {
await new Promise(resolve => { await new Promise(resolve => {
setTimeout(resolve, timeout) setTimeout(resolve, timeout)
}); });
} }
} }
const seq = [ const seq = [
@@ -90,7 +88,6 @@ const seq = [
]; ];
class RandomUtil { class RandomUtil {
static randomIntRange(min, max) { static randomIntRange(min, max) {
return parseInt(Math.random() * (max - min) + min, 10); return parseInt(Math.random() * (max - min) + min, 10);
} }
@@ -137,17 +134,17 @@ class RandomUtil {
}); });
} }
static randomText() { static randomText(minLen = 6, varLen = 5) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = ''; var string = '';
var len = 6 + Math.floor(Math.random() * 5) var len = minLen + Math.floor(Math.random() * varLen);
for(var ii=0; ii<len; ii++){ for (var ii = 0; ii < len; ii++) {
string += chars[Math.floor(Math.random() * chars.length)]; string += chars[Math.floor(Math.random() * chars.length)];
} }
return string; return string;
} }
static randomShadowsocksPassword(){ static randomShadowsocksPassword() {
let array = new Uint8Array(32); let array = new Uint8Array(32);
window.crypto.getRandomValues(array); window.crypto.getRandomValues(array);
return btoa(String.fromCharCode.apply(null, array)); return btoa(String.fromCharCode.apply(null, array));
@@ -155,7 +152,6 @@ class RandomUtil {
} }
class ObjectUtil { class ObjectUtil {
static getPropIgnoreCase(obj, prop) { static getPropIgnoreCase(obj, prop) {
for (const name in obj) { for (const name in obj) {
if (!obj.hasOwnProperty(name)) { if (!obj.hasOwnProperty(name)) {
@@ -303,5 +299,4 @@ class ObjectUtil {
} }
return true; return true;
} }
} }

View File

@@ -90,7 +90,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{} inbound := &model.Inbound{}
err := c.ShouldBind(inbound) err := c.ShouldBind(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.addTo"), err) jsonMsg(c, I18n(c, "pages.inbounds.create"), err)
return return
} }
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
@@ -98,7 +98,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound.Enable = true inbound.Enable = true
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
inbound, err = a.inboundService.AddInbound(inbound) inbound, err = a.inboundService.AddInbound(inbound)
jsonMsgObj(c, I18n(c, "pages.inbounds.addTo"), inbound, err) jsonMsgObj(c, I18n(c, "pages.inbounds.create"), inbound, err)
if err == nil { if err == nil {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
@@ -120,7 +120,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
func (a *InboundController) updateInbound(c *gin.Context) { func (a *InboundController) updateInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
inbound := &model.Inbound{ inbound := &model.Inbound{
@@ -128,11 +128,11 @@ func (a *InboundController) updateInbound(c *gin.Context) {
} }
err = c.ShouldBind(inbound) err = c.ShouldBind(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
inbound, err = a.inboundService.UpdateInbound(inbound) inbound, err = a.inboundService.UpdateInbound(inbound)
jsonMsgObj(c, I18n(c, "pages.inbounds.revise"), inbound, err) jsonMsgObj(c, I18n(c, "pages.inbounds.update"), inbound, err)
if err == nil { if err == nil {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
@@ -142,7 +142,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
data := &model.Inbound{} data := &model.Inbound{}
err := c.ShouldBind(data) err := c.ShouldBind(data)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
@@ -160,7 +160,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
func (a *InboundController) delInboundClient(c *gin.Context) { func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
clientId := c.Param("clientId") clientId := c.Param("clientId")
@@ -182,7 +182,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
inbound := &model.Inbound{} inbound := &model.Inbound{}
err := c.ShouldBind(inbound) err := c.ShouldBind(inbound)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
@@ -200,7 +200,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
func (a *InboundController) resetClientTraffic(c *gin.Context) { func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
email := c.Param("email") email := c.Param("email")
@@ -228,7 +228,7 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
func (a *InboundController) resetAllClientTraffics(c *gin.Context) { func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
@@ -243,7 +243,7 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
func (a *InboundController) delDepletedClients(c *gin.Context) { func (a *InboundController) delDepletedClients(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err) jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return return
} }
err = a.inboundService.DelDepletedClients(id) err = a.inboundService.DelDepletedClients(id)

View File

@@ -75,9 +75,11 @@ func (a *IndexController) login(c *gin.Context) {
logger.Infof("Unable to get session's max age from DB") logger.Infof("Unable to get session's max age from DB")
} }
err = session.SetMaxAge(c, sessionMaxAge*60) if sessionMaxAge > 0 {
if err != nil { err = session.SetMaxAge(c, sessionMaxAge*60)
logger.Infof("Unable to set session's max age") if err != nil {
logger.Infof("Unable to set session's max age")
}
} }
err = session.SetLoginUser(c, user) err = session.SetLoginUser(c, user)

View File

@@ -1,6 +1,9 @@
package controller package controller
import ( import (
"fmt"
"net/http"
"regexp"
"time" "time"
"x-ui/web/global" "x-ui/web/global"
"x-ui/web/service" "x-ui/web/service"
@@ -8,6 +11,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`)
type ServerController struct { type ServerController struct {
BaseController BaseController
@@ -41,6 +46,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.POST("/logs/:count", a.getLogs) g.POST("/logs/:count", a.getLogs)
g.POST("/getConfigJson", a.getConfigJson) g.POST("/getConfigJson", a.getConfigJson)
g.GET("/getDb", a.getDb) g.GET("/getDb", a.getDb)
g.POST("/importDB", a.importDB)
g.POST("/getNewX25519Cert", a.getNewX25519Cert) g.POST("/getNewX25519Cert", a.getNewX25519Cert)
} }
@@ -99,8 +105,8 @@ func (a *ServerController) stopXrayService(c *gin.Context) {
return return
} }
jsonMsg(c, "Xray stoped", err) jsonMsg(c, "Xray stoped", err)
} }
func (a *ServerController) restartXrayService(c *gin.Context) { func (a *ServerController) restartXrayService(c *gin.Context) {
err := a.serverService.RestartXrayService() err := a.serverService.RestartXrayService()
if err != nil { if err != nil {
@@ -108,7 +114,6 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
return return
} }
jsonMsg(c, "Xray restarted", err) jsonMsg(c, "Xray restarted", err)
} }
func (a *ServerController) getLogs(c *gin.Context) { func (a *ServerController) getLogs(c *gin.Context) {
@@ -136,14 +141,44 @@ func (a *ServerController) getDb(c *gin.Context) {
jsonMsg(c, "get Database", err) jsonMsg(c, "get Database", err)
return return
} }
filename := "x-ui.db"
if !filenameRegex.MatchString(filename) {
c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename"))
return
}
// Set the headers for the response // Set the headers for the response
c.Header("Content-Type", "application/octet-stream") c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename=x-ui.db") c.Header("Content-Disposition", "attachment; filename="+filename)
// Write the file contents to the response // Write the file contents to the response
c.Writer.Write(db) c.Writer.Write(db)
} }
func (a *ServerController) importDB(c *gin.Context) {
// Get the file from the request body
file, _, err := c.Request.FormFile("db")
if err != nil {
jsonMsg(c, "Error reading db file", err)
return
}
defer file.Close()
// Always restart Xray before return
defer a.serverService.RestartXrayService()
defer func() {
a.lastGetStatusTime = time.Now()
}()
// Import it
err = a.serverService.ImportDB(file)
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, "Import DB", nil)
}
func (a *ServerController) getNewX25519Cert(c *gin.Context) { func (a *ServerController) getNewX25519Cert(c *gin.Context) {
cert, err := a.serverService.GetNewX25519Cert() cert, err := a.serverService.GetNewX25519Cert()
if err != nil { if err != nil {

View File

@@ -43,7 +43,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
func (a *SettingController) getAllSetting(c *gin.Context) { func (a *SettingController) getAllSetting(c *gin.Context) {
allSetting, err := a.settingService.GetAllSetting() allSetting, err := a.settingService.GetAllSetting()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return return
} }
jsonObj(c, allSetting, nil) jsonObj(c, allSetting, nil)
@@ -52,22 +52,22 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
func (a *SettingController) getDefaultSettings(c *gin.Context) { func (a *SettingController) getDefaultSettings(c *gin.Context) {
expireDiff, err := a.settingService.GetExpireDiff() expireDiff, err := a.settingService.GetExpireDiff()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return return
} }
trafficDiff, err := a.settingService.GetTrafficDiff() trafficDiff, err := a.settingService.GetTrafficDiff()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return return
} }
defaultCert, err := a.settingService.GetCertFile() defaultCert, err := a.settingService.GetCertFile()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return return
} }
defaultKey, err := a.settingService.GetKeyFile() defaultKey, err := a.settingService.GetKeyFile()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return return
} }
result := map[string]interface{}{ result := map[string]interface{}{
@@ -83,27 +83,27 @@ func (a *SettingController) updateSetting(c *gin.Context) {
allSetting := &entity.AllSetting{} allSetting := &entity.AllSetting{}
err := c.ShouldBind(allSetting) err := c.ShouldBind(allSetting)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
return return
} }
err = a.settingService.UpdateAllSetting(allSetting) err = a.settingService.UpdateAllSetting(allSetting)
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
} }
func (a *SettingController) updateUser(c *gin.Context) { func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{} form := &updateUserForm{}
err := c.ShouldBind(form) err := c.ShouldBind(form)
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
return return
} }
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
if user.Username != form.OldUsername || user.Password != form.OldPassword { if user.Username != form.OldUsername || user.Password != form.OldPassword {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.originalUserPassIncorrect"))) jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.originalUserPassIncorrect")))
return return
} }
if form.NewUsername == "" || form.NewPassword == "" { if form.NewUsername == "" || form.NewPassword == "" {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.userPassMustBeNotEmpty"))) jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
return return
} }
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword) err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
@@ -112,18 +112,18 @@ func (a *SettingController) updateUser(c *gin.Context) {
user.Password = form.NewPassword user.Password = form.NewPassword
session.SetLoginUser(c, user) session.SetLoginUser(c, user)
} }
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err)
} }
func (a *SettingController) restartPanel(c *gin.Context) { func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3) err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, I18n(c, "pages.setting.restartPanel"), err) jsonMsg(c, I18n(c, "pages.settings.restartPanel"), err)
} }
func (a *SettingController) getDefaultXrayConfig(c *gin.Context) { func (a *SettingController) getDefaultXrayConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig() defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig()
if err != nil { if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err) jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return return
} }
jsonObj(c, defaultJsonConfig, nil) jsonObj(c, defaultJsonConfig, nil)

View File

@@ -29,7 +29,7 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
func (a *SUBController) subs(c *gin.Context) { func (a *SUBController) subs(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
host := strings.Split(c.Request.Host, ":")[0] host := strings.Split(c.Request.Host, ":")[0]
subs, header, err := a.subService.GetSubs(subId, host) subs, headers, err := a.subService.GetSubs(subId, host)
if err != nil || len(subs) == 0 { if err != nil || len(subs) == 0 {
c.String(400, "Error!") c.String(400, "Error!")
} else { } else {
@@ -38,8 +38,10 @@ func (a *SUBController) subs(c *gin.Context) {
result += sub + "\n" result += sub + "\n"
} }
// Add subscription-userinfo // Add headers
c.Writer.Header().Set("Subscription-Userinfo", header) c.Writer.Header().Set("Subscription-Userinfo", headers[0])
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
c.Writer.Header().Set("Profile-Title", headers[2])
c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
} }

View File

@@ -23,7 +23,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index) g.GET("/", a.index)
g.GET("/inbounds", a.inbounds) g.GET("/inbounds", a.inbounds)
g.GET("/setting", a.setting) g.GET("/settings", a.settings)
a.inboundController = NewInboundController(g) a.inboundController = NewInboundController(g)
a.settingController = NewSettingController(g) a.settingController = NewSettingController(g)
@@ -37,6 +37,6 @@ func (a *XUIController) inbounds(c *gin.Context) {
html(c, "inbounds.html", "pages.inbounds.title", nil) html(c, "inbounds.html", "pages.inbounds.title", nil)
} }
func (a *XUIController) setting(c *gin.Context) { func (a *XUIController) settings(c *gin.Context) {
html(c, "setting.html", "pages.setting.title", nil) html(c, "settings.html", "pages.settings.title", nil)
} }

View File

@@ -1,7 +1,7 @@
{{define "promptModal"}} {{define "promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title" <a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
:closable="true" @ok="promptModal.ok" :mask-closable="false" :closable="true" @ok="promptModal.ok" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'> :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
<a-input id="prompt-modal-input" :type="promptModal.type" <a-input id="prompt-modal-input" :type="promptModal.type"
v-model="promptModal.value" v-model="promptModal.value"
@@ -36,11 +36,11 @@
}, },
confirm() {}, confirm() {},
open({ open({
title='', title = '',
type='text', type = 'text',
value='', value = '',
okText='{{ i18n "sure"}}', okText = '{{ i18n "sure"}}',
confirm=() => {}, confirm = () => {},
}) { }) {
this.title = title; this.title = title;
this.type = type; this.type = type;

View File

@@ -1,11 +1,14 @@
{{define "qrcodeModal"}} {{define "qrcodeModal"}}
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title" <a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
:closable="true" :closable="true"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:footer="null" :footer="null"
width="300px"> width="300px">
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag> <a-tag v-if="qrModal.clientName" color="orange" style="margin-bottom: 10px;display: block;text-align: center;">
{{ i18n "pages.inbounds.email" }}: "[[ qrModal.clientName ]]"
</a-tag>
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas> <canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas>
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
</a-modal> </a-modal>
<script> <script>
@@ -16,14 +19,16 @@
inbound: new Inbound(), inbound: new Inbound(),
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
copyText: '', copyText: '',
clientName: null,
qrcode: null, qrcode: null,
clipboard: null, clipboard: null,
visible: false, visible: false,
show: function (title='', content='', dbInbound=new DBInbound(), copyText='') { show: function (title = '', content = '', dbInbound = new DBInbound(), copyText = '', clientName = null) {
this.title = title; this.title = title;
this.content = content; this.content = content;
this.dbInbound = dbInbound; this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.clientName = clientName;
if (ObjectUtil.isEmpty(copyText)) { if (ObjectUtil.isEmpty(copyText)) {
this.copyText = content; this.copyText = content;
} else { } else {
@@ -48,6 +53,7 @@
}; };
const qrModalApp = new Vue({ const qrModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#qrcode-modal', el: '#qrcode-modal',
data: { data: {
qrModal: qrModal, qrModal: qrModal,

View File

@@ -1,10 +1,11 @@
{{define "textModal"}} {{define "textModal"}}
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title" <a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}' :closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}"> :ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;" <a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName"> :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
:download="txtModal.fileName">
{{ i18n "download" }} [[ txtModal.fileName ]] {{ i18n "download" }} [[ txtModal.fileName ]]
</a-button> </a-button>
<a-input type="textarea" v-model="txtModal.content" <a-input type="textarea" v-model="txtModal.content"
@@ -20,7 +21,7 @@
qrcode: null, qrcode: null,
clipboard: null, clipboard: null,
visible: false, visible: false,
show: function (title='', content='', fileName='') { show: function (title = '', content = '', fileName = '') {
this.title = title; this.title = title;
this.content = content; this.content = content;
this.fileName = fileName; this.fileName = fileName;

View File

@@ -9,7 +9,6 @@
h1 { h1 {
text-align: center; text-align: center;
color: #fff;
margin: 20px 0 50px 0; margin: 20px 0 50px 0;
} }
@@ -18,6 +17,12 @@
border-radius: 30px; border-radius: 30px;
} }
.ant-input-group-addon {
border-radius: 0 30px 30px 0;
width: 50px;
font-size: 18px;
}
.ant-input-affix-wrapper .ant-input-prefix { .ant-input-affix-wrapper .ant-input-prefix {
left: 23px; left: 23px;
} }
@@ -26,20 +31,26 @@
padding-left: 50px; padding-left: 50px;
} }
.selectLang{ .centered {
display: flex; display: flex;
text-align: center; text-align: center;
align-items: center;
justify-content: center; justify-content: center;
} }
.title {
font-size: 32px;
font-weight: bold;
}
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak :class="themeSwitcher.darkCardClass">
<transition name="list" appear> <transition name="list" appear>
<a-layout-content> <a-layout-content>
<a-row type="flex" justify="center"> <a-row type="flex" justify="center">
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8"> <a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
<h1>{{ i18n "pages.login.title" }}</h1> <h1 class="title" :style="themeSwitcher.textStyle">{{ i18n "pages.login.title" }}</h1>
</a-col> </a-col>
</a-row> </a-row>
<a-row type="flex" justify="center"> <a-row type="flex" justify="center">
@@ -48,40 +59,38 @@
<a-form-item> <a-form-item>
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}' <a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
@keydown.enter.native="login" autofocus> @keydown.enter.native="login" autofocus>
<a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/> <a-icon slot="prefix" type="user" :style="'font-size: 16px;' + themeSwitcher.textStyle"/>
</a-input> </a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-input type="password" v-model.trim="user.password" <password-input icon="lock" v-model.trim="user.password"
placeholder='{{ i18n "password" }}' @keydown.enter.native="login"> placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
<a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/> </password-input>
</a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-button block @click="login" :loading="loading">{{ i18n "login" }}</a-button> <a-row justify="center" class="centered">
<a-button type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined"
:style="loading ? { width: '50px' } : { display: 'block', width: '100%' }">
[[ loading ? '' : '{{ i18n "login" }}' ]]
</a-button>
</a-row>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-row justify="center" class="centered">
<a-row justify="center" class="selectLang"> <a-col :span="12">
<a-col :span="4"><span>Language : </span></a-col> <a-select ref="selectLang" v-model="lang" @change="setLang(lang)" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<a-col :span="6">
<a-select
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
>
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" >
<span role="img" aria-label="l.name" v-text="l.icon"></span> <span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span> &nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-col> </a-col>
</a-row> </a-row>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<theme-switch />
</a-row>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-col> </a-col>
@@ -90,22 +99,21 @@
</transition> </transition>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "component/password" .}}
<script> <script>
const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16);
const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16);
const deg = RandomUtil.randomIntRange(0, 360);
const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`;
document.querySelector('#app').style.background = background;
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
data: { data: {
themeSwitcher,
loading: false, loading: false,
user: new User(), user: new User(),
lang : "" lang: ""
}, },
created(){ created() {
this.lang = getLang(); this.updateBackground();
this.lang = getLang();
}, },
methods: { methods: {
async login() { async login() {
@@ -115,8 +123,16 @@
if (msg.success) { if (msg.success) {
location.href = basePath + 'xui/'; location.href = basePath + 'xui/';
} }
} },
} updateBackground() {
document.querySelector('#app').style.background = colors[this.themeSwitcher.currentTheme].bg;
},
},
watch: {
'themeSwitcher.isDarkTheme'(newVal, oldVal) {
this.updateBackground();
},
},
}); });
</script> </script>
</body> </body>

View File

@@ -1,7 +1,7 @@
{{define "clientsBulkModal"}} {{define "clientsBulkModal"}}
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok" <a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'> :ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
<a-form layout="inline"> <a-form layout="inline">
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
@@ -10,7 +10,7 @@
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 250px" <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 250px"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option :value="0">Random</a-select-option> <a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</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="2">Random+Prefix+Num</a-select-option>
@@ -64,7 +64,7 @@
<td>Flow</td> <td>Flow</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="clientsBulkModal.flow" style="width: 250px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="clientsBulkModal.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option 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-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
@@ -89,7 +89,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -121,7 +121,7 @@
</tr> </tr>
<tr v-else> <tr v-else>
<td> <td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -132,7 +132,7 @@
<td> <td>
<a-form-item> <a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="clientsBulkModal.expiryTime" style="width: 250px;"></a-date-picker> v-model="clientsBulkModal.expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item> </a-form-item>
</td> </td>

View File

@@ -1,7 +1,7 @@
{{define "clientsModal"}} {{define "clientsModal"}}
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok" <a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'> :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/client"}} {{template "form/client"}}
</a-modal> </a-modal>
@@ -19,7 +19,6 @@
clientStats: [], clientStats: [],
oldClientId: "", oldClientId: "",
index: null, index: null,
isExpired: false,
delayedStart: false, delayedStart: false,
ok() { ok() {
if(clientModal.isEdit){ if(clientModal.isEdit){
@@ -37,7 +36,6 @@
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings); this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
this.index = index === null ? this.clients.length : index; this.index = index === null ? this.clients.length : index;
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
this.delayedStart = false; this.delayedStart = false;
if (isEdit){ if (isEdit){
if (this.clients[index].expiryTime < 0){ if (this.clients[index].expiryTime < 0){
@@ -108,13 +106,10 @@
return true return true
}, },
get isExpiry() { get isExpiry() {
return this.clientModal.isExpired return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
}, },
get statsColor() { get statsColor() {
if(!clientStats) return 'blue' return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
if(clientStats.total <= 0) return 'blue'
else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan'
else return 'red'
}, },
get delayedExpireDays() { get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0; return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
@@ -137,7 +132,7 @@
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {

View File

@@ -7,14 +7,10 @@
<a-icon type="user"></a-icon> <a-icon type="user"></a-icon>
<span>{{ i18n "menu.inbounds"}}</span> <span>{{ i18n "menu.inbounds"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="{{ .base_path }}xui/setting"> <a-menu-item key="{{ .base_path }}xui/settings">
<a-icon type="setting"></a-icon> <a-icon type="setting"></a-icon>
<span>{{ i18n "menu.setting"}}</span> <span>{{ i18n "menu.settings"}}</span>
</a-menu-item> </a-menu-item>
<!--<a-menu-item key="{{ .base_path }}xui/clients">-->
<!-- <a-icon type="laptop"></a-icon>-->
<!-- <span>Client</span>-->
<!--</a-menu-item>-->
<a-sub-menu> <a-sub-menu>
<template slot="title"> <template slot="title">
<a-icon type="link"></a-icon> <a-icon type="link"></a-icon>
@@ -33,17 +29,14 @@
{{define "commonSider"}} {{define "commonSider"}}
<a-layout-sider :theme="siderDrawer.theme" id="sider" collapsible breakpoint="md" collapsed-width="0"> <a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0">
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys=""> <a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu-item mode="inline"> <a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon> <a-icon type="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme" <theme-switch />
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']" <a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key"> @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}} {{template "menuItems" .}}
</a-menu> </a-menu>
@@ -51,32 +44,25 @@
<a-drawer id="sider-drawer" placement="left" :closable="false" <a-drawer id="sider-drawer" placement="left" :closable="false"
@close="siderDrawer.close()" @close="siderDrawer.close()"
:visible="siderDrawer.visible" :visible="siderDrawer.visible"
:wrap-class-name="siderDrawer.isDarkTheme ? 'ant-drawer-dark' : ''" :wrap-class-name="themeSwitcher.darkDrawerClass"
:wrap-style="{ padding: 0 }"> :wrap-style="{ padding: 0 }">
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle"> <div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon> <a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
</div> </div>
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys=""> <a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu-item mode="inline"> <a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon> <a-icon type="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme" <theme-switch />
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']" <a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key"> @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}} {{template "menuItems" .}}
</a-menu> </a-menu>
</a-drawer> </a-drawer>
<script> <script>
const darkClass = "ant-card-dark";
const bgDarkStyle = "background-color: #242c3a";
const siderDrawer = { const siderDrawer = {
visible: false, visible: false,
collapsed: false,
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
show() { show() {
this.visible = true; this.visible = true;
}, },
@@ -85,16 +71,6 @@
}, },
change() { change() {
this.visible = !this.visible; 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

@@ -0,0 +1,35 @@
{{define "component/passwordInput"}}
<template>
<a-input :value="value" :type="showPassword ? 'text' : 'password'"
:placeholder="placeholder"
@input="$emit('input', $event.target.value)">
<template v-if="icon" #prefix>
<a-icon :type="icon" :style="'font-size: 16px;' + themeSwitcher.textStyle" />
</template>
<template #addonAfter>
<a-icon :type="showPassword ? 'eye-invisible' : 'eye'"
@click="toggleShowPassword"
:style="'font-size: 16px;' + themeSwitcher.textStyle" />
</template>
</a-input>
</template>
{{end}}
{{define "component/password"}}
<script>
Vue.component('password-input', {
props: ["title", "value", "placeholder", "icon"],
template: `{{template "component/passwordInput"}}`,
data() {
return {
showPassword: false,
};
},
methods: {
toggleShowPassword() {
this.showPassword = !this.showPassword;
},
},
});
</script>
{{end}}

View File

@@ -1,6 +1,12 @@
{{define "component/settingListItem"}} {{define "component/settingListItem"}}
<a-list-item style="padding: 20px"> <a-list-item style="padding: 20px">
<a-row> <a-row v-if="type === 'textarea'">
<a-col>
<a-list-item-meta :title="title" :description="desc"/>
<a-textarea class="ant-setting-textarea" :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 5 }"></a-textarea>
</a-col>
</a-row>
<a-row v-else>
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<a-list-item-meta :title="title" :description="desc"/> <a-list-item-meta :title="title" :description="desc"/>
</a-col> </a-col>
@@ -9,10 +15,7 @@
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input> <a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template> </template>
<template v-else-if="type === 'number'"> <template v-else-if="type === 'number'">
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)" :min="min"></a-input> <a-input-number :value="value" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number>
</template>
<template v-else-if="type === 'textarea'">
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
</template> </template>
<template v-else-if="type === 'switch'"> <template v-else-if="type === 'switch'">
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch> <a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>

View File

@@ -0,0 +1,58 @@
{{define "component/themeSwitchTemplate"}}
<template>
<a-switch :default-checked="themeSwitcher.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="themeSwitcher.toggleTheme()">
</a-switch>
</template>
{{end}}
{{define "component/themeSwitcher"}}
<script>
const colors = {
dark: {
bg: "#242c3a",
text: "hsla(0,0%,100%,.65)"
},
light: {
bg: '#f0f2f5',
text: "rgba(0, 0, 0, 0.7)",
}
}
function createThemeSwitcher() {
const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
const theme = isDarkTheme ? 'dark' : 'light';
return {
isDarkTheme,
bgStyle: `background: ${colors[theme].bg};`,
textStyle: `color: ${colors[theme].text};`,
darkClass: isDarkTheme ? 'ant-dark' : '',
darkCardClass: isDarkTheme ? 'ant-card-dark' : '',
darkDrawerClass: isDarkTheme ? 'ant-drawer-dark' : '',
get currentTheme() {
return this.isDarkTheme ? 'dark' : 'light';
},
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme;
this.theme = this.isDarkTheme ? 'dark' : 'light';
localStorage.setItem('dark-mode', this.isDarkTheme);
this.bgStyle = `background: ${colors[this.theme].bg};`;
this.textStyle = `color: ${colors[this.theme].text};`;
this.darkClass = this.isDarkTheme ? 'ant-dark' : '';
this.darkCardClass = this.isDarkTheme ? 'ant-card-dark' : '';
this.darkDrawerClass = this.isDarkTheme ? 'ant-drawer-dark' : '';
},
};
}
const themeSwitcher = createThemeSwitcher();
Vue.component('theme-switch', {
props: [],
template: `{{template "component/themeSwitchTemplate"}}`,
data: () => ({ themeSwitcher }),
});
</script>
{{end}}

View File

@@ -14,10 +14,10 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon> <a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip> </a-tooltip>
@@ -39,7 +39,7 @@
</td> </td>
</tr> </tr>
<tr v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS"> <tr v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<td>ID</td> <td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
<td> <td>
<a-form-item> <a-form-item>
<a-input v-model.trim="client.id" style="width: 250px"></a-input> <a-input v-model.trim="client.id" style="width: 250px"></a-input>
@@ -55,7 +55,7 @@
</td> </td>
</tr> </tr>
<tr v-if="client.email"> <tr v-if="client.email">
<td>Subscription</td> <td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
<td> <td>
<a-form-item> <a-form-item>
<a-input v-model.trim="client.subId" style="width: 250px"></a-input> <a-input v-model.trim="client.subId" style="width: 250px"></a-input>
@@ -74,7 +74,7 @@
<td>Flow</td> <td>Flow</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="client.flow" style="width: 250px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="client.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option 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-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
@@ -83,7 +83,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -129,7 +129,7 @@
</tr> </tr>
<tr v-else> <tr v-else>
<td> <td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -140,7 +140,7 @@
<td> <td>
<a-form-item> <a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 250px;"></a-date-picker> v-model="client._expiryTime" style="width: 250px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag> <a-tag color="red" v-if="isExpiry">Expired</a-tag>
</a-form-item> </a-form-item>

View File

@@ -22,7 +22,7 @@
<td>{{ i18n "protocol" }}</td> <td>{{ i18n "protocol" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.protocol" style="width: 250px;" :disabled="isEdit" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.protocol" style="width: 250px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option> <a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -53,7 +53,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -69,7 +69,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -80,7 +80,7 @@
<td> <td>
<a-form-item> <a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker> v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item> </a-form-item>
</td> </td>

View File

@@ -21,7 +21,7 @@
<td>{{ i18n "pages.inbounds.network"}}</td> <td>{{ i18n "pages.inbounds.network"}}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="tcp,udp">tcp+udp</a-select-option> <a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option> <a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option> <a-select-option value="udp">udp</a-select-option>

View File

@@ -1,14 +1,14 @@
{{define "form/shadowsocks"}} {{define "form/shadowsocks"}}
<a-form layout="inline"> <a-form layout="inline">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}"> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
<tr> <tr>
<td> <td>
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon> <a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
</a-tooltip> </a-tooltip>
@@ -30,7 +30,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Subscription</td> <td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
<td> <td>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input> <a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
@@ -47,7 +47,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -79,7 +79,7 @@
</tr> </tr>
<tr v-else> <tr v-else>
<td> <td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -90,7 +90,7 @@
<td> <td>
<a-form-item> <a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker> v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item> </a-form-item>
</td> </td>
@@ -115,7 +115,7 @@
<td>{{ i18n "encryption" }}</td> <td>{{ i18n "encryption" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option> <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -135,7 +135,7 @@
<td>{{ i18n "pages.inbounds.network" }}</td> <td>{{ i18n "pages.inbounds.network" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="tcp,udp">tcp+udp</a-select-option> <a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option> <a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option> <a-select-option value="udp">udp</a-select-option>

View File

@@ -1,6 +1,5 @@
{{define "form/socks"}} {{define "form/socks"}}
<a-form layout="inline"> <a-form layout="inline">
<!-- <a-form-item label="Password authentication">-->
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
<tr> <tr>
<td>{{ i18n "password" }}</td> <td>{{ i18n "password" }}</td>

View File

@@ -1,14 +1,14 @@
{{define "form/trojan"}} {{define "form/trojan"}}
<a-form layout="inline"> <a-form layout="inline">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit"> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}"> <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
<tr> <tr>
<td> <td>
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon> <a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
</a-tooltip> </a-tooltip>
@@ -28,7 +28,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Subscription</td> <td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
<td> <td>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input> <a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
@@ -45,7 +45,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -77,7 +77,7 @@
</tr> </tr>
<tr v-else> <tr v-else>
<td> <td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -88,7 +88,7 @@
<td> <td>
<a-form-item> <a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker> v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item> </a-form-item>
</td> </td>

View File

@@ -5,10 +5,10 @@
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
<tr> <tr>
<td> <td>
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon> <a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
</a-tooltip> </a-tooltip>
@@ -20,7 +20,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>id</td> <td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
<td> <td>
<a-form-item> <a-form-item>
<a-input v-model.trim="client.id" style="width: 200px;"></a-input> <a-input v-model.trim="client.id" style="width: 200px;"></a-input>
@@ -31,7 +31,7 @@
<td>flow</td> <td>flow</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option 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-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
@@ -39,7 +39,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Subscription</td> <td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
<td> <td>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input> <a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
@@ -56,7 +56,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -88,7 +88,7 @@
</tr> </tr>
<tr v-else> <tr v-else>
<td> <td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -99,7 +99,7 @@
<td> <td>
<a-form-item> <a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker> v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item> </a-form-item>
</td> </td>

View File

@@ -5,10 +5,10 @@
<table width="100%" class="ant-table-tbody"> <table width="100%" class="ant-table-tbody">
<tr> <tr>
<td> <td>
<span>{{ i18n "pages.inbounds.Email" }}</span> <span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon> <a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip> </a-tooltip>
@@ -20,7 +20,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>id</td> <td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
<td> <td>
<a-form-item> <a-form-item>
<a-input v-model.trim="client.id" style="width: 200px;"></a-input> <a-input v-model.trim="client.id" style="width: 200px;"></a-input>
@@ -36,7 +36,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Subscription</td> <td>Subscription <a-icon @click="client.subId = RandomUtil.randomText(16,16)" type="sync"></a-icon></td>
<td> <td>
<a-form-item v-if="client.email"> <a-form-item v-if="client.email">
<a-input v-model.trim="client.subId" style="width: 200px;"></a-input> <a-input v-model.trim="client.subId" style="width: 200px;"></a-input>
@@ -53,7 +53,7 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span> 0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -85,7 +85,7 @@
</tr> </tr>
<tr v-else> <tr v-else>
<td> <td>
<span >{{ i18n "pages.inbounds.expireDate" }}</span> <span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -96,7 +96,7 @@
<td> <td>
<a-form-item> <a-form-item>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''" :dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 200px;"></a-date-picker> v-model="client._expiryTime" style="width: 200px;"></a-date-picker>
</a-form-item> </a-form-item>
</td> </td>

View File

@@ -5,7 +5,7 @@
sniffing sniffing
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span> <span>{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>

View File

@@ -5,7 +5,7 @@
<td>{{ i18n "camouflage" }}</td> <td>{{ i18n "camouflage" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.kcp.type" style="width: 250px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.kcp.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="none">none (not camouflage)</a-select-option> <a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (video call)</a-select-option> <a-select-option value="srtp">srtp (video call)</a-select-option>
<a-select-option value="utp">utp (BT download)</a-select-option> <a-select-option value="utp">utp (BT download)</a-select-option>

View File

@@ -5,7 +5,7 @@
<td>{{ i18n "pages.inbounds.stream.quic.encryption" }}</td> <td>{{ i18n "pages.inbounds.stream.quic.encryption" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.quic.security" style="width: 200px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.quic.security" style="width: 200px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="none">none</a-select-option> <a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option> <a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option> <a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
@@ -25,7 +25,7 @@
<td>{{ i18n "camouflage" }}</td> <td>{{ i18n "camouflage" }}</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.quic.type" style="width: 200px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.quic.type" style="width: 200px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="none">none (not camouflage)</a-select-option> <a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (video call)</a-select-option> <a-select-option value="srtp">srtp (video call)</a-select-option>
<a-select-option value="utp">utp (BT download)</a-select-option> <a-select-option value="utp">utp (BT download)</a-select-option>

View File

@@ -3,7 +3,7 @@
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="{{ i18n "transmission" }}"> <a-form-item label="{{ i18n "transmission" }}">
<a-select v-model="inbound.stream.network" @change="streamNetworkChange" <a-select v-model="inbound.stream.network" @change="streamNetworkChange"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="tcp">tcp</a-select-option> <a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="kcp">kcp</a-select-option> <a-select-option value="kcp">kcp</a-select-option>
<a-select-option value="ws">ws</a-select-option> <a-select-option value="ws">ws</a-select-option>

View File

@@ -25,7 +25,7 @@
<td>CipherSuites</td> <td>CipherSuites</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 250px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">auto</a-select-option> <a-select-option value="">auto</a-select-option>
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
@@ -36,7 +36,7 @@
<td>MinVersion</td> <td>MinVersion</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.tls.minVersion" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -46,7 +46,7 @@
<td>MaxVersion</td> <td>MaxVersion</td>
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> <a-select v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -57,7 +57,7 @@
<td> <td>
<a-form-item> <a-form-item>
<a-select v-model="inbound.stream.tls.settings.fingerprint" <a-select v-model="inbound.stream.tls.settings.fingerprint"
style="width: 250px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value=''>None</a-select-option> <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-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
@@ -90,57 +90,61 @@
</a-form-item> </a-form-item>
</td> </td>
</tr> </tr>
<tr> <template v-for="cert,index in inbound.stream.tls.certs">
<td colspan="2"> <tr>
<a-form-item label="{{ i18n "certificate" }}"> <td colspan="2">
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid"> <a-form-item label='{{ i18n "certificate" }}'>
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button> <a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button> <a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
</a-radio-group> <a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-form-item> <a-button type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin: 0 10px">+</a-button>
</td> <a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)">-</a-button>
</tr> </a-radio-group>
<template v-if="inbound.stream.tls.certs[0].useFile"> </a-form-item>
<tr> </td>
<td>{{ i18n "pages.inbounds.publicKeyPath" }}</td> </tr>
<td> <template v-if="cert.useFile">
<a-form-item> <tr>
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile" style="width:250px;"></a-input> <td>{{ i18n "pages.inbounds.publicKeyPath" }}</td>
</a-form-item> <td>
</td> <a-form-item>
</tr> <a-input v-model.trim="cert.certFile" style="width:250px;"></a-input>
<tr> </a-form-item>
<td>{{ i18n "pages.inbounds.keyPath" }}</td> </td>
<td> </tr>
<a-form-item> <tr>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:250px;"></a-input> <td>{{ i18n "pages.inbounds.keyPath" }}</td>
</a-form-item> <td>
</td> <a-form-item>
</tr> <a-input v-model.trim="cert.keyFile" style="width:250px;"></a-input>
<tr> </a-form-item>
<td></td> </td>
<td> </tr>
<a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button> <tr>
</td> <td></td>
</tr> <td>
</template> <a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
<template v-else> </td>
<tr> </tr>
<td>{{ i18n "pages.inbounds.publicKeyContent" }}</td> </template>
<td> <template v-else>
<a-form-item> <tr>
<a-input type="textarea" :rows="3" style="width:250px;" v-model="inbound.stream.tls.certs[0].cert"></a-input> <td>{{ i18n "pages.inbounds.publicKeyContent" }}</td>
</a-form-item> <td>
</td> <a-form-item>
</tr> <a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.cert"></a-input>
<tr> </a-form-item>
<td>{{ i18n "pages.inbounds.keyContent" }}</td> </td>
<td> </tr>
<a-form-item> <tr>
<a-input type="textarea" :rows="3" style="width:250px;" v-model="inbound.stream.tls.certs[0].key"></a-input> <td>{{ i18n "pages.inbounds.keyContent" }}</td>
</a-form-item> <td>
</td> <a-form-item>
</tr> <a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.key"></a-input>
</a-form-item>
</td>
</tr>
</template>
</template> </template>
</table> </table>
</a-form> </a-form>
@@ -175,9 +179,9 @@
<tr> <tr>
<td>uTLS</td> <td>uTLS</td>
<td> <td>
<a-form-item > <a-form-item>
<a-select v-model="inbound.stream.reality.settings.fingerprint" <a-select v-model="inbound.stream.reality.settings.fingerprint"
style="width: 250px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> style="width: 250px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>

View File

@@ -2,7 +2,7 @@
<template slot="actions" slot-scope="text, client, index"> <template slot="actions" slot-scope="text, client, index">
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "qrCode" }}</template> <template slot="title">{{ i18n "qrCode" }}</template>
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon> <a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index,'',client.email);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.client.edit" }}</template> <template slot="title">{{ i18n "pages.client.edit" }}</template>
@@ -29,16 +29,15 @@
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag> <a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
</template> </template>
<template slot="traffic" slot-scope="text, client"> <template slot="traffic" slot-scope="text, client">
<a-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag> <a-tag :color="statsColor(record, client.email)" @click="alert(usageColor(0,1024,512))">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
<template v-if="client._totalGB > 0"> <template v-if="client._totalGB > 0">
<a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]]GB</a-tag> <a-tag :color="statsColor(record, client.email)">[[client._totalGB]]GB</a-tag>
<a-tag v-else color="cyan">[[client._totalGB]]GB</a-tag>
</template> </template>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</template> </template>
<template slot="expiryTime" slot-scope="text, client, index"> <template slot="expiryTime" slot-scope="text, client, index">
<template v-if="client.expiryTime > 0"> <template v-if="client.expiryTime > 0">
<a-tag :color="isExpiry(record, index)? 'red' : 'blue'"> <a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)">
[[ DateUtil.formatMillis(client._expiryTime) ]] [[ DateUtil.formatMillis(client._expiryTime) ]]
</a-tag> </a-tag>
</template> </template>

View File

@@ -3,7 +3,7 @@
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
:closable="true" :closable="true"
:mask-closable="true" :mask-closable="true"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:footer="null" :footer="null"
width="600px" width="600px"
> >
@@ -81,7 +81,7 @@
<th>{{ i18n "pages.inbounds.expireDate" }}</th> <th>{{ i18n "pages.inbounds.expireDate" }}</th>
<tr> <tr>
<td> <td>
<a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)"> <a-tag v-if="infoModal.clientStats" color="green">
[[ sizeFormat(infoModal.clientStats['up']) ]] / [[ sizeFormat(infoModal.clientStats['up']) ]] /
[[ sizeFormat(infoModal.clientStats['down']) ]] [[ sizeFormat(infoModal.clientStats['down']) ]]
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]]) ([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
@@ -93,7 +93,7 @@
</td> </td>
<td> <td>
<template v-if="infoModal.clientSettings.expiryTime > 0"> <template v-if="infoModal.clientSettings.expiryTime > 0">
<a-tag :color="infoModal.isExpired ? 'red' : 'blue'"> <a-tag :color="usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]] [[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
</a-tag> </a-tag>
</template> </template>
@@ -105,7 +105,10 @@
<table v-if="infoModal.clientSettings.subId + infoModal.clientSettings.tgId" style="margin-bottom: 10px;"> <table v-if="infoModal.clientSettings.subId + infoModal.clientSettings.tgId" style="margin-bottom: 10px;">
<tr v-if="infoModal.clientSettings.subId"> <tr v-if="infoModal.clientSettings.subId">
<td>Subscription link</td> <td>Subscription link</td>
<td><a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a></td> <td>
<a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a>
<a-icon id="copy-sub-link" type="snippets" @click="copyToClipboard('copy-sub-link', subBase + infoModal.clientSettings.subId)"></a-icon>
</td>
</tr> </tr>
<tr v-if="infoModal.clientSettings.tgId"> <tr v-if="infoModal.clientSettings.tgId">
<td>Telegram Username</td> <td>Telegram Username</td>
@@ -176,7 +179,9 @@
<div v-if="dbInbound.hasLink()"> <div v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider> <a-divider>URL</a-divider>
<p>[[ infoModal.link ]]</p> <p>[[ infoModal.link ]]</p>
<button class="ant-btn ant-btn-primary" id="copy-url-link"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button> <button class="ant-btn ant-btn-primary" id="copy-url-link" @click="copyToClipboard('copy-url-link', infoModal.link)">
<a-icon type="snippets"></a-icon>{{ i18n "copy" }}
</button>
</div> </div>
</a-modal> </a-modal>
<script> <script>
@@ -203,14 +208,6 @@
this.isExpired = this.inbound.isExpiry(index); this.isExpired = this.inbound.isExpiry(index);
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : []; this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
this.visible = true; this.visible = true;
infoModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#copy-url-link', {
text: () => this.link,
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
}
});
}, },
close() { close() {
infoModal.visible = false; infoModal.visible = false;
@@ -248,7 +245,7 @@
}, },
}, },
methods: { methods: {
copyTextToClipboard(elmentId,content) { copyToClipboard(elmentId,content) {
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, { this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content, text: () => content,
}); });
@@ -258,10 +255,7 @@
}); });
}, },
statsColor(stats) { statsColor(stats) {
if(!stats) return 'blue' return usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
if(stats['total'] === 0) return 'blue'
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
else return 'red'
} }
}, },

View File

@@ -1,7 +1,7 @@
{{define "inboundModal"}} {{define "inboundModal"}}
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok" <a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'> :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}} {{template "form/inbound"}}
</a-modal> </a-modal>
@@ -100,9 +100,9 @@
this.inModal.inbound.reality = false; this.inModal.inbound.reality = false;
} }
}, },
setDefaultCertData(){ setDefaultCertData(index){
inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert; inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey; inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;
}, },
async getNewX25519Cert(){ async getNewX25519Cert(){
inModal.loading(true); inModal.loading(true);

View File

@@ -12,10 +12,11 @@
margin-top: 10px; margin-top: 10px;
} }
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak>
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''"> <a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading"> <a-spin :spinning="spinning" :delay="500" tip="loading">
<transition name="list" appear> <transition name="list" appear>
@@ -24,7 +25,7 @@
</a-tag> </a-tag>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable style="margin-bottom: 20px;" :class="themeSwitcher.darkCardClass">
<a-row> <a-row>
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.totalDownUp" }}: {{ i18n "pages.inbounds.totalDownUp" }}:
@@ -41,19 +42,19 @@
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
{{ i18n "clients" }}: {{ i18n "clients" }}:
<a-tag color="green">[[ total.clients ]]</a-tag> <a-tag color="green">[[ total.clients ]]</a-tag>
<a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p> <p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
</template> </template>
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag> <a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p> <p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
</template> </template>
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag> <a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p> <p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
</template> </template>
@@ -64,14 +65,14 @@
</a-card> </a-card>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
<div slot="title"> <div slot="title">
<a-row> <a-row>
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button> <a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button> <a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="siderDrawer.theme"> <a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
<a-menu-item key="export"> <a-menu-item key="export">
<a-icon type="export"></a-icon> <a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }} {{ i18n "pages.inbounds.export" }}
@@ -95,15 +96,15 @@
<a-select v-model="refreshInterval" <a-select v-model="refreshInterval"
v-if="isRefreshEnabled" v-if="isRefreshEnabled"
@change="changeRefreshInterval" @change="changeRefreshInterval"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option> <a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
</a-select> </a-select>
<a-icon type="sync" :spin="isRefreshEnabled" @click="manualRefresh"></a-icon> <a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon>
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch> <a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
</a-col> </a-col>
</a-row> </a-row>
</div> </div>
<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input> <a-input v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id" <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds" :data-source="searchedInbounds"
:loading="spinning" :scroll="{ x: 1300 }" :loading="spinning" :scroll="{ x: 1300 }"
@@ -113,7 +114,7 @@
<template slot="action" slot-scope="text, dbInbound"> <template slot="action" slot-scope="text, dbInbound">
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="menu"></a-icon> <a-icon @click="e => e.preventDefault()" type="menu"></a-icon>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme"> <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
<a-menu-item key="edit"> <a-menu-item key="edit">
<a-icon type="edit"></a-icon> <a-icon type="edit"></a-icon>
{{ i18n "edit" }} {{ i18n "edit" }}
@@ -150,7 +151,7 @@
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }} <a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
</a-menu-item> </a-menu-item>
<a-menu-item key="clone"> <a-menu-item key="clone">
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.Clone"}} <a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
</a-menu-item> </a-menu-item>
<a-menu-item key="delete"> <a-menu-item key="delete">
<span style="color: #FF4D4F"> <span style="color: #FF4D4F">
@@ -162,7 +163,7 @@
</template> </template>
<template slot="protocol" slot-scope="text, dbInbound"> <template slot="protocol" slot-scope="text, dbInbound">
<a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag> <a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan"> <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag> <a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">tls</a-tag> <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">tls</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">reality</a-tag> <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">reality</a-tag>
@@ -171,19 +172,19 @@
<template slot="clients" slot-scope="text, dbInbound"> <template slot="clients" slot-scope="text, dbInbound">
<template v-if="clientCount[dbInbound.id]"> <template v-if="clientCount[dbInbound.id]">
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag> <a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p> <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
</template> </template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag> <a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p> <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template> </template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag> <a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''"> <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content"> <template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p> <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template> </template>
@@ -215,20 +216,20 @@
</template> </template>
<template slot="expandedRowRender" slot-scope="record"> <template slot="expandedRowRender" slot-scope="record">
<a-table <a-table
v-if="(record.protocol === Protocols.VLESS) || (record.protocol === Protocols.VMESS)" v-if="(record.protocol === Protocols.VLESS) || (record.protocol === Protocols.VMESS)"
:row-key="client => client.id" :row-key="client => client.id"
:columns="innerColumns" :columns="innerColumns"
:data-source="getInboundClients(record)" :data-source="getInboundClients(record)"
:pagination="false" :pagination="false"
> >
{{template "client_table"}} {{template "client_table"}}
</a-table> </a-table>
<a-table <a-table
v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS" v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS"
:row-key="client => client.id" :row-key="client => client.id"
:columns="innerTrojanColumns" :columns="innerTrojanColumns"
:data-source="getInboundClients(record)" :data-source="getInboundClients(record)"
:pagination="false" :pagination="false"
> >
{{template "client_table"}} {{template "client_table"}}
</a-table> </a-table>
@@ -241,6 +242,7 @@
</a-layout> </a-layout>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}}
<script> <script>
const columns = [{ const columns = [{
title: '{{ i18n "pages.inbounds.operate" }}', title: '{{ i18n "pages.inbounds.operate" }}',
@@ -312,6 +314,7 @@
el: '#app', el: '#app',
data: { data: {
siderDrawer, siderDrawer,
themeSwitcher,
spinning: false, spinning: false,
inbounds: [], inbounds: [],
dbInbounds: [], dbInbounds: [],
@@ -323,18 +326,23 @@
defaultKey: '', defaultKey: '',
clientCount: {}, clientCount: {},
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false, isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000, refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
}, },
methods: { methods: {
loading(spinning=true) { loading(spinning = true) {
this.spinning = spinning; this.spinning = spinning;
}, },
async getDBInbounds() { async getDBInbounds() {
this.refreshing = true;
const msg = await HttpUtil.post('/xui/inbound/list'); const msg = await HttpUtil.post('/xui/inbound/list');
if (!msg.success) { if (!msg.success) {
return; return;
} }
this.setInbounds(msg.obj); this.setInbounds(msg.obj);
setTimeout(() => {
this.refreshing = false;
}, 500);
}, },
async getDefaultSettings() { async getDefaultSettings() {
const msg = await HttpUtil.post('/xui/setting/defaultSettings'); const msg = await HttpUtil.post('/xui/setting/defaultSettings');
@@ -354,29 +362,29 @@
to_inbound = dbInbound.toInbound() to_inbound = dbInbound.toInbound()
this.inbounds.push(to_inbound); this.inbounds.push(to_inbound);
this.dbInbounds.push(dbInbound); this.dbInbounds.push(dbInbound);
if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){ if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol)) {
this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound); this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
} }
} }
this.searchInbounds(this.searchKey); this.searchInbounds(this.searchKey);
}, },
getClientCounts(dbInbound,inbound){ getClientCounts(dbInbound, inbound) {
let clientCount = 0,active = [], deactive = [], depleted = [], expiring = []; let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [];
clients = this.getClients(dbInbound.protocol, inbound.settings); clients = this.getClients(dbInbound.protocol, inbound.settings);
clientStats = dbInbound.clientStats clientStats = dbInbound.clientStats
now = new Date().getTime() now = new Date().getTime()
if(clients){ if (clients) {
clientCount = clients.length; clientCount = clients.length;
if(dbInbound.enable){ if (dbInbound.enable) {
clients.forEach(client => { clients.forEach(client => {
client.enable ? active.push(client.email) : deactive.push(client.email); client.enable ? active.push(client.email) : deactive.push(client.email);
}); });
clientStats.forEach(client => { clientStats.forEach(client => {
if(!client.enable) { if (!client.enable) {
depleted.push(client.email); depleted.push(client.email);
} else { } else {
if ((client.expiryTime > 0 && (client.expiryTime-now < this.expireDiff)) || if ((client.expiryTime > 0 && (client.expiryTime - now < this.expireDiff)) ||
(client.total > 0 && (client.total-(client.up+client.down) < this.trafficDiff ))) expiring.push(client.email); (client.total > 0 && (client.total - (client.up + client.down) < this.trafficDiff))) expiring.push(client.email);
} }
}); });
} else { } else {
@@ -402,10 +410,10 @@
if (ObjectUtil.deepSearch(inbound, key)) { if (ObjectUtil.deepSearch(inbound, key)) {
const newInbound = new DBInbound(inbound); const newInbound = new DBInbound(inbound);
const inboundSettings = JSON.parse(inbound.settings); const inboundSettings = JSON.parse(inbound.settings);
if (inboundSettings.hasOwnProperty('clients')){ if (inboundSettings.hasOwnProperty('clients')) {
const searchedSettings = { "clients": [] }; const searchedSettings = { "clients": [] };
inboundSettings.clients.forEach(client => { inboundSettings.clients.forEach(client => {
if (ObjectUtil.deepSearch(client, key)){ if (ObjectUtil.deepSearch(client, key)) {
searchedSettings.clients.push(client); searchedSettings.clients.push(client);
} }
}); });
@@ -416,7 +424,7 @@
}); });
} }
}, },
generalActions(action){ generalActions(action) {
switch (action.key) { switch (action.key) {
case "export": case "export":
this.exportAllLinks(); this.exportAllLinks();
@@ -472,7 +480,7 @@
openAddInbound() { openAddInbound() {
inModal.show({ inModal.show({
title: '{{ i18n "pages.inbounds.addInbound"}}', title: '{{ i18n "pages.inbounds.addInbound"}}',
okText: '{{ i18n "pages.inbounds.addTo"}}', okText: '{{ i18n "pages.inbounds.create"}}',
cancelText: '{{ i18n "close" }}', cancelText: '{{ i18n "close" }}',
confirm: async (inbound, dbInbound) => { confirm: async (inbound, dbInbound) => {
inModal.loading(); inModal.loading();
@@ -487,7 +495,7 @@
const inbound = dbInbound.toInbound(); const inbound = dbInbound.toInbound();
inModal.show({ inModal.show({
title: '{{ i18n "pages.inbounds.modifyInbound"}}', title: '{{ i18n "pages.inbounds.modifyInbound"}}',
okText: '{{ i18n "pages.inbounds.revise"}}', okText: '{{ i18n "pages.inbounds.update"}}',
cancelText: '{{ i18n "close" }}', cancelText: '{{ i18n "close" }}',
inbound: inbound, inbound: inbound,
dbInbound: dbInbound, dbInbound: dbInbound,
@@ -501,9 +509,9 @@
}, },
openCloneInbound(dbInbound) { openCloneInbound(dbInbound) {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.cloneInbound"}} ' + dbInbound.remark, title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}', content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
okText: '{{ i18n "pages.inbounds.revise"}}', okText: '{{ i18n "pages.inbounds.update"}}',
cancelText: '{{ i18n "cancel" }}', cancelText: '{{ i18n "cancel" }}',
onOk: () => { onOk: () => {
const baseInbound = dbInbound.toInbound(); const baseInbound = dbInbound.toInbound();
@@ -514,7 +522,6 @@
}); });
}, },
async cloneInbound(baseInbound, dbInbound) { async cloneInbound(baseInbound, dbInbound) {
const inbound = new Inbound();
const data = { const data = {
up: dbInbound.up, up: dbInbound.up,
down: dbInbound.down, down: dbInbound.down,
@@ -523,10 +530,10 @@
enable: dbInbound.enable, enable: dbInbound.enable,
expiryTime: dbInbound.expiryTime, expiryTime: dbInbound.expiryTime,
listen: inbound.listen, listen: '',
port: inbound.port, port: RandomUtil.randomIntRange(10000, 60000),
protocol: baseInbound.protocol, protocol: baseInbound.protocol,
settings: inbound.settings.toString(), settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
streamSettings: baseInbound.stream.toString(), streamSettings: baseInbound.stream.toString(),
sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}', sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
}; };
@@ -614,21 +621,21 @@
isEdit: true isEdit: true
}); });
}, },
findIndexOfClient(clients,client) { findIndexOfClient(clients, client) {
firstKey = Object.keys(client)[0]; firstKey = Object.keys(client)[0];
return clients.findIndex(c => c[firstKey] === client[firstKey]); return clients.findIndex(c => c[firstKey] === client[firstKey]);
}, },
async addClient(clients, dbInboundId) { async addClient(clients, dbInboundId) {
const data = { const data = {
id: dbInboundId, id: dbInboundId,
settings: '{"clients": [' + clients.toString() +']}', settings: '{"clients": [' + clients.toString() + ']}',
}; };
await this.submit(`/xui/inbound/addClient`, data); await this.submit(`/xui/inbound/addClient`, data);
}, },
async updateClient(client, dbInboundId, clientId) { async updateClient(client, dbInboundId, clientId) {
const data = { const data = {
id: dbInboundId, id: dbInboundId,
settings: '{"clients": [' + client.toString() +']}', settings: '{"clients": [' + client.toString() + ']}',
}; };
await this.submit(`/xui/inbound/updateClient/${clientId}`, data); await this.submit(`/xui/inbound/updateClient/${clientId}`, data);
}, },
@@ -637,7 +644,7 @@
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => { onOk: () => {
@@ -652,26 +659,26 @@
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}', title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId), onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
}); });
}, },
delClient(dbInboundId,client) { delClient(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientId = clientId = getClientId(dbInbound.protocol,client);; clientId = this.getClientId(dbInbound.protocol, client);
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}', title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`), onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`),
}); });
}, },
getClients(protocol, clientSettings) { getClients(protocol, clientSettings) {
switch(protocol){ switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses; case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses; case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans; case Protocols.TROJAN: return clientSettings.trojans;
@@ -680,15 +687,16 @@
} }
}, },
getClientId(protocol, client) { getClientId(protocol, client) {
switch(protocol){ switch (protocol) {
case Protocols.TROJAN: return client.password; case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email; case Protocols.SHADOWSOCKS: return client.email;
default: return client.id; default: return client.id;
} }
}, },
showQrcode(dbInbound, clientIndex) { showQrcode(dbInbound, clientIndex) {
const clientName = JSON.parse(dbInbound.settings).clients[clientIndex].email;
const link = dbInbound.genLink(clientIndex); const link = dbInbound.genLink(clientIndex);
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound); qrModal.show('{{ i18n "qrCode"}}', link, dbInbound, '', clientName);
}, },
showInfo(dbInbound, index) { showInfo(dbInbound, index) {
infoModal.show(dbInbound, index); infoModal.show(dbInbound, index);
@@ -704,8 +712,8 @@
clients = this.getClients(dbInbound.protocol, inbound.settings); clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client); index = this.findIndexOfClient(clients, client);
clients[index].enable = !clients[index].enable; clients[index].enable = !clients[index].enable;
clientId = getClientId(dbInbound.protocol,clients[index]); clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index],dbInboundId, clientId); await this.updateClient(clients[index], dbInboundId, clientId);
this.loading(false); this.loading(false);
}, },
async submit(url, data) { async submit(url, data) {
@@ -715,31 +723,31 @@
} }
}, },
getInboundClients(dbInbound) { getInboundClients(dbInbound) {
if(dbInbound.protocol == Protocols.VLESS) { if (dbInbound.protocol == Protocols.VLESS) {
return dbInbound.toInbound().settings.vlesses; return dbInbound.toInbound().settings.vlesses;
} else if(dbInbound.protocol == Protocols.VMESS) { } else if (dbInbound.protocol == Protocols.VMESS) {
return dbInbound.toInbound().settings.vmesses; return dbInbound.toInbound().settings.vmesses;
} else if(dbInbound.protocol == Protocols.TROJAN) { } else if (dbInbound.protocol == Protocols.TROJAN) {
return dbInbound.toInbound().settings.trojans; return dbInbound.toInbound().settings.trojans;
} else if(dbInbound.protocol == Protocols.SHADOWSOCKS) { } else if (dbInbound.protocol == Protocols.SHADOWSOCKS) {
return dbInbound.toInbound().settings.shadowsockses; return dbInbound.toInbound().settings.shadowsockses;
} }
}, },
resetClientTraffic(client,dbInboundId) { resetClientTraffic(client, dbInboundId) {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email), onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
}) })
}, },
resetAllTraffic() { resetAllTraffic() {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}', title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllTraffics'), onOk: () => this.submit('/xui/inbound/resetAllTraffics'),
@@ -747,9 +755,9 @@
}, },
resetAllClientTraffics(dbInboundId) { resetAllClientTraffics(dbInboundId) {
this.$confirm({ this.$confirm({
title: dbInboundId>0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}', title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: dbInboundId>0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}', content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId), onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
@@ -759,7 +767,7 @@
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}', title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}', content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId), onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId),
@@ -769,37 +777,37 @@
return dbInbound.toInbound().isExpiry(index) return dbInbound.toInbound().isExpiry(index)
}, },
getUpStats(dbInbound, email) { getUpStats(dbInbound, email) {
if(email.length == 0) return 0 if (email.length == 0) return 0
clientStats = dbInbound.clientStats.find(stats => stats.email === email) clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.up : 0 return clientStats ? clientStats.up : 0
}, },
getDownStats(dbInbound, email) { getDownStats(dbInbound, email) {
if(email.length == 0) return 0 if (email.length == 0) return 0
clientStats = dbInbound.clientStats.find(stats => stats.email === email) clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.down : 0 return clientStats ? clientStats.down : 0
}, },
isTrafficExhausted(dbInbound, email) { statsColor(dbInbound, email) {
if(email.length == 0) return false if (email.length == 0) return 'blue';
clientStats = dbInbound.clientStats.find(stats => stats.email === email) clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
}, },
isClientEnabled(dbInbound, email) { isClientEnabled(dbInbound, email) {
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
return clientStats ? clientStats['enable'] : true return clientStats ? clientStats['enable'] : true
}, },
isRemovable(dbInbound_id){ isRemovable(dbInbound_id) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1 return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
}, },
inboundLinks(dbInboundId) { inboundLinks(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark); txtModal.show('{{ i18n "pages.inbounds.export"}}', dbInbound.genInboundLinks, dbInbound.remark);
}, },
exportAllLinks() { exportAllLinks() {
let copyText = ''; let copyText = '';
for (const dbInbound of this.dbInbounds) { for (const dbInbound of this.dbInbounds) {
copyText += dbInbound.genInboundLinks copyText += dbInbound.genInboundLinks
} }
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds'); txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds');
}, },
async startDataRefreshLoop() { async startDataRefreshLoop() {
while (this.isRefreshEnabled) { while (this.isRefreshEnabled) {
@@ -817,11 +825,11 @@
this.startDataRefreshLoop(); this.startDataRefreshLoop();
} }
}, },
changeRefreshInterval(){ changeRefreshInterval() {
localStorage.setItem("refreshInterval", this.refreshInterval); localStorage.setItem("refreshInterval", this.refreshInterval);
}, },
async manualRefresh(){ async manualRefresh() {
if(!this.isRefreshEnabled){ if (!this.refreshing) {
this.spinning = true; this.spinning = true;
await this.getDBInbounds(); await this.getDBInbounds();
this.spinning = false; this.spinning = false;

View File

@@ -19,26 +19,26 @@
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak>
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''"> <a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/> <a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row> <a-row>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-row> <a-row>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color" :stroke-color="status.cpu.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:percent="status.cpu.percent"></a-progress> :percent="status.cpu.percent"></a-progress>
<div>CPU</div> <div>CPU</div>
</a-col> </a-col>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color" :stroke-color="status.mem.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:percent="status.mem.percent"></a-progress> :percent="status.mem.percent"></a-progress>
<div> <div>
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]] {{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
@@ -51,7 +51,7 @@
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color" :stroke-color="status.swap.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:percent="status.swap.percent"></a-progress> :percent="status.swap.percent"></a-progress>
<div> <div>
swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]] swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
@@ -60,7 +60,7 @@
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color" :stroke-color="status.disk.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
:percent="status.disk.percent"></a-progress> :percent="status.disk.percent"></a-progress>
<div> <div>
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]] {{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
@@ -75,13 +75,13 @@
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
x-ui: <a-tag color="green">{{ .cur_ver }}</a-tag> x-ui: <a href="https://github.com/alireza0/x-ui/releases" target="_blank"><a-tag color="green">{{ .cur_ver }}</a-tag></a>
xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag> xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.operationHours" }}: {{ i18n "pages.index.operationHours" }}:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag> <a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tooltip> <a-tooltip>
@@ -93,7 +93,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.xrayStatus" }}: {{ i18n "pages.index.xrayStatus" }}:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag> <a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.state === State.Error"> <a-tooltip v-if="status.xray.state === State.Error">
@@ -108,20 +108,20 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "menu.link" }}: {{ i18n "menu.link" }}:
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">Logs</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">Config</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="getBackup">Backup</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]] {{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
tcp / udp {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]] tcp / udp {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
@@ -132,7 +132,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-icon type="arrow-up"></a-icon> <a-icon type="arrow-up"></a-icon>
@@ -158,7 +158,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''"> <a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-icon type="cloud-upload"></a-icon> <a-icon type="cloud-upload"></a-icon>
@@ -187,9 +187,10 @@
</transition> </transition>
</a-layout-content> </a-layout-content>
</a-layout> </a-layout>
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
:closable="true" @ok="() => versionModal.visible = false" :closable="true" @ok="() => versionModal.visible = false"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
footer=""> footer="">
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2> <h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2> <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
@@ -200,9 +201,10 @@
</a-tag> </a-tag>
</template> </template>
</a-modal> </a-modal>
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs" <a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false" :closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
:class="siderDrawer.isDarkTheme ? darkClass : ''" :class="themeSwitcher.darkCardClass"
width="800px" width="800px"
footer=""> footer="">
<a-form layout="inline"> <a-form layout="inline">
@@ -210,7 +212,7 @@
<a-select v-model="logModal.rows" <a-select v-model="logModal.rows"
style="width: 80px" style="width: 80px"
@change="openLogs(logModal.rows)" @change="openLogs(logModal.rows)"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"> :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="10">10</a-select-option> <a-select-option value="10">10</a-select-option>
<a-select-option value="20">20</a-select-option> <a-select-option value="20">20</a-select-option>
<a-select-option value="50">50</a-select-option> <a-select-option value="50">50</a-select-option>
@@ -230,8 +232,27 @@
<a-input type="textarea" v-model="logModal.logs" disabled="true" <a-input type="textarea" v-model="logModal.logs" disabled="true"
:autosize="{ minRows: 10, maxRows: 22}"></a-input> :autosize="{ minRows: 10, maxRows: 22}"></a-input>
</a-modal> </a-modal>
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
:closable="true" :class="themeSwitcher.darkCardClass"
@ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
<p style="color: inherit; font-size: 16px; padding: 4px 2px;">
<a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
[[ backupModal.description ]]
</p>
<a-space direction="horizontal" align="center" style="margin-bottom: 10px;">
<a-button type="primary" @click="exportDatabase()">
[[ backupModal.exportText ]]
</a-button>
<a-button type="primary" @click="importDatabase()">
[[ backupModal.importText ]]
</a-button>
</a-space>
</a-modal>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "textModal"}} {{template "textModal"}}
<script> <script>
@@ -338,14 +359,39 @@
}, },
}; };
const backupModal = {
visible: false,
title: '',
description: '',
exportText: '',
importText: '',
show({
title = '{{ i18n "pages.index.backupTitle" }}',
description = '{{ i18n "pages.index.backupDescription" }}',
exportText = '{{ i18n "pages.index.exportDatabase" }}',
importText = '{{ i18n "pages.index.importDatabase" }}',
}) {
this.title = title;
this.description = description;
this.exportText = exportText;
this.importText = importText;
this.visible = true;
},
hide() {
this.visible = false;
},
};
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
data: { data: {
siderDrawer, siderDrawer,
themeSwitcher,
status: new Status(), status: new Status(),
versionModal, versionModal,
logModal, logModal,
backupModal,
spinning: false, spinning: false,
loadingTip: '{{ i18n "loading"}}', loadingTip: '{{ i18n "loading"}}',
}, },
@@ -377,17 +423,16 @@
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}', title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`, content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
okText: '{{ i18n "confirm"}}', okText: '{{ i18n "confirm"}}',
class: siderDrawer.isDarkTheme ? darkClass : '', class: themeSwitcher.darkCardClass,
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
versionModal.hide(); versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefreshh"}}'); this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
await HttpUtil.post(`/server/installXray/${version}`); await HttpUtil.post(`/server/installXray/${version}`);
this.loading(false); this.loading(false);
}, },
}); });
}, },
//here add stop xray function
async stopXrayService() { async stopXrayService() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService'); const msg = await HttpUtil.post('server/stopXrayService');
@@ -396,7 +441,6 @@
return; return;
} }
}, },
//here add restart xray function
async restartXrayService() { async restartXrayService() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService'); const msg = await HttpUtil.post('server/restartXrayService');
@@ -412,20 +456,60 @@
if (!msg.success) { if (!msg.success) {
return; return;
} }
logModal.show(msg.obj,rows); logModal.show(msg.obj, rows);
}, },
async openConfig(){ async openConfig() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson'); const msg = await HttpUtil.post('server/getConfigJson');
this.loading(false); this.loading(false);
if (!msg.success) { if (!msg.success) {
return; return;
} }
txtModal.show('config.json',JSON.stringify(msg.obj, null, 2),'config.json'); txtModal.show('config.json', JSON.stringify(msg.obj, null, 2), 'config.json');
}, },
getBackup(){ openBackup() {
backupModal.show({
title: '{{ i18n "pages.index.backupTitle" }}',
description: '{{ i18n "pages.index.backupDescription" }}',
exportText: '{{ i18n "pages.index.exportDatabase" }}',
importText: '{{ i18n "pages.index.importDatabase" }}',
});
},
exportDatabase() {
window.location = basePath + 'server/getDb'; window.location = basePath + 'server/getDb';
} },
importDatabase() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.db';
fileInput.addEventListener('change', async (event) => {
const dbFile = event.target.files[0];
if (dbFile) {
const formData = new FormData();
formData.append('db', dbFile);
backupModal.hide();
this.loading(true);
const uploadMsg = await HttpUtil.post('server/importDB', formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
});
this.loading(false);
if (!uploadMsg.success) {
return;
}
this.loading(true);
const restartMsg = await HttpUtil.post("/xui/setting/restartPanel");
this.loading(false);
if (restartMsg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
location.reload();
}
}
});
fileInput.click();
},
}, },
async mounted() { async mounted() {
while (true) { while (true) {

View File

@@ -1,325 +0,0 @@
<!DOCTYPE html>
<html lang="en">
{{template "head" .}}
<style>
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
.ant-col-sm-24 {
margin-top: 10px;
}
.ant-tabs-bar {
margin: 0;
}
.ant-list-item {
display: block;
}
: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" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading">
<a-space direction="vertical">
<a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.setting.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.setting.restartPanel" }}</a-button>
</a-space>
<a-tabs default-active-key="1" :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-tab-pane key="1" tab='{{ i18n "pages.setting.panelConfig"}}'>
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.sessionMaxAge" }}' desc='{{ i18n "pages.setting.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.expireTimeDiff" }}' desc='{{ i18n "pages.setting.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.trafficDiff" }}' desc='{{ i18n "pages.setting.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title="Language"/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
style="width: 100%"
>
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="2" tab='{{ i18n "pages.setting.userSetting"}}'>
<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>
<a-form-item label='{{ i18n "pages.setting.currentPassword"}}'>
<a-input type="password" v-model="user.oldPassword"
style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.setting.newUsername"}}'>
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.setting.newPassword"}}'>
<a-input type="password" v-model="user.newPassword"
style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item>
<!-- <a-button type="primary" @click="updateUser">Revise</a-button>-->
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
<a-list item-layout="horizontal" :style="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>
<a-collapse>
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigInbounds"}}">
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigInbounds"}}' desc='{{ i18n "pages.setting.xrayConfigInboundsDesc"}}' v-model ="inboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigOutbounds"}}">
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigOutbounds"}}' desc='{{ i18n "pages.setting.xrayConfigOutboundsDesc"}}' v-model ="outboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigRoutings"}}">
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigRoutings"}}' desc='{{ i18n "pages.setting.xrayConfigRoutingsDesc"}}' v-model ="routingRuleSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
<a-divider>{{ i18n "pages.setting.completeTemplate"}}</a-divider>
<a-space direction="horizontal" style="padding: 0 20px">
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.setting.resetDefaultConfig" }}</a-button>
</a-space>
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="4" tab='{{ i18n "pages.setting.TGReminder"}}'>
<a-list item-layout="horizontal" :style="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="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.tgNotifyCpu" }}' desc='{{ i18n "pages.setting.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
<a-list item-layout="horizontal" :style="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>
</a-tabs>
</a-space>
</a-spin>
</a-layout-content>
</a-layout>
</a-layout>
{{template "js" .}}
{{template "component/setting"}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
siderDrawer,
spinning: false,
oldAllSetting: new AllSetting(),
allSetting: new AllSetting(),
saveBtnDisable: true,
user: {},
lang : getLang()
},
methods: {
loading(spinning = true) {
this.spinning = spinning;
},
async getAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/all");
this.loading(false);
if (msg.success) {
this.oldAllSetting = new AllSetting(msg.obj);
this.allSetting = new AllSetting(msg.obj);
this.saveBtnDisable = true;
}
},
async updateAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/update", this.allSetting);
this.loading(false);
if (msg.success) {
await this.getAllSetting();
}
},
async updateUser() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/updateUser", this.user);
this.loading(false);
if (msg.success) {
this.user = {};
}
},
async restartPanel() {
await new Promise(resolve => {
this.$confirm({
title: '{{ i18n "pages.setting.restartPanel" }}',
content: '{{ i18n "pages.setting.restartPanelDesc" }}',
okText: '{{ i18n "sure" }}',
cancelText: '{{ i18n "cancel" }}',
onOk: () => resolve(),
});
});
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/restartPanel");
this.loading(false);
if (msg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
location.reload();
}
},
async resetXrayConfigToDefault() {
this.loading(true);
const msg = await HttpUtil.get("/xui/setting/getDefaultJsonConfig");
this.loading(false);
if (msg.success) {
this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
this.saveBtnDisable = true;
}
},
},
async mounted() {
await this.getAllSetting();
while (true) {
await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
}
},
computed: {
templateSettings: {
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null ; },
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
},
inboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.inbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
outboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.outbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.rules = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
torrentSettings: {
get: function () {
torrentFilter = false
if(this.templateSettings != null){
this.templateSettings.routing.rules.forEach(routingRule => {
if(routingRule.hasOwnProperty("protocol")){
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
torrentFilter = true
}
}
});
}
return torrentFilter
},
set: function (newValue) {
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
if (newValue){
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"protocol\": [\"bittorrent\"],\"type\": \"field\"}"))
}
else {
newTemplateSettings.routing.rules = [];
this.templateSettings.routing.rules.forEach(routingRule => {
if (routingRule.hasOwnProperty('protocol')){
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
return;
}
}
newTemplateSettings.routing.rules.push(routingRule);
});
}
this.templateSettings = newTemplateSettings
},
},
privateIpSettings: {
get: function () {
localIpFilter = false
if(this.templateSettings != null){
this.templateSettings.routing.rules.forEach(routingRule => {
if(routingRule.hasOwnProperty("ip")){
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
localIpFilter = true
}
}
});
}
return localIpFilter
},
set: function (newValue) {
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
if (newValue){
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"ip\": [\"geoip:private\"],\"type\": \"field\"}"))
}
else {
newTemplateSettings.routing.rules = [];
this.templateSettings.routing.rules.forEach(routingRule => {
if (routingRule.hasOwnProperty('ip')){
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
return;
}
}
newTemplateSettings.routing.rules.push(routingRule);
});
}
this.templateSettings = newTemplateSettings
},
},
}
});
</script>
</body>
</html>

836
web/html/xui/settings.html Normal file
View File

@@ -0,0 +1,836 @@
<!DOCTYPE html>
<html lang="en">
{{template "head" .}}
<style>
@media (min-width: 769px) {
.ant-layout-content {
margin: 24px 16px;
}
}
.ant-col-sm-24 {
margin-top: 10px;
}
.ant-tabs-bar {
margin: 0;
}
.ant-list-item {
display: block;
}
: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" :style="themeSwitcher.bgStyle">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading">
<a-space direction="vertical">
<a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
</a-space>
<a-tabs default-active-key="1" :class="themeSwitcher.darkCardClass">
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelConfig"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 20px 20px; text-align: center;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.infoDesc" }}
</h2>
</a-row>
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.panelUrlPath"}}' desc='{{ i18n "pages.settings.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.sessionMaxAge" }}' desc='{{ i18n "pages.settings.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.expireTimeDiff" }}' desc='{{ i18n "pages.settings.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.trafficDiff" }}' desc='{{ i18n "pages.settings.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.timeZone"}}' desc='{{ i18n "pages.settings.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title="Language"/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
:dropdown-class-name="themeSwitcher.darkCardClass"
style="width: 100%"
>
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="2" tab='{{ i18n "pages.settings.userSettings"}}'>
<a-form :style="'padding: 20px;' + themeSwitcher.textStyle">
<a-form-item label='{{ i18n "pages.settings.oldUsername"}}'>
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.settings.currentPassword"}}'>
<password-input v-model="user.oldPassword" style="max-width: 300px"></password-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.settings.newUsername"}}'>
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.settings.newPassword"}}'>
<password-input v-model="user.newPassword" style="max-width: 300px"></password-input>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.settings.xrayConfiguration"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 20px 20px; text-align: center;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.infoDesc" }}
</h2>
</a-row>
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<a-tabs class="ant-card-dark-box-nohover" default-active-key="tpl-1" :class="themeSwitcher.darkCardClass" style="padding: 20px 20px;">
<a-tab-pane key="tpl-1" tab='{{ i18n "pages.settings.templates.basicTemplate"}}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.generalConfigsDesc" }}
</h2>
</a-row>
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategy" }}'
description='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategyDesc" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
v-model="freedomStrategy"
:dropdown-class-name="themeSwitcher.darkCardClass"
style="width: 100%">
<a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategy" }}'
description='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategyDesc" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
v-model="routingStrategy"
:dropdown-class-name="themeSwitcher.darkCardClass"
style="width: 100%">
<a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.blockConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.blockConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigTorrent"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigAds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigFamily"}}' desc='{{ i18n "pages.settings.templates.xrayConfigFamilyDesc"}}' v-model="familyProtectSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.blockCountryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.blockCountryConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.directCountryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.directCountryConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRIpDesc"}}' v-model="IRIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomainDesc"}}' v-model="IRDomainDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIpDesc"}}' v-model="ChinaIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomainDesc"}}' v-model="ChinaDomainDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIpDesc"}}' v-model="RussiaIpDirectSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomainDesc"}}' v-model="RussiaDomainDirectSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.ipv4ConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualLists"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.manualListsDesc" }}
</h2>
</a-row>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedIPs"}}' v-model="manualBlockedIPs"></setting-list-item>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedDomains"}}' v-model="manualBlockedDomains"></setting-list-item>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectIPs"}}' v-model="manualDirectIPs"></setting-list-item>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectDomains"}}' v-model="manualDirectDomains"></setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOutboundsDesc"}}' v-model="outboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
<a-space direction="horizontal" style="padding: 0 20px">
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
</a-space>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-tab-pane>
</a-tabs>
</a-list>
</a-tab-pane>
<a-tab-pane key="4" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 20px 20px; text-align: center;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.infoDesc" }}
</h2>
</a-row>
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramNotifyTime"}}' desc='{{ i18n "pages.settings.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyBackup" }}' desc='{{ i18n "pages.settings.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.tgNotifyCpu" }}' desc='{{ i18n "pages.settings.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
</a-list>
</a-tab-pane>
</a-tabs>
</a-space>
</a-spin>
</a-layout-content>
</a-layout>
</a-layout>
{{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "component/password" .}}
{{template "component/setting"}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
siderDrawer,
themeSwitcher,
spinning: false,
oldAllSetting: new AllSetting(),
allSetting: new AllSetting(),
saveBtnDisable: true,
user: {},
lang: getLang(),
ipv4Settings: {
tag: "IPv4",
protocol: "freedom",
settings: {
domainStrategy: "UseIPv4"
}
},
directSettings: {
tag: "direct",
protocol: "freedom"
},
outboundDomainStrategies: ["AsIs", "UseIP", "UseIPv4", "UseIPv6"],
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
settingsData: {
protocols: {
bittorrent: ["bittorrent"],
},
ips: {
local: ["geoip:private"],
google: ["geoip:google"],
cn: ["geoip:cn"],
ir: ["geoip:ir"],
ru: ["geoip:ru"],
},
domains: {
ads: [
"geosite:category-ads-all",
"geosite:category-ads",
"geosite:google-ads",
"geosite:spotify-ads"
],
porn: ["geosite:category-porn"],
openai: ["geosite:openai"],
google: ["geosite:google"],
spotify: ["geosite:spotify"],
netflix: ["geosite:netflix"],
cn: [
"geosite:cn",
"regexp:.*\\.cn$"
],
ru: [
"geosite:category-gov-ru",
"regexp:.*\\.ru$"
],
ir: [
"regexp:.*\\.ir$",
"ext:iran.dat:ir",
"ext:iran.dat:other",
"ext:iran.dat:ads",
"geosite:category-ir"
]
},
familyProtectDNS: {
"servers": [
"1.1.1.2",
"1.0.0.2",
"94.140.14.14",
"94.140.15.15"
],
"queryStrategy": "UseIPv4"
},
}
},
methods: {
loading(spinning = true) {
this.spinning = spinning;
},
async getAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/all");
this.loading(false);
if (msg.success) {
this.oldAllSetting = new AllSetting(msg.obj);
this.allSetting = new AllSetting(msg.obj);
this.saveBtnDisable = true;
}
},
async updateAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/update", this.allSetting);
this.loading(false);
if (msg.success) {
await this.getAllSetting();
}
},
async updateUser() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/updateUser", this.user);
this.loading(false);
if (msg.success) {
this.user = {};
window.location.replace(basePath + "logout")
}
},
async restartPanel() {
await new Promise(resolve => {
this.$confirm({
title: '{{ i18n "pages.settings.restartPanel" }}',
content: '{{ i18n "pages.settings.restartPanelDesc" }}',
okText: '{{ i18n "sure" }}',
cancelText: '{{ i18n "cancel" }}',
onOk: () => resolve(),
});
});
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/restartPanel");
this.loading(false);
if (msg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
window.location.replace(this.allSetting.webBasePath + "xui/settings");
}
},
async resetXrayConfigToDefault() {
this.loading(true);
const msg = await HttpUtil.get("/xui/setting/getDefaultJsonConfig");
this.loading(false);
if (msg.success) {
this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
this.saveBtnDisable = true;
}
},
syncRulesWithOutbound(tag, setting) {
const newTemplateSettings = this.templateSettings;
const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag);
const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag);
if (!haveRules && outboundIndex > 0) {
newTemplateSettings.outbounds.splice(outboundIndex);
}
if (haveRules && outboundIndex < 0) {
newTemplateSettings.outbounds.push(setting);
}
this.templateSettings = newTemplateSettings;
},
templateRuleGetter(routeSettings) {
const { property, outboundTag } = routeSettings;
let result = [];
if (this.templateSettings != null) {
this.templateSettings.routing.rules.forEach(
(routingRule) => {
if (
routingRule.hasOwnProperty(property) &&
routingRule.hasOwnProperty("outboundTag") &&
routingRule.outboundTag === outboundTag
) {
result.push(...routingRule[property]);
}
}
);
}
return result;
},
templateRuleSetter(routeSettings) {
const { data, property, outboundTag } = routeSettings;
const oldTemplateSettings = this.templateSettings;
const newTemplateSettings = oldTemplateSettings;
currentProperty = this.templateRuleGetter({ outboundTag, property })
if (currentProperty.length == 0) {
const propertyRule = {
type: "field",
outboundTag,
[property]: data
};
newTemplateSettings.routing.rules.push(propertyRule);
}
else {
const newRules = [];
insertedOnce = false;
newTemplateSettings.routing.rules.forEach(
(routingRule) => {
if (
routingRule.hasOwnProperty(property) &&
routingRule.hasOwnProperty("outboundTag") &&
routingRule.outboundTag === outboundTag
) {
if (!insertedOnce && data.length > 0) {
insertedOnce = true;
routingRule[property] = data;
newRules.push(routingRule);
}
}
else {
newRules.push(routingRule);
}
}
);
newTemplateSettings.routing.rules = newRules;
}
this.templateSettings = newTemplateSettings;
}
},
async mounted() {
await this.getAllSetting();
while (true) {
await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
}
},
computed: {
templateSettings: {
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; },
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
},
inboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.inbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
outboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.outbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.rules = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
freedomStrategy: {
get: function () {
if (!this.templateSettings) return "AsIs";
freedomOutbound = this.templateSettings.outbounds.find((o) => o.tag === "direct");
if (!freedomOutbound) return "AsIs";
if (!freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return "AsIs";
return freedomOutbound.settings.domainStrategy;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && !o.tag);
if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) {
newTemplateSettings.outbounds[freedomOutboundIndex].settings = { "domainStrategy": newValue };
} else {
newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;
}
this.templateSettings = newTemplateSettings;
}
},
routingStrategy: {
get: function () {
if (!this.templateSettings || !this.templateSettings.routing || !this.templateSettings.routing.domainStrategy) return "AsIs";
return this.templateSettings.routing.domainStrategy;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.domainStrategy = newValue;
this.templateSettings = newTemplateSettings;
}
},
blockedIPs: {
get: function () {
return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "blocked", property: "ip", data: newValue });
}
},
blockedDomains: {
get: function () {
return this.templateRuleGetter({ outboundTag: "blocked", property: "domain" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "blocked", property: "domain", data: newValue });
}
},
blockedProtocols: {
get: function () {
return this.templateRuleGetter({ outboundTag: "blocked", property: "protocol" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "blocked", property: "protocol", data: newValue });
}
},
directIPs: {
get: function () {
return this.templateRuleGetter({ outboundTag: "direct", property: "ip" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "direct", property: "ip", data: newValue });
this.syncRulesWithOutbound("direct", this.directSettings);
}
},
directDomains: {
get: function () {
return this.templateRuleGetter({ outboundTag: "direct", property: "domain" });
},
set: function (newValue) {
this.templateRuleSetter({ outboundTag: "direct", property: "domain", data: newValue });
this.syncRulesWithOutbound("direct", this.directSettings);
}
},
manualBlockedIPs: {
get: function () { return JSON.stringify(this.blockedIPs, null, 2); },
set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000)
},
manualBlockedDomains: {
get: function () { return JSON.stringify(this.blockedDomains, null, 2); },
set: debounce(function (value) { this.blockedDomains = JSON.parse(value); }, 1000)
},
manualDirectIPs: {
get: function () { return JSON.stringify(this.directIPs, null, 2); },
set: debounce(function (value) { this.directIPs = JSON.parse(value); }, 1000)
},
manualDirectDomains: {
get: function () { return JSON.stringify(this.directDomains, null, 2); },
set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000)
},
torrentSettings: {
get: function () {
return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
},
set: function (newValue) {
if (newValue) {
this.blockedProtocols = [...this.blockedProtocols, ...this.settingsData.protocols.bittorrent];
} else {
this.blockedProtocols = this.blockedProtocols.filter(data => !this.settingsData.protocols.bittorrent.includes(data));
}
},
},
privateIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.local, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.local];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.local.includes(data));
}
},
},
AdsSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ads, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ads];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ads.includes(data));
}
},
},
familyProtectSettings: {
get: function () {
if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false;
return doAllItemsExist(this.templateSettings.dns.servers, this.settingsData.familyProtectDNS.servers);
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
if (newValue) {
newTemplateSettings.dns = this.settingsData.familyProtectDNS;
} else {
delete newTemplateSettings.dns;
}
this.templateSettings = newTemplateSettings;
},
},
GoogleIPv4Settings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.google, this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }));
},
set: function (newValue) {
oldData = this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
if (newValue) {
oldData = [...oldData, ...this.settingsData.domains.google];
} else {
oldData = oldData.filter(data => !this.settingsData.domains.google.includes(data))
}
this.templateRuleSetter({
outboundTag: "IPv4",
property: "domain",
data: oldData
});
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
},
},
NetflixIPv4Settings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.netflix, this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" }));
},
set: function (newValue) {
oldData = this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
if (newValue) {
oldData = [...oldData, ...this.settingsData.domains.netflix];
} else {
oldData = oldData.filter(data => !this.settingsData.domains.netflix.includes(data))
}
this.templateRuleSetter({
outboundTag: "IPv4",
property: "domain",
data: oldData
});
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
},
},
IRIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ir, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ir];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ir.includes(data));
}
}
},
IRDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ir, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ir];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ir.includes(data));
}
}
},
ChinaIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.cn, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.cn];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.cn.includes(data));
}
}
},
ChinaDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.cn, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.cn];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.cn.includes(data));
}
}
},
RussiaIpSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ru, this.blockedIPs);
},
set: function (newValue) {
if (newValue) {
this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ru];
} else {
this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ru.includes(data));
}
}
},
RussiaDomainSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ru, this.blockedDomains);
},
set: function (newValue) {
if (newValue) {
this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ru];
} else {
this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ru.includes(data));
}
}
},
IRIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ir, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.ir];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ir.includes(data));
}
}
},
IRDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ir, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.ir];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ir.includes(data));
}
}
},
ChinaIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.cn, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.cn];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.cn.includes(data));
}
}
},
ChinaDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.cn, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.cn];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.cn.includes(data));
}
}
},
RussiaIpDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.ips.ru, this.directIPs);
},
set: function (newValue) {
if (newValue) {
this.directIPs = [...this.directIPs, ...this.settingsData.ips.ru];
} else {
this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ru.includes(data));
}
}
},
RussiaDomainDirectSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.ru, this.directDomains);
},
set: function (newValue) {
if (newValue) {
this.directDomains = [...this.directDomains, ...this.settingsData.domains.ru];
} else {
this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ru.includes(data));
}
}
},
},
});
</script>
</body>
</html>

View File

@@ -2,24 +2,19 @@
"log": { "log": {
"loglevel": "warning" "loglevel": "warning"
}, },
"api": { "api": {
"services": [ "tag": "api",
"HandlerService", "services": ["HandlerService", "LoggerService", "StatsService"]
"LoggerService",
"StatsService"
],
"tag": "api"
}, },
"inbounds": [ "inbounds": [
{ {
"tag": "api",
"listen": "127.0.0.1", "listen": "127.0.0.1",
"port": 62789, "port": 62789,
"protocol": "dokodemo-door", "protocol": "dokodemo-door",
"settings": { "settings": {
"address": "127.0.0.1" "address": "127.0.0.1"
}, }
"tag": "api"
} }
], ],
"outbounds": [ "outbounds": [
@@ -28,16 +23,16 @@
"settings": {} "settings": {}
}, },
{ {
"tag": "blocked",
"protocol": "blackhole", "protocol": "blackhole",
"settings": {}, "settings": {}
"tag": "blocked"
} }
], ],
"policy": { "policy": {
"levels": { "levels": {
"0": { "0": {
"statsUserUplink": true, "statsUserDownlink": true,
"statsUserDownlink": true "statsUserUplink": true
} }
}, },
"system": { "system": {
@@ -46,27 +41,22 @@
} }
}, },
"routing": { "routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [ "rules": [
{ {
"inboundTag": [ "type": "field",
"api" "inboundTag": ["api"],
], "outboundTag": "api"
"outboundTag": "api",
"type": "field"
}, },
{ {
"ip": [ "type": "field",
"geoip:private"
],
"outboundTag": "blocked", "outboundTag": "blocked",
"type": "field" "ip": ["geoip:private"]
}, },
{ {
"type": "field",
"outboundTag": "blocked", "outboundTag": "blocked",
"protocol": [ "protocol": ["bittorrent"]
"bittorrent"
],
"type": "field"
} }
] ]
}, },

View File

@@ -572,6 +572,7 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
count := result.RowsAffected count := result.RowsAffected
return count, err return count, err
} }
func (s *InboundService) DisableInvalidClients() (int64, error) { func (s *InboundService) DisableInvalidClients() (int64, error) {
db := database.GetDB() db := database.GetDB()
now := time.Now().Unix() * 1000 now := time.Now().Unix() * 1000
@@ -582,7 +583,8 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
count := result.RowsAffected count := result.RowsAffected
return count, err return count, err
} }
func (s *InboundService) RemoveOrphanedTraffics() {
func (s *InboundService) MigrationRemoveOrphanedTraffics() {
db := database.GetDB() db := database.GetDB()
db.Exec(` db.Exec(`
DELETE FROM client_traffics DELETE FROM client_traffics
@@ -593,6 +595,7 @@ func (s *InboundService) RemoveOrphanedTraffics() {
) )
`) `)
} }
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error { func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
db := database.GetDB() db := database.GetDB()
@@ -611,6 +614,7 @@ func (s *InboundService) AddClientStat(inboundId int, client *model.Client) erro
} }
return nil return nil
} }
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error { func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
db := database.GetDB() db := database.GetDB()
@@ -917,3 +921,8 @@ func (s *InboundService) MigrationRequirements() {
// Remove orphaned traffics // Remove orphaned traffics
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{}) db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
} }
func (s *InboundService) MigrateDB() {
s.MigrationRequirements()
s.MigrationRemoveOrphanedTraffics()
}

View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"mime/multipart"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
@@ -14,7 +15,9 @@ import (
"strings" "strings"
"time" "time"
"x-ui/config" "x-ui/config"
"x-ui/database"
"x-ui/logger" "x-ui/logger"
"x-ui/util/common"
"x-ui/util/sys" "x-ui/util/sys"
"x-ui/xray" "x-ui/xray"
@@ -73,7 +76,8 @@ type Release struct {
} }
type ServerService struct { type ServerService struct {
xrayService XrayService xrayService XrayService
inboundService InboundService
} }
func (s *ServerService) GetStatus(lastStatus *Status) *Status { func (s *ServerService) GetStatus(lastStatus *Status) *Status {
@@ -393,6 +397,106 @@ func (s *ServerService) GetDb() ([]byte, error) {
return fileContents, nil return fileContents, nil
} }
func (s *ServerService) ImportDB(file multipart.File) error {
// Check if the file is a SQLite database
isValidDb, err := database.IsSQLiteDB(file)
if err != nil {
return common.NewErrorf("Error checking db file format: %v", err)
}
if !isValidDb {
return common.NewError("Invalid db file format")
}
// Reset the file reader to the beginning
_, err = file.Seek(0, 0)
if err != nil {
return common.NewErrorf("Error resetting file reader: %v", err)
}
// Save the file as temporary file
tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
// Remove the existing fallback file (if any) before creating one
_, err = os.Stat(tempPath)
if err == nil {
errRemove := os.Remove(tempPath)
if errRemove != nil {
return common.NewErrorf("Error removing existing temporary db file: %v", errRemove)
}
}
// Create the temporary file
tempFile, err := os.Create(tempPath)
if err != nil {
return common.NewErrorf("Error creating temporary db file: %v", err)
}
defer tempFile.Close()
// Remove temp file before returning
defer os.Remove(tempPath)
// Save uploaded file to temporary file
_, err = io.Copy(tempFile, file)
if err != nil {
return common.NewErrorf("Error saving db: %v", err)
}
// Check if we can init db or not
err = database.InitDB(tempPath)
if err != nil {
return common.NewErrorf("Error checking db: %v", err)
}
// Stop Xray
s.StopXrayService()
// Backup the current database for fallback
fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
// Remove the existing fallback file (if any)
_, err = os.Stat(fallbackPath)
if err == nil {
errRemove := os.Remove(fallbackPath)
if errRemove != nil {
return common.NewErrorf("Error removing existing fallback db file: %v", errRemove)
}
}
// Move the current database to the fallback location
err = os.Rename(config.GetDBPath(), fallbackPath)
if err != nil {
return common.NewErrorf("Error backing up temporary db file: %v", err)
}
// Remove the temporary file before returning
defer os.Remove(fallbackPath)
// Move temp to DB path
err = os.Rename(tempPath, config.GetDBPath())
if err != nil {
errRename := os.Rename(fallbackPath, config.GetDBPath())
if errRename != nil {
return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename)
}
return common.NewErrorf("Error moving db file: %v", err)
}
// Migrate DB
err = database.InitDB(config.GetDBPath())
if err != nil {
errRename := os.Rename(fallbackPath, config.GetDBPath())
if errRename != nil {
return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
}
return common.NewErrorf("Error migrating db: %v", err)
}
s.inboundService.MigrateDB()
// Start Xray
err = s.RestartXrayService()
if err != nil {
return common.NewErrorf("Imported DB but Failed to start Xray: %v", err)
}
return nil
}
func (s *ServerService) GetNewX25519Cert() (interface{}, error) { func (s *ServerService) GetNewX25519Cert() (interface{}, error) {
// Run the command // Run the command
cmd := exec.Command(xray.GetBinaryPath(), "x25519") cmd := exec.Command(xray.GetBinaryPath(), "x25519")

View File

@@ -11,7 +11,6 @@ import (
"x-ui/xray" "x-ui/xray"
"github.com/goccy/go-json" "github.com/goccy/go-json"
"gorm.io/gorm"
) )
type SubService struct { type SubService struct {
@@ -19,15 +18,15 @@ type SubService struct {
inboundService InboundService inboundService InboundService
} }
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) { func (s *SubService) GetSubs(subId string, host string) ([]string, []string, error) {
s.address = host s.address = host
var result []string var result []string
var header string var headers []string
var traffic xray.ClientTraffic var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId) inbounds, err := s.getInboundsBySubId(subId)
if err != nil { if err != nil {
return nil, "", err return nil, nil, err
} }
for _, inbound := range inbounds { for _, inbound := range inbounds {
clients, err := s.inboundService.getClients(inbound) clients, err := s.inboundService.getClients(inbound)
@@ -38,7 +37,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
continue continue
} }
for _, client := range clients { for _, client := range clients {
if client.SubID == subId { if client.Enable && client.SubID == subId {
link := s.getLink(inbound, client.Email) link := s.getLink(inbound, client.Email)
result = append(result, link) result = append(result, link)
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email)) clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
@@ -66,15 +65,17 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
} }
} }
} }
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
return result, header, nil headers = append(headers, "12")
headers = append(headers, subId)
return result, headers, nil
} }
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil {
return nil, err return nil, err
} }
return inbounds, nil return inbounds, nil
@@ -107,9 +108,10 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
if inbound.Protocol != model.VMess { if inbound.Protocol != model.VMess {
return "" return ""
} }
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
obj := map[string]interface{}{ obj := map[string]interface{}{
"v": "2", "v": "2",
"ps": email, "ps": remark,
"add": s.address, "add": s.address,
"port": inbound.Port, "port": inbound.Port,
"type": "none", "type": "none",
@@ -353,7 +355,8 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
// Set the new query values on the URL // Set the new query values on the URL
url.RawQuery = q.Encode() url.RawQuery = q.Encode()
url.Fragment = email remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
url.Fragment = remark
return url.String() return url.String()
} }
@@ -502,7 +505,8 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
// Set the new query values on the URL // Set the new query values on the URL
url.RawQuery = q.Encode() url.RawQuery = q.Encode()
url.Fragment = email remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
url.Fragment = remark
return url.String() return url.String()
} }
@@ -511,6 +515,8 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
if inbound.Protocol != model.Shadowsocks { if inbound.Protocol != model.Shadowsocks {
return "" return ""
} }
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.getClients(inbound) clients, _ := s.inboundService.getClients(inbound)
var settings map[string]interface{} var settings map[string]interface{}
@@ -524,8 +530,66 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
break break
} }
} }
streamNetwork := stream["network"].(string)
params := make(map[string]string)
params["type"] = streamNetwork
switch streamNetwork {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
typeStr, _ := header["type"].(string)
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
params["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
params["headerType"] = "http"
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
params["seed"] = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
params["path"] = ws["path"].(string)
headers, _ := ws["headers"].(map[string]interface{})
params["host"] = searchHost(headers)
case "http":
http, _ := stream["httpSettings"].(map[string]interface{})
params["path"] = http["path"].(string)
params["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
params["quicSecurity"] = quic["security"].(string)
params["key"] = quic["key"].(string)
header := quic["header"].(map[string]interface{})
params["headerType"] = header["type"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
}
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password) encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, clients[clientIndex].Email) link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
url, _ := url.Parse(link)
q := url.Query()
for k, v := range params {
q.Add(k, v)
}
// Set the new query values on the URL
url.RawQuery = q.Encode()
remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email)
url.Fragment = remark
return url.String()
} }
func searchKey(data interface{}, key string) (interface{}, bool) { func searchKey(data interface{}, key string) (interface{}, bool) {

View File

@@ -216,6 +216,9 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
} }
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) { func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
if !isRunning {
return
}
var allMessages []string var allMessages []string
limit := 2000 limit := 2000
// paging message if it is big // paging message if it is big

View File

@@ -22,11 +22,11 @@
"unlimited" = "Unlimited" "unlimited" = "Unlimited"
"none" = "None" "none" = "None"
"qrCode" = "QR Code" "qrCode" = "QR Code"
"info" = "More information" "info" = "More Information"
"edit" = "Edit" "edit" = "Edit"
"delete" = "Delete" "delete" = "Delete"
"reset" = "Reset" "reset" = "Reset"
"copySuccess" = "Copy successfully" "copySuccess" = "Copied successfully"
"sure" = "Sure" "sure" = "Sure"
"encryption" = "Encryption" "encryption" = "Encryption"
"transmission" = "Transmission" "transmission" = "Transmission"
@@ -42,7 +42,7 @@
"additional" = "Alter ID" "additional" = "Alter ID"
"monitor" = "Listen IP" "monitor" = "Listen IP"
"certificate" = "Certificate" "certificate" = "Certificate"
"fail" = "Fail" "fail" = " Fail"
"success" = " Success" "success" = " Success"
"getVersion" = "Get version" "getVersion" = "Get version"
"install" = "Install" "install" = "Install"
@@ -52,8 +52,8 @@
[menu] [menu]
"dashboard" = "System Status" "dashboard" = "System Status"
"inbounds" = "Inbounds" "inbounds" = "Inbounds"
"setting" = "Panel Setting" "settings" = "Panel Settings"
"logout" = "LogOut" "logout" = "Logout"
"link" = "Other" "link" = "Other"
[pages.login] [pages.login]
@@ -61,104 +61,114 @@
"loginAgain" = "The login time limit has expired, please log in again" "loginAgain" = "The login time limit has expired, please log in again"
[pages.login.toasts] [pages.login.toasts]
"invalidFormData" = "Input Data Format Is Invalid" "invalidFormData" = "Input data format is invalid."
"emptyUsername" = "Please Enter Username" "emptyUsername" = "Please enter username."
"emptyPassword" = "Please Enter Password" "emptyPassword" = "Please enter password."
"wrongUsernameOrPassword" = "Invalid username or password" "wrongUsernameOrPassword" = "Invalid username or password."
"successLogin" = "Login" "successLogin" = "Login"
[pages.index] [pages.index]
"title" = "System status" "title" = "System Status"
"memory" = "Memory" "memory" = "Memory"
"hard" = "Hard disk" "hard" = "Hard Disk"
"xrayStatus" = "xray Status" "xrayStatus" = "Xray Status"
"stopXray" = "Stop" "stopXray" = "Stop"
"restartXray" = "Restart" "restartXray" = "Restart"
"xraySwitch" = "Switch Version" "xraySwitch" = "Switch Version"
"xraySwitchClick" = "Click on the version you want to switch" "xraySwitchClick" = "Choose the version you want to switch to."
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations" "xraySwitchClickDesk" = "Choose wisely, as older versions may not be compatible with current configurations."
"operationHours" = "Operation Hours" "operationHours" = "Operation Hours"
"operationHoursDesc" = "The running time of the system since it was started" "operationHoursDesc" = "System uptime: time since startup."
"systemLoad" = "System Load" "systemLoad" = "System Load"
"connectionCount" = "Connection Count" "connectionCount" = "Connection Count"
"connectionCountDesc" = "The total number of connections for all network cards" "connectionCountDesc" = "Total connections across all network cards."
"upSpeed" = "Total upload speed for all network cards" "upSpeed" = "Total upload speed for all network cards."
"downSpeed" = "Total download speed for all network cards" "downSpeed" = "Total download speed for all network cards."
"totalSent" = "Total upload traffic of all network cards since system startup" "totalSent" = "Total upload traffic of all network cards since system startup."
"totalReceive" = "Total download traffic of all network cards since system startup" "totalReceive" = "Total download data across all network cards since system startup."
"xraySwitchVersionDialog" = "Switch xray version" "xraySwitchVersionDialog" = "Switch Xray Version"
"xraySwitchVersionDialogDesc" = "Whether to switch the xray version to" "xraySwitchVersionDialogDesc" = "Are you sure you want to switch the Xray version to"
"dontRefreshh" = "Installation is in progress, please do not refresh this page" "dontRefresh" = "Installation is in progress, please do not refresh this page."
"logs" = "Logs"
"config" = "Config"
"backup" = "Backup & Restore"
"backupTitle" = "Backup & Restore Database"
"backupDescription" = "Remember to backup before importing a new database."
"exportDatabase" = "Download Database"
"importDatabase" = "Upload Database"
[pages.inbounds] [pages.inbounds]
"title" = "Inbounds" "title" = "Inbounds"
"totalDownUp" = "Total uploads/downloads" "totalDownUp" = "Total Uploads/Downloads"
"totalUsage" = "Total usage" "totalUsage" = "Total Usage"
"inboundCount" = "Number of inbound" "inboundCount" = "Number of Inbounds"
"operate" = "Operate" "operate" = "Menu"
"enable" = "Enable" "enable" = "Enable"
"remark" = "Remark" "remark" = "Remark"
"protocol" = "Protocol" "protocol" = "Protocol"
"port" = "Port" "port" = "Port"
"traffic" = "Traffic" "traffic" = "Traffic"
"details" = "Details" "details" = "Details"
"transportConfig" = "Transport config" "transportConfig" = "Transport Config"
"expireDate" = "Expire date" "expireDate" = "Expire Date"
"resetTraffic" = "Reset traffic" "resetTraffic" = "Reset Traffic"
"addInbound" = "Add Inbound" "addInbound" = "Add Inbound"
"generalActions" = "General Actions" "generalActions" = "General Actions"
"addTo" = "Create" "create" = "Create"
"revise" = "Update" "update" = "Update"
"modifyInbound" = "Modify InBound" "modifyInbound" = "Modify Inbound"
"deleteInbound" = "Delete Inbound" "deleteInbound" = "Delete Inbound"
"deleteInboundContent" = "Are you sure you want to delete inbound?" "deleteInboundContent" = "Are you sure you want to delete inbound?"
"resetTrafficContent" = "Are you sure you want to reset traffic?" "resetTrafficContent" = "Are you sure you want to reset traffic?"
"copyLink" = "Copy Link" "copyLink" = "Copy Link"
"address" = "Address" "address" = "Address"
"network" = "Network" "network" = "Network"
"destinationPort" = "Destination port" "destinationPort" = "Destination Port"
"targetAddress" = "Target address" "targetAddress" = "Target Address"
"disableInsecureEncryption" = "Disable insecure encryption" "disableInsecureEncryption" = "Disable Insecure Encryption"
"monitorDesc" = "Leave blank by default" "monitorDesc" = "Leave blank by default"
"meansNoLimit" = "Means no limit" "meansNoLimit" = "Means No Limit"
"totalFlow" = "Total flow" "totalFlow" = "Total Flow"
"leaveBlankToNeverExpire" = "Leave blank to never expire" "leaveBlankToNeverExpire" = "Leave blank to never expire"
"noRecommendKeepDefault" = "There are no special requirements to keep the default" "noRecommendKeepDefault" = "No special requirements to keep the default"
"certificatePath" = "Certificate file path" "certificatePath" = "Certificate File Path"
"certificateContent" = "Certificate file content" "certificateContent" = "Certificate File Content"
"publicKeyPath" = "Public key file path" "publicKeyPath" = "Public Key Path"
"publicKeyContent" = "Public key content" "publicKeyContent" = "Public Key Content"
"keyPath" = "Key file path" "keyPath" = "Private Key Path"
"keyContent" = "Key content" "keyContent" = "Private Key Content"
"clickOnQRcode" = "Click on QR Code to Copy" "clickOnQRcode" = "Click on QR Code to Copy"
"client" = "Client" "client" = "Client"
"export" = "Export Links" "export" = "Export Links"
"Clone" = "Clone" "clone" = "Clone"
"cloneInbound" = "Create" "cloneInbound" = "Clone"
"cloneInboundContent" = "All items of this inbound except Port, Listening IP, Clients will be applied to the clone" "cloneInboundContent" = "All settings of this inbound, except for Port, Listening IP, and Clients, will be applied to the clone."
"cloneInboundOk" = "Clone"
"resetAllTraffic" = "Reset All Inbounds Traffic" "resetAllTraffic" = "Reset All Inbounds Traffic"
"resetAllTrafficTitle" = "Reset all inbounds traffic" "resetAllTrafficTitle" = "Reset all inbounds traffic"
"resetAllTrafficContent" = "Are you sure to reset all inbounds traffic ?" "resetAllTrafficContent" = "Are you sure you want to reset all inbounds traffic?"
"resetInboundClientTraffics" = "Reset Clients Traffic" "resetInboundClientTraffics" = "Reset Clients Traffic"
"resetInboundClientTrafficTitle" = "Reset all clients traffic" "resetInboundClientTrafficTitle" = "Reset all clients traffic"
"resetInboundClientTrafficContent" = "Are you sure to reset all traffics of this inbound's clients ?" "resetInboundClientTrafficContent" = "Are you sure you want to reset all traffic for this inbound's clients?"
"resetAllClientTraffics" = "Reset All Clients Traffic" "resetAllClientTraffics" = "Reset All Clients Traffic"
"resetAllClientTrafficTitle" = "Reset all clients traffic" "resetAllClientTrafficTitle" = "Reset all clients traffic"
"resetAllClientTrafficContent" = "Are you sure to reset all traffics of all clients ?" "resetAllClientTrafficContent" = "Are you sure you want to reset all traffics for all clients?"
"delDepletedClients" = "Delete depleted clients" "delDepletedClients" = "Delete Depleted Clients"
"delDepletedClientsTitle" = "Delete depleted clients" "delDepletedClientsTitle" = "Delete depleted clients"
"delDepletedClientsContent" = "Are you sure to delete all depleted clients ?" "delDepletedClientsContent" = "Are you sure you want to delete all depleted clients?"
"Email" = "Email" "email" = "Email"
"EmailDesc" = "The Email Must Be Completely Unique" "emailDesc" = "Please provide a unique email address."
"setDefaultCert" = "Set cert from panel" "setDefaultCert" = "Set cert from panel"
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot )"
"subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
[pages.client] [pages.client]
"add" = "Add client" "add" = "Add Client"
"edit" = "Edit client" "edit" = "Edit Client"
"submitAdd" = "Add client" "submitAdd" = "Add Client"
"submitEdit" = "Save changes" "submitEdit" = "Save changes"
"clientCount" = "Number of clients" "clientCount" = "Number of Clients"
"bulk" = "Add bulk" "bulk" = "Add Bulk"
"method" = "Method" "method" = "Method"
"first" = "First" "first" = "First"
"last" = "Last" "last" = "Last"
@@ -188,69 +198,125 @@
[pages.inbounds.stream.quic] [pages.inbounds.stream.quic]
"encryption" = "Encryption" "encryption" = "Encryption"
[pages.setting] [pages.settings]
"title" = "Setting" "title" = "Settings"
"save" = "Save" "save" = "Save"
"restartPanel" = "Restart Panel" "infoDesc" = "Every change made here needs to be saved. Please restart the panel for the changes to take effect."
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information" "restartPanel" = "Restart Panel "
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log information on the server."
"resetDefaultConfig" = "Reset to default config" "resetDefaultConfig" = "Reset to default config"
"panelConfig" = "Panel Configuration" "panelConfig" = "Panel Configurations"
"userSetting" = "User Setting" "userSettings" = "User Settings"
"xrayConfiguration" = "xray Configuration" "xrayConfiguration" = "Xray Configurations"
"TGReminder" = "TG Reminder Related Settings" "TGBotSettings" = "Telegram Bot Settings"
"otherSetting" = "Other Setting" "panelListeningIP" = "Panel Listening IP"
"panelListeningIP" = "Panel listening IP" "panelListeningIPDesc" = "Leave blank by default to monitor all IPs."
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs, restart the panel to take effect"
"panelPort" = "Panel Port" "panelPort" = "Panel Port"
"panelPortDesc" = "Restart the panel to take effect" "panelPortDesc" = "Port number for serving the panel."
"publicKeyPath" = "Panel certificate public key file path" "publicKeyPath" = "Panel Certificate Public Key File Path"
"publicKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect" "publicKeyPathDesc" = "Fill in an absolute path starting with '/'"
"privateKeyPath" = "Panel certificate private key file path" "privateKeyPath" = "Panel Certificate Private Key File Path"
"privateKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect" "privateKeyPathDesc" = "Fill in an absolute path starting with '/'"
"panelUrlPath" = "Panel url root path" "panelUrlPath" = "Panel URL Root Path"
"panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect" "panelUrlPathDesc" = "Must start with '/' and end with '/'"
"oldUsername" = "Current Username" "oldUsername" = "Current Username"
"currentPassword" = "Current Password" "currentPassword" = "Current Password"
"newUsername" = "New Username" "newUsername" = "New Username"
"newPassword" = "New Password" "newPassword" = "New Password"
"advancedTemplate" = "Advanced template parts" "telegramBotEnable" = "Enable Telegram bot"
"completeTemplate" = "Complete template of Xray configuration" "telegramBotEnableDesc" = "Your telegram bot will interact with the panel"
"xrayConfigTemplate" = "Xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect."
"xrayConfigTorrent" = "Ban bittorrent usage"
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using bittorrent by users, restart the panel to take effect"
"xrayConfigPrivateIp" = "Ban private ip range to connect"
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting with private IP ranges, restart the panel to take effect"
"xrayConfigInbounds" = "Configuration of Inbounds"
"xrayConfigInboundsDesc" = "Change the configuration template to accept special clients, restart the panel to take effect"
"xrayConfigOutbounds" = "Configuration of Outbounds"
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server, restart the panel to take effect"
"xrayConfigRoutings" = "Configuration of Routing rules"
"xrayConfigRoutingsDesc" = "Change the configuration template to define Routing rules for this server, restart the panel to take effect"
"telegramBotEnable" = "Enable telegram bot"
"telegramBotEnableDesc" = "Restart the panel to take effect"
"telegramToken" = "Telegram Token" "telegramToken" = "Telegram Token"
"telegramTokenDesc" = "Restart the panel to take effect" "telegramTokenDesc" = "The Token you have got from @BotFather"
"telegramChatId" = "Telegram Admin ChatIds" "telegramChatId" = "Telegram Admin ChatIDs"
"telegramChatIdDesc" = "Multi chatIDs separated by comma. Restart the panel to take effect" "telegramChatIdDesc" = "Multi chatIDs separated by comma."
"telegramNotifyTime" = "Telegram bot notification time" "telegramNotifyTime" = "Telegram bot notification time"
"telegramNotifyTimeDesc" = "Using Crontab timing format. Restart the panel to take effect" "telegramNotifyTimeDesc" = "Use Crontab timing format."
"tgNotifyBackup" = "Database backup" "tgNotifyBackup" = "Database Backup"
"tgNotifyBackupDesc" = "Sending database backup file with report notification. Restart the panel to take effect" "tgNotifyBackupDesc" = "Send database backup file with report notification"
"sessionMaxAge" = "Session maximum age" "sessionMaxAge" = "Session maximum age"
"sessionMaxAgeDesc" = "The time that you can stay login (unit: minute)" "sessionMaxAgeDesc" = "The time that you can stay login (unit: minute)"
"expireTimeDiff" = "Exhaustion time threshold" "expireTimeDiff" = "Expiration threshold for notification"
"expireTimeDiffDesc" = "Detect exhaustion before expiration (unit:day)" "expireTimeDiffDesc" = "Get notified about account expiration before the threshold (unit: day)"
"trafficDiff" = "Exhaustion traffic threshold" "trafficDiff" = "Traffic threshold for notification"
"trafficDiffDesc" = "Detect exhaustion before finishing traffic (unit:GB)" "trafficDiffDesc" = "Get notified about traffic exhaustion before reaching the threshold (unit: GB)"
"tgNotifyCpu" = "CPU percentage alert threshold" "tgNotifyCpu" = "CPU percentage alert threshold"
"tgNotifyCpuDesc" = "This telegram bot will send you a notification if CPU usage is more than this percentage (unit:%)" "tgNotifyCpuDesc" = "Receive notification if CPU usage exceeds this threshold (unit: %)"
"timeZonee" = "Time Zone" "timeZone" = "Time Zone"
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect" "timeZoneDesc" = "Scheduled tasks run according to the time in this time zone."
[pages.setting.toasts] [pages.settings.templates]
"modifySetting" = "Modify setting" "title" = "Templates"
"getSetting" = "Get setting" "basicTemplate" = "Basic Template"
"modifyUser" = "Modify user" "advancedTemplate" = "Advanced Template"
"originalUserPassIncorrect" = "The original user name or original password is incorrect" "completeTemplate" = "Complete Template"
"userPassMustBeNotEmpty" = "New username and new password cannot be empty" "generalConfigs" = "General Configs"
"generalConfigsDesc" = "These options will provide general adjustments."
"blockConfigs" = "Blocking Configs"
"blockConfigsDesc" = "These options will prevent users from connecting to specific protocols and websites."
"blockCountryConfigs" = "Block Country Configs"
"blockCountryConfigsDesc" = "These options will prevent users from connecting to specific country domains."
"directCountryConfigs" = "Direct Country Configs"
"directCountryConfigsDesc" = "These options will connect users directly to specific country domains."
"ipv4Configs" = "IPv4 Configs"
"ipv4ConfigsDesc" = "These options will route to target domains only via IPv4."
"xrayConfigTemplate" = "Xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final Xray configuration file based on this template."
"xrayConfigFreedomStrategy" = "Configure Strategy for Freedom Protocol"
"xrayConfigFreedomStrategyDesc" = "Set the output strategy of the network in the Freedom Protocol."
"xrayConfigRoutingStrategy" = "Configure Domains Routing Strategy"
"xrayConfigRoutingStrategyDesc" = "Set the overall routing strategy for DNS resolving."
"xrayConfigTorrent" = "Ban BitTorrent Usage"
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using BitTorrent by users."
"xrayConfigPrivateIp" = "Ban Private IP Ranges to Connect"
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting to private IP ranges."
"xrayConfigAds" = "Block Ads"
"xrayConfigAdsDesc" = "Change the configuration template to block ads"
"xrayConfigFamily" = "Enable Family-Friendly Configuration"
"xrayConfigFamilyDesc" = "Avoid connecting to unsafe websites for family protection."
"xrayConfigIRIp" = "Disable connection to Iran IP ranges"
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting to Iran IP ranges."
"xrayConfigIRDomain" = "Disable connection to Iran domains"
"xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting to Iran domains."
"xrayConfigChinaIp" = "Disable connection to China IP ranges"
"xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting to China IP ranges."
"xrayConfigChinaDomain" = "Disable connection to China domains"
"xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting to China domains."
"xrayConfigRussiaIp" = "Disable connection to Russia IP ranges"
"xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting to Russia IP ranges."
"xrayConfigRussiaDomain" = "Disable connection to Russia domains"
"xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting to Russia domains."
"xrayConfigDirectIRIp" = "Direct connection to Iran IP ranges"
"xrayConfigDirectIRIpDesc" = "Change the configuration template for direct connecting to Iran IP ranges."
"xrayConfigDirectIRDomain" = "Direct connection to Iran domains"
"xrayConfigDirectIRDomainDesc" = "Change the configuration template for direct connecting to Iran domains."
"xrayConfigDirectChinaIp" = "Direct connection to China IP ranges"
"xrayConfigDirectChinaIpDesc" = "Change the configuration template for direct connecting to China IP ranges."
"xrayConfigDirectChinaDomain" = "Direct connection to China domains"
"xrayConfigDirectChinaDomainDesc" = "Change the configuration template for direct connecting to China domains."
"xrayConfigDirectRussiaIp" = "Direct connection to Russia IP ranges"
"xrayConfigDirectRussiaIpDesc" = "Change the configuration template for direct connecting to Russia IP ranges."
"xrayConfigDirectRussiaDomain" = "Direct connection to Russia domains"
"xrayConfigDirectRussiaDomainDesc" = "Change the configuration template for direct connecting to Russia domains."
"xrayConfigGoogleIPv4" = "Use IPv4 for Google"
"xrayConfigGoogleIPv4Desc" = "Add routing for Google to connect with IPv4."
"xrayConfigNetflixIPv4" = "Use IPv4 for Netflix"
"xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4."
"xrayConfigInbounds" = "Configuration of Inbounds"
"xrayConfigInboundsDesc" = "Change the configuration template to accept specific clients."
"xrayConfigOutbounds" = "Configuration of Outbounds"
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server."
"xrayConfigRoutings" = "Configuration of routing rules"
"xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server."
"manualLists" = "Manual Lists"
"manualListsDesc" = "Please use the JSON array format."
"manualBlockedIPs" = "List of Blocked IPs"
"manualBlockedDomains" = "List of Blocked Domains"
"manualDirectIPs" = "List of Direct IPs"
"manualDirectDomains" = "List of Direct Domains"
[pages.settings.toasts]
"modifySettings" = "Modify Settings "
"getSettings" = "Get Settings "
"modifyUser" = "Modify User "
"originalUserPassIncorrect" = "Incorrect original username or password"
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"

View File

@@ -12,7 +12,7 @@
"protocol" = "پروتکل" "protocol" = "پروتکل"
"search" = "جستجو" "search" = "جستجو"
"loading" = "در حال بروزرسانی..." "loading" = "در حال بروزرسانی.."
"second" = "ثانیه" "second" = "ثانیه"
"minute" = "دقیقه" "minute" = "دقیقه"
"hour" = "ساعت" "hour" = "ساعت"
@@ -51,8 +51,8 @@
[menu] [menu]
"dashboard" = "وضعیت سیستم" "dashboard" = "وضعیت سیستم"
"inbounds" = "سرویس ها" "inbounds" = "سرویس ها"
"setting" = "تنظیمات پنل" "settings" = "تنظیمات پنل"
"logout" = "خروج" "logout" = "خروج"
"link" = "دیگر" "link" = "دیگر"
@@ -76,7 +76,7 @@
"restartXray" = "شروع مجدد" "restartXray" = "شروع مجدد"
"xraySwitch" = "تغییر ورژن" "xraySwitch" = "تغییر ورژن"
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید" "xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ." "xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد "
"operationHours" = "مدت فعالیت" "operationHours" = "مدت فعالیت"
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن" "operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
"systemLoad" = "بار روی سیستم" "systemLoad" = "بار روی سیستم"
@@ -88,7 +88,14 @@
"totalReceive" = "جمع کل ترافیک دانلود مصرفی" "totalReceive" = "جمع کل ترافیک دانلود مصرفی"
"xraySwitchVersionDialog" = "تغییر ورژن Xray" "xraySwitchVersionDialog" = "تغییر ورژن Xray"
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین" "xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید " "dontRefresh" = "در حال نصب ، لطفا رفرش نکنید "
"logs" = "گزارش ها"
"config" = "تنظیمات"
"backup" = "پشتیبان گیری"
"backupTitle" = "پشتیبان گیری دیتابیس"
"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید"
"exportDatabase" = "دانلود دیتابیس"
"importDatabase" = "آپلود دیتابیس"
[pages.inbounds] [pages.inbounds]
"title" = "کاربران" "title" = "کاربران"
@@ -107,8 +114,8 @@
"resetTraffic" = "ریست ترافیک" "resetTraffic" = "ریست ترافیک"
"addInbound" = "اضافه کردن سرویس" "addInbound" = "اضافه کردن سرویس"
"generalActions" = "عملیات کلی" "generalActions" = "عملیات کلی"
"addTo" = "اضافه کردن" "create" = "اضافه کردن"
"revise" = "ویرایش" "update" = "ویرایش"
"modifyInbound" = "ویرایش سرویس" "modifyInbound" = "ویرایش سرویس"
"deleteInbound" = "حذف سرویس" "deleteInbound" = "حذف سرویس"
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟" "deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
@@ -133,7 +140,7 @@
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید" "clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
"client" = "کاربر" "client" = "کاربر"
"export" = "استخراج لینک‌ها" "export" = "استخراج لینک‌ها"
"Clone" = "شبیه سازی" "clone" = "شبیه سازی"
"cloneInbound" = "ایجاد" "cloneInbound" = "ایجاد"
"cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد" "cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد"
"resetAllTraffic" = "ریست ترافیک کل سرویس ها" "resetAllTraffic" = "ریست ترافیک کل سرویس ها"
@@ -148,9 +155,11 @@
"delDepletedClients" = "حذف کاربران منقضی" "delDepletedClients" = "حذف کاربران منقضی"
"delDepletedClientsTitle" = "حذف کاربران منقضی" "delDepletedClientsTitle" = "حذف کاربران منقضی"
"delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟" "delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟"
"Email" = "ایمیل" "email" = "ایمیل"
"EmailDesc" = "ایمیل باید کاملا منحصر به فرد باشد" "emailDesc" = "ایمیل باید کاملا منحصر به فرد باشد"
"setDefaultCert" = "استفاده از گواهی پنل" "setDefaultCert" = "استفاده از گواهی پنل"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)"
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
[pages.client] [pages.client]
"add" = "کاربر جدید" "add" = "کاربر جدید"
@@ -188,53 +197,39 @@
[pages.inbounds.stream.quic] [pages.inbounds.stream.quic]
"encryption" = "رمزنگاری" "encryption" = "رمزنگاری"
[pages.setting] [pages.settings]
"title" = "تنظیمات" "title" = "تنظیمات"
"save" = "ذخیره" "save" = "ذخیره"
"infoDesc" = "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید"
"restartPanel" = "ریستارت پنل" "restartPanel" = "ریستارت پنل"
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید" "restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
"resetDefaultConfig" = "برگشت به تنظیمات پیشفرض" "resetDefaultConfig" = "برگشت به تنظیمات پیشفرض"
"panelConfig" = "تنظیمات پنل" "panelConfig" = "تنظیمات پنل"
"userSetting" = "تنظیمات مدیر" "userSettings" = "تنظیمات مدیر"
"xrayConfiguration" = "تنظیمات Xray" "xrayConfiguration" = "تنظیمات Xray"
"TGReminder" = "تنظیمات ربات تلگرام" "TGBotSettings" = "تنظیمات ربات تلگرام"
"otherSetting" = "دیگر تنظیمات"
"panelListeningIP" = "محدودیت آی پی پنل" "panelListeningIP" = "محدودیت آی پی پنل"
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود" "panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید"
"panelPort" = "پورت پنل" "panelPort" = "پورت پنل"
"panelPortDesc" = نل را مجدداً راه اندازی کنید تا اعمال شود" "panelPortDesc" = ورت مورد استفاده برای نمایش این پنل"
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل" "publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
"publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود" "publicKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
"privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل" "privateKeyPath" = "مسیر فایل گواهی کلید خصوصی پنل"
"privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود . پنل را مجدداً راه اندازی کنید تا اعمال شود" "privateKeyPathDesc" = "باید یک مسیر مطلق باشد که با / شروع می شود "
"panelUrlPath" = "آدرس روت پنل" "panelUrlPath" = "آدرس روت پنل"
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود. پنل را مجدداً راه اندازی کنید تا اعمال شود" "panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود"
"oldUsername" = "نام کاربری فعلی" "oldUsername" = "نام کاربری فعلی"
"currentPassword" = "رمز عبور فعلی" "currentPassword" = "رمز عبور فعلی"
"newUsername" = "نام کاربری جدید" "newUsername" = "نام کاربری جدید"
"newPassword" = "رمز عبور جدید" "newPassword" = "رمز عبور جدید"
"advancedTemplate" = "بخش های پیشرفته الگو"
"completeTemplate" = "الگوی کامل تنظیمات ایکس ری"
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigPrivateIp" = "جلوگیری از اتصال آی پی های نامعتبر"
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آی پی های نامعتبر و بسته های سرگردان تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigInbounds" = "تنظیمات ورودی"
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigOutbounds" = "تنظیمات خروجی"
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramBotEnable" = "فعالسازی ربات تلگرام" "telegramBotEnable" = "فعالسازی ربات تلگرام"
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود" "telegramBotEnableDesc" = "از طریق بات تلگرام به امکانات ابن پنل متصل شوید"
"telegramToken" = "توکن تلگرام" "telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود" "telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
"telegramChatId" = "آی دی تلگرام مدیریت" "telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود" "telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید"
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام" "telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود" "telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده" "tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای" "tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"sessionMaxAge" = "بیشینه زمان جلسه وب" "sessionMaxAge" = "بیشینه زمان جلسه وب"
@@ -245,12 +240,82 @@
"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)" "trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده" "tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)" "tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
"timeZonee" = "منظقه زمانی" "timeZone" = "منظقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود" "timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
[pages.setting.toasts] [pages.settings.templates]
"modifySetting" = "ویرایش تنظیمات" "title" = "الگوها"
"getSetting" = "دریافت تنظیمات" "basicTemplate" = "بخش الگو پایه"
"advancedTemplate" = "بخش الگو پیشرفته"
"completeTemplate" = "بخش الگو کامل"
"generalConfigs" = "تنظیمات عمومی"
"generalConfigsDesc" = "این تنظیمات میتواند ترافیک کلی سرویس را متاثر کند"
"blockConfigs" = "مسدود سازی"
"blockConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند"
"blockCountryConfigs" = "تنظیمات برای مسدودسازی کشورها"
"blockCountryConfigsDesc" = "این گزینه اتصال کاربران به دامنه های کشوری خاص را مسدود می کند"
"directCountryConfigs" = "تنظیمات برای اتصال مستقیم کشورها"
"directCountryConfigsDesc" = "این گزینه کاربران را به دامنه های کشوری خاص را به طور مستقیم، متصل می کند"
"ipv4Configs" = "تنظیمات برای IPv4"
"ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن ۴ به دامنه های هدف هدایت می شود"
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید!"
"xrayConfigFreedomStrategy" = "روش استفاده از شبکه خروجی مستقیم"
"xrayConfigFreedomStrategyDesc" = "تعیین روش استفاده از خروجی برای پرتکل مستقیم"
"xrayConfigRoutingStrategy" = "پیکربندی استراتژی حل دامنه در مسیریابی"
"xrayConfigRoutingStrategyDesc" = "تعیین استراتژی مسیریابی کلی برای پیدا کردن دامنه"
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد"
"xrayConfigPrivateIp" = "جلوگیری از اتصال آیپی های خصوصی یا محلی"
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد"
"xrayConfigAds" = "مسدود کردن تبلیغات"
"xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد"
"xrayConfigFamily" = "فعال کردن حالت خانواده"
"xrayConfigFamilyDesc" = "برای جلوگیری از ارتباط با وبسایت های ناامن"
"xrayConfigIRIp" = "جلوگیری از اتصال آیپی های ایران"
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد"
"xrayConfigIRDomain" = "جلوگیری از اتصال دامنه های ایران"
"xrayConfigIRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد"
"xrayConfigChinaIp" = "جلوگیری از اتصال آیپی های چین"
"xrayConfigChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد"
"xrayConfigChinaDomain" = "جلوگیری از اتصال دامنه های چین"
"xrayConfigChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد"
"xrayConfigRussiaIp" = "جلوگیری از اتصال آیپی های روسیه"
"xrayConfigRussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد"
"xrayConfigRussiaDomain" = "جلوگیری از اتصال دامنه های روسیه"
"xrayConfigRussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد"
"xrayConfigDirectIRIp" = "ارتباط مستقیم به آیپی های ایران"
"xrayConfigDirectIRIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های ایران تغییر میدهد"
"xrayConfigDirectIRDomain" = "ارتباط مستقیم به دامنه های ایران"
"xrayConfigDirectIRDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های ایران تغییر میدهد"
"xrayConfigDirectChinaIp" = "ارتباط مستقیم به آیپی های چین"
"xrayConfigDirectChinaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های چین تغییر میدهد"
"xrayConfigDirectChinaDomain" = "ارتباط مستقیم به دامنه های چین"
"xrayConfigDirectChinaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های چین تغییر میدهد"
"xrayConfigDirectRussiaIp" = "ارتباط مستقیم به آیپی های روسیه"
"xrayConfigDirectRussiaIpDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به آیپی های روسیه تغییر میدهد"
"xrayConfigDirectRussiaDomain" = "ارتباط مستقیم به دامنه های روسیه"
"xrayConfigDirectRussiaDomainDesc" = "الگوی تنظیمات را برای ارتباط مستقیم به دامنه های روسیه تغییر میدهد"
"xrayConfigGoogleIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به گوگل"
"xrayConfigGoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند"
"xrayConfigNetflixIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به نتفلیکس"
"xrayConfigNetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند"
"xrayConfigInbounds" = "تنظیمات ورودی"
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید"
"xrayConfigOutbounds" = "تنظیمات خروجی"
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید"
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید"
"manualLists" = "لیست های دستی"
"manualListsDesc" = "فرمت: JSON Array"
"manualBlockedIPs" = "لیست آی‌پی های مسدود شده"
"manualBlockedDomains" = "لیست دامنه های مسدود شده"
"manualDirectIPs" = "لیست آی‌پی های مستقیم"
"manualDirectDomains" = "لیست دامنه های مستقیم"
[pages.settings.toasts]
"modifySettings" = "ویرایش تنظیمات"
"getSettings" = "دریافت تنظیمات"
"modifyUser" = "ویرایش کاربر" "modifyUser" = "ویرایش کاربر"
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد ." "originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد "
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد ." "userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد "

View File

@@ -0,0 +1,322 @@
"username" = "имя пользователя"
"password" = "пароль"
"login" = "логин"
"confirm" = "подтвердить"
"cancel" = "отмена"
"close" = "закрыть"
"copy" = "копировать"
"copied" = "скопировано"
"download" = "скачать"
"remark" = "примечание"
"enable" = "включить"
"protocol" = "протокол"
"search" = "поиск"
"loading" = "загрузка"
"second" = "секунда"
"minute" = "минута"
"hour" = "час"
"day" = "день"
"check" = "просмотр"
"indefinite" = "бессрочно"
"unlimited" = "безлимитно"
"none" = "пусто"
"qrCode" = "QR-код"
"info" = "больше информации"
"edit" = "изменить"
"delete" = "удалить"
"reset" = "обнулить"
"copySuccess" = "скопировано"
"sure" = "да"
"encryption" = "Шифрование"
"transmission" = "протокол передачи"
"host" = "хост"
"path" = "путь"
"camouflage" = "маскировка"
"status" = "статус"
"enabled" = "включено"
"disabled" = "отключено"
"depleted" = "исчерпано"
"depletingSoon" = "почти исчерпано"
"domainName" = "домен"
"additional" = "допольнительно"
"monitor" = "порт IP"
"certificate" = "сертификат"
"fail" = "неудача"
"success" = "успешно"
"getVersion" = "узнать версию"
"install" = "установка"
"clients" = "клиенты"
"usage" = "использование"
[menu]
"dashboard" = "статус системы"
"inbounds" = "пользователи"
"settings" = "настройки"
"logout" = "выход"
"link" = "другое"
[pages.login]
"title" = "логин"
"loginAgain" = "Время пребывания в сети вышло. Пожалуйста, войдите в систему снова"
[pages.login.toasts]
"invalidFormData" = "Недопустимый формат данных"
"emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверное имя пользователя или пароль"
"successLogin" = "успешный вход"
[pages.index]
"title" = "статус системы"
"memory" = "память"
"hard" = "жесткий диск"
"xrayStatus" = "статус Xray"
"stopXray" = "стоп"
"restartXray" = "рестарт Xray"
"xraySwitch" = "переключить версию"
"xraySwitchClick" = "Выберите желаемую версию"
"xraySwitchClickDesk" = "Выбирайте внимательно, так как старые версии могут быть несовместимы с текущими конфигурациями"
"operationHours" = "Часы работы"
"operationHoursDesc" = "Аптайм системы: время системы в сети"
"systemLoad" = "Системная нагрузка"
"connectionCount" = "количество соединений"
"connectionCountDesc" = "Всего подключений по всем сетям»"
"upSpeed" = "Общая скорость upload"
"downSpeed" = "Общая скорость download"
"totalSent" = "Общий объем загруженных данных с момента запуска системы"
"totalReceive" = "Общий объем полученных данных с момента запуска системы"
"xraySwitchVersionDialog" = "переключить версию Xray"
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
"dontRefresh" = "Установка. Не обновляйте эту страницу"
"logs" = "Логи"
"config" = "Конфиг"
"backup" = "Бекап и восстановление"
"backupTitle" = "База данных бекапа и восстановления"
"backupDescription" = "Не забудьте сделать резервную копию перед импортом новой базы данных"
"exportDatabase" = "Экспорт базы данных"
"importDatabase" = "Импорт базы данных"
[pages.inbounds]
"title" = "пользователи"
"totalDownUp" = "Всего входящих/исходящих"
"totalUsage" = "Всего использовано"
"inboundCount" = "Количество пользователей"
"operate" = "Меню"
"enable" = "Включить"
"remark" = "Примечание"
"protocol" = "Протокол"
"port" = "Порт"
"traffic" = "Траффик"
"details" = "Подробнее"
"transportConfig" = "Перенести"
"expireDate" = "Дата окончания"
"resetTraffic" = "Обнулить траффик"
"addInbound" = "Добавить пользователя"
"generalActions" = "Общие действия"
"create" = "Создать"
"update" = "Обновить"
"modifyInbound" = "Изменить данные"
"deleteInbound" = "Удалить пользователя"
"deleteInboundContent" = "Подтвердите удаление пользователя?"
"resetTrafficContent" = "Подтвердите обнуление траффика?"
"copyLink" = "Копировать ключ"
"address" = "Адрес"
"network" = "Сеть"
"destinationPort" = "Порт назначения"
"targetAddress" = "Целевой адрес"
"disableInsecureEncryption" = "Отключить небезопасное шифрование"
"monitorDesc" = "Оставьте пустым по умолчанию"
"meansNoLimit" = "Значит без ограничений"
"totalFlow" = "Общий расход"
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы никогда не истекать"
"noRecommendKeepDefault" = "Нет требований для сохранения настроек по умолчанию"
"certificatePath" = "Путь файла сертификата"
"certificateContent" = "Содержимое файла сертификата"
"publicKeyPath" = "Путь к публичному ключу"
"publicKeyContent" = "Содержимое публичного ключа"
"keyPath" = "Путь к приватному ключу"
"keyContent" = "Содержимое приватного ключа"
"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать"
"client" = "Клиент"
"export" = "Поделиться ключом"
"clone" = "Клонировать"
"cloneInbound" = "Клонировать пользователя"
"cloneInboundContent" = "Все настройки этого пользователя, кроме порта, порт прослушки и клиентов, будут клонированы"
"cloneInboundOk" = "Клонировать"
"resetAllTraffic" = "Обнулить весь траффик"
"resetAllTrafficTitle" = "Обнуление всего траффика"
"resetAllTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
"resetInboundClientTraffics" = "Обнулить траффик пользователей"
"resetInboundClientTrafficTitle" = "Обнуление траффика пользователей"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите обнулить весь трафик для этих пользователей?"
"resetAllClientTraffics" = "Обнулить весь траффик пользователей"
"resetAllClientTrafficTitle" = "Обнуление всего траффика пользователей"
"resetAllClientTrafficContent" = "Подтверждаете обнуление всего траффика пользователей?"
"delDepletedClients" = "Удалить отключенных пользователей"
"delDepletedClientsTitle" = "Удаление отключенных пользователей"
"delDepletedClientsContent" = "Подтверждаете удаление отключенных пользователей?"
"email" = "Email"
"emailDesc" = "Пожалуйста, укажите уникальный Email"
"setDefaultCert" = "Установить сертификат с панели"
"telegramDesc" = "используйте Telegram ID (вы можете получить его у @userinfobot)"
"subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов"
[pages.client]
"add" = "Добавить пользователя"
"edit" = "Редактировать пользователя"
"submitAdd" = "Добавить пользователя"
"submitEdit" = "Сохранить изменения"
"clientCount" = "Количество пользователей"
"bulk" = "Добавить несколько"
"method" = "Метод"
"first" = "Первый"
"last" = "Последний"
"prefix" = "Префикс"
"postfix" = "Постфикс"
"delayedStart" = "Начать со времени первого подключения"
"expireDays" = "Срок действия"
"days" = "дней"
[pages.inbounds.toasts]
"obtain" = "Получить"
[pages.inbounds.stream.general]
"requestHeader" = "Требуется заголовок"
"name" = "Имя"
"value" = "Значение"
[pages.inbounds.stream.tcp]
"requestVersion" = "Требуется версия"
"requestMethod" = "Требуется метод"
"requestPath" = "Требуется путь"
"responseVersion" = "Указать версию"
"responseStatus" = "Указать статус"
"responseStatusDescription" = "Указать примечание статуса"
"responseHeader" = "Указать заголовок"
[pages.inbounds.stream.quic]
"encryption" = "Шифрование"
[pages.settings]
"title" = "Настройки"
"save" = "Сохранить"
"infoDesc" = "Каждое изменение здесь необходимо сохранить и перезапустить панель, чтобы оно вступило в силу"
"restartPanel" = "Рестарт панели"
"restartPanelDesc" = "Подтвердите рестарт панели? ОК для рестарта панели через 3 сек. Если вы не можете пользоваться панелью после рестарта, пожалуйста, посмотрите лог панели на сервере"
"resetDefaultConfig" = "Сбросить всё по-умолчанию"
"panelConfig" = "Настройки панели"
"userSettings" = "Настройки безопасности"
"xrayConfiguration" = "Конфигурация Xray"
"TGBotSettings" = "Настройки Телеграм-бота"
"panelListeningIP" = "IP-порт панели"
"panelListeningIPDesc" = "Оставьте пустым для работы с любого IP. Перезагрузите панель для применения настроек"
"panelPort" = "Порт панели"
"panelPortDesc" = "Перезагрузите панель для применения настроек"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
"publicKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек"
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
"privateKeyPathDesc" = "Введите полный путь, начинающийся с «/». Перезагрузите панель для применения настроек"
"panelUrlPath" = "Корневой путь URL-адреса панели"
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на «/». Перезагрузите панель для применения настроек"
"oldUsername" = "Имя пользователя сейчас"
"currentPassword" = "Пароль сейчас"
"newUsername" = "Новое имя пользователя"
"newPassword" = "Новый пароль"
"telegramBotEnable" = "Включить Телеграм-бота"
"telegramBotEnableDesc" = "Перезагрузите панель для применения настроек"
"telegramToken" = "Токен Телеграм-бота"
"telegramTokenDesc" = "Перезагрузите панель для применения настроек"
"telegramChatId" = "Телеграм-ID админа бота"
"telegramChatIdDesc" = "Если несколько Телеграм-ID, разделить запятой. Используйте @userinfobot, чтобы получить Телеграм-ID. Перезагрузите панель для применения настроек"
"telegramNotifyTime" = "Частота уведомлений телеграм-бота"
"telegramNotifyTimeDesc" = "Используйте формат Crontab. Перезагрузите панель для применения настроек"
"tgNotifyBackup" = "Резервное копирование базы данных"
"tgNotifyBackupDesc" = "Включать файл резервной копии базы данных с уведомлением об отчете. Перезагрузите панель для применения настроек"
"sessionMaxAge" = "Продолжительность сессии"
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
"expireTimeDiff" = "Порог истечения срока сессии для уведомления"
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)"
"trafficDiff" = "Порог траффика для уведомления"
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)"
"tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления"
"tgNotifyCpuDesc" = "Получение уведомления, если нагрузка на ЦП превышает этот порог (значение:%)"
"timeZone" = "Временная зона"
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе. Перезагрузите панель для применения настроек"
[pages.settings.templates]
"title" = "Шаблоны"
"basicTemplate" = "Базовые шаблоны"
"advancedTemplate" = "Расширенные шаблоны"
"completeTemplate" = "Конфигурация шаблона"
"generalConfigs" = "Основные настройки"
"generalConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
"blockConfigs" = "Блокировка конфигураций"
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам."
"blockCountryConfigs" = "Заблокировать конфигурации страны"
"blockCountryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны."
"directCountryConfigs" = "Прямые настройки страны"
"directCountryConfigsDesc" = "Эти параметры будут подключать пользователей напрямую к доменам определенной страны."
"ipv4Configs" = "Настройки IPv4 "
"ipv4ConfigsDesc" = "Эти параметры будут маршрутизироваться к целевым доменам только через IPv4"
"xrayConfigTemplate" = "Шаблон конфигурации Xray"
"xrayConfigTemplateDesc" = "Создание файла конфигурации Xray на основе этого шаблона. Перезагрузите панель для применения настроек"
"xrayConfigFreedomStrategy" = "Настроить стратегию протокола Freedom"
"xrayConfigFreedomStrategyDesc" = "Установить стратегию вывода сети в протоколе Freedom"
"xrayConfigRoutingStrategy" = "Настроить доменную стратегию маршрутизации"
"xrayConfigRoutingStrategyDesc" = "Установить общую стратегию маршрутизации разрешения DNS"
"xrayConfigTorrent" = "Запретить использование BitTorrent"
"xrayConfigTorrentDesc" = "Измените конфигурацию, чтобы пользователи не использовали BitTorrent. Перезагрузите панель для применения настроек"
"xrayConfigPrivateIp" = "Запрет частных диапазонов IP-адресов для подключения"
"xrayConfigPrivateIpDesc" = "Измените конфигурацию, чтобы избежать подключения к диапазонам частных IP-адресов. Перезагрузите панель для применения настроек"
"xrayConfigAds" = "Бокировка рекламы"
"xrayConfigAdsDesc" = "Измените конфигурацию, чтобы заблокировать рекламу. Перезагрузите панель для применения настроек"
"xrayConfigFamily" = "Включить семейную конфигурацию"
"xrayConfigFamilyDesc" = "Избегайте подключения к небезопасным веб-сайтам для всей семьи"
"xrayConfigIRIp" = "Отключить подключение к диапазонам IP-адресов Ирана"
"xrayConfigIRIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Ирана. Перезагрузите панель для применения настроек"
"xrayConfigIRDomain" = "Отключить подключение к доменам Ирана"
"xrayConfigIRDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Ирана. Перезагрузите панель для применения настроек"
"xrayConfigChinaIp" = "Отключить подключение к диапазонам IP-адресов Китая"
"xrayConfigChinaIpDesc" = "Измените конфигурацию, чтобы отключить подключение к диапазонам IP-адресов Китая. Перезагрузите панель для применения настроек"
"xrayConfigChinaDomain" = "Отключить подключение к доменам Китая"
"xrayConfigChinaDomainDesc" = "Измените конфигурацию, чтобы отключить подключение к доменам Китая. Перезагрузите панель для применения настроек"
"xrayConfigRussiaIp" = "Отключить подключение к диапазонам IP-адресов России"
"xrayConfigRussiaIpDesc" = "Измените конфигурацию, чтобы отключить соединения с диапазонами IP-адресов России. Перезагрузите панель для применения настроек"
"xrayConfigRussiaDomain" = "Отключить подключение к доменам России"
"xrayConfigRussiaDomainDesc" = "Измените конфигурацию, чтобы избежать подключения к доменам России. Перезагрузите панель для применения настроек"
"xrayConfigDirectIRIp" = "Прямое подключение к диапазонам IP-адресов Ирана"
"xrayConfigDirectIRIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Ирана"
"xrayConfigDirectIRDomain" = "Прямое подключение к доменам Ирана"
"xrayConfigDirectIRDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Ирана"
"xrayConfigDirectChinaIp" = "Прямое подключение к диапазонам IP-адресов Китая"
"xrayConfigDirectChinaIpDesc" = "Измените шаблон конфигурации для прямого подключения к диапазонам IP-адресов Китая"
"xrayConfigDirectChinaDomain" = "Прямое подключение к доменам Китая"
"xrayConfigDirectChinaDomainDesc" = "Измените шаблон конфигурации для прямого подключения к доменам Китая"
"xrayConfigDirectRussiaIp" = "Прямое подключение к диапазонам IP-адресов России"
"xrayConfigDirectRussiaIpDesc" = "Изменить шаблон конфигурации для прямого подключения к диапазонам IP-адресов России"
"xrayConfigDirectRussiaDomain" = "Прямое подключение к доменам России"
"xrayConfigDirectRussiaDomainDesc" = "Изменить шаблон конфигурации для прямого подключения к доменам России"
"xrayConfigGoogleIPv4" = "Использовать IPv4 для Google"
"xrayConfigGoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4. Перезагрузите панель для применения настроек"
"xrayConfigNetflixIPv4" = "Использовать IPv4 для Netflix"
"xrayConfigNetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4. Перезагрузите панель для применения настроек"
"xrayConfigInbounds" = "Конфигурация подключений"
"xrayConfigInboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей. Перезагрузите панель для применения настроек"
"xrayConfigOutbounds" = "Конфигурация исходящих"
"xrayConfigOutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера. Перезагрузите панель для применения настроек"
"xrayConfigRoutings" = "Настройка правил маршрутизации"
"xrayConfigRoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера. Перезагрузите панель для применения настроек"
"manualLists" = "ручные списки"
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
"manualBlockedIPs" = "Список заблокированных IP-адресов"
"manualBlockedDomains" = "Список заблокированных доменов"
"manualDirectIPs" = "Список прямых IP-адресов"
"manualDirectDomains" = "Список прямых доменов"
[pages.settings.toasts]
"modifySettings" = "Изменение настроек"
"getSettings" = "Просмотр настроек"
"modifyUser" = "Изменение пользователя "
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"

View File

@@ -52,7 +52,7 @@
[menu] [menu]
"dashboard" = "系统状态" "dashboard" = "系统状态"
"inbounds" = "入站列表" "inbounds" = "入站列表"
"setting" = "面板设置" "settings" = "面板设置"
"logout" = "退出登录" "logout" = "退出登录"
"link" = "其他" "link" = "其他"
@@ -71,7 +71,7 @@
"title" = "系统状态" "title" = "系统状态"
"memory" = "内存" "memory" = "内存"
"hard" = "硬盘" "hard" = "硬盘"
"xrayStatus" = "xray 状态" "xrayStatus" = "Xray 状态"
"stopXray" = "停止" "stopXray" = "停止"
"restartXray" = "重启" "restartXray" = "重启"
"xraySwitch" = "切换版本" "xraySwitch" = "切换版本"
@@ -86,9 +86,16 @@
"downSpeed" = "所有网卡的总下载速度" "downSpeed" = "所有网卡的总下载速度"
"totalSent" = "系统启动以来所有网卡的总上传流量" "totalSent" = "系统启动以来所有网卡的总上传流量"
"totalReceive" = "系统启动以来所有网卡的总下载流量" "totalReceive" = "系统启动以来所有网卡的总下载流量"
"xraySwitchVersionDialog" = "切换 xray 版本" "xraySwitchVersionDialog" = "切换 Xray 版本"
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至" "xraySwitchVersionDialogDesc" = "是否切换 Xray 版本至"
"dontRefreshh" = "安装中,请不要刷新此页面" "dontRefresh" = "安装中,请不要刷新此页面"
"logs" = "日志"
"config" = "配置"
"backup" = "备份"
"backupTitle" = "备份数据库"
"backupDescription" = "请记住在导入新数据库之前进行备份"
"exportDatabase" = "下载数据库"
"importDatabase" = "上传数据库"
[pages.inbounds] [pages.inbounds]
"title" = "入站列表" "title" = "入站列表"
@@ -107,8 +114,8 @@
"resetTraffic" = "重置流量" "resetTraffic" = "重置流量"
"addInbound" = "添加入" "addInbound" = "添加入"
"generalActions" = "通用操作" "generalActions" = "通用操作"
"addTo" = "添加" "create" = "添加"
"revise" = "修改" "update" = "修改"
"modifyInbound" = "修改入站" "modifyInbound" = "修改入站"
"deleteInbound" = "删除入站" "deleteInbound" = "删除入站"
"deleteInboundContent" = "确定要删除入站吗?" "deleteInboundContent" = "确定要删除入站吗?"
@@ -133,9 +140,10 @@
"clickOnQRcode" = "点击二维码复制" "clickOnQRcode" = "点击二维码复制"
"client" = "客户" "client" = "客户"
"export" = "导出链接" "export" = "导出链接"
"Clone" = "克隆" "clone" = "克隆"
"cloneInbound" = "创造" "cloneInbound" = "创造"
"cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆" "cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆"
"cloneInboundOk" = "创造"
"resetAllTraffic" = "重置所有入站流量" "resetAllTraffic" = "重置所有入站流量"
"resetAllTrafficTitle" = "重置所有入站流量" "resetAllTrafficTitle" = "重置所有入站流量"
"resetAllTrafficContent" = "您确定要重置所有入站流量吗?" "resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
@@ -148,9 +156,11 @@
"delDepletedClients" = "删除耗尽的客户端" "delDepletedClients" = "删除耗尽的客户端"
"delDepletedClientsTitle" = "删除耗尽的客户" "delDepletedClientsTitle" = "删除耗尽的客户"
"delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?" "delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?"
"Email" = "电子邮件" "email" = "电子邮件"
"EmailDesc" = "电子邮件必须完全唯" "emailDesc" = "电子邮件必须完全唯"
"setDefaultCert" = "从面板设置证书" "setDefaultCert" = "从面板设置证书"
"telegramDesc" = "使用不带@的电报 ID 或聊天 ID您可以在此处获取 @userinfobot"
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
[pages.client] [pages.client]
"add" = "添加客户端" "add" = "添加客户端"
@@ -188,55 +198,41 @@
[pages.inbounds.stream.quic] [pages.inbounds.stream.quic]
"encryption" = "加密" "encryption" = "加密"
[pages.setting] [pages.settings]
"title" = "设置" "title" = "设置"
"save" = "保存配置" "save" = "保存配置"
"infoDesc" = "此处的所有更改都需要保存并重启面板才能生效"
"restartPanel" = "重启面板" "restartPanel" = "重启面板"
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息" "restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
"resetDefaultConfig" = "重置为默认配置" "resetDefaultConfig" = "重置为默认配置"
"panelConfig" = "面板配置" "panelConfig" = "面板配置"
"userSetting" = "用户设置" "userSettings" = "用户设置"
"xrayConfiguration" = "xray 相关设置" "xrayConfiguration" = "Xray 相关设置"
"TGReminder" = "TG提醒相关设置" "TGBotSettings" = "TG提醒相关设置"
"otherSetting" = "其他设置"
"panelListeningIP" = "面板监听 IP" "panelListeningIP" = "面板监听 IP"
"panelListeningIPDesc" = "默认留空监听所有 IP,重启面板生效" "panelListeningIPDesc" = "默认留空监听所有 IP"
"panelPort" = "面板监听端口" "panelPort" = "面板监听端口"
"panelPortDesc" = "重启面板生效" "panelPortDesc" = "重启面板生效"
"publicKeyPath" = "面板证书公钥文件路径" "publicKeyPath" = "面板证书公钥文件路径"
"publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效" "publicKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
"privateKeyPath" = "面板证书密钥文件路径" "privateKeyPath" = "面板证书密钥文件路径"
"privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径,重启面板生效" "privateKeyPathDesc" = "填写一个 '/' 开头的绝对路径"
"panelUrlPath" = "面板 url 根路径" "panelUrlPath" = "面板 url 根路径"
"panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾,重启面板生效" "panelUrlPathDesc" = "必须以 '/' 开头,以 '/' 结尾"
"oldUsername" = "原用户名" "oldUsername" = "原用户名"
"currentPassword" = "原密码" "currentPassword" = "原密码"
"newUsername" = "新用户名" "newUsername" = "新用户名"
"newPassword" = "新密码" "newPassword" = "新密码"
"advancedTemplate" = "高级模板部件"
"completeTemplate" = "Xray 配置的完整模板"
"xrayConfigTemplate" = "xray 配置模板"
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件重新启动面板生成效率"
"xrayConfigTorrent" = "禁止使用 bittorrent"
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent重启面板生效"
"xrayConfigPrivateIp" = "禁止私人 ip 范围连接"
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围,重启面板生效"
"xrayConfigInbounds" = "入站配置"
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端,重启面板生效"
"xrayConfigOutbounds" = "出站配置"
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效"
"xrayConfigRoutings" = "路由规则配置"
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效"
"telegramBotEnable" = "启用电报机器人" "telegramBotEnable" = "启用电报机器人"
"telegramBotEnableDesc" = "重启面板生效" "telegramBotEnableDesc" = "重启面板生效"
"telegramToken" = "电报机器人TOKEN" "telegramToken" = "电报机器人TOKEN"
"telegramTokenDesc" = "重启面板生效" "telegramTokenDesc" = "重启面板生效"
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效" "telegramChatId" = "以逗号分隔的多个 chatID"
"telegramChatIdDesc" = "重启面板生效" "telegramChatIdDesc" = "重启面板生效"
"telegramNotifyTime" = "电报机器人通知时间" "telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效" "telegramNotifyTimeDesc" = "采用Crontab定时格式"
"tgNotifyBackup" = "数据库备份" "tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效" "tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知"
"sessionMaxAge" = "会话最大年龄" "sessionMaxAge" = "会话最大年龄"
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)" "sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
"expireTimeDiff" = "耗尽时间阈值" "expireTimeDiff" = "耗尽时间阈值"
@@ -245,12 +241,82 @@
"trafficDiffDesc" = "完成流量前检测耗尽单位GB" "trafficDiffDesc" = "完成流量前检测耗尽单位GB"
"tgNotifyCpu" = "CPU 百分比警报阈值" "tgNotifyCpu" = "CPU 百分比警报阈值"
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知" "tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
"timeZonee" = "时区" "timeZone" = "时区"
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效" "timeZoneDesc" = "定时任务按照该时区的时间运行"
[pages.setting.toasts] [pages.settings.templates]
"modifySetting" = "修改设置" "title" = "模板"
"getSetting" = "获取设置" "basicTemplate" = "基本模板"
"advancedTemplate" = "高级模板部件"
"completeTemplate" = "Xray 配置的完整模板"
"generalConfigs" = "通用配置"
"generalConfigsDesc" = "这些选项将提供一般调整"
"blockConfigs" = "阻塞配置"
"blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站"
"blockCountryConfigs" = "阻止国家配置"
"blockCountryConfigsDesc" = "这些选项将阻止用户连接到特定国家/地区的域。"
"directCountryConfigs" = "直接国家配置"
"directCountryConfigsDesc" = "这些选项会将用户直接连接到特定国家/地区的域。"
"ipv4Configs" = "IPv4 配置"
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域"
"xrayConfigTemplate" = "Xray 配置模板"
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的Xray配置文件重新启动面板生成效率"
"xrayConfigFreedomStrategy" = "配置自由协议的策略"
"xrayConfigFreedomStrategyDesc" = "在自由协议中设置网络输出策略"
"xrayConfigRoutingStrategy" = "配置路由域策略"
"xrayConfigRoutingStrategyDesc" = "设置DNS解析的整体路由策略"
"xrayConfigTorrent" = "禁止使用 bittorrent"
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent"
"xrayConfigPrivateIp" = "禁止私人 IP 范围连接"
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围"
"xrayConfigAds" = "屏蔽广告"
"xrayConfigAdsDesc" = "修改配置模板屏蔽广告"
"xrayConfigFamily" = "启用家庭友好配置"
"xrayConfigFamilyDesc" = "避免为家人连接到不安全的网站"
"xrayConfigIRIp" = "禁止伊朗 IP 范围连接"
"xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP段"
"xrayConfigIRDomain" = "禁止伊朗域连接"
"xrayConfigIRDomainDesc" = "更改配置模板避免连接伊朗域名"
"xrayConfigChinaIp" = "禁止中国 IP 范围连接"
"xrayConfigChinaIpDesc" = "修改配置模板避免连接中国IP段"
"xrayConfigChinaDomain" = "禁止中国域名连接"
"xrayConfigChinaDomainDesc" = "更改配置模板避免连接中国域"
"xrayConfigRussiaIp" = "禁止俄罗斯 IP 范围连接"
"xrayConfigRussiaIpDesc" = "修改配置模板避免连接俄罗斯IP范围"
"xrayConfigRussiaDomain" = "禁止俄罗斯域连接"
"xrayConfigRussiaDomainDesc" = "更改配置模板避免连接俄罗斯域"
"xrayConfigDirectIRIp" = "直接连接到伊朗 IP 范围"
"xrayConfigDirectIRIpDesc" = "更改直接连接到伊朗 IP 范围的配置模板"
"xrayConfigDirectIRDomain" = "直接连接到伊朗域"
"xrayConfigDirectIRDomainDesc" = "更改直接连接到伊朗域的配置模板"
"xrayConfigDirectChinaIp" = "直连中国IP范围"
"xrayConfigDirectChinaIpDesc" = "更改直连中国 IP 范围的配置模板"
"xrayConfigDirectChinaDomain" = "直连中国域名"
"xrayConfigDirectChinaDomainDesc" = "修改中国域名直连配置模板"
"xrayConfigDirectRussiaIp" = "直接连接到俄罗斯 IP 范围"
"xrayConfigDirectRussiaIpDesc" = "更改直接连接到俄罗斯 IP 范围的配置模板"
"xrayConfigDirectRussiaDomain" = "直接连接到俄罗斯域"
"xrayConfigDirectRussiaDomainDesc" = "更改直接连接到俄罗斯域的配置模板"
"xrayConfigGoogleIPv4" = "为谷歌使用 IPv4"
"xrayConfigGoogleIPv4Desc" = "添加谷歌连接IPv4的路由"
"xrayConfigNetflixIPv4" = "为 Netflix 使用 IPv4"
"xrayConfigNetflixIPv4Desc" = "添加Netflix连接IPv4的路由"
"xrayConfigInbounds" = "入站配置"
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端"
"xrayConfigOutbounds" = "出站配置"
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式"
"xrayConfigRoutings" = "路由规则配置"
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则"
"manualLists" = "手动列表"
"manualListsDesc" = "请使用 JSON 数组格式"
"manualBlockedIPs" = "被阻止的 IP 列表"
"manualBlockedDomains" = "被阻止的域列表"
"manualDirectIPs" = "直接 IP 列表"
"manualDirectDomains" = "直接域列表"
[pages.settings.toasts]
"modifySettings" = "修改设置"
"getSettings" = "获取设置"
"modifyUser" = "修改用户" "modifyUser" = "修改用户"
"originalUserPassIncorrect" = "原用户名或原密码错误" "originalUserPassIncorrect" = "原用户名或原密码错误"
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空" "userPassMustBeNotEmpty" = "新用户名和新密码不能为空"

View File

@@ -239,11 +239,11 @@ func (s *Server) initI18n(engine *gin.Engine) error {
names := make([]string, 0) names := make([]string, 0)
keyLen := len(key) keyLen := len(key)
for i := 0; i < keyLen-1; i++ { for i := 0; i < keyLen-1; i++ {
if key[i:i+2] == "{{" { // 判断开头 "{{" if key[i:i+2] == "{{" {
j := i + 2 j := i + 2
isFind := false isFind := false
for ; j < keyLen-1; j++ { for ; j < keyLen-1; j++ {
if key[j:j+2] == "}}" { // 结尾 "}}" if key[j:j+2] == "}}" {
isFind = true isFind = true
break break
} }

View File

@@ -520,7 +520,7 @@ show_menu() {
${green}14.${plain} Cancel x-ui Autostart ${green}14.${plain} Cancel x-ui Autostart
———————————————— ————————————————
${green}15.${plain} 一A key installation bbr (latest kernel) ${green}15.${plain} 一A key installation bbr (latest kernel)
${green}16.${plain} 一Apply for an SSL certificate with one click(acme script) ${green}16.${plain} 一Apply for a SSL certificate with one click(acme script)
" "
show_status show_status
echo && read -p "Please enter your selection [0-16]: " num echo && read -p "Please enter your selection [0-16]: " num

View File

@@ -6,7 +6,7 @@ import (
) )
type InboundConfig struct { type InboundConfig struct {
Listen json_util.RawMessage `json:"listen"` // listen 不能为空字符串 Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string
Port int `json:"port"` Port int `json:"port"`
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Settings json_util.RawMessage `json:"settings"` Settings json_util.RawMessage `json:"settings"`