Compare commits

...

10 Commits

Author SHA1 Message Date
mhsanaei
49bfff9fa5 v2.5.2 2025-02-04 11:38:57 +01:00
Sanaei
d18a1a37ce revert group management (#2656)
* Revert "json post base path bug fixed (#2647)"

This reverts commit 04cf250a54.

* Revert "Group Management of Subscription Clients"

* Revert "fix getSubGroupClients for enable/disable and edit clients."

* Revert "Enhance database initialization in db.go (#2645)"

This reverts commit 66fe84181b.

* Revert "Add checkpoint handling in CloseDB function (#2646)"

This reverts commit 4dd40f6f19.

* Revert "Improved database model migration and added indexing (#2655)"

This reverts commit b922d986d6.
2025-02-04 11:27:58 +01:00
mhsanaei
04c6b2722b README: Persian 2025-02-03 20:30:42 +01:00
mhsanaei
94d651fc93 v2.5.1 2025-02-03 17:40:33 +01:00
mhsanaei
aae0cb37b7 Xray Core v25.1.30 2025-02-03 17:37:01 +01:00
Zahar Izmailov
b922d986d6 Improved database model migration and added indexing (#2655) 2025-02-03 13:36:03 +01:00
Abolfazl Fazilat
8a7cffd63f Completed translation for missing text (#2653) 2025-01-31 18:54:52 +01:00
Sanaei
c8e8c97afc Merge pull request #2652 from Incognito-Coder/main
Some Improvement
2025-01-31 16:01:14 +01:00
AghayeCoder
46ba4c4518 fix getSubGroupClients for enable/disable and edit clients. 2025-01-31 17:27:09 +03:30
AghayeCoder
a787ab497c switcher for outbound traffic 2025-01-31 17:24:03 +03:30
38 changed files with 656 additions and 858 deletions

View File

@@ -83,7 +83,7 @@ jobs:
cd x-ui/bin
# Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.1.1/"
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.1.30/"
if [ "${{ matrix.platform }}" == "amd64" ]; then
wget -q ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip

View File

@@ -27,7 +27,7 @@ case $1 in
esac
mkdir -p build/bin
cd build/bin
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.1.1/Xray-linux-${ARCH}.zip"
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.1.30/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}"

View File

@@ -1,4 +1,4 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
[English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>

527
README.fa_IR.md Normal file
View File

@@ -0,0 +1,527 @@
[English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/3x-ui-dark.png">
<img alt="3x-ui" src="./media/3x-ui-light.png">
</picture>
</p>
**یک پنل وب پیشرفته • ساخته شده بر پایه Xray Core**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
> **سلب مسئولیت:** این پروژه صرفاً برای اهداف آموزشی و تحقیقاتی است. استفاده از آن برای مقاصد غیرقانونی یا در محیط‌های عملیاتی ممنوع است.
**اگر این پروژه برای شما مفید بوده، می‌توانید با دادن یک**:star2: از آن حمایت کنید.
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
<img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
## نصب و ارتقا
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
## نصب نسخه‌های قدیمی (توصیه نمی‌شود)
برای نصب نسخه خاصی از دستور زیر استفاده کنید. مثال برای نسخه `v1.7.9`:
```
VERSION=v1.7.9 && bash <(curl -Ls "https://raw.githubusercontent.com/mhsanaei/3x-ui/$VERSION/install.sh") $VERSION
```
## گواهی SSL
<details>
<summary>جزئیات گواهی SSL</summary>
### ACME
برای مدیریت گواهی‌های SSL با استفاده از ACME:
1. اطمینان حاصل کنید دامنه شما به درستی به سرور متصل است.
2. دستور `x-ui` را در ترمینال اجرا کرده و گزینه `مدیریت گواهی SSL` را انتخاب کنید.
3. گزینه‌های زیر نمایش داده می‌شوند:
- **دریافت SSL:** دریافت گواهی SSL
- **لغو:** لغو گواهی‌های موجود
- **تمدید اجباری:** تمدید اجباری گواهی‌ها
- **نمایش دامنه‌های موجود:** نمایش تمام دامنه‌های دارای گواهی
- **تنظیم مسیر گواهی برای پنل:** تنظیم مسیر گواهی برای دامنه شما
### Certbot
نصب و استفاده از Certbot:
```sh
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
### Cloudflare
اسکریپت داخلی برای دریافت گواهی SSL از Cloudflare. نیازمند:
- ایمیل ثبت‌شده در Cloudflare
- کلید API جهانی Cloudflare
- دامنه باید از طریق Cloudflare به سرور متصل باشد
**دریافت کلید API جهانی Cloudflare:**
1. دستور `x-ui` را اجرا و گزینه `گواهی SSL کلادفلر` را انتخاب کنید.
2. به لینک [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens) مراجعه کنید.
3. روی "View Global API Key" کلیک کنید:
![](media/APIKey1.PNG)
4. پس از احراز هویت، کلید API نمایش داده می‌شود:
![](media/APIKey2.png)
در هنگام استفاده، نام دامنه، ایمیل و کلید API را وارد کنید:
![](media/DetailEnter.png)
</details>
## نصب دستی و ارتقا
<details>
<summary>جزئیات نصب دستی</summary>
#### استفاده
1. دریافت آخرین نسخه از سرور:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
s390x) echo 's390x' ;;
*) XUI_ARCH="amd64" ;;
esac
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
2. نصب یا ارتقا:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
s390x) echo 's390x' ;;
*) XUI_ARCH="amd64" ;;
esac
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui
```
</details>
## نصب با Docker
<details>
<summary>جزئیات Docker</summary>
#### استفاده
1. **نصب Docker:**
```sh
bash <(curl -sSL https://get.docker.com)
```
2. **کلون پروژه:**
```sh
git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui
```
3. **راه‌اندازی سرویس:**
```sh
docker compose up -d
```
یا
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
4. **به‌روزرسانی:**
```sh
cd 3x-ui
docker compose down
docker compose pull 3x-ui
docker compose up -d
```
5. **حذف:**
```sh
docker stop 3x-ui
docker rm 3x-ui
cd --
rm -r 3x-ui
```
</details>
## تنظیمات Nginx
<details>
<summary>پیکربندی Reverse Proxy</summary>
#### Nginx Reverse Proxy
```nginx
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
#### مسیر فرعی در Nginx
- اطمینان حاصل کنید "URI Path" در تنظیمات پنل یکسان باشد.
- `url` در تنظیمات پنل باید با `/` پایان یابد.
```nginx
location /sub {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_redirect off;
proxy_pass http://127.0.0.1:2053;
}
```
</details>
## سیستم‌عامل‌های توصیه شده
- Ubuntu 20.04+
- Debian 11+
- CentOS 8+
- OpenEuler 22.03+
- Fedora 36+
- Arch Linux
- Parch Linux
- Manjaro
- Armbian
- AlmaLinux 8.0+
- Rocky Linux 8+
- Oracle Linux 8+
- OpenSUSE Tubleweed
- Amazon Linux 2023
- Windows x64
## معماری‌ها و دستگاه‌های پشتیبانی شده
<details>
<summary>جزئیات معماری‌ها و دستگاه‌ها</summary>
- **amd64**: معماری استاندارد برای کامپیوترهای شخصی و سرورها
- **x86 / i386**: سیستم‌های دسکتاپ و لپ‌تاپ
- **armv8 / arm64 / aarch64**: دستگاه‌های موبایل و embedded مانند Raspberry Pi 4
- **armv7 / arm / arm32**: دستگاه‌های قدیمی مانند Orange Pi Zero
- **armv6 / arm / arm32**: دستگاه‌های بسیار قدیمی مانند Raspberry Pi 1
- **armv5 / arm / arm32**: سیستم‌های embedded قدیمی
- **s390x**: کامپیوترهای IBM mainframe
</details>
## زبان‌های پشتیبانی شده
- انگلیسی
- فارسی
- چینی سنتی
- چینی ساده‌شده
- ژاپنی
- روسی
- ویتنامی
- اسپانیایی
- اندونزیایی
- اوکراینی
- ترکی
- پرتغالی (برزیل)
## ویژگی‌ها
- مانیتورینگ وضعیت سیستم
- جستجو در بین inboundها و کلاینت‌ها
- تم تاریک/روشن
- پشتیبانی از چند کاربر و پروتکل
- پروتکل‌های VMESS، VLESS، Trojan، Shadowsocks، Dokodemo-door، Socks، HTTP، WireGuard
- پشتیبانی از XTLS شامل RPRX-Direct، Vision، REALITY
- آمار ترافیک، محدودیت ترافیک، محدودیت زمانی
- تنظیمات سفارشی Xray
- پشتیبانی از HTTPS برای پنل
- دریافت خودکار گواهی SSL
- مسیرهای API اصلاح شده
- پشتیبانی از تغییر تنظیمات از طریق پنل
- امکان export/import دیتابیس
## تنظیمات پیش‌فرض پنل
<details>
<summary>جزئیات تنظیمات پیش‌فرض</summary>
### نام کاربری، رمز عبور، پورت و مسیر وب
در صورت عدم تغییر، این موارد به صورت تصادفی ایجاد می‌شوند (به جز Docker).
**تنظیمات پیش‌فرض Docker:**
- **نام کاربری:** admin
- **رمز عبور:** admin
- **پورت:** 2053
### مدیریت دیتابیس:
امکان Backup و Restore دیتابیس از طریق پنل.
- **مسیر دیتابیس:**
- `/etc/x-ui/x-ui.db`
### مسیر پایه وب
1. **بازنشانی مسیر:**
- اجرای دستور `x-ui`
- انتخاب گزینه `Reset Web Base Path`
2. **ساخت یا تنظیم مسیر:**
- مسیر به صورت تصادفی ساخته شده یا قابل تنظیم است
3. **مشاهده تنظیمات فعلی:**
- استفاده از دستور `x-ui settings` یا `View Current Settings` در `x-ui`
**توصیه امنیتی:**
- استفاده از مسیرهای طولانی و تصادفی برای افزایش امنیت
**مثال:**
- `http://ip:port/*webbasepath*/panel`
- `http://domain:port/*webbasepath*/panel`
</details>
## پیکربندی WARP
<details>
<summary>جزئیات WARP</summary>
#### استفاده
**برای نسخه‌های `v2.1.0` و جدیدتر:**
WARP به صورت داخلی پشتیبانی می‌شود. تنها نیاز به فعال‌سازی در پنل است.
</details>
## محدودیت IP
<details>
<summary>جزئیات محدودیت IP</summary>
#### استفاده
**توجه:** محدودیت IP در صورت استفاده از IP Tunnel کار نمی‌کند.
- **تا نسخه `v1.6.1`:**
- محدودیت IP به صورت داخلی در پنل وجود دارد
**برای نسخه‌های `v1.7.0` و جدیدتر:**
برای فعال‌سازی نیاز به نصب `fail2ban` است:
1. اجرای دستور `x-ui` و انتخاب `مدیریت محدودیت IP`
2. گزینه‌های موجود:
- **تغییر مدت زمان Ban**
- **حذف تمام Banها**
- **مشاهده لاگ‌ها**
- **وضعیت Fail2ban**
- **راه‌اندازی مجدد Fail2ban**
- **حذف Fail2ban**
3. تنظیم مسیر `Access log` در پنل به `./access.log` و ذخیره و راه‌اندازی مجدد Xray
- **قبل از نسخه `v2.1.3`:**
- تنظیم دستی `access.log` در تنظیمات Xray:
```sh
"log": {
"access": "./access.log",
"dnsLog": false,
"loglevel": "warning"
},
```
- **از نسخه `v2.1.3`:**
- امکان تنظیم `access.log` از طریق پنل
</details>
## ربات تلگرام
<details>
<summary>جزئیات ربات تلگرام</summary>
#### استفاده
ربات تلگرام برای اطلاع‌رسانی ترافیک، ورود به پنل، Backup دیتابیس و ... استفاده می‌شود. نیازمند تنظیم:
- توکن تلگرام
- Chat ID ادمین‌ها
- زمان اطلاع‌رسانی (Cron syntax)
- اطلاع‌رسانی انقضا
- اطلاع‌رسانی ترافیک
- Backup دیتابیس
- اطلاع‌رسانی مصرف CPU
**سینتکس نمونه:**
- `30 \* \* \* \* \*` - اطلاع در ثانیه 30 هر دقیقه
- `@hourly` - هر ساعت
- `@daily` - هر روز
### ویژگی‌های ربات
- گزارش دوره‌ای
- اطلاع ورود به پنل
- اطلاع مصرف CPU
- اطلاع پیش‌از موعد انقضا و ترافیک
- گزارش ترافیک کلاینت‌ها
- منوی مبتنی بر دستور
- جستجوی کلاینت بر اساس ایمیل
- بررسی inboundها
- بررسی وضعیت سرور
- دریافت Backup
- چندزبانه
### راه‌اندازی ربات
- شروع [Botfather](https://t.me/BotFather) در تلگرام:
![Botfather](./media/botfather.png)
- ساخت ربات جدید با دستور /newbot:
![Create new bot](./media/newbot.png)
- شروع ربات ساخته شده:
![token](./media/token.png)
- تنظیمات پنل:
![Panel Config](./media/panel-bot-config.png)
وارد کردن توکن و Chat ID (دریافت از [این ربات](https://t.me/useridinfobot)):
![User ID](./media/user-id.png)
</details>
## مسیرهای API
<details>
<summary>جزئیات API</summary>
#### استفاده
- [مستندات API](https://www.postman.com/hsanaei/3x-ui/collection/q1l5l0u/3x-ui)
- `/login` با `POST` داده کاربر: `{username: '', password: ''}`
| Method | مسیر | عملکرد |
| :----: | ---------------------------------- | ------------------------------------------- |
| `GET` | `"/list"` | دریافت تمام inboundها |
| `GET` | `"/get/:id"` | دریافت inbound بر اساس id |
| `POST` | `"/add"` | افزودن inbound |
| `POST` | `"/del/:id"` | حذف inbound |
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D5146551-dda3cab3-0e33-485f-96f9-d4262f437ac5%26entityType%3Dcollection%26workspaceId%3Dd64f609f-485a-4951-9b8f-876b3f917124)
</details>
## متغیرهای محیطی
<details>
<summary>جزئیات متغیرها</summary>
#### استفاده
| متغیر | نوع | پیش‌فرض |
| ------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER| `string` | `"bin"` |
مثال:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
</details>
## پیش‌نمایش
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/01-overview-dark.png">
<img alt="3x-ui" src="./media/01-overview-light.png">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./media/02-inbounds-dark.png">
<img alt="3x-ui" src="./media/02-inbounds-light.png">
</picture>
## قدردانی ویژه از
- [alireza0](https://github.com/alireza0/)
## تشکر و قدردانی
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (مجوز: **GPL-3.0**)
- [Russia v2ray rules](https://github.com/runetfreedom/russia-v2ray-rules-dat) (مجوز: **GPL-3.0**)
## Stargazers over Time
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg?variant=adaptive)](https://starchart.cc/MHSanaei/3x-ui)

View File

@@ -1,4 +1,4 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
[English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>

View File

@@ -1,4 +1,4 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
[English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>

View File

@@ -1,4 +1,4 @@
[English](/README.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
[English](/README.md) | [فارسی](/README.fa_IR.md) | [中文](/README.zh_CN.md) | [Español](/README.es_ES.md) | [Русский](/README.ru_RU.md)
<p align="center">
<picture>

View File

@@ -1 +1 @@
2.5.0
2.5.2

View File

@@ -82,31 +82,9 @@ func InitDB(dbPath string) error {
}
c := &gorm.Config{
Logger: gormLogger,
SkipDefaultTransaction: true,
PrepareStmt: true,
Logger: gormLogger,
}
dsn := dbPath + "?cache=shared&_journal_mode=WAL&_synchronous=NORMAL"
db, err = gorm.Open(sqlite.Open(dsn), c)
if err != nil {
return err
}
sqlDB, err := db.DB()
if err != nil {
return err
}
_, err = sqlDB.Exec("PRAGMA cache_size = -64000;")
if err != nil {
return err
}
_, err = sqlDB.Exec("PRAGMA temp_store = MEMORY;")
if err != nil {
return err
}
_, err = sqlDB.Exec("PRAGMA foreign_keys = ON;")
db, err = gorm.Open(sqlite.Open(dbPath), c)
if err != nil {
return err
}
@@ -123,11 +101,6 @@ func InitDB(dbPath string) error {
func CloseDB() error {
if db != nil {
if err := Checkpoint(); err != nil {
log.Printf("error executing checkpoint: %v", err)
}
sqlDB, err := db.DB()
if err != nil {
return err

16
go.mod
View File

@@ -3,18 +3,18 @@ module x-ui
go 1.23.5
require (
github.com/gin-contrib/gzip v1.2.0
github.com/gin-contrib/gzip v1.2.2
github.com/gin-contrib/sessions v1.0.2
github.com/gin-gonic/gin v1.10.0
github.com/goccy/go-json v0.10.4
github.com/goccy/go-json v0.10.5
github.com/mymmrac/telego v0.32.0
github.com/nicksnyder/go-i18n/v2 v2.5.0
github.com/nicksnyder/go-i18n/v2 v2.5.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.3
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.24.12
github.com/shirou/gopsutil/v4 v4.25.1
github.com/valyala/fasthttp v1.58.0
github.com/xtls/xray-core v1.8.25-0.20250126155934-7b59379d73c3
github.com/xtls/xray-core v1.8.25-0.20250130105737-0a8470cb14eb
go.uber.org/atomic v1.11.0
golang.org/x/text v0.21.0
google.golang.org/grpc v1.70.0
@@ -39,7 +39,7 @@ require (
github.com/go-playground/validator/v10 v10.24.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/pprof v0.0.0-20250125003558-7fdb3d7e6fa0 // indirect
github.com/google/pprof v0.0.0-20250202011525-fc3143867406 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
@@ -84,7 +84,7 @@ require (
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.13.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.10.0 // indirect
@@ -93,7 +93,7 @@ require (
golang.org/x/tools v0.29.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect
google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0 // indirect

36
go.sum
View File

@@ -29,8 +29,8 @@ github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3G
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
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/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/gzip v1.2.0 h1:JzN6DT3/xYL5zAdviN1ORNzKeklrwafXCIDKIR+qmUA=
github.com/gin-contrib/gzip v1.2.0/go.mod h1:C1a5cacjlDsS20cKnHlZRCPUu57D3qH6B2pV0rl+Y/s=
github.com/gin-contrib/gzip v1.2.2 h1:iUU/EYCM8ENfkjmZaVrxbjF/ZC267Iqv5S0MMCMEliI=
github.com/gin-contrib/gzip v1.2.2/go.mod h1:C1a5cacjlDsS20cKnHlZRCPUu57D3qH6B2pV0rl+Y/s=
github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA=
github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
@@ -54,8 +54,8 @@ github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -67,8 +67,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250125003558-7fdb3d7e6fa0 h1:my2ucqBZmv+cWHIhZNSIYKzgN8EBGyHdC7zD5sASRAg=
github.com/google/pprof v0.0.0-20250125003558-7fdb3d7e6fa0/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250202011525-fc3143867406 h1:wlQI2cYY0BsWmmPPAnxfQ8SDW0S3Jasn+4B8kXFxprg=
github.com/google/pprof v0.0.0-20250202011525-fc3143867406/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
@@ -105,8 +105,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -114,8 +114,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.32.0 h1:4X8C1l3k+opkk86r95+eQE8DxiS2LYlR61L/G7yreDY=
github.com/mymmrac/telego v0.32.0/go.mod h1:qS6NaRhJgcuEEBEMVCV79S2xCAuHq9O+ixwfLuRW31M=
github.com/nicksnyder/go-i18n/v2 v2.5.0 h1:3wH1gpaekcgGuwzWdSu7JwJhH9Tk87k1ezt0i1p2/Is=
github.com/nicksnyder/go-i18n/v2 v2.5.0/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
@@ -152,8 +152,8 @@ github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.24.12 h1:qvePBOk20e0IKA1QXrIIU+jmk+zEiYVVx06WjBRlZo4=
github.com/shirou/gopsutil/v4 v4.24.12/go.mod h1:DCtMPAad2XceTeIAbGyVfycbYQNBGk2P8cvDi7/VN9o=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -190,8 +190,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463 h1:g1Cj7d+my6k/HHxLAyxPwyX8i7FGRr6ulBDMkBzg2BM=
github.com/xtls/reality v0.0.0-20240909153216-e26ae2305463/go.mod h1:BjIOLmkEEtAgloAiVUcYj0Mt+YU00JARZw8AEU0IwAg=
github.com/xtls/xray-core v1.8.25-0.20250126155934-7b59379d73c3 h1:JzofxKb13MvNZeSLPS9B07Few0fu11dPEa40EQNV/hE=
github.com/xtls/xray-core v1.8.25-0.20250126155934-7b59379d73c3/go.mod h1:KmpYBu4IFdODX7qgG7DP7v9sHJBcHpdKE+qxUZ2N304=
github.com/xtls/xray-core v1.8.25-0.20250130105737-0a8470cb14eb h1:VBzDZ4XHT9FM0S3qvXp6KuTOk7mXQQm0pjOW0Neeog4=
github.com/xtls/xray-core v1.8.25-0.20250130105737-0a8470cb14eb/go.mod h1:EKhNEDY/WIB+ylEbVv2pzUaP08W/lt0sYmJQ9FJruko=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -216,8 +216,8 @@ golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
@@ -242,8 +242,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47 h1:91mG8dNTpkC0uChJUQ9zCiRqx3GEEFOWaRZ0mI6Oj2I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250124145028-65684f501c47/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=

View File

@@ -26,7 +26,6 @@ class AllSetting {
this.xrayTemplateConfig = "";
this.secretEnable = false;
this.subEnable = false;
this.subSyncEnable = true;
this.subListen = "";
this.subPort = 2096;
this.subPath = "/sub/";

View File

@@ -70,41 +70,6 @@ class HttpUtil {
}
return msg;
}
static async jsonPost(url, data) {
let msg;
try {
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
const resp = await fetch(basePath + url.replace(/^\/+|\/+$/g, ''), requestOptions);
const response = await resp.json();
msg = this._respToMsg({data : response});
} catch (e) {
msg = new Msg(false, e.toString());
}
this._handleMsg(msg);
return msg;
}
static async postWithModalJson(url, data, modal) {
if (modal) {
modal.loading(true);
}
const msg = await this.jsonPost(url, data);
if (modal) {
modal.loading(false);
if (msg instanceof Msg && msg.success) {
modal.close();
}
}
return msg;
}
}
class PromiseUtil {

View File

@@ -1,10 +1,10 @@
package controller
import (
"errors"
"encoding/json"
"fmt"
"strconv"
"x-ui/database/model"
"x-ui/web/service"
"x-ui/web/session"
@@ -33,13 +33,9 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/addClient", a.addInboundClient)
g.POST("/addGroupClient", a.addGroupInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/delGroupClients", a.delGroupClients)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/updateClients", a.updateGroupInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetGroupClientTraffic", a.resetGroupClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
@@ -194,34 +190,6 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
}
}
func (a *InboundController) addGroupInboundClient(c *gin.Context) {
var requestData []model.Inbound
err := c.ShouldBindJSON(&requestData)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
needRestart := true
for _, data := range requestData {
needRestart, err = a.inboundService.AddInboundClient(&data)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
}
jsonMsg(c, "Client(s) added", nil)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
@@ -243,38 +211,6 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
}
}
func (a *InboundController) delGroupClients(c *gin.Context) {
var requestData []struct {
InboundID int `json:"inboundId"`
ClientID string `json:"clientId"`
}
if err := c.ShouldBindJSON(&requestData); err != nil {
jsonMsg(c, "Invalid request data", err)
return
}
needRestart := false
for _, req := range requestData {
needRestartTmp, err := a.inboundService.DelInboundClient(req.InboundID, req.ClientID)
if err != nil {
jsonMsg(c, "Failed to delete client", err)
return
}
if needRestartTmp {
needRestart = true
}
}
jsonMsg(c, "Clients deleted successfully", nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) updateInboundClient(c *gin.Context) {
clientId := c.Param("clientId")
@@ -298,56 +234,6 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
}
}
func (a *InboundController) updateGroupInboundClient(c *gin.Context) {
var requestData []map[string]interface{}
if err := c.ShouldBindJSON(&requestData); err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
needRestart := false
for _, item := range requestData {
inboundMap, ok := item["inbound"].(map[string]interface{})
if !ok {
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'inbound' to map"))
return
}
clientId, ok := item["clientId"].(string)
if !ok {
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'clientId' to string"))
return
}
inboundJSON, err := json.Marshal(inboundMap)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
var inboundModel model.Inbound
if err := json.Unmarshal(inboundJSON, &inboundModel); err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
if restart, err := a.inboundService.UpdateInboundClient(&inboundModel, clientId); err != nil {
jsonMsg(c, "Something went wrong!", err)
return
} else {
needRestart = needRestart || restart
}
}
jsonMsg(c, "Client updated", nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
@@ -367,44 +253,6 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
}
}
func (a *InboundController) resetGroupClientTraffic(c *gin.Context) {
var requestData []struct {
InboundID int `json:"inboundId"` // Map JSON "inboundId" to struct field "InboundID"
Email string `json:"email"` // Map JSON "email" to struct field "Email"
}
// Parse JSON body directly using ShouldBindJSON
if err := c.ShouldBindJSON(&requestData); err != nil {
jsonMsg(c, "Invalid request data", err)
return
}
needRestart := false
// Process each request data
for _, req := range requestData {
needRestartTmp, err := a.inboundService.ResetClientTraffic(req.InboundID, req.Email)
if err != nil {
jsonMsg(c, "Failed to reset client traffic", err)
return
}
// If any request requires a restart, set needRestart to true
if needRestartTmp {
needRestart = true
}
}
// Send response back to the client
jsonMsg(c, "Traffic reset for all clients", nil)
// Restart the service if required
if needRestart {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) resetAllTraffics(c *gin.Context) {
err := a.inboundService.ResetAllTraffics()
if err != nil {

View File

@@ -40,7 +40,6 @@ type AllSetting struct {
TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubSyncEnable bool `json:"subSyncEnable" form:"subSyncEnable"`
SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"`
SubPath string `json:"subPath" form:"subPath"`

View File

@@ -23,15 +23,13 @@
</tr-qr-bg>
</tr-qr-box>
</template>
<template v-if="!isJustSub">
<template v-for="(row, index) in qrModal.qrcodes">
<tr-qr-box class="qr-box">
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
<tr-qr-bg class="qr-bg">
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
</tr-qr-bg>
</tr-qr-box>
</template>
<template v-for="(row, index) in qrModal.qrcodes">
<tr-qr-box class="qr-box">
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
<tr-qr-bg class="qr-bg">
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
</tr-qr-bg>
</tr-qr-box>
</template>
</tr-qr-modal>
</a-modal>
@@ -45,14 +43,12 @@
qrcodes: [],
clipboard: null,
visible: false,
isJustSub: false,
subId: '',
show: function(title = '', dbInbound, client, isJustSub = false) {
show: function(title = '', dbInbound, client) {
this.title = title;
this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound();
this.client = client;
this.isJustSub = isJustSub;
this.subId = '';
this.qrcodes = [];
if (this.inbound.protocol == Protocols.WIREGUARD) {
@@ -80,9 +76,7 @@
delimiters: ['[[', ']]'],
el: '#qrcode-modal',
data: {
qrModal: qrModal,get isJustSub(){
return qrModal.isJustSub
}
qrModal: qrModal,
},
methods: {
copyToClipboard(elementId, content) {

View File

@@ -16,16 +16,7 @@
title: '',
okText: '',
isEdit: false,
group: {
canGroup: true,
isGroup: false,
currentClient: null,
inbounds: [],
clients: [],
editIds: []
},
dbInbound: new DBInbound(),
dbInbounds: null,
inbound: new Inbound(),
clients: [],
clientStats: [],
@@ -34,126 +25,33 @@
clientIps: null,
delayedStart: false,
ok() {
if (app.subSettings.enable && clientModal.group.isGroup && clientModal.group.canGroup) {
const currentClient = clientModal.group.currentClient;
const { limitIp, comment, totalGB, expiryTime, reset, enable, subId, tgId, flow } = currentClient;
const uniqueEmails = clientModalApp.makeGroupEmailsUnique(clientModal.dbInbounds, currentClient.email, clientModal.group.clients);
clientModal.group.clients.forEach((client, index) => {
client.email = uniqueEmails[index];
client.limitIp = limitIp;
client.comment = comment;
client.totalGB = totalGB;
client.expiryTime = expiryTime;
client.reset = reset;
client.enable = enable;
if (subId) {
client.subId = subId;
}
if (tgId) {
client.tgId = tgId;
}
if (flow) {
client.flow = flow;
}
});
if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModal.group.clients, clientModal.group.inbounds, clientModal.group.editIds);
} else {
ObjectUtil.execute(clientModal.confirm, clientModal.group.clients, clientModal.group.inbounds);
}
if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
} else {
if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
} else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
}
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
}
},
show({
title = '',
okText = '{{ i18n "sure" }}',
index = null,
dbInbound = null,
dbInbounds = null,
confirm = () => {
},
isEdit = false
}) {
this.group = {
canGroup: true,
isGroup: false,
currentClient: null,
inbounds: [],
clients: [],
editIds: []
}
this.dbInbounds = dbInbounds;
show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {
this.visible = true;
this.title = title;
this.okText = okText;
this.isEdit = isEdit;
if (app.subSettings.enable && dbInbounds !== null && Array.isArray(dbInbounds)) {
if (isEdit) {
this.showProcess(dbInbound, index);
let processSingleEdit = true
if (this.group.canGroup) {
this.group.currentClient = this.clients[this.index]
const response = app.getSubGroupClients(dbInbounds, this.group.currentClient)
if (response.clients.length > 1) {
this.group.isGroup = true;
this.group.inbounds = response.inbounds
this.group.clients = response.clients
this.group.editIds = response.editIds
if (this.clients[index].expiryTime < 0) {
this.delayedStart = true;
}
processSingleEdit = false
}
}
if (processSingleEdit) {
this.singleEditClientProcess(index)
}
} else {
this.group.isGroup = true;
dbInbounds.forEach((dbInboundItem) => {
this.showProcess(dbInboundItem);
if (this.dbInbound.isMultiUser()) {
this.addClient(this.inbound.protocol, this.clients);
this.group.inbounds.push(dbInboundItem.id)
this.group.clients.push(this.clients[this.index])
}
})
this.group.currentClient = this.clients[this.index]
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.clients = this.inbound.clients;
this.index = index === null ? this.clients.length : index;
this.delayedStart = false;
if (isEdit) {
if (this.clients[index].expiryTime < 0) {
this.delayedStart = true;
}
this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
} else {
this.showProcess(dbInbound, index);
if (isEdit) {
this.singleEditClientProcess(index)
} else {
this.addClient(this.inbound.protocol, this.clients);
}
this.addClient(this.inbound.protocol, this.clients);
}
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm;
},
showProcess(dbInbound, index = null) {
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
if (this.dbInbound.isMultiUser()) {
this.clients = this.inbound.clients;
this.index = index === null ? this.clients.length : index;
this.delayedStart = false;
}
},
singleEditClientProcess(index) {
if (this.clients[index].expiryTime < 0) {
this.delayedStart = true;
}
this.oldClientId = this.getClientId(this.dbInbound.protocol, this.clients[index]);
},
},
getClientId(protocol, client) {
switch (protocol) {
case Protocols.TROJAN: return client.password;
@@ -174,7 +72,7 @@
clientModal.visible = false;
clientModal.loading(false);
},
loading(loading = true) {
loading(loading=true) {
clientModal.confirmLoading = loading;
},
};
@@ -196,18 +94,6 @@
get isEdit() {
return this.clientModal.isEdit;
},
get isGroup() {
return this.clientModal.group.isGroup;
},
get isGroupEdit() {
return this.clientModal.group.canGroup;
},
set isGroupEdit(value) {
this.clientModal.group.canGroup = value;
if (!value) {
this.clientModal.singleEditClientProcess(this.clientModal.index)
}
},
get datepicker() {
return app.datepicker;
},
@@ -234,36 +120,6 @@
},
},
methods: {
makeGroupEmailsUnique(dbInbounds, baseEmail, groupClients) {
// Extract the base part of the email (before the "__" if present)
const match = baseEmail.match(/^(.*?)__/);
const base = match ? match[1] : baseEmail;
// Generate initial emails for each client in the group
const generatedEmails = groupClients.map((_, index) => `${base}__${index + 1}`);
// Function to check if an email already exists in dbInbounds but belongs to a different subId
const isDuplicate = (emailToCheck, clientSubId) => {
return dbInbounds.some((dbInbound) => {
const settings = JSON.parse(dbInbound.settings);
const clients = settings && settings.clients ? settings.clients : [];
return clients.some(client => client.email === emailToCheck && client.subId !== clientSubId);
});
};
// Check if any of the generated emails are duplicates
const hasDuplicates = generatedEmails.some((email, index) => {
return isDuplicate(email, groupClients[index].subId);
});
// If duplicates exist, add a random string to the base email to ensure uniqueness
if (hasDuplicates) {
const randomString = `-${RandomUtil.randomLowerAndNum(4)}`;
return groupClients.map((_, index) => `${base}${randomString}__${index + 1}`);
}
return generatedEmails;
},
async getDBClientIps(email) {
const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`);
if (!msg.success) {
@@ -291,22 +147,7 @@
} catch (error) {
}
},
async resetClientTrafficHandler(client, dbInboundId, clients = []) {
if (clients.length > 0) {
const resetRequests = clients
.filter(client => {
const inbound = clientModal.dbInbounds.find(inbound => inbound.id === client.inboundId);
return inbound && app.hasClientStats(inbound, client.email);
}).map(client => ({ inboundId: client.inboundId, email: client.email}));
return HttpUtil.postWithModalJson('/panel/inbound/resetGroupClientTraffic', resetRequests, null)
} else {
return HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email)
}
},
resetClientTraffic(client, dbInboundId, iconElement) {
const subGroup = app.subSettings.enable && clientModal.group.isGroup && clientModal.group.canGroup && clientModal.dbInbounds && clientModal.dbInbounds.length > 0 ? app.getSubGroupClients(clientModal.dbInbounds, client) : [];
const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
resetClientTraffic(email, dbInboundId, iconElement) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
@@ -315,8 +156,8 @@
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
iconElement.disabled = true;
const msg = await this.resetClientTrafficHandler(client, dbInboundId, clients);
if (msg && msg.success) {
const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email);
if (msg.success) {
this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0;
}

View File

@@ -3,18 +3,6 @@
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
<a-switch v-model="client.enable"></a-switch>
</a-form-item>
<a-form-item v-if="isEdit && app.subSettings.enable && isGroup">
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.client.isGroupEditDesc" }}</span>
</template>
{{ i18n "pages.client.isGroupEdit" }}
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-switch v-model="isGroupEdit"></a-switch>
</a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
@@ -146,7 +134,7 @@
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet"
@click="resetClientTraffic(client,clientStats.inboundId,$event.target)"
@click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"></a-icon>
</a-tooltip>
</a-form-item>

View File

@@ -12,7 +12,7 @@
<template slot="title">{{ i18n "info" }}</template>
<a-icon style="font-size: 24px;" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
</a-tooltip>
<a-tooltip v-if="hasClientStats(record, client.email)">
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: var(--color-primary-100)' : 'color: var(--color-primary-100)'"></a-icon>

View File

@@ -14,7 +14,6 @@
confirmLoading: false,
okText: '{{ i18n "sure" }}',
isEdit: false,
isGroup: false,
confirm: null,
inbound: new Inbound(),
dbInbound: new DBInbound(),
@@ -62,9 +61,6 @@
get isEdit() {
return inModal.isEdit;
},
get isGroup() {
return inModal.isGroup;
},
get client() {
return inModal.inbound.clients[0];
},

View File

@@ -224,10 +224,6 @@
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
<a-menu-item v-if="subSettings.enable && dbInbounds.length > 0" key="addGroupClient">
<a-icon type="usergroup-add"></a-icon>
{{ i18n "pages.client.groupAdd"}}
</a-menu-item>
</a-menu>
</a-dropdown>
</a-col>
@@ -863,9 +859,6 @@
case "delDepletedClients":
this.delDepletedClients(-1)
break;
case "addGroupClient":
this.openGroupAddClient()
break;
}
},
clickAction(action, dbInbound) {
@@ -1011,21 +1004,6 @@
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
},
openGroupAddClient() {
clientModal.show({
title: '{{ i18n "pages.client.groupAdd"}}',
okText: '{{ i18n "pages.client.submitAdd"}}',
dbInbounds: this.dbInbounds,
confirm: async (clients, dbInboundIds) => {
await this.addGroupClient(clients, dbInboundIds, clientModal).then((res) => {
if(res){
this.showQrcode(dbInboundIds[0],clients[0], true)
}
});
},
isEdit: false
});
},
openAddClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientModal.show({
@@ -1033,11 +1011,7 @@
okText: '{{ i18n "pages.client.submitAdd"}}',
dbInbound: dbInbound,
confirm: async (clients, dbInboundId) => {
await this.addClient(clients, dbInboundId, clientModal).then((res) => {
if(res){
this.showQrcode(dbInboundId,clients)
}
});
await this.addClient(clients, dbInboundId, clientModal);
},
isEdit: false
});
@@ -1060,7 +1034,6 @@
clientModal.show({
title: '{{ i18n "pages.client.edit"}}',
okText: '{{ i18n "pages.client.submitEdit"}}',
dbInbounds: this.dbInbounds,
dbInbound: dbInbound,
index: index,
confirm: async (client, dbInboundId, clientId) => {
@@ -1086,36 +1059,12 @@
};
await this.submit(`/panel/inbound/addClient`, data, modal);
},
async addGroupClient(clients, dbInboundIds, modal) {
const data = []
dbInboundIds.forEach((dbInboundId, index) => {
data.push({
id: dbInboundId,
settings: '{"clients": [' + clients[index].toString() + ']}',
})
})
return await this.submit(`/panel/inbound/addGroupClient`, data, modal, true)
},
async updateClient(client, dbInboundId, clientId) {
if (Array.isArray(client) && Array.isArray(dbInboundId) && Array.isArray(clientId)){
const data = []
client.forEach((client, index) => {
data.push({
clientId: clientId[index],
inbound: {
id: dbInboundId[index],
settings: '{"clients": [' + client.toString() + ']}',
}
})
})
await this.submit(`/panel/inbound/updateClients`, data, clientModal, true);
}else{
const data = {
id: dbInboundId,
settings: '{"clients": [' + client.toString() + ']}',
};
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
}
const data = {
id: dbInboundId,
settings: '{"clients": [' + client.toString() + ']}',
};
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
},
resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -1143,55 +1092,20 @@
onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
});
},
async delClientHandler(dbInboundId, currentClient, clients = []) {
if (clients.length > 0) {
const deleteRequestData = [];
for (const client of clients) {
const dbInbound = this.dbInbounds.find(inbound => inbound.id === client.inboundId);
if (dbInbound) {
const inbound = dbInbound.toInbound();
if (inbound && inbound.clients && inbound.clients.length === 1) {
let newClient = Inbound.Settings.getSettings(inbound.protocol).toString();
newClient = JSON.parse(newClient);
newClient = newClient && newClient.clients && newClient.clients.length > 0 ? JSON.stringify(newClient.clients[0], null, 2) : null;
if (newClient) {
const data = {
id: client.inboundId,
settings: '{"clients": [' + newClient + ']}',
};
await this.submit(`/panel/inbound/addClient`, data, null);
}
}
deleteRequestData.push({
inboundId: client.inboundId,
clientId: client.clientId,
});
}
}
await this.submit('/panel/inbound/delGroupClients', deleteRequestData, null, true);
} else {
const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
const clientId = this.getClientId(dbInbound.protocol, currentClient);
await this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
}
},
delClient(dbInboundId, currentClient, confirmation = true) {
const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, currentClient) : [];
const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
delClient(dbInboundId, client,confirmation = true) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientId = this.getClientId(dbInbound.protocol, client);
if (confirmation){
const clientEmails = clients.length > 0 ? clients.map(item => item.email) : currentClient.email
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + clientEmails,
title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.delClientHandler(dbInboundId, currentClient, clients),
onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
});
} else {
this.delClientHandler(dbInboundId, currentClient, clients)
this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
}
},
getSubGroupClients(dbInbounds, currentClient) {
@@ -1251,10 +1165,10 @@
}
return newDbInbound;
},
showQrcode(dbInboundId, client, isJustSub = false) {
showQrcode(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
newDbInbound = this.checkFallback(dbInbound);
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client, isJustSub);
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
},
showInfo(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -1274,61 +1188,36 @@
},
async switchEnableClient(dbInboundId, client) {
this.loading()
const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, client) : [];
if (subGroup && subGroup.clients && subGroup.clients.length > 0){
await this.updateClient(subGroup.clients.map(item => {
item.enable = !item.enable
return item
}), subGroup.inbounds, subGroup.editIds);
}else{
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
inbound = dbInbound.toInbound();
clients = inbound.clients;
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index], dbInboundId, clientId);
}
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
inbound = dbInbound.toInbound();
clients = inbound.clients;
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index], dbInboundId, clientId);
this.loading(false);
},
async submit(url, data, model, isJson = false) {
const msg = isJson ? await HttpUtil.postWithModalJson(url, data, model) : await HttpUtil.postWithModal(url, data, model);
async submit(url, data, modal) {
const msg = await HttpUtil.postWithModal(url, data, modal);
if (msg.success) {
await this.getDBInbounds();
}
return msg
},
getInboundClients(dbInbound) {
return dbInbound.toInbound().clients;
},
resetClientTrafficHandler(client, dbInboundId, clients = []) {
if (clients.length > 0){
const resetRequests = clients
.filter(client => {
const inbound = this.dbInbounds.find(inbound => inbound.id === client.inboundId);
return inbound && this.hasClientStats(inbound, client.email);
}).map(client => ({ inboundId: client.inboundId, email: client.email}));
this.submit('/panel/inbound/resetGroupClientTraffic', resetRequests, null, true)
}else {
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
}
},
resetClientTraffic(client, dbInboundId, confirmation = true) {
const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, client) : [];
const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
if (confirmation){
const clientEmails = clients.length > 0 ? clients.map(item => item.email) : client.email
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + clientEmails,
title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + client.email,
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.resetClientTrafficHandler(client, dbInboundId, clients),
onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
})
} else {
this.resetClientTrafficHandler(client, dbInboundId, clients);
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
}
},
resetAllTraffic() {
@@ -1364,10 +1253,6 @@
isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index);
},
hasClientStats(dbInbound, email) {
if (email.length == 0) return 0;
return !!dbInbound.clientStats.find(stats => stats.email === email);
},
getUpStats(dbInbound, email) {
if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);

View File

@@ -268,7 +268,6 @@
<a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}'>
<a-list item-layout="horizontal">
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subSyncEnable"}}' desc='{{ i18n "pages.settings.subSyncEnableDesc"}}' v-model="allSetting.subSyncEnable"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>

View File

@@ -143,6 +143,7 @@
</a-select>
</a-col>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.xray.outboundTraffic"}}' desc='{{ i18n "pages.xray.outboundTrafficDesc"}}' v-model="outboundTraffic"></setting-list-item>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.logConfigs" }}'>
@@ -1823,6 +1824,18 @@
this.templateSettings = newTemplateSettings;
}
},
outboundTraffic: {
get: function () {
if (!this.templateSettings || !this.templateSettings.policy.system || !this.templateSettings.policy.system.statsOutboundDownlink) return false;
return this.templateSettings.policy.system.statsOutboundDownlink;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.policy.system.statsOutboundDownlink = newValue;
newTemplateSettings.policy.system.statsOutboundUplink = newValue;
this.templateSettings = newTemplateSettings;
}
},
maskAddressLog: {
get: function () {
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.maskAddress) return "";

View File

@@ -1,178 +0,0 @@
package job
import (
"encoding/json"
"fmt"
"time"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/xray"
"gorm.io/gorm"
)
type SyncClientTrafficJob struct {
subClientsCollection map[string][]string
}
func NewClientTrafficSyncJob() *SyncClientTrafficJob {
return new(SyncClientTrafficJob)
}
func (j *SyncClientTrafficJob) Run() {
// Step 1: Group clients by SubID
subClientsCollection := j.collectClientsGroupedBySubId()
// Step 2: Sync client traffics for each SubID group
for subId, emails := range subClientsCollection {
err := j.syncClientTraffics(map[string][]string{subId: emails})
if err != nil {
logger.Error("Failed to sync traffics for SubID ", subId, ": ", err)
}
}
}
// collectClientsGroupedBySubId groups clients by their SubIDs
func (j *SyncClientTrafficJob) collectClientsGroupedBySubId() map[string][]string {
db := database.GetDB()
result := make(map[string][]string)
// Fetch all inbounds
var inbounds []*model.Inbound
if err := db.Model(&model.Inbound{}).Find(&inbounds).Error; err != nil {
logger.Error("Error fetching inbounds: ", err)
return result // Return empty map on error
}
// Process each inbound
for _, inbound := range inbounds {
if inbound.Settings == "" {
continue
}
settingsMap, err := parseSettings(inbound.Settings, uint(inbound.Id))
if err != nil {
logger.Error(err)
continue
}
clients, ok := settingsMap["clients"].([]interface{})
if !ok {
continue
}
processClients(clients, result)
}
// Remove SubIDs with one or fewer emails
filterSingleEmailSubIDs(result)
return result
}
// parseSettings unmarshals the JSON settings and returns it as a map
func parseSettings(settings string, inboundID uint) (map[string]interface{}, error) {
if !json.Valid([]byte(settings)) {
return nil, fmt.Errorf("Invalid JSON format in Settings for inbound ID %d", inboundID)
}
var tempData map[string]interface{}
if err := json.Unmarshal([]byte(settings), &tempData); err != nil {
return nil, fmt.Errorf("Error unmarshalling settings for inbound ID %d: %v", inboundID, err)
}
return tempData, nil
}
// processClients extracts SubID and email from the clients and populates the result map
func processClients(clients []interface{}, result map[string][]string) {
for _, client := range clients {
clientMap, ok := client.(map[string]interface{})
if !ok {
continue
}
subId, ok := clientMap["subId"].(string)
if !ok || subId == "" {
continue
}
email, ok := clientMap["email"].(string)
if !ok || email == "" {
continue
}
result[subId] = append(result[subId], email)
}
}
// filterSingleEmailSubIDs removes SubIDs with one or fewer emails from the result map
func filterSingleEmailSubIDs(result map[string][]string) {
for subId, emails := range result {
if len(emails) <= 1 {
delete(result, subId)
}
}
}
// syncClientTraffics synchronizes traffic data for each SubID group
func (j *SyncClientTrafficJob) syncClientTraffics(result map[string][]string) error {
for subId, emails := range result {
db := database.GetDB()
// Step 1: Calculate maxUp and maxDown (outside transaction)
var maxUp, maxDown int64
err := calculateMaxTraffic(db, emails, &maxUp, &maxDown)
if err != nil {
logger.Error("Failed to calculate max traffic for SubID ", subId, ": ", err)
continue
}
// Step 2: Update traffic data with retry mechanism
err = retryOperation(func() error {
return updateTraffic(db, emails, maxUp, maxDown)
}, 5, 100*time.Millisecond)
if err != nil {
logger.Error("Failed to update client traffics for SubID ", subId, ": ", err)
}
}
return nil
}
// calculateMaxTraffic calculates max up and down traffic for a group of emails
func calculateMaxTraffic(db *gorm.DB, emails []string, maxUp, maxDown *int64) error {
return db.Model(&xray.ClientTraffic{}).
Where("email IN ?", emails).
Select("MAX(up) AS max_up, MAX(down) AS max_down").
Row().
Scan(maxUp, maxDown)
}
// updateTraffic updates the traffic data in the database within a transaction
func updateTraffic(db *gorm.DB, emails []string, maxUp, maxDown int64) error {
return db.Transaction(func(tx *gorm.DB) error {
return tx.Model(&xray.ClientTraffic{}).
Where("email IN ?", emails).
Updates(map[string]interface{}{
"up": maxUp,
"down": maxDown,
}).Error
})
}
// retryOperation retries an operation multiple times with a delay
func retryOperation(operation func() error, maxRetries int, delay time.Duration) error {
var err error
for i := 0; i < maxRetries; i++ {
err = operation()
if err == nil {
return nil
}
logger.Info(fmt.Sprintf("Retry %d/%d failed: %v", i+1, maxRetries, err))
time.Sleep(delay)
}
return err
}

View File

@@ -50,7 +50,6 @@ var defaultValueMap = map[string]string{
"tgLang": "en-US",
"secretEnable": "false",
"subEnable": "false",
"subSyncEnable": "true",
"subListen": "",
"subPort": "2096",
"subPath": "/sub/",
@@ -417,14 +416,6 @@ func (s *SettingService) GetSubEnable() (bool, error) {
return s.getBool("subEnable")
}
func (s *SettingService) GetSubSyncEnable() (bool, error) {
return s.getBool("subSyncEnable")
}
func (s *SettingService) SetSubSyncEnable(value bool) error {
return s.setBool("subSyncEnable", value)
}
func (s *SettingService) GetSubListen() (string, error) {
return s.getString("subListen")
}
@@ -553,7 +544,6 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
"defaultKey": func() (interface{}, error) { return s.GetKeyFile() },
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotEnabled() },
"subEnable": func() (interface{}, error) { return s.GetSubEnable() },
"subSyncEnable": func() (interface{}, error) { return s.GetSubSyncEnable() },
"subURI": func() (interface{}, error) { return s.GetSubURI() },
"subJsonURI": func() (interface{}, error) { return s.GetSubJsonURI() },
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },

View File

@@ -190,9 +190,6 @@
[pages.client]
"add" = "Add Client"
"groupAdd" = "Add subscription user"
"isGroupEdit" = "Group editing"
"isGroupEditDesc" = "All clients with the same subscription are edited"
"edit" = "Edit Client"
"submitAdd" = "Add Client"
"submitEdit" = "Save Changes"
@@ -290,8 +287,6 @@
"subSettings" = "Subscription"
"subEnable" = "Enable Subscription Service"
"subEnableDesc" = "Enables the subscription service."
"subSyncEnable" = "Enable Subscription Sync"
"subSyncEnableDesc" = "Traffic from clients with the same subscription will be synchronized every 10 seconds."
"subListen" = "Listen IP"
"subListenDesc" = "The IP address for the subscription service. (leave blank to listen on all IPs)"
"subPort" = "Listen Port"
@@ -373,6 +368,8 @@
"errorLogDesc" = "The file path for the error log. The special value 'none' disabled error logs"
"dnsLog" = "DNS Log"
"dnsLogDesc" = "Whether to enable DNS query logs"
"outboundTraffic" = "Outbounds Traffic"
"outboundTrafficDesc" = "Whether to enable outbound traffic"
"maskAddress" = "Mask Address"
"maskAddressDesc" = "IP address mask, when enabled, will automatically replace the IP address that appears in the log."

View File

@@ -190,7 +190,6 @@
[pages.client]
"add" = "Agregar Cliente"
"groupAdd" = "Agregar usuario de suscripción"
"edit" = "Editar Cliente"
"submitAdd" = "Agregar Cliente"
"submitEdit" = "Guardar Cambios"
@@ -288,8 +287,6 @@
"subSettings" = "Suscripción"
"subEnable" = "Habilitar Servicio"
"subEnableDesc" = "Función de suscripción con configuración separada."
"subSyncEnable" = "Habilitar sincronización de suscripciones"
"subSyncEnableDesc" = "El tráfico de los clientes con la misma suscripción se sincronizará cada 10 segundos."
"subListen" = "Listening IP"
"subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs."
"subPort" = "Puerto de Suscripción"
@@ -371,6 +368,8 @@
"errorLogDesc" = "La ruta del archivo para el registro de errores. El valor especial 'none' desactiva los registros de errores."
"dnsLog" = "Registro DNS"
"dnsLogDesc" = "Si habilitar los registros de consulta DNS"
"outboundTraffic" = "Tráfico saliente"
"outboundTrafficDesc" = "Si se debe habilitar el tráfico saliente"
"maskAddress" = "Enmascarar Dirección"
"maskAddressDesc" = "Máscara de dirección IP, cuando se habilita, reemplazará automáticamente la dirección IP que aparece en el registro."

View File

@@ -190,9 +190,6 @@
[pages.client]
"add" = "کاربر جدید"
"groupAdd" = "افزودن کاربر سابسکریپشن"
"isGroupEdit" = "ویرایش گروهی"
"isGroupEditDesc" = "همه کاربران با سابسکریپشن یکسان ویرایش می شوند"
"edit" = "ویرایش کاربر"
"submitAdd" = "اضافه کردن"
"submitEdit" = "ذخیره تغییرات"
@@ -290,8 +287,6 @@
"subSettings" = "سابسکریپشن"
"subEnable" = "فعال‌سازی سرویس سابسکریپشن"
"subEnableDesc" = "سرویس سابسکریپشن‌ را فعال‌می‌کند"
"subSyncEnable" = "فعال‌سازی همگام سازی سابسکریپشن"
"subSyncEnableDesc" = "ترافیک کلاینت هایی که سابسکریپشن یکسان دارند هر ۱۰ ثانیه همگام می‌شوند."
"subListen" = "آدرس آی‌پی"
"subListenDesc" = "آدرس آی‌پی برای سرویس سابسکریپشن. برای گوش دادن به‌تمام آی‌پی‌ها خالی‌بگذارید"
"subPort" = "پورت"
@@ -373,6 +368,8 @@
"errorLogDesc" = "مسیر فایل برای ورود به سیستم خطا. مقدار ویژه «هیچ» گزارش های خطا را غیرفعال میکند"
"dnsLog" = "گزارش DNS"
"dnsLogDesc" = "آیا ثبت‌های درخواست DNS را فعال کنید"
"outboundTraffic" = "ترافیک خروجی"
"outboundTrafficDesc" = "فعال کردن ترافیک خروجی"
"maskAddress" = "پنهان کردن آدرس"
"maskAddressDesc" = "پوشش آدرس IP، هنگامی که فعال می‌شود، به طور خودکار آدرس IP که در لاگ ظاهر می‌شود را جایگزین می‌کند."

View File

@@ -190,9 +190,6 @@
[pages.client]
"add" = "Tambah Klien"
"groupAdd" = "Tambahkan pengguna langganan"
"isGroupEdit" = "Pengeditan grup"
"isGroupEditDesc" = "Semua klien dengan langganan yang sama akan diedit"
"edit" = "Edit Klien"
"submitAdd" = "Tambah Klien"
"submitEdit" = "Simpan Perubahan"
@@ -290,8 +287,6 @@
"subSettings" = "Langganan"
"subEnable" = "Aktifkan Layanan Langganan"
"subEnableDesc" = "Mengaktifkan layanan langganan."
"subSyncEnable" = "Aktifkan Sinkronisasi Langganan"
"subSyncEnableDesc" = "Lalu lintas dari klien dengan langganan yang sama akan disinkronkan setiap 10 detik."
"subListen" = "IP Pendengar"
"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)"
"subPort" = "Port Pendengar"
@@ -373,6 +368,8 @@
"errorLogDesc" = "Jalur file untuk log kesalahan. Nilai khusus 'tidak ada' menonaktifkan log kesalahan"
"dnsLog" = "Log DNS"
"dnsLogDesc" = "Apakah akan mengaktifkan log kueri DNS"
"outboundTraffic" = "Lalu Lintas Keluar"
"outboundTrafficDesc" = "Apakah mengaktifkan lalu lintas keluar"
"maskAddress" = "Alamat Masker"
"maskAddressDesc" = "Masker alamat IP, ketika diaktifkan, akan secara otomatis mengganti alamat IP yang muncul di log."

View File

@@ -190,9 +190,6 @@
[pages.client]
"add" = "クライアント追加"
"groupAdd" = "サブスクリプション ユーザーの追加"
"isGroupEdit" = "グループの編集"
"isGroupEditDesc" = "同じサブスクリプションを持つすべてのクライアントが編集されます"
"edit" = "クライアント編集"
"submitAdd" = "クライアント追加"
"submitEdit" = "変更を保存"
@@ -290,8 +287,6 @@
"subSettings" = "サブスクリプション設定"
"subEnable" = "サブスクリプションサービスを有効にする"
"subEnableDesc" = "サブスクリプションサービス機能を有効にする"
"subSyncEnable" = "サブスクリプション同期を有効にする"
"subSyncEnableDesc" = "同じサブスクリプションを持つクライアントからのトラフィックは 10 秒ごとに同期されます。"
"subListen" = "監視IP"
"subListenDesc" = "サブスクリプションサービスが監視するIPアドレス空白にするとすべてのIPを監視"
"subPort" = "監視ポート"
@@ -373,6 +368,8 @@
"errorLogDesc" = "エラーログのファイルパス。特殊値 'none' はエラーログを無効にします"
"dnsLog" = "DNS ログ"
"dnsLogDesc" = "DNSクエリのログを有効にするかどうか"
"outboundTraffic" = "アウトバウンドトラフィック"
"outboundTrafficDesc" = "アウトバウンドトラフィックを有効にするかどうか"
"maskAddress" = "アドレスをマスク"
"maskAddressDesc" = "IPアドレスをマスクし、有効にするとログに表示されるIPアドレスを自動的に置き換えます"

View File

@@ -190,9 +190,6 @@
[pages.client]
"add" = "Adicionar Cliente"
"groupAdd" = "Adicionar usuário de assinatura"
"isGroupEdit" = "Edição de grupo"
"isGroupEditDesc" = "Todos os clientes com a mesma assinatura são editados"
"edit" = "Editar Cliente"
"submitAdd" = "Adicionar Cliente"
"submitEdit" = "Salvar Alterações"
@@ -290,8 +287,6 @@
"subSettings" = "Assinatura"
"subEnable" = "Ativar Serviço de Assinatura"
"subEnableDesc" = "Ativa o serviço de assinatura."
"subSyncEnable" = "Habilitar sincronização de assinatura"
"subSyncEnableDesc" = "O tráfego de clientes com a mesma assinatura será sincronizado a cada 10 segundos."
"subListen" = "IP de Escuta"
"subListenDesc" = "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)"
"subPort" = "Porta de Escuta"
@@ -373,6 +368,8 @@
"errorLogDesc" = "O caminho do arquivo para o log de erros. O valor especial 'none' desativa os logs de erro."
"dnsLog" = "Log DNS"
"dnsLogDesc" = "Se ativar logs de consulta DNS"
"outboundTraffic" = "Tráfego de saída"
"outboundTrafficDesc" = "Se deve habilitar o tráfego de saída"
"maskAddress" = "Mascarar Endereço"
"maskAddressDesc" = "Máscara de endereço IP, quando ativado, substitui automaticamente o endereço IP que aparece no log."

View File

@@ -190,9 +190,6 @@
[pages.client]
"add" = "Добавить пользователя"
"groupAdd" = "Добавить пользователя подписки"
"isGroupEdit" = "Групповое редактирование"
"isGroupEditDesc" = "Все клиенты с одинаковой подпиской редактируются"
"edit" = "Редактировать пользователя"
"submitAdd" = "Добавить пользователя"
"submitEdit" = "Сохранить изменения"
@@ -290,8 +287,6 @@
"subSettings" = "Подписка"
"subEnable" = "Включить службу"
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
"subSyncEnable" = "Включить синхронизацию подписки"
"subSyncEnableDesc" = "Трафик от клиентов с одинаковой подпиской будет синхронизироваться каждые 10 секунд."
"subListen" = "Прослушивание IP"
"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса"
"subPort" = "Порт подписки"
@@ -373,6 +368,8 @@
"errorLogDesc" = "Путь к файлу журнала ошибок. Специальное значение «none» отключает журналы ошибок."
"dnsLog" = "DNS Журнал"
"dnsLogDesc" = "Включить логи запросов DNS"
"outboundTraffic" = "Исходящий трафик"
"outboundTrafficDesc" = "Включить исходящий трафик"
"maskAddress" = "Маскировать Адрес"
"maskAddressDesc" = "Маска IP-адреса, при активации автоматически заменяет IP-адрес, который появляется в логе."

View File

@@ -190,9 +190,6 @@
[pages.client]
"add" = "Müşteri Ekle"
"groupAdd" = "Abonelik kullanıcısı ekle"
"isGroupEdit" = "Grup düzenleme"
"isGroupEditDesc" = "Aynı aboneliğe sahip tüm istemciler düzenlendi"
"edit" = "Müşteriyi Düzenle"
"submitAdd" = "Müşteri Ekle"
"submitEdit" = "Değişiklikleri Kaydet"
@@ -290,8 +287,6 @@
"subSettings" = "Abonelik"
"subEnable" = "Abonelik Hizmetini Etkinleştir"
"subEnableDesc" = "Abonelik hizmetini etkinleştirir."
"subSyncEnable" = "Abonelik Senkronizasyonunu Etkinleştir"
"subSyncEnableDesc" = "Aynı aboneliğe sahip istemcilerden gelen trafik her 10 saniyede bir senkronize edilecektir."
"subListen" = "Dinleme IP"
"subListenDesc" = "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)"
"subPort" = "Dinleme Portu"
@@ -373,6 +368,8 @@
"errorLogDesc" = "Hata günlüğü için dosya yolu. 'none' özel değeri hata günlüklerini devre dışı bırakır"
"dnsLog" = "DNS Günlüğü"
"dnsLogDesc" = "DNS sorgu günlüklerini etkinleştirin"
"outboundTraffic" = "Gidenler trafiği"
"outboundTrafficDesc" = ıkış trafiğini etkinleştirip etkinleştirmeyeceğiniz"
"maskAddress" = "Adres Maskesi"
"maskAddressDesc" = "IP adresi maskesi, etkinleştirildiğinde, günlükte görünen IP adresini otomatik olarak değiştirecektir."

View File

@@ -190,9 +190,6 @@
[pages.client]
"add" = "Додати клієнта"
"groupAdd" = "Додати підписаного користувача"
"isGroupEdit" = "Редагування групи"
"isGroupEditDesc" = "Всі клієнти з однаковою підпискою редагуються"
"edit" = "Редагувати клієнта"
"submitAdd" = "Додати клієнта"
"submitEdit" = "Зберегти зміни"
@@ -290,8 +287,6 @@
"subSettings" = "Підписка"
"subEnable" = "Увімкнути службу підписки"
"subEnableDesc" = "Вмикає службу підписки."
"subSyncEnable" = "Увімкнути синхронізацію підписки"
"subSyncEnableDesc" = "Трафік від клієнтів з однаковою підпискою буде синхронізовано кожні 10 секунд."
"subListen" = "Слухати IP"
"subListenDesc" = "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)"
"subPort" = "Слухати порт"
@@ -373,6 +368,8 @@
"errorLogDesc" = "Шлях до файлу журналу помилок. Спеціальне значення 'none' вимикає журнали помилок"
"dnsLog" = "Журнал DNS"
"dnsLogDesc" = "Чи включити журнали запитів DNS"
"outboundTraffic" = "Вихідний трафік"
"outboundTrafficDesc" = "Чи потрібно увімкнути вихідний трафік"
"maskAddress" = "Маскувати Адресу"
"maskAddressDesc" = "Маска IP-адреси, при активації автоматично замінює IP-адресу, яка з'являється у журналі."

View File

@@ -190,9 +190,6 @@
[pages.client]
"add" = "Thêm người dùng"
"groupAdd" = "Thêm người dùng đăng ký"
"isGroupEdit" = "Chỉnh sửa nhóm"
"isGroupEditDesc" = "Tất cả người dùng có cùng đăng ký đều có thể được chỉnh sửa"
"edit" = "Chỉnh sửa người dùng"
"submitAdd" = "Thêm"
"submitEdit" = "Lưu thay đổi"
@@ -290,8 +287,6 @@
"subSettings" = "Gói đăng ký"
"subEnable" = "Bật dịch vụ"
"subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng"
"subSyncEnable" = "Bật đồng bộ đăng ký"
"subSyncEnableDesc" = "Lưu lượng truy cập từ các máy khách có cùng đăng ký sẽ được đồng bộ sau mỗi 10 giây."
"subListen" = "Listening IP"
"subListenDesc" = "Mặc định để trống để nghe tất cả các IP"
"subPort" = "Cổng gói đăng ký"
@@ -373,6 +368,8 @@
"errorLogDesc" = "Đường dẫn tệp cho nhật ký lỗi. Nhật ký lỗi bị vô hiệu hóa có giá trị đặc biệt 'không'"
"dnsLog" = "Nhật ký DNS"
"dnsLogDesc" = "Có bật nhật ký truy vấn DNS không"
"outboundTraffic" = "Lưu lượng đi ra"
"outboundTrafficDesc" = "Có nên bật lưu lượng ra không"
"maskAddress" = "Ẩn Địa Chỉ"
"maskAddressDesc" = "Mặt nạ địa chỉ IP, khi được bật, sẽ tự động thay thế địa chỉ IP xuất hiện trong nhật ký."

View File

@@ -190,9 +190,6 @@
[pages.client]
"add" = "添加客户端"
"groupAdd" = "添加订阅用户"
"isGroupEdit" = "群组编辑"
"isGroupEditDesc" = "所有具有相同订阅的客户端均被编辑"
"edit" = "编辑客户端"
"submitAdd" = "添加客户端"
"submitEdit" = "保存修改"
@@ -290,8 +287,6 @@
"subSettings" = "订阅设置"
"subEnable" = "启用订阅服务"
"subEnableDesc" = "启用订阅服务功能"
"subSyncEnable" = "启用订阅同步"
"subSyncEnableDesc" = "具有相同订阅的客户端的流量将每 10 秒同步一次。"
"subListen" = "监听 IP"
"subListenDesc" = "订阅服务监听的 IP 地址(留空表示监听所有 IP"
"subPort" = "监听端口"
@@ -373,6 +368,8 @@
"errorLogDesc" = "错误日志的文件路径。特殊值 'none' 禁用错误日志"
"dnsLog" = "DNS 日志"
"dnsLogDesc" = "是否启用 DNS 查询日志"
"outboundTraffic" = "出站流量"
"outboundTrafficDesc" = "是否启用出站流量"
"maskAddress" = "隐藏地址"
"maskAddressDesc" = "IP 地址掩码,启用时会自动替换日志中出现的 IP 地址。"

View File

@@ -190,9 +190,6 @@
[pages.client]
"add" = "新增客戶端"
"groupAdd" = "新增訂閱使用者"
"isGroupEdit" = "群組編輯"
"isGroupEditDesc" = "所有具有相同訂閱的用戶端都被編輯"
"edit" = "編輯客戶端"
"submitAdd" = "新增客戶端"
"submitEdit" = "儲存修改"
@@ -290,8 +287,6 @@
"subSettings" = "訂閱設定"
"subEnable" = "啟用訂閱服務"
"subEnableDesc" = "啟用訂閱服務功能"
"subSyncEnable" = "啟用訂閱同步"
"subSyncEnableDesc" = "來自具有相同訂閱的客戶端的流量將每 10 秒同步一次。"
"subListen" = "監聽 IP"
"subListenDesc" = "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP"
"subPort" = "監聽埠"
@@ -373,6 +368,8 @@
"errorLogDesc" = "錯誤日誌的檔案路徑。特殊值 'none' 禁用錯誤日誌"
"dnsLog" = "DNS 日誌"
"dnsLogDesc" = "是否啟用 DNS 查詢日誌"
"outboundTraffic" = "出站流量"
"outboundTrafficDesc" = "是否啟用出站流量"
"maskAddress" = "隱藏地址"
"maskAddressDesc" = "IP 地址掩碼,啟用時會自動替換日誌中出現的 IP 地址。"

View File

@@ -260,13 +260,6 @@ func (s *Server) startTask() {
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
}()
isSubEnable, err1 := s.settingService.GetSubEnable()
isSubSyncEnable, err2 := s.settingService.GetSubSyncEnable()
if err1 == nil && err2 == nil && isSubEnable && isSubSyncEnable {
// Sync the client traffic with the same SubId every 10 seconds
s.cron.AddJob("@every 10s", job.NewClientTrafficSyncJob())
}
// check client ips from log file every 10 sec
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())