Compare commits

..

316 Commits

Author SHA1 Message Date
MHSanaei
31339d6bf8 v1.6.1 2023-05-25 19:16:09 +03:30
MHSanaei
8865443438 update dependencies 2023-05-25 19:15:22 +03:30
Tara Rostami
835deb77f4 Minor correction In UI (#537)
* Update antd.min.css

* Update settings.html

* Update custom.css
2023-05-25 16:21:29 +03:30
MHSanaei
6a7c3716ac fix MHz view 2023-05-25 16:13:54 +03:30
MHSanaei
15211f81b1 New - CPU Speed
logical Processors  removed
2023-05-25 15:48:23 +03:30
Tara Rostami
b3f7a6572e Minor changes in the UI (#535)
* Update antd.min.css

* Update settings.html
2023-05-25 13:15:24 +03:30
MHSanaei
6f28a3a2fe change blockedips to AllowedIps
now only those IPs that are allowed are able to establish a connection; other connections are dropped it will happen every 10 sec
after user offline that IPs will be removed from AllowedIps
2023-05-25 03:21:31 +03:30
MHSanaei
896cc5386c new - show cores, public ipv4 and ipv6
logical Processors
you can see them on first page
2023-05-25 03:11:09 +03:30
dependabot[bot]
76f70ce1e9 Bump github.com/pelletier/go-toml/v2 from 2.0.7 to 2.0.8 (#523)
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.0.7 to 2.0.8.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.0.7...v2.0.8)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-24 13:35:36 +03:30
dependabot[bot]
6aa3c8d4a2 Bump svenstaro/upload-release-action from 2.5.0 to 2.6.0 (#514)
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.5.0 to 2.6.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.5.0...2.6.0)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-24 01:37:30 +03:30
MHSanaei
aff9d0ea15 v1.6.0 2023-05-23 18:09:04 +03:30
MHSanaei
526426e2dd Alireza 💯
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-23 17:55:42 +03:30
MHSanaei
2223a21cfc bug fix - limit ip 2023-05-23 17:54:15 +03:30
MHSanaei
47ccc7b501 [feature] fallback link calculation
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-23 03:15:34 +03:30
MHSanaei
c38e1e0cfe a lot of improvement 2023-05-23 02:43:15 +03:30
MHSanaei
f36034541e update - SSL Certificate Management
Get SSL
Revoke
Force Renew
2023-05-23 02:34:36 +03:30
MHSanaei
783fa856c3 Fix deprecated gRPC Dial options for Insecure connections 2023-05-23 00:52:15 +03:30
MHSanaei
6139effb8a update - style
tcp,ws,tls
2023-05-22 22:36:15 +03:30
MHSanaei
8ae7f4a564 small fixes 2023-05-22 21:52:35 +03:30
MHSanaei
6f46f3e636 sub
remaining code
2023-05-22 21:51:52 +03:30
Hamidreza
ba278a4269 Merge pull request #509 from hamid-gh98/main
FIX restart redirect :)
2023-05-22 21:32:17 +03:30
Hamidreza Ghavami
3825e36ef7 FIX restart redirect :) 2023-05-22 22:30:35 +04:30
MHSanaei
f6e0e1b3cf translate update
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-22 18:16:24 +03:30
MHSanaei
769590d779 [feature] separate subscription service
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-22 18:06:34 +03:30
MHSanaei
1fa9101b40 [feature] add multi domain tls (CDN ready)
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-22 17:31:41 +03:30
MHSanaei
3f2e1aede9 features - random ShortId button 2023-05-22 16:44:27 +03:30
MHSanaei
235e6880c1 update - utils 2023-05-22 16:41:41 +03:30
MHSanaei
ffa23a43c6 update - URIComponent just for cookie value 2023-05-22 16:40:08 +03:30
MHSanaei
66e3c505e3 update dependencies 2023-05-22 14:41:23 +03:30
MHSanaei
0938519f49 bug fix - KeyContent 2023-05-22 11:39:21 +03:30
Ho3ein
5f489c3d08 Merge pull request #491 from hamid-gh98/main
[tgbot] Multi language + More...
2023-05-22 11:26:14 +03:30
Hamidreza Ghavami
f82d0051b2 Update hashstorage functionality 2023-05-21 08:33:08 +04:30
Hamidreza Ghavami
76267b23a0 fix tgbot 2023-05-21 07:41:59 +04:30
Hamidreza Ghavami
40a926a54a Update README.md 2023-05-21 06:50:57 +04:30
Hamidreza Ghavami
3a835fbeb8 FIX ERROR 'msg and reply are same' in tgbot 2023-05-21 05:48:19 +04:30
Hamidreza Ghavami
f6461b8386 FIX hashStorage remove expire job 2023-05-21 05:33:01 +04:30
Hamidreza Ghavami
586663c840 Update translations 2023-05-21 03:30:39 +04:30
Hamidreza Ghavami
82ead05093 Update tgbot locale + add I18nBot 2023-05-21 03:30:26 +04:30
Hamidreza Ghavami
d9b1b200ce rename I18n to I18nWeb 2023-05-21 03:29:27 +04:30
Hamidreza Ghavami
786a3ac992 FIX hashStorage 2023-05-20 21:46:42 +04:30
Hamidreza Ghavami
8c5648eb09 FIX callback query and BUTTON_DATA_INVALID error with hashStorage 2023-05-20 20:29:28 +04:30
Hamidreza Ghavami
4dfe527f20 init i18n in tgbot 2023-05-20 20:08:01 +04:30
Hamidreza Ghavami
980ebd99ca add tgBot localizer 2023-05-20 19:59:04 +04:30
Hamidreza Ghavami
9b7cddbec7 update to use WebI18n func 2023-05-20 19:51:45 +04:30
Hamidreza Ghavami
2bd706aa92 update I18n function for controller 2023-05-20 19:49:39 +04:30
Hamidreza Ghavami
95e0d9a468 create LocalizerMiddleware func 2023-05-20 19:46:34 +04:30
Hamidreza Ghavami
4865754b3d add localizer middleware to web.go 2023-05-20 19:46:05 +04:30
Hamidreza Ghavami
92eaff9608 replace new localizer to web.go 2023-05-20 19:43:59 +04:30
Hamidreza Ghavami
0b7aa8a9e0 Refactor i18n localizer 2023-05-20 19:41:08 +04:30
Hamidreza Ghavami
678962d4ca some fix and prune for tgbot 2023-05-20 19:39:01 +04:30
Hamidreza Ghavami
795835c54f Update translations 2023-05-20 19:21:10 +04:30
Hamidreza Ghavami
91360a3f49 add tgLang to settings 2023-05-20 19:20:54 +04:30
Hamidreza Ghavami
4831c2f1b2 Add tgLang option 2023-05-20 19:15:20 +04:30
Hamidreza Ghavami
3166d497f9 Update .gitignore 2023-05-20 19:03:31 +04:30
Hamidreza Ghavami
f50ccce9ec Add manual list for ipv4 and warp and fixed it 2023-05-20 19:02:37 +04:30
Hamidreza Ghavami
c7e300f14d FIX redirect after restart panel 2023-05-20 18:58:51 +04:30
Hamidreza Ghavami
419a1938ee update settings ui 2023-05-20 18:57:10 +04:30
Hamidreza Ghavami
a48745cb3e fix tls settings 2023-05-20 18:46:50 +04:30
Hamidreza Ghavami
ac9408c37f update sub remark for shadowsocks 2023-05-20 18:44:18 +04:30
dependabot[bot]
3d71289075 Bump github.com/Workiva/go-datastructures from 1.0.53 to 1.1.0 (#481)
Bumps [github.com/Workiva/go-datastructures](https://github.com/Workiva/go-datastructures) from 1.0.53 to 1.1.0.
- [Release notes](https://github.com/Workiva/go-datastructures/releases)
- [Commits](https://github.com/Workiva/go-datastructures/compare/v1.0.53...v1.1.0)

---
updated-dependencies:
- dependency-name: github.com/Workiva/go-datastructures
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-19 18:31:56 +03:30
Minobi
fbd3772788 Updated Russian translation of the web UI (#478) 2023-05-19 06:45:33 +03:30
Mahdi Nemati
334b28cddc Api for backup Telegram (#468) 2023-05-19 00:31:05 +03:30
MHSanaei
2fbfc88bc1 bug fix - xtls 2023-05-18 12:18:13 +03:30
MHSanaei
f781979d38 update - config 2023-05-18 11:52:53 +03:30
Ho3ein
aba37be6eb Merge pull request #467 from mmrabbani/patch-1
Simplifying ad blocking syntax
2023-05-18 00:59:01 +03:30
mmrabbani
30042bc047 Update traffic+block-ads+warp.json 2023-05-18 00:51:36 +03:30
mmrabbani
49f60f7775 Update traffic+block-ads+ipv4-google.json 2023-05-18 00:50:57 +03:30
mmrabbani
5f44c80cd5 Simplifying ad blocking syntax
I simplified the ad blocking syntax.
"category-ads-all" includes "category-ads". Also "category-ads" includes "google-ads" and "spotify-ads".
So if "category-ads-all" is blocked, there is no need to block 3 other lists (category-ads,google-ads,spotify-ads)

References:
https://github.com/v2fly/domain-list-community/blob/master/data/category-ads
https://github.com/v2fly/domain-list-community/blob/master/data/category-ads-all
2023-05-18 00:48:37 +03:30
MHSanaei
85d42ce94f bug fix - expiry Time 2023-05-17 18:57:34 +03:30
MHSanaei
2f3c3d0ed2 v1.5.0 2023-05-17 18:19:39 +03:30
MHSanaei
7debf96610 fix bug - mobile view 2023-05-17 18:17:12 +03:30
Ho3ein
d8b60c3cd5 Update docker.yml
always use the latest version :D
2023-05-17 16:34:44 +03:30
MHSanaei
f8eb548376 [feature] SpiderX for Reality
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 16:10:11 +03:30
MHSanaei
88fc4f81d4 [feature] filter inbound clients
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 15:57:47 +03:30
MHSanaei
911f2b0bb5 adguard Family protection 2023-05-17 03:05:36 +03:30
MHSanaei
2d16eabc6e [feature] interactive deplete soon
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 02:01:56 +03:30
MHSanaei
be50be75fe [sub] add more headers
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 01:07:35 +03:30
MHSanaei
70e7987df5 [feature] multi cert per inbound
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-17 00:55:19 +03:30
MHSanaei
8ceeb454ee random - trojan password 2023-05-17 00:09:32 +03:30
MHSanaei
f311bf1dbf update - ui 2023-05-16 23:40:27 +03:30
MHSanaei
837d7f77a1 update - xray configuration
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-16 23:39:59 +03:30
Ho3ein
d0213ce50b Merge pull request #452 from MHSanaei/dependabot/github_actions/actions/setup-go-4.0.1
Bump actions/setup-go from 4.0.0 to 4.0.1
2023-05-16 14:36:51 +03:30
dependabot[bot]
622e440366 Bump actions/setup-go from 4.0.0 to 4.0.1
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4.0.0...v4.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-16 10:04:13 +00:00
MHSanaei
1dc5452f1d 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
2023-05-15 15:59:27 +03:30
Ho3ein
a0daf2fae2 Merge pull request #438 from masoud-hidden/main
[tgbot] Some new changes in the bot
2023-05-14 23:58:54 +03:30
Masoud Hidden
a21bdc9396 [tgbot] Fix reply keyboard height 2023-05-14 22:55:01 +03:30
Masoud Hidden
4cf7f75749 [tgbot] Ability to remove telegram user from the client in the bot 2023-05-14 22:43:23 +03:30
Masoud Hidden
bcdac5aad6 [tgbot] Ability to select telegram user for client from bot 2023-05-14 22:07:49 +03:30
Masoud Hidden
3d5f851fce [tgbot] Changed to telego 2023-05-14 18:50:01 +03:30
Ho3ein
5487dc41cc Merge pull request #434 from hamid-gh98/main
[FIX] siderDrawer button functionality + [Update] redirect restart panel
2023-05-14 10:49:30 +03:30
Hamidreza Ghavami
5a908b9f58 FIX redirect after restart panel 2023-05-14 06:19:18 +04:30
Hamidreza Ghavami
61288db11e FIX siderDrawer button function 2023-05-14 06:08:49 +04:30
Hamidreza Ghavami
317f7fe9da FIX sideBar style 2023-05-14 06:08:34 +04:30
Ho3ein
7b5dd2d0ee Merge pull request #432 from hamid-gh98/main
[HOTFIX] Add basePath to Redirect Middleware
2023-05-14 01:51:02 +03:30
Hamidreza Ghavami
b1302c70fb Merge branch 'main' of https://github.com/hamid-gh98/3x-ui into main 2023-05-14 02:32:17 +04:30
Hamidreza Ghavami
addedb1adf HOTFIX redirect middleware to add basePath 2023-05-14 02:31:23 +04:30
MHSanaei
62bb42cfab v1.4.6 2023-05-14 01:10:51 +03:30
MHSanaei
f4be9f234a lang show 2023-05-14 01:09:31 +03:30
MHSanaei
947129a62a update UI - calendar 2023-05-14 01:08:29 +03:30
Ho3ein
66f0a13145 Merge pull request #431 from hamid-gh98/main
[HOTFIX] Redirect `/xui` to `/panel`
2023-05-14 01:07:17 +03:30
Hamidreza Ghavami
9626379731 Update README.md 2023-05-14 01:42:29 +04:30
Hamidreza Ghavami
c2c61cdd5b Add Redirect Middleware for Router 2023-05-14 01:42:08 +04:30
Hamidreza Ghavami
b5ae580d12 Update '/xui/API' to new path '/panel/api' 2023-05-14 01:41:18 +04:30
MHSanaei
63939244a4 v1.4.5 2023-05-13 22:31:13 +03:30
MHSanaei
213b693bd3 Merge branch 'main' of https://github.com/MHSanaei/3x-ui 2023-05-13 19:06:19 +03:30
MHSanaei
a289ef5d10 bug fixed - random user pass 2023-05-13 19:06:16 +03:30
Ho3ein
955eb8f142 Merge pull request #428 from LOVECHEN/main
Update docker-compose.yml
2023-05-13 18:43:53 +03:30
LOVECHEN
d396fb5d06 Update docker-compose.yml
Define your hostname to identify the host in telegram
2023-05-13 22:55:47 +08:00
Ho3ein
b5dd258074 Merge pull request #426 from hamid-gh98/main
FIX input bg color in login page
2023-05-13 18:15:39 +03:30
MHSanaei
c855a292cb random sub button 2023-05-13 17:22:13 +03:30
Hamidreza Ghavami
f2132c62e9 fix input style 2023-05-13 18:00:47 +04:30
Hamidreza Ghavami
94a3807353 fix input bg color in login page 2023-05-13 17:42:11 +04:30
MHSanaei
7cacfc074e remove duplicate random text gen
randomText by default
length set to 8
2023-05-13 15:42:46 +03:30
MHSanaei
9e8ac8a087 remove search Data files 2023-05-13 15:03:46 +03:30
MHSanaei
e64a9eeee6 random UUID 2023-05-13 14:51:07 +03:30
MHSanaei
a55a1a7102 fix 2023-05-13 13:53:17 +03:30
MHSanaei
46bc39c160 [bug] fix cloned inbound settings
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:33:13 +03:30
MHSanaei
2a182d8b9a [bug] fix login failure when tgbot is not active
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:31:46 +03:30
MHSanaei
77241c7fcf pruning some codes
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:29:10 +03:30
MHSanaei
fd6a85afd9 Set session max-age to default if defined zero
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:26:04 +03:30
MHSanaei
9a89d7bfab spin only in reload time
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:24:44 +03:30
MHSanaei
edd6b22109 remove duplicate remark assignments
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-13 13:21:27 +03:30
Ho3ein
5468069bef Merge pull request #420 from hamid-gh98/main
[fix] russia domains in settings and More....
2023-05-13 13:18:36 +03:30
Hamidreza Ghavami
0cce35784e FIX Login UI style 2023-05-13 00:52:12 +04:30
Hamidreza Ghavami
80c1e58ed5 Update README.md 2023-05-12 22:45:33 +04:30
Hamidreza Ghavami
b0871a6ef6 Change route path '/xui' to '/panel' 2023-05-12 22:45:32 +04:30
Hamidreza Ghavami
288374d5fa Update README.md 2023-05-12 22:45:32 +04:30
Hamidreza Ghavami
1f7c79c735 Add docker-compose.yml 2023-05-12 22:45:31 +04:30
Hamidreza Ghavami
251fd608df update translation 2023-05-12 22:45:30 +04:30
Hamidreza Ghavami
456941323b await secret status in login page 2023-05-12 22:45:30 +04:30
Hamidreza Ghavami
a6a77688dc Add block speedtest switch template 2023-05-12 22:45:29 +04:30
Hamidreza Ghavami
09cd2248dc fix show client name in QR modal 2023-05-12 22:45:28 +04:30
Hamidreza Ghavami
8143379645 Add copy button for sub link 2023-05-12 22:45:28 +04:30
Hamidreza Ghavami
5bd6baa055 Fix darkClass in ThemeSwitcher 2023-05-12 22:45:27 +04:30
Hamidreza Ghavami
41e9290574 Show client email in QR Modal 2023-05-12 22:45:26 +04:30
Hamidreza Ghavami
cf7d50617b add service function to search data files 2023-05-12 22:45:26 +04:30
Hamidreza Ghavami
95e006963c add searchDatafiles route 2023-05-12 22:45:25 +04:30
Hamidreza Ghavami
65588a4492 add check for geosite function 2023-05-12 22:45:24 +04:30
Hamidreza Ghavami
d39c7e4ae3 only get enabled inbounds and clients 2023-05-12 22:45:24 +04:30
Tara Rostami
3bec9ee273 Minor changes in UI (#415)
* Update custom.css

* Update setting.html

* Update settings.html

* Update antd.min.css

* Update antd.min.css

* Update settings.html

* Update custom.css

* Update custom.css

* Update antd.min.css

* Update setting.html

* Update custom.css

* Update custom.css

* Update antd.min.css
2023-05-12 09:13:08 +03:30
Tara Rostami
7b3628d33b Optimized Settings UI (#408)
* Update custom.css

* Update setting.html

* Update settings.html

* Update antd.min.css

* Update antd.min.css

* Update settings.html

* Update custom.css

* Update custom.css

* Update antd.min.css

* Update setting.html

* Update custom.css
2023-05-11 14:04:35 +03:30
Hossein Abaiyani
ad1aa5b2f9 Cleaner Docker file with much lighter base image (#387)
* updated dockerfile

* updated dockerfile

* Update Dockerfile

added platform

* added iran.dat

* added iran.dat

---------

Co-authored-by: Hossein Abaiyani <hossein.abaiyani@arvancloud.com>
2023-05-11 13:08:44 +03:30
Ho3ein
46ef8c503e Merge pull request #392 from MHSanaei/dependabot/go_modules/gorm.io/gorm-1.25.1
Bump gorm.io/gorm from 1.25.0 to 1.25.1
2023-05-09 19:57:19 +03:30
dependabot[bot]
721fec3b5a 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-09 10:03:47 +00:00
Ho3ein
30a5f66f26 Merge pull request #381 from hamid-gh98/main
[FIX] bug logout path + [UPDATE] login UI and more ...
2023-05-08 19:40:02 +03:30
Hamidreza Ghavami
bb6e6861ca fix style bg :D 2023-05-08 20:19:11 +04:30
Hamidreza Ghavami
4c0e391597 update theme-switch 2023-05-08 19:44:18 +04:30
Hamidreza Ghavami
43c1fc9aad Merge branch 'main' of https://github.com/hamid-gh98/3x-ui into main 2023-05-08 19:41:14 +04:30
Hamidreza Ghavami
7a48cbb191 fix style login ui 2023-05-08 19:38:36 +04:30
Hamidreza Ghavami
004d69392b fix use password component 2023-05-08 19:26:01 +04:30
Hamidreza Ghavami
fc0882805d update UI to use password-input component 2023-05-08 19:24:44 +04:30
Hamidreza Ghavami
f553922d53 add password component 2023-05-08 19:21:58 +04:30
Hamidreza Ghavami
7b2764566c update login UI 2023-05-08 19:20:13 +04:30
Hamidreza Ghavami
55d38dfa48 [FIX] bug logout path 2023-05-08 19:15:33 +04:30
Hamidreza Ghavami
0e266b88f0 update UI to use themeSwitcher 2023-05-08 19:14:22 +04:30
MHSanaei
7bb3e517b2 update pic v1.4.1 2023-05-08 17:39:29 +03:30
Hamidreza Ghavami
7d0c3b6517 remove themeChanger from siderDrawer 2023-05-08 18:19:59 +04:30
Hamidreza Ghavami
67201fc678 create theme-switch component 2023-05-08 18:15:08 +04:30
Hamidreza Ghavami
d137deccfa fix style height when rotating + move cookie util to their specific file 2023-05-08 18:04:12 +04:30
MHSanaei
00777e3a25 [feature] Russian lang 2023-05-08 14:43:02 +03:30
koid38
bcb2f125ff Create translate.ru_RU.toml (#375)
ru translate
2023-05-08 14:05:16 +03:30
MHSanaei
37ab8f42e9 domain-list-community (category update)
add cn and ru regexp
2023-05-08 13:50:43 +03:30
Ho3ein
cf1cfbee96 v1.4.1 2023-05-08 10:55:28 +03:30
MHSanaei
d89e03023f + 2023-05-08 10:40:32 +03:30
Tara Rostami
5ec1559c7b UI optimized by Tara (#370)
* Update antd.min.css

* Update custom.css

---------

Co-authored-by: Ho3ein <ho3ein.sanaei@gmail.com>
2023-05-08 10:32:51 +03:30
Ho3ein
cf2b1fd9ec Merge pull request #364 from M4hbod/main
Update UI
2023-05-08 10:20:47 +03:30
itspooya
ddbc1602ce Finally - Fix WorkDIR 2023-05-08 10:04:47 +03:30
itspooya
77cee098ad Finally 2023-05-08 10:04:47 +03:30
itspooya
89b94c2c90 Enable CGO 2023-05-08 10:04:47 +03:30
itspooya
350743fea3 Enable CGO 2023-05-08 10:04:47 +03:30
itspooya
8011d0b6c6 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
9e5d7ac1d0 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
e01fb9b605 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
52a468d586 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
c73c71cc83 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
2141d62069 Fixed Dockerfile 2023-05-08 10:04:47 +03:30
itspooya
f286c9a86a Added ARM64 2023-05-08 10:04:47 +03:30
itspooya
b4fd254c71 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
0a8cf7e41b 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
abd48551fd 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
bb9a10051f 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
5c6406ab58 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
a25137f215 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
50c296eb28 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
b1a302de95 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
7b567458ff 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
13e3e23f20 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
efecdf5fd0 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
f21904caf2 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
15a97af215 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
20a55c086e 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
c727b81772 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
itspooya
77692e0298 👷 Added Docker CI 2023-05-08 10:04:47 +03:30
Mahbod
5e3e965647 add margin to .ant-modal 2023-05-08 03:36:52 +03:30
Mahbod
88cde18bb2 update .ant-modal ui 2023-05-08 02:55:06 +03:30
MHSanaei
7fd93e25fd only blake3-aes support multi-user 2023-05-07 23:57:59 +03:30
Ho3ein
1c9643e6a3 Update docker.yml 2023-05-07 22:55:39 +03:30
Ho3ein
eb83516fa5 Merge pull request #360 from itspooya/main
Added Github Container registry auto build
2023-05-07 22:45:39 +03:30
itspooya
1b8df3c0a1 👷 Added Docker CI 2023-05-07 21:30:58 +03:30
itspooya
54d45cc029 👷 Added Docker CI 2023-05-07 20:45:30 +03:30
itspooya
e35767aff2 👷 Added Docker CI 2023-05-07 20:29:37 +03:30
MHSanaei
9bbcb74db6 sni for xtls 2023-05-07 19:59:06 +03:30
MHSanaei
515e7f7fef client bulk bug fix 2023-05-07 19:58:56 +03:30
MHSanaei
87a5190b7d v1.4.0 2023-05-07 17:12:47 +03:30
MHSanaei
9d47d74a7a design update 2023-05-07 14:36:43 +03:30
Ho3ein
92f5dfed1c Merge pull request #356 from M4hbod/main
Update UI
2023-05-07 14:12:56 +03:30
Mahbod
867e5ea022 update tabs 2023-05-07 11:34:36 +03:30
Mahbod
a24814104c update input 2023-05-07 11:33:52 +03:30
MHSanaei
024b65524d delayedStart 2023-05-06 20:33:56 +03:30
MHSanaei
f22dd6b53d [feature] multi-user shadowsocks @alireza0
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-06 20:21:14 +03:30
MHSanaei
735df6bd4e [feature] inbounds manual refresh
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-05-06 19:20:42 +03:30
MHSanaei
0e77547e98 update translate 2023-05-06 16:25:41 +03:30
MHSanaei
747a1e1f60 ant design selected - active item menu - new update
https://ant.design/components/menu
2023-05-06 16:17:51 +03:30
Ho3ein
ac31d6d9fb Merge pull request #347 from hamid-gh98/main
[Feature] import/export database in the panel
2023-05-06 12:53:41 +03:30
Hamidreza Ghavami
83c853ffb6 update ImportDB and enhancement 2023-05-06 04:47:57 +04:30
Hamidreza
058ab5f901 Merge branch 'MHSanaei:main' into main 2023-05-06 03:37:04 +03:30
Ho3ein
78638a9737 Merge pull request #346 from masoud-hidden/main
Some new buttons for bot and ability to use userId in tgId
2023-05-06 02:48:14 +03:30
Masoud Hidden
7f8f0b0f2d Fix ability to use userId in tgId 2023-05-06 02:36:46 +03:30
Masoud Hidden
7b9e0b946e Merge branch 'MHSanaei:main' into main 2023-05-06 01:36:22 +03:30
Hamidreza Ghavami
6c087ceb1a fix import db and always restart xray 2023-05-06 02:22:45 +04:30
Hamidreza
6602c55f3c Update main.go 2023-05-06 00:00:56 +03:30
Hamidreza
d405141ad0 Merge branch 'MHSanaei:main' into main 2023-05-05 23:57:06 +03:30
Ho3ein
3ee3432d8f Merge pull request #348 from MHSanaei/revert-331-main
Revert "feat: Adding Cobra to provide a more intuitive command line interface"
2023-05-05 23:35:20 +03:30
Ho3ein
b2d70a2a9b Revert "feat: Adding Cobra to provide a more intuitive command line interface" 2023-05-05 23:34:38 +03:30
Hamidreza Ghavami
26f160fb89 add MigrateDB func for a single source of truth 2023-05-06 00:22:39 +04:30
Hamidreza Ghavami
0a5811adf8 update style 2023-05-05 23:16:15 +04:30
Hamidreza Ghavami
733a011b28 update .gitignore 2023-05-05 22:59:31 +04:30
Hamidreza Ghavami
c8023b7c8d update README.md 2023-05-05 22:53:07 +04:30
Hamidreza Ghavami
bed3cd445d update translation 2023-05-05 22:52:48 +04:30
Hamidreza Ghavami
c8baf5ceee add modal and button for import/export db 2023-05-05 22:52:35 +04:30
Hamidreza Ghavami
85c715a2f6 update axios-init and db.go 2023-05-05 22:51:39 +04:30
Hamidreza Ghavami
55c1fe26fb add import db api route 2023-05-05 22:49:42 +04:30
Masoud Hidden
8d11f83ac7 Fix get client ips 2023-05-05 20:07:47 +03:30
Masoud Hidden
d349bffcd6 Fix bot client enable button 2023-05-05 19:50:40 +03:30
Masoud Hidden
5856160c30 Added some new buttons to bot and ability to use userId in tgId 2023-05-05 18:20:56 +03:30
MHSanaei
3cd3693b2c setting style fix bug 2023-05-05 17:48:01 +03:30
Masoud Hidden
bc3003be54 Change some InlineKeyboard data length 2023-05-05 16:02:16 +03:30
Ho3ein
e53615d119 Merge pull request #336 from masoud-hidden/main
Buttons for the client report in the telegram bot
2023-05-05 14:41:19 +03:30
Ho3ein
55232f9033 Merge pull request #339 from M4hbod/main
Update UI
2023-05-05 14:34:42 +03:30
Mahbod
fd40e97008 update ant-card 2023-05-05 13:44:09 +03:30
Mahbod
1598ad804d update ant-tabs 2023-05-05 13:43:21 +03:30
Mahbod
c8e666d8ae update selected menu item 2023-05-05 13:42:04 +03:30
Mahbod
e1533b9418 update menu background 2023-05-05 13:41:10 +03:30
Ho3ein
a60a8d8a2f Merge pull request #331 from kaveh-ahangar/main
feat: Adding Cobra to provide a more intuitive command line interface
2023-05-05 13:23:27 +03:30
kaveh-ahangar
73704e38d5 Merge branch 'MHSanaei:main' into main 2023-05-05 13:06:59 +03:30
kaveh-ahangar
1680bb36c3 feat: Rollback files (.gitignore) 2023-05-05 12:43:09 +03:30
kaveh-ahangar
cd483c191a feat: Rollback files (.gitignore) 2023-05-05 12:42:05 +03:30
kaveh-ahangar
7d09b4e840 feat: Rollback files (main.go , Makefile) 2023-05-05 12:41:21 +03:30
kaveh-ahangar
83ffa25d6f feat: Rollback files (main.go , Makefile) 2023-05-05 12:40:20 +03:30
Masoud Hidden
a53d2b927f fix ResetClientExpiryTimeByEmail 2023-05-05 04:34:39 +03:30
Mahbod
146dc6ce4a improve circle bar ui 2023-05-05 03:54:32 +03:30
Mahbod
e597ea5ab2 improve ant-tabs ui 2023-05-05 03:52:31 +03:30
Mahbod
3e833fca9b add custom scrollbar 2023-05-05 03:51:56 +03:30
Mahbod
c96cf85619 improve .ant-card-dark ui 2023-05-05 03:51:24 +03:30
Mahbod
0a63c75138 improve menu ui 2023-05-05 03:46:57 +03:30
Masoud Hidden
c6295085fe Fix restart service 2023-05-05 02:47:26 +03:30
Masoud Hidden
4b3bdebfa5 Fix typo 2023-05-05 02:14:56 +03:30
Ho3ein
1a603b2501 Merge pull request #335 from MHSanaei/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-05 02:05:00 +03:30
dependabot[bot]
3fa5f834b8 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:34:24 +00:00
Masoud Hidden
ff33539fba Refresh button for client report in telegram bot
Added unlimited button for expire days.
2023-05-05 01:48:37 +03:30
Masoud Hidden
961636b510 Client reset buttons for telegram bot 2023-05-05 01:16:43 +03:30
Ho3ein
88b7d0dc44 Merge pull request #328 from MHSanaei/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-05 00:49:54 +03:30
Ho3ein
e164c7e780 Merge pull request #332 from hamid-gh98/main
[Update] sub remark + settings UI
2023-05-05 00:49:23 +03:30
Hamidreza Ghavami
481d4beabb update translation 2023-05-04 22:22:54 +04:30
Hamidreza Ghavami
19c991014e updated settings.html UI 2023-05-04 21:34:15 +04:30
Hamidreza Ghavami
12ec487241 updated custom.css 2023-05-04 21:28:36 +04:30
Hamidreza Ghavami
a18cbdcf11 changed number input tags to 'a-input-number' 2023-05-04 21:27:42 +04:30
Hamidreza Ghavami
4f8de18d1f renamed setting.html to settings.html and update its route name 2023-05-04 21:09:08 +04:30
Hamidreza Ghavami
dbac48f05d updated sub remark to include inbound name 2023-05-04 20:58:16 +04:30
kaveh-ahangar
3a02359325 feat: Adding Cobra to provide a more intuitive command line interface 2023-05-04 19:46:45 +03:30
kaveh-ahangar
9e63b0e2b3 feat: add Makefile and improve building 2023-05-04 19:20:52 +03:30
dependabot[bot]
07dc8c4803 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-04 10:04:21 +00:00
MHSanaei
20bfd71cf1 update dependencies 2023-05-02 16:26:19 +03:30
MHSanaei
6a33a48a9a logout after update your password or secret token 2023-05-02 16:19:49 +03:30
MHSanaei
1885a8c0bf fixed - set Default Cert for Xtls 2023-05-01 20:58:00 +03:30
Ho3ein
4ce53920fe v1.3.4 2023-05-01 02:29:24 +03:30
MHSanaei
5100bbba52 simplify ssl cert 2023-04-30 00:57:15 +03:30
MHSanaei
f93d912644 primary type for setDefaultCert 2023-04-29 22:51:33 +03:30
MHSanaei
ce8551b8c4 [darkmode] better colors + add sec to calendar
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 22:38:41 +03:30
MHSanaei
d26c21d900 [feature] inbounds auto refresh option
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 22:33:45 +03:30
MHSanaei
f5f9347661 Merge branch 'main' of https://github.com/MHSanaei/3x-ui 2023-04-29 22:28:55 +03:30
MHSanaei
a0f5875cb3 [darkmode] fix UTLS - cipherSuites 2023-04-29 22:28:50 +03:30
Ho3ein
3055c68615 Postman Collection thanks to @mehdikhody #303
thanks to @mehdikhody #303
2023-04-29 20:18:24 +03:30
MHSanaei
c3ed8051f3 [feature] add sniffing DestOverride options #298
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 18:47:44 +03:30
MHSanaei
d2cdc51c54 [feature] add quic to sniffingObject
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 16:26:39 +03:30
MHSanaei
ee896662f5 remove favicon from web root
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-28 18:49:32 +03:30
MHSanaei
177bd036a3 [bug] fix GetClientTrafficByEmail
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-28 18:40:33 +03:30
MHSanaei
d03e049320 v1.3.3 2023-04-28 01:03:59 +03:30
MHSanaei
957d9e24fb Revert "grpc.WithInsecure is deprecated"
This reverts commit 0b896d9c31.
2023-04-28 00:47:56 +03:30
MHSanaei
865e47e9a6 Update check_client_ip_job.go 2023-04-28 00:30:49 +03:30
MHSanaei
607c5d3598 [feature] add grpc multiMode 2023-04-28 00:15:06 +03:30
MHSanaei
8879541999 dark mode - default 2023-04-27 23:48:58 +03:30
MHSanaei
0b896d9c31 grpc.WithInsecure is deprecated 2023-04-27 23:48:22 +03:30
MHSanaei
6f4a2809e2 tls for ss - remove unused 2023-04-27 19:25:48 +03:30
MHSanaei
103a26edb6 [migrate] remove orphaned traffics
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-27 19:05:36 +03:30
MHSanaei
7419592626 v1.3.2 2023-04-26 13:00:51 +03:30
MHSanaei
edabfad559 fix expiry time
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-26 12:57:49 +03:30
MHSanaei
2849cc0b2e v1.3.1 2023-04-26 02:12:38 +03:30
MHSanaei
46eb174af1 remove unused 2023-04-26 02:11:11 +03:30
MHSanaei
f8878208ca upgrade sonic to 1.8.8 2023-04-26 02:10:25 +03:30
MHSanaei
e126095949 bug fix 2023-04-26 02:09:56 +03:30
MHSanaei
df2f292b68 v1.3.0 2023-04-25 21:46:24 +03:30
MHSanaei
9f5ba0cf93 [bug] vision-udp443 only for client + Translation
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 21:43:29 +03:30
MHSanaei
b5c5539501 add hostname to page title
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 20:29:13 +03:30
MHSanaei
252afe47c0 [api] support for delete depleted clients
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 18:46:09 +03:30
MHSanaei
379451135d [feature] delete depleted clients
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 18:43:37 +03:30
MHSanaei
bc06dbab21 [migration] add fix for omitted traffics
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 18:36:06 +03:30
MHSanaei
6a71ea7f5e [feature] reset traffics of all client
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 17:23:38 +03:30
MHSanaei
942b9862d8 [feature] add login session timeout
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 15:00:21 +03:30
MHSanaei
ae55fdc38a fix bug in http link without host
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 14:39:09 +03:30
MHSanaei
cc3ff61ae2 update by client id
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 14:38:35 +03:30
MHSanaei
045717010a Revert "bug fix - h2 sub"
This reverts commit cd5ad78f56.
2023-04-25 14:21:57 +03:30
MHSanaei
aae32d9211 Merge branch 'main' of https://github.com/MHSanaei/3x-ui 2023-04-25 01:19:15 +03:30
MHSanaei
cd5ad78f56 bug fix - h2 sub 2023-04-25 01:19:07 +03:30
Ho3ein
b450aacebd Update README.md 2023-04-25 00:25:55 +03:30
Ho3ein
640068b279 Update README.md 2023-04-25 00:25:40 +03:30
110 changed files with 7763 additions and 3383 deletions

41
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Release X-ui dockerhub
on:
push:
tags:
- "*"
workflow_dispatch:
jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Check out the code
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.5.0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4.4.0
with:
images: ghcr.io/${{ github.repository }}
- name: Build and push Docker image
uses: docker/build-push-action@v4.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64, linux/arm64/v8
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@v3.5.2
- name: Set up Go
uses: actions/setup-go@v4.0.0
uses: actions/setup-go@v4.0.1
with:
go-version: "stable"
- name: build linux amd64 version
@@ -38,7 +38,7 @@ jobs:
- name: package
run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui
- name: upload
uses: svenstaro/upload-release-action@2.5.0
uses: svenstaro/upload-release-action@2.6.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
@@ -52,7 +52,7 @@ jobs:
steps:
- uses: actions/checkout@v3.5.2
- name: Set up Go
uses: actions/setup-go@v4.0.0
uses: actions/setup-go@v4.0.1
with:
go-version: "stable"
- name: build linux arm64 version
@@ -80,7 +80,7 @@ jobs:
- name: package
run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui
- name: upload
uses: svenstaro/upload-release-action@2.5.0
uses: svenstaro/upload-release-action@2.6.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}

17
.gitignore vendored
View File

@@ -1,14 +1,15 @@
.idea
.vscode
.cache
.sync*
*.tar.gz
access.log
error.log
tmp
main
backup/
bin/
dist/
x-ui-*.tar.gz
/x-ui
/release.sh
.sync*
main
release/
access.log
error.log
.cache
/release.sh
/x-ui

22
DockerInit.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/sh
if [ $1 == "amd64" ]; then
ARCH="64";
FNAME="amd64";
elif [ $1 == "arm64" ]; then
ARCH="arm64-v8a"
FNAME="arm64";
else
ARCH="64";
FNAME="amd64";
fi
mkdir -p build/bin
cd build/bin
wget "https://github.com/mhsanaei/xray-core/releases/latest/download/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat iran.dat
mv xray "xray-linux-${FNAME}"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
wget "https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat"
cd ../../

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
#Build latest x-ui from source
FROM --platform=$BUILDPLATFORM golang:1.20.4-alpine AS builder
WORKDIR /app
ARG TARGETARCH
RUN apk --no-cache --update add build-base gcc wget unzip
COPY . .
RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
RUN ./DockerInit.sh "$TARGETARCH"
#Build app image using latest x-ui
FROM alpine
ENV TZ=Asia/Tehran
WORKDIR /app
RUN apk add ca-certificates tzdata
COPY --from=builder /app/build/ /app/
VOLUME [ "/etc/x-ui" ]
ENTRYPOINT [ "/app/x-ui" ]

View File

@@ -1,16 +1,20 @@
# 3x-ui
> **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**
[![](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)
> **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**
3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
**If you think this project is helpful to you, you may wish to give a** :star2:
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
**Buy Me a Coffee :**
- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
# Install & Upgrade
@@ -20,10 +24,10 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
## Install custom version
To install your desired version you can add the version to the end of install command. Example for ver `v1.2.6`:
To install your desired version you can add the version to the end of install command. Example for ver `v1.6.0`:
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.2.6
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.6.0
```
# SSL
@@ -34,6 +38,8 @@ certbot certonly --standalone --agree-tos --register-unsafely-without-email -d y
certbot renew --dry-run
```
or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
# Default settings
- Port: 2053
@@ -43,12 +49,12 @@ certbot renew --dry-run
Before you set ssl on settings
- http://ip:2053/xui
- http://domain:2053/xui
- http://ip:2053/panel
- http://domain:2053/panel
After you set ssl on settings
- https://yourdomain:2053/xui
- https://yourdomain:2053/panel
# Environment Variables
@@ -65,6 +71,31 @@ Example:
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
# Install with Docker
1. Install Docker:
```sh
bash <(curl -sSL https://get.docker.com)
```
2. Run 3x-ui:
```sh
docker compose up -d
```
OR
```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
```
# Xray Configurations:
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
@@ -114,6 +145,7 @@ If you want to use routing to WARP follow steps as below:
- For more advanced configuration items, please refer to the panel
- Fix api routes (user setting will create with api)
- Support to change configs by different items provided in panel
- Support export/import database from panel
# Tg robot use
@@ -134,6 +166,7 @@ Reference syntax:
- 0 \*/10 \* \* \* \* //Notify at the first second of each 10 minutes
- @hourly // hourly notification
- @daily // Daily notification (00:00 in the morning)
- @weekly // weekly notification
- @every 8h // notify every 8 hours
# Telegram Bot Features
@@ -154,23 +187,34 @@ Reference syntax:
## API routes
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
- `/xui/API/inbounds` base for following actions:
- `/panel/api/inbounds` base for following actions:
| Method | Path | Action |
| :----: | ---------------------------------- | ------------------------------------------- |
| `GET` | `"/list"` | Get all inbounds |
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
| `GET` | `"/getClientTraffics/:email"` | Get Client Traffics with email |
| `GET` | `"/createbackup"` | Telegram bot sends backup to admins |
| `POST` | `"/add"` | Add inbound |
| `POST` | `"/del/:id"` | Delete Inbound |
| `POST` | `"/update/:id"` | Update Inbound |
| `POST` | `"/clientIps/:email"` | Client Ip address |
| `POST` | `"/clearClientIps/:email"` | Clear Client Ip address |
| `POST` | `"/addClient/"` | Add Client to inbound |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by UID/Password as clientId |
| `POST` | `"/updateClient/:index"` | Update Client |
| `POST` | `"/addClient"` | Add Client to inbound |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId\* |
| `POST` | `"/updateClient/:clientId"` | Update Client by clientId\* |
| `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
| `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
- [Postman Collection](https://gist.github.com/mehdikhody/9a862801a2e41f6b5fb6bbc7e1326044)
# A Special Thanks To
@@ -183,20 +227,14 @@ Reference syntax:
- CentOS 8+
- Fedora 36+
# Buy Me a Coffee
[![](https://img.shields.io/badge/Wallet-USDT__TRC20-green.svg)](#)
```
TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC
```
# Pictures
![1](./media/1.png)
![2](./media/2.png)
![3](./media/3.png)
![4](./media/4.png)
![5](./media/5.png)
![6](./media/6.png)
## Stargazers over time

View File

@@ -1 +1 @@
1.2.8
1.6.1

View File

@@ -1,6 +1,8 @@
package database
import (
"bytes"
"io"
"io/fs"
"os"
"path"
@@ -15,7 +17,16 @@ import (
var db *gorm.DB
var initializers = []func() error{
initUser,
initInbound,
initSetting,
initInboundClientIps,
initClientTraffic,
}
func initUser() error {
err := db.AutoMigrate(&model.User{})
if err != nil {
return err
@@ -52,7 +63,7 @@ func initClientTraffic() error {
func InitDB(dbPath string) error {
dir := path.Dir(dbPath)
err := os.MkdirAll(dir, fs.ModeDir)
err := os.MkdirAll(dir, fs.ModePerm)
if err != nil {
return err
}
@@ -73,25 +84,10 @@ func InitDB(dbPath string) error {
return err
}
err = initUser()
if err != nil {
return err
}
err = initInbound()
if err != nil {
return err
}
err = initSetting()
if err != nil {
return err
}
err = initInboundClientIps()
if err != nil {
return err
}
err = initClientTraffic()
if err != nil {
return err
for _, initialize := range initializers {
if err := initialize(); err != nil {
return err
}
}
return nil
@@ -104,3 +100,13 @@ func GetDB() *gorm.DB {
func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound
}
func IsSQLiteDB(file io.ReaderAt) (bool, error) {
signature := []byte("SQLite format 3\x00")
buf := make([]byte, len(signature))
_, err := file.ReadAt(buf, 0)
if err != nil {
return false, err
}
return bytes.Equal(buf, signature), nil
}

16
docker-compose.yml Normal file
View File

@@ -0,0 +1,16 @@
---
version: "3.9"
services:
3x-ui:
image: ghcr.io/mhsanaei/3x-ui:latest
container_name: 3x-ui
hostname: yourhostname
volumes:
- $PWD/db/:/etc/x-ui/
- $PWD/cert/:/root/cert/
environment:
XRAY_VMESS_AEAD_FORCED: "false"
tty: true
network_mode: host
restart: unless-stopped

43
go.mod
View File

@@ -3,34 +3,37 @@ module x-ui
go 1.20
require (
github.com/Workiva/go-datastructures v1.0.53
github.com/Workiva/go-datastructures v1.1.0
github.com/gin-contrib/sessions v0.0.4
github.com/gin-gonic/gin v1.9.0
github.com/go-cmd/cmd v1.4.1
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/goccy/go-json v0.10.2
github.com/mymmrac/telego v0.24.0
github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.0.7
github.com/pelletier/go-toml/v2 v2.0.8
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
go.uber.org/atomic v1.10.0
go.uber.org/atomic v1.11.0
golang.org/x/text v0.9.0
google.golang.org/grpc v1.54.0
gorm.io/driver/sqlite v1.5.0
gorm.io/gorm v1.25.0
google.golang.org/grpc v1.55.0
gorm.io/driver/sqlite v1.5.1
gorm.io/gorm v1.25.1
)
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/bytedance/sonic v1.8.7 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/bytedance/sonic v1.8.10 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/fasthttp/router v1.4.19 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.12.0 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
@@ -38,26 +41,30 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.3 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/shoenig/go-m1cpu v0.1.5 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.47.0 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
google.golang.org/genproto v0.0.0-20230524185152-1884fd1fac28 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

89
go.sum
View File

@@ -1,16 +1,17 @@
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/Workiva/go-datastructures v1.1.0 h1:hu20UpgZneBhQ3ZvwiOGlqJSKIosin2Rd5wAKUHEO/k=
github.com/Workiva/go-datastructures v1.1.0/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/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/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.8.10 h1:XFSQg4/rwpQnNWSybNDr8oz6QtQY9uRGfRKDVWVsvP8=
github.com/bytedance/sonic v1.8.10/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
@@ -18,7 +19,11 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
github.com/fasthttp/router v1.4.19 h1:RLE539IU/S4kfb4MP56zgP0TIBU9kEg0ID9GpWO0vqk=
github.com/fasthttp/router v1.4.19/go.mod h1:+Fh3YOd8x1+he6ZS+d2iUDBH9MGGZ1xQFUor0DE9rKE=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
@@ -42,11 +47,9 @@ 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/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@@ -81,20 +84,20 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/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.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
@@ -105,14 +108,16 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.24.0 h1:0fd+v2/dToL6/DtsnWr+2saK7ZxIgLY+LI9kqJQbPEo=
github.com/mymmrac/telego v0.24.0/go.mod h1:y557P/iMHSaOVDi5Nmy1gNelqrw+jaBMvP9guPaNJsQ=
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
@@ -133,14 +138,16 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk=
github.com/sagernet/sing-shadowsocks v0.2.1 h1:FvdLQOqpvxHBJUcUe4fvgiYP2XLLwH5i1DtXQviVEPw=
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
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.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU=
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/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8=
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
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=
@@ -150,8 +157,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
@@ -165,16 +173,21 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/xtls/reality v0.0.0-20230331223127-176a94313eda h1:psRJD2RrZbnI0OWyHvXfgYCPqlRM5q5SPDcjDoDBWhE=
github.com/xtls/xray-core v1.8.1 h1:iSTTqXj82ZdwC1ah+eV331X4JTcnrDz+WuKuB/EB3P4=
github.com/xtls/xray-core v1.8.1/go.mod h1:AXxSso0MZwUE4NhRocCfHCg73BtJ+T2dSpQVo1Cg9VM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
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.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
@@ -182,8 +195,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -193,8 +206,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -212,8 +225,9 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -233,10 +247,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/genproto v0.0.0-20230524185152-1884fd1fac28 h1:+55/MuGJORMxCrkAgo2595fMAnN/4rweCuwibbqrvpc=
google.golang.org/genproto v0.0.0-20230524185152-1884fd1fac28/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
@@ -250,11 +264,10 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
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.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/driver/sqlite v1.5.1 h1:hYyrLkAWE71bcarJDPdZNTLWtr8XrSjOWyjUYI6xdL4=
gorm.io/driver/sqlite v1.5.1/go.mod h1:7MZZ2Z8bqyfSQA1gYEV6MagQWj3cpUkJj9Z+d1HEMEQ=
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -25,9 +25,9 @@ echo "The OS release is: $release"
arch3xui() {
case "$(uname -m)" in
x86_64 | x64 | amd64 ) echo 'amd64' ;;
armv8 | arm64 | aarch64 ) echo 'arm64' ;;
* ) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
x86_64 | x64 | amd64) echo 'amd64' ;;
armv8 | arm64 | aarch64) echo 'arm64' ;;
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
esac
}
echo "arch: $(arch3xui)"
@@ -39,7 +39,7 @@ if [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1
fi
@@ -59,21 +59,20 @@ fi
install_base() {
case "${release}" in
centos|fedora)
yum install -y -q wget curl tar
;;
*)
apt install -y -q wget curl tar
;;
centos | fedora)
yum install -y -q wget curl tar
;;
*)
apt install -y -q wget curl tar
;;
esac
}
#This function will be called when user installed x-ui out of sercurity
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}"
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
echo -e "${yellow}Your username will be:${config_account}${plain}"
read -p "Please set up your password:" config_password
@@ -101,6 +100,7 @@ config_after_install() {
echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type x-ui and then type 7 to check${plain}"
fi
fi
/usr/local/x-ui/x-ui migrate
}
install_x-ui() {

View File

@@ -1,17 +1,29 @@
package logger
import (
"github.com/op/go-logging"
"os"
"sync"
"github.com/op/go-logging"
)
var logger *logging.Logger
var (
logger *logging.Logger
mu sync.Mutex
)
func init() {
InitLogger(logging.INFO)
}
func InitLogger(level logging.Level) {
mu.Lock()
defer mu.Unlock()
if logger != nil {
return
}
format := logging.MustStringFormatter(
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
)
@@ -20,39 +32,55 @@ func InitLogger(level logging.Level) {
backendFormatter := logging.NewBackendFormatter(backend, format)
backendLeveled := logging.AddModuleLevel(backendFormatter)
backendLeveled.SetLevel(level, "")
newLogger.SetBackend(backendLeveled)
newLogger.SetBackend(logging.MultiLogger(backendLeveled))
logger = newLogger
}
func Debug(args ...interface{}) {
logger.Debug(args...)
if logger != nil {
logger.Debug(args...)
}
}
func Debugf(format string, args ...interface{}) {
logger.Debugf(format, args...)
if logger != nil {
logger.Debugf(format, args...)
}
}
func Info(args ...interface{}) {
logger.Info(args...)
if logger != nil {
logger.Info(args...)
}
}
func Infof(format string, args ...interface{}) {
logger.Infof(format, args...)
if logger != nil {
logger.Infof(format, args...)
}
}
func Warning(args ...interface{}) {
logger.Warning(args...)
if logger != nil {
logger.Warning(args...)
}
}
func Warningf(format string, args ...interface{}) {
logger.Warningf(format, args...)
if logger != nil {
logger.Warningf(format, args...)
}
}
func Error(args ...interface{}) {
logger.Error(args...)
if logger != nil {
logger.Error(args...)
}
}
func Errorf(format string, args ...interface{}) {
logger.Errorf(format, args...)
if logger != nil {
logger.Errorf(format, args...)
}
}

30
main.go
View File

@@ -11,6 +11,7 @@ import (
"x-ui/config"
"x-ui/database"
"x-ui/logger"
"x-ui/sub"
"x-ui/v2ui"
"x-ui/web"
"x-ui/web/global"
@@ -50,6 +51,16 @@ func runWebServer() {
return
}
var subServer *sub.Server
subServer = sub.NewServer()
global.SetSubServer(subServer)
err = subServer.Start()
if err != nil {
log.Println(err)
return
}
sigCh := make(chan os.Signal, 1)
// Trap shutdown signals
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
@@ -62,6 +73,11 @@ func runWebServer() {
if err != nil {
logger.Warning("stop server err:", err)
}
err = subServer.Stop()
if err != nil {
logger.Warning("stop server err:", err)
}
server = web.NewServer()
global.SetWebServer(server)
err = server.Start()
@@ -69,8 +85,18 @@ func runWebServer() {
log.Println(err)
return
}
subServer = sub.NewServer()
global.SetSubServer(subServer)
err = subServer.Start()
if err != nil {
log.Println(err)
return
}
default:
server.Stop()
subServer.Stop()
return
}
}
@@ -133,7 +159,6 @@ func updateTgbotEnableSts(status bool) {
logger.Infof("SetTgbotenabled[%v] success", status)
}
}
return
}
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
@@ -212,8 +237,7 @@ func migrateDb() {
log.Fatal(err)
}
fmt.Println("Start migrating database...")
inboundService.MigrationRequirements()
inboundService.RemoveOrphanedTraffics()
inboundService.MigrateDB()
fmt.Println("Migration done!")
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 23 KiB

BIN
media/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
media/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -6,7 +6,11 @@
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
@@ -54,35 +58,41 @@
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
"protocol": [
"bittorrent"
]
},
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"geosite:category-ads-all",
"geosite:category-ads",
"geosite:google-ads",
"geosite:spotify-ads"
"ext:iran.dat:ads"
]
},
{
"type": "field",
"outboundTag": "IPv4",
"domain": ["geosite:google"]
"domain": [
"geosite:google"
]
}
]
},
"stats": {}
}
}

View File

@@ -6,7 +6,11 @@
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
@@ -59,40 +63,44 @@
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
"protocol": [
"bittorrent"
]
},
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"geosite:category-ads-all",
"geosite:category-ads",
"geosite:google-ads",
"geosite:spotify-ads"
"ext:iran.dat:ads"
]
},
{
"type": "field",
"outboundTag": "WARP",
"domain": [
"geosite:google",
"geosite:netflix",
"geosite:spotify",
"geosite:openai"
"geosite:netflix",
"geosite:openai",
"geosite:google"
]
}
]
},
"stats": {}
}
}

View File

@@ -6,7 +6,11 @@
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
@@ -47,18 +51,24 @@
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
"protocol": [
"bittorrent"
]
},
{
"type": "field",
@@ -73,4 +83,4 @@
]
},
"stats": {}
}
}

View File

@@ -6,7 +6,11 @@
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
@@ -47,30 +51,27 @@
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
"ip": [
"geoip:private",
"geoip:ir"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:ir"]
"protocol": [
"bittorrent"
]
}
]
},
"stats": {}
}
}

171
sub/sub.go Normal file
View File

@@ -0,0 +1,171 @@
package sub
import (
"context"
"crypto/tls"
"io"
"net"
"net/http"
"strconv"
"strings"
"x-ui/config"
"x-ui/logger"
"x-ui/util/common"
"x-ui/web/network"
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type Server struct {
httpServer *http.Server
listener net.Listener
sub *SUBController
settingService service.SettingService
ctx context.Context
cancel context.CancelFunc
}
func NewServer() *Server {
ctx, cancel := context.WithCancel(context.Background())
return &Server{
ctx: ctx,
cancel: cancel,
}
}
func (s *Server) initRouter() (*gin.Engine, error) {
if config.IsDebug() {
gin.SetMode(gin.DebugMode)
} else {
gin.DefaultWriter = io.Discard
gin.DefaultErrorWriter = io.Discard
gin.SetMode(gin.ReleaseMode)
}
engine := gin.Default()
subPath, err := s.settingService.GetSubPath()
if err != nil {
return nil, err
}
subDomain, err := s.settingService.GetSubDomain()
if err != nil {
return nil, err
}
if subDomain != "" {
validateDomain := func(c *gin.Context) {
host := strings.Split(c.Request.Host, ":")[0]
if host != subDomain {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Next()
}
engine.Use(validateDomain)
}
g := engine.Group(subPath)
s.sub = NewSUBController(g)
return engine, nil
}
func (s *Server) Start() (err error) {
//This is an anonymous function, no function name
defer func() {
if err != nil {
s.Stop()
}
}()
subEnable, err := s.settingService.GetSubEnable()
if err != nil {
return err
}
if !subEnable {
return nil
}
engine, err := s.initRouter()
if err != nil {
return err
}
certFile, err := s.settingService.GetSubCertFile()
if err != nil {
return err
}
keyFile, err := s.settingService.GetSubKeyFile()
if err != nil {
return err
}
listen, err := s.settingService.GetSubListen()
if err != nil {
return err
}
port, err := s.settingService.GetSubPort()
if err != nil {
return err
}
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
return err
}
if certFile != "" || keyFile != "" {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
listener.Close()
return err
}
c := &tls.Config{
Certificates: []tls.Certificate{cert},
}
listener = network.NewAutoHttpsListener(listener)
listener = tls.NewListener(listener, c)
}
if certFile != "" || keyFile != "" {
logger.Info("Sub server run https on", listener.Addr())
} else {
logger.Info("Sub server run http on", listener.Addr())
}
s.listener = listener
s.httpServer = &http.Server{
Handler: engine,
}
go func() {
s.httpServer.Serve(listener)
}()
return nil
}
func (s *Server) Stop() error {
s.cancel()
var err1 error
var err2 error
if s.httpServer != nil {
err1 = s.httpServer.Shutdown(s.ctx)
}
if s.listener != nil {
err2 = s.listener.Close()
}
return common.Combine(err1, err2)
}
func (s *Server) GetCtx() context.Context {
return s.ctx
}

View File

@@ -1,17 +1,14 @@
package controller
package sub
import (
"encoding/base64"
"strings"
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type SUBController struct {
BaseController
subService service.SubService
subService SubService
}
func NewSUBController(g *gin.RouterGroup) *SUBController {
@@ -21,7 +18,7 @@ func NewSUBController(g *gin.RouterGroup) *SUBController {
}
func (a *SUBController) initRouter(g *gin.RouterGroup) {
g = g.Group("/sub")
g = g.Group("/")
g.GET("/:subid", a.subs)
}
@@ -29,7 +26,7 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
func (a *SUBController) subs(c *gin.Context) {
subId := c.Param("subid")
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 {
c.String(400, "Error!")
} else {
@@ -38,8 +35,10 @@ func (a *SUBController) subs(c *gin.Context) {
result += sub + "\n"
}
// Add subscription-userinfo
c.Writer.Header().Set("Subscription-Userinfo", header)
// Add headers
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
c.Writer.Header().Set("Profile-Title", headers[2])
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
}

View File

@@ -1,4 +1,4 @@
package service
package sub
import (
"encoding/base64"
@@ -8,37 +8,53 @@ import (
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/web/service"
"x-ui/xray"
"github.com/goccy/go-json"
"gorm.io/gorm"
)
type SubService struct {
address string
inboundService InboundService
inboundService service.InboundService
settingServics service.SettingService
}
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
var result []string
var header string
var headers []string
var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId)
if err != nil {
return nil, "", err
return nil, nil, err
}
for _, inbound := range inbounds {
clients, err := s.inboundService.getClients(inbound)
clients, err := s.inboundService.GetClients(inbound)
if err != nil {
logger.Error("SubService - GetSub: Unable to get clients from inbound")
}
if clients == nil {
continue
}
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
fallbackMaster, err := s.getFallbackMaster(inbound.Listen)
if err == nil {
inbound.Listen = fallbackMaster.Listen
inbound.Port = fallbackMaster.Port
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
var masterStream map[string]interface{}
json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
stream["security"] = masterStream["security"]
stream["tlsSettings"] = masterStream["tlsSettings"]
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
inbound.StreamSettings = string(modifiedStream)
}
}
for _, client := range clients {
if client.SubID == subId {
if client.Enable && client.SubID == subId {
link := s.getLink(inbound, client.Email)
result = append(result, link)
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
@@ -66,15 +82,18 @@ 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)
return result, header, nil
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
updateInterval, _ := s.settingServics.GetSubUpdates()
headers = append(headers, fmt.Sprintf("%d", updateInterval))
headers = append(headers, subId)
return result, headers, nil
}
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error
if err != nil {
return nil, err
}
return inbounds, nil
@@ -89,6 +108,19 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
return xray.ClientTraffic{}
}
func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
db := database.GetDB()
var inbound *model.Inbound
err := db.Model(model.Inbound{}).
Where("JSON_TYPE(settings, '$.fallbacks') = 'array'").
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
Find(&inbound).Error
if err != nil {
return nil, err
}
return inbound, nil
}
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
switch inbound.Protocol {
case "vmess":
@@ -97,89 +129,105 @@ func (s *SubService) getLink(inbound *model.Inbound, email string) string {
return s.genVlessLink(inbound, email)
case "trojan":
return s.genTrojanLink(inbound, email)
case "shadowsocks":
return s.genShadowsocksLink(inbound, email)
}
return ""
}
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
address := s.address
if inbound.Protocol != model.VMess {
return ""
}
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
obj := map[string]interface{}{
"v": "2",
"ps": remark,
"add": s.address,
"port": inbound.Port,
"type": "none",
}
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
network, _ := stream["network"].(string)
typeStr := "none"
host := ""
path := ""
sni := ""
fp := ""
var alpn []string
allowInsecure := false
obj["net"] = network
switch network {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
typeStr, _ = header["type"].(string)
typeStr, _ := header["type"].(string)
obj["type"] = typeStr
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
path = requestPath[0].(string)
obj["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
host = searchHost(headers)
obj["host"] = searchHost(headers)
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
typeStr, _ = header["type"].(string)
path, _ = kcp["seed"].(string)
obj["type"], _ = header["type"].(string)
obj["path"], _ = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
path = ws["path"].(string)
obj["path"] = ws["path"].(string)
headers, _ := ws["headers"].(map[string]interface{})
host = searchHost(headers)
obj["host"] = searchHost(headers)
case "http":
network = "h2"
obj["net"] = "h2"
http, _ := stream["httpSettings"].(map[string]interface{})
path, _ = http["path"].(string)
host = searchHost(http)
obj["path"], _ = http["path"].(string)
obj["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
header := quic["header"].(map[string]interface{})
typeStr, _ = header["type"].(string)
host, _ = quic["security"].(string)
path, _ = quic["key"].(string)
obj["type"], _ = header["type"].(string)
obj["host"], _ = quic["security"].(string)
obj["path"], _ = quic["key"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
path = grpc["serviceName"].(string)
obj["path"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
obj["type"] = "multi"
}
}
security, _ := stream["security"].(string)
var domains []interface{}
obj["tls"] = security
if security == "tls" {
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{})
for _, a := range alpns {
alpn = append(alpn, a.(string))
if len(alpns) > 0 {
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
obj["alpn"] = strings.Join(alpn, ",")
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
sni, _ = sniValue.(string)
obj["sni"], _ = sniValue.(string)
}
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
fp, _ = fpValue.(string)
obj["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
allowInsecure, _ = insecure.(bool)
obj["allowInsecure"], _ = insecure.(bool)
}
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
domains, _ = domainSettings.([]interface{})
}
}
serverName, _ := tlsSetting["serverName"].(string)
if serverName != "" {
address = serverName
obj["add"] = serverName
}
}
clients, _ := s.inboundService.getClients(inbound)
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
@@ -187,24 +235,24 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
break
}
}
obj["id"] = clients[clientIndex].ID
obj["aid"] = clients[clientIndex].AlterIds
obj := map[string]interface{}{
"v": "2",
"ps": email,
"add": address,
"port": inbound.Port,
"id": clients[clientIndex].ID,
"aid": clients[clientIndex].AlterIds,
"net": network,
"type": typeStr,
"host": host,
"path": path,
"tls": security,
"sni": sni,
"fp": fp,
"alpn": strings.Join(alpn, ","),
"allowInsecure": allowInsecure,
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
obj["ps"] = remark + "-" + domain["remark"].(string)
obj["add"] = domain["domain"].(string)
if index > 0 {
links += "\n"
}
jsonStr, _ := json.MarshalIndent(obj, "", " ")
links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
}
return links
}
jsonStr, _ := json.MarshalIndent(obj, "", " ")
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
}
@@ -216,7 +264,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
}
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.getClients(inbound)
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
@@ -266,9 +314,13 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
}
security, _ := stream["security"].(string)
var domains []interface{}
if security == "tls" {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@@ -293,6 +345,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["allowInsecure"] = "1"
}
}
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
domains, _ = domainSettings.([]interface{})
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@@ -326,6 +381,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["fp"] = fp
}
}
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
params["spx"] = spx
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
@@ -360,6 +420,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["allowInsecure"] = "1"
}
}
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@@ -383,7 +446,22 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
// Set the new query values on the URL
url.RawQuery = q.Encode()
url.Fragment = email
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
url.Fragment = remark + "-" + domain["remark"].(string)
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
url.Fragment = remark
return url.String()
}
@@ -394,7 +472,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
}
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.getClients(inbound)
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
@@ -444,9 +522,13 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
}
security, _ := stream["security"].(string)
var domains []interface{}
if security == "tls" {
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
@@ -471,6 +553,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["allowInsecure"] = "1"
}
}
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
domains, _ = domainSettings.([]interface{})
}
}
serverName, _ := tlsSetting["serverName"].(string)
@@ -500,6 +585,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["fp"] = fp
}
}
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
params["spx"] = spx
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
@@ -534,6 +624,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["allowInsecure"] = "1"
}
}
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@@ -558,10 +651,49 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
// Set the new query values on the URL
url.RawQuery = q.Encode()
url.Fragment = email
remark := fmt.Sprintf("%s-%s", inbound.Remark, email)
if len(domains) > 0 {
links := ""
for index, d := range domains {
domain := d.(map[string]interface{})
url.Fragment = remark + "-" + domain["remark"].(string)
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
if index > 0 {
links += "\n"
}
links += url.String()
}
return links
}
url.Fragment = remark
return url.String()
}
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
address := s.address
if inbound.Protocol != model.Shadowsocks {
return ""
}
clients, _ := s.inboundService.GetClients(inbound)
var settings map[string]interface{}
json.Unmarshal([]byte(inbound.Settings), &settings)
inboundPassword := settings["password"].(string)
method := settings["method"].(string)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
clientIndex = i
break
}
}
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
remark := fmt.Sprintf("%s-%s", inbound.Remark, clients[clientIndex].Email)
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, remark)
}
func searchKey(data interface{}, key string) (interface{}, bool) {
switch val := data.(type) {
case map[string]interface{}:
@@ -590,7 +722,11 @@ func searchHost(headers interface{}) string {
switch v.(type) {
case []interface{}:
hosts, _ := v.([]interface{})
return hosts[0].(string)
if len(hosts) > 0 {
return hosts[0].(string)
} else {
return ""
}
case interface{}:
return v.(string)
}

View File

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

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

View File

View File

@@ -24,8 +24,8 @@ func getLinesNum(filename string) (int, error) {
var buffPosition int
for {
i := bytes.IndexByte(buf[buffPosition:], '\n')
if i < 0 || n == buffPosition {
i := bytes.IndexByte(buf[buffPosition:n], '\n')
if i < 0 {
break
}
buffPosition += i + 1
@@ -33,11 +33,12 @@ func getLinesNum(filename string) (int, error) {
}
if err == io.EOF {
return sum, nil
break
} else if err != nil {
return sum, err
return 0, err
}
}
return sum, nil
}
func GetTCPCount() (int, error) {
@@ -45,11 +46,11 @@ func GetTCPCount() (int, error) {
tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root))
if err != nil {
return tcp4, err
return 0, err
}
tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root))
if err != nil {
return tcp4 + tcp6, nil
return 0, err
}
return tcp4 + tcp6, nil
@@ -60,11 +61,11 @@ func GetUDPCount() (int, error) {
udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root))
if err != nil {
return udp4, err
return 0, err
}
udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root))
if err != nil {
return udp4 + udp6, nil
return 0, err
}
return udp4 + udp6, nil

View File

@@ -4,21 +4,27 @@
package sys
import (
"errors"
"github.com/shirou/gopsutil/v3/net"
)
func GetTCPCount() (int, error) {
stats, err := net.Connections("tcp")
func GetConnectionCount(proto string) (int, error) {
if proto != "tcp" && proto != "udp" {
return 0, errors.New("invalid protocol")
}
stats, err := net.Connections(proto)
if err != nil {
return 0, err
}
return len(stats), nil
}
func GetUDPCount() (int, error) {
stats, err := net.Connections("udp")
if err != nil {
return 0, err
}
return len(stats), nil
func GetTCPCount() (int, error) {
return GetConnectionCount("tcp")
}
func GetUDPCount() (int, error) {
return GetConnectionCount("udp")
}

File diff suppressed because one or more lines are too long

View File

@@ -31,9 +31,9 @@ small{font-size:80%}
sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}
sub{bottom:-.25em}
sup{top:-.5em}
a{color:#1890ff;text-decoration:none;background-color:transparent;outline:0;cursor:pointer;transition:color .3s;-webkit-text-decoration-skip:objects}
a:hover{color:#40a9ff}
a:active{color:#096dd9}
a{color:rgb(0 150 112);text-decoration:none;background-color:transparent;outline:0;cursor:pointer;transition:color .3s;-webkit-text-decoration-skip:objects}
a:hover{color:rgb(0 150 112)}
a:active{color:rgb(10, 105, 82)}
a:active,a:hover{text-decoration:none;outline:0}
a[disabled]{color:rgba(0,0,0,.25);cursor:not-allowed;pointer-events:none}
code,kbd,pre,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}
@@ -520,8 +520,8 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-select-arrow .ant-select-arrow-icon{display:block}
.ant-select-arrow .ant-select-arrow-icon svg{transition:transform .3s}
.ant-select-selection{display:block;box-sizing:border-box;background-color:#fff;border:1px solid #d9d9d9;border-top:1.02px solid #d9d9d9;border-radius:4px;outline:0;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-select-selection:hover{border-color:#40a9ff;border-right-width:1px!important}
.ant-select-focused .ant-select-selection,.ant-select-selection:active,.ant-select-selection:focus{border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
.ant-select-selection:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important}
.ant-select-focused .ant-select-selection,.ant-select-selection:active,.ant-select-selection:focus{border-color:rgb(0, 150, 112);border-right-width:1px!important;outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
.ant-select-selection__clear{position:absolute;top:50%;right:11px;z-index:1;display:inline-block;width:12px;height:12px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;font-style:normal;line-height:12px;text-align:center;text-transform:none;background:#fff;cursor:pointer;opacity:0;transition:color .3s ease,opacity .15s ease;text-rendering:auto}
.ant-select-selection__clear:before{display:block}
.ant-select-selection__clear:hover{color:rgba(0,0,0,.45)}
@@ -529,7 +529,7 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-select-selection-selected-value{float:left;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
.ant-select-no-arrow .ant-select-selection-selected-value{padding-right:0}
.ant-select-disabled{color:rgba(0,0,0,.25)}
.ant-select-disabled .ant-select-selection{background:#f5f5f5;cursor:not-allowed}
.ant-select-disabled .ant-select-selection{background:rgb(221, 221, 221);cursor:not-allowed}
.ant-select-disabled .ant-select-selection:active,.ant-select-disabled .ant-select-selection:focus,.ant-select-disabled .ant-select-selection:hover{border-color:#d9d9d9;box-shadow:none}
.ant-select-disabled .ant-select-selection__clear{display:none;visibility:hidden;pointer-events:none}
.ant-select-disabled .ant-select-selection--multiple .ant-select-selection__choice{padding-right:10px;color:rgba(0,0,0,.33);background:#f5f5f5}
@@ -582,7 +582,7 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-select-selection--multiple .ant-select-arrow,.ant-select-selection--multiple .ant-select-selection__clear{top:16px}
.ant-select-allow-clear .ant-select-selection--multiple .ant-select-selection__rendered,.ant-select-show-arrow .ant-select-selection--multiple .ant-select-selection__rendered{margin-right:20px}
.ant-select-open .ant-select-arrow-icon svg{transform:rotate(180deg)}
.ant-select-open .ant-select-selection{border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
.ant-select-open .ant-select-selection{border-color:rgb(0 150 112);border-right-width:1px!important;outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
.ant-select-combobox .ant-select-arrow{display:none}
.ant-select-combobox .ant-select-search--inline{float:none;width:100%;height:100%}
.ant-select-combobox .ant-select-search__field__wrap{width:100%;height:100%}
@@ -629,8 +629,8 @@ to{transform:scaleY(0);transform-origin:0 0;opacity:0}
.ant-input:-moz-placeholder-shown{text-overflow:ellipsis}
.ant-input:-ms-input-placeholder{text-overflow:ellipsis}
.ant-input:placeholder-shown{text-overflow:ellipsis}
.ant-input:focus,.ant-input:hover{border-color:#40a9ff;border-right-width:1px!important}
.ant-input:focus{outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
.ant-input:focus,.ant-input:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important;}
.ant-input:focus{outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
.ant-input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
.ant-input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}
.ant-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
@@ -716,7 +716,7 @@ textarea.ant-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;ve
.ant-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:4px}
.ant-btn>a:only-child{color:currentColor}
.ant-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn:focus,.ant-btn:hover{color:#40a9ff;background-color:#fff;border-color:#40a9ff}
.ant-btn:focus,.ant-btn:hover{color:rgb(0 150 112);background-color:#fff;border-color:rgb(0 150 112)}
.ant-btn:focus>a:only-child,.ant-btn:hover>a:only-child{color:currentColor}
.ant-btn:focus>a:only-child:after,.ant-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn.active,.ant-btn:active{color:#096dd9;background-color:#fff;border-color:#096dd9}
@@ -727,16 +727,16 @@ textarea.ant-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;ve
.ant-btn-disabled.active>a:only-child:after,.ant-btn-disabled:active>a:only-child:after,.ant-btn-disabled:focus>a:only-child:after,.ant-btn-disabled:hover>a:only-child:after,.ant-btn-disabled>a:only-child:after,.ant-btn.disabled.active>a:only-child:after,.ant-btn.disabled:active>a:only-child:after,.ant-btn.disabled:focus>a:only-child:after,.ant-btn.disabled:hover>a:only-child:after,.ant-btn.disabled>a:only-child:after,.ant-btn[disabled].active>a:only-child:after,.ant-btn[disabled]:active>a:only-child:after,.ant-btn[disabled]:focus>a:only-child:after,.ant-btn[disabled]:hover>a:only-child:after,.ant-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn.active,.ant-btn:active,.ant-btn:focus,.ant-btn:hover{text-decoration:none;background:#fff}
.ant-btn>i,.ant-btn>span{display:inline-block;transition:margin-left .3s cubic-bezier(.645,.045,.355,1);pointer-events:none}
.ant-btn-primary{color:#fff;background-color:#1890ff;border-color:#1890ff;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)}
.ant-btn-primary{color:#fff;background-color:rgb(0, 150, 112);border-color:rgb(0, 150, 112);text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)}
.ant-btn-primary>a:only-child{color:currentColor}
.ant-btn-primary>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-primary:focus,.ant-btn-primary:hover{color:#fff;background-color:#40a9ff;border-color:#00000017}
.ant-btn-primary:focus,.ant-btn-primary:hover{color:#fff;background-color:rgb(0, 185, 138);border-color:#00000017}
.ant-btn-primary:focus>a:only-child,.ant-btn-primary:hover>a:only-child{color:currentColor}
.ant-btn-primary:focus>a:only-child:after,.ant-btn-primary:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-primary.active,.ant-btn-primary:active{color:#fff;background-color:#096dd9;border-color:#096dd9}
.ant-btn-primary.active,.ant-btn-primary:active{color:#fff;background-color:rgb(25, 191, 149);border-color:rgb(25, 191, 149)}
.ant-btn-primary.active>a:only-child,.ant-btn-primary:active>a:only-child{color:currentColor}
.ant-btn-primary.active>a:only-child:after,.ant-btn-primary:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-primary-disabled,.ant-btn-primary-disabled.active,.ant-btn-primary-disabled:active,.ant-btn-primary-disabled:focus,.ant-btn-primary-disabled:hover,.ant-btn-primary.disabled,.ant-btn-primary.disabled.active,.ant-btn-primary.disabled:active,.ant-btn-primary.disabled:focus,.ant-btn-primary.disabled:hover,.ant-btn-primary[disabled],.ant-btn-primary[disabled].active,.ant-btn-primary[disabled]:active,.ant-btn-primary[disabled]:focus,.ant-btn-primary[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}
.ant-btn-primary-disabled,.ant-btn-primary-disabled.active,.ant-btn-primary-disabled:active,.ant-btn-primary-disabled:focus,.ant-btn-primary-disabled:hover,.ant-btn-primary.disabled,.ant-btn-primary.disabled.active,.ant-btn-primary.disabled:active,.ant-btn-primary.disabled:focus,.ant-btn-primary.disabled:hover,.ant-btn-primary[disabled],.ant-btn-primary[disabled].active,.ant-btn-primary[disabled]:active,.ant-btn-primary[disabled]:focus,.ant-btn-primary[disabled]:hover{color:rgb(189 185 185);background-color:rgb(189 189 189 / 10%);border:1px solid rgb(199 199 199 / 50%);text-shadow:none;box-shadow:none}
.ant-btn-primary-disabled.active>a:only-child,.ant-btn-primary-disabled:active>a:only-child,.ant-btn-primary-disabled:focus>a:only-child,.ant-btn-primary-disabled:hover>a:only-child,.ant-btn-primary-disabled>a:only-child,.ant-btn-primary.disabled.active>a:only-child,.ant-btn-primary.disabled:active>a:only-child,.ant-btn-primary.disabled:focus>a:only-child,.ant-btn-primary.disabled:hover>a:only-child,.ant-btn-primary.disabled>a:only-child,.ant-btn-primary[disabled].active>a:only-child,.ant-btn-primary[disabled]:active>a:only-child,.ant-btn-primary[disabled]:focus>a:only-child,.ant-btn-primary[disabled]:hover>a:only-child,.ant-btn-primary[disabled]>a:only-child{color:currentColor}
.ant-btn-primary-disabled.active>a:only-child:after,.ant-btn-primary-disabled:active>a:only-child:after,.ant-btn-primary-disabled:focus>a:only-child:after,.ant-btn-primary-disabled:hover>a:only-child:after,.ant-btn-primary-disabled>a:only-child:after,.ant-btn-primary.disabled.active>a:only-child:after,.ant-btn-primary.disabled:active>a:only-child:after,.ant-btn-primary.disabled:focus>a:only-child:after,.ant-btn-primary.disabled:hover>a:only-child:after,.ant-btn-primary.disabled>a:only-child:after,.ant-btn-primary[disabled].active>a:only-child:after,.ant-btn-primary[disabled]:active>a:only-child:after,.ant-btn-primary[disabled]:focus>a:only-child:after,.ant-btn-primary[disabled]:hover>a:only-child:after,.ant-btn-primary[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-group .ant-btn-primary:not(:first-child):not(:last-child){border-right-color:#40a9ff;border-left-color:#40a9ff}
@@ -769,16 +769,16 @@ textarea.ant-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;ve
.ant-btn-dashed-disabled,.ant-btn-dashed-disabled.active,.ant-btn-dashed-disabled:active,.ant-btn-dashed-disabled:focus,.ant-btn-dashed-disabled:hover,.ant-btn-dashed.disabled,.ant-btn-dashed.disabled.active,.ant-btn-dashed.disabled:active,.ant-btn-dashed.disabled:focus,.ant-btn-dashed.disabled:hover,.ant-btn-dashed[disabled],.ant-btn-dashed[disabled].active,.ant-btn-dashed[disabled]:active,.ant-btn-dashed[disabled]:focus,.ant-btn-dashed[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}
.ant-btn-dashed-disabled.active>a:only-child,.ant-btn-dashed-disabled:active>a:only-child,.ant-btn-dashed-disabled:focus>a:only-child,.ant-btn-dashed-disabled:hover>a:only-child,.ant-btn-dashed-disabled>a:only-child,.ant-btn-dashed.disabled.active>a:only-child,.ant-btn-dashed.disabled:active>a:only-child,.ant-btn-dashed.disabled:focus>a:only-child,.ant-btn-dashed.disabled:hover>a:only-child,.ant-btn-dashed.disabled>a:only-child,.ant-btn-dashed[disabled].active>a:only-child,.ant-btn-dashed[disabled]:active>a:only-child,.ant-btn-dashed[disabled]:focus>a:only-child,.ant-btn-dashed[disabled]:hover>a:only-child,.ant-btn-dashed[disabled]>a:only-child{color:currentColor}
.ant-btn-dashed-disabled.active>a:only-child:after,.ant-btn-dashed-disabled:active>a:only-child:after,.ant-btn-dashed-disabled:focus>a:only-child:after,.ant-btn-dashed-disabled:hover>a:only-child:after,.ant-btn-dashed-disabled>a:only-child:after,.ant-btn-dashed.disabled.active>a:only-child:after,.ant-btn-dashed.disabled:active>a:only-child:after,.ant-btn-dashed.disabled:focus>a:only-child:after,.ant-btn-dashed.disabled:hover>a:only-child:after,.ant-btn-dashed.disabled>a:only-child:after,.ant-btn-dashed[disabled].active>a:only-child:after,.ant-btn-dashed[disabled]:active>a:only-child:after,.ant-btn-dashed[disabled]:focus>a:only-child:after,.ant-btn-dashed[disabled]:hover>a:only-child:after,.ant-btn-dashed[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-danger{color:#fff;background-color:#ff4d4f;border-color:#ff4d4f;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)}
.ant-btn-danger{color:#ff4d4f;background-color:rgb(255 77 79 / 0%);border-color:#ff4d4f;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045)}
.ant-btn-danger>a:only-child{color:currentColor}
.ant-btn-danger>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-danger:focus,.ant-btn-danger:hover{color:#fff;background-color:#ff7875;border-color:#ff7875}
.ant-btn-danger:focus,.ant-btn-danger:hover{color:#fff;background-color:#ff4d4f;border-color:#ff4d4f}
.ant-btn-danger:focus>a:only-child,.ant-btn-danger:hover>a:only-child{color:currentColor}
.ant-btn-danger:focus>a:only-child:after,.ant-btn-danger:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-danger.active,.ant-btn-danger:active{color:#fff;background-color:#d9363e;border-color:#d9363e}
.ant-btn-danger.active>a:only-child,.ant-btn-danger:active>a:only-child{color:currentColor}
.ant-btn-danger.active>a:only-child:after,.ant-btn-danger:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-danger-disabled,.ant-btn-danger-disabled.active,.ant-btn-danger-disabled:active,.ant-btn-danger-disabled:focus,.ant-btn-danger-disabled:hover,.ant-btn-danger.disabled,.ant-btn-danger.disabled.active,.ant-btn-danger.disabled:active,.ant-btn-danger.disabled:focus,.ant-btn-danger.disabled:hover,.ant-btn-danger[disabled],.ant-btn-danger[disabled].active,.ant-btn-danger[disabled]:active,.ant-btn-danger[disabled]:focus,.ant-btn-danger[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}
.ant-btn-danger-disabled,.ant-btn-danger-disabled.active,.ant-btn-danger-disabled:active,.ant-btn-danger-disabled:focus,.ant-btn-danger-disabled:hover,.ant-btn-danger.disabled,.ant-btn-danger.disabled.active,.ant-btn-danger.disabled:active,.ant-btn-danger.disabled:focus,.ant-btn-danger.disabled:hover,.ant-btn-danger[disabled],.ant-btn-danger[disabled].active,.ant-btn-danger[disabled]:active,.ant-btn-danger[disabled]:focus,.ant-btn-danger[disabled]:hover{color:rgb(189 185 185);background-color:rgb(189 189 189 / 10%);border:1px solid rgb(199 199 199 / 50%);text-shadow:none;box-shadow:none}
.ant-btn-danger-disabled.active>a:only-child,.ant-btn-danger-disabled:active>a:only-child,.ant-btn-danger-disabled:focus>a:only-child,.ant-btn-danger-disabled:hover>a:only-child,.ant-btn-danger-disabled>a:only-child,.ant-btn-danger.disabled.active>a:only-child,.ant-btn-danger.disabled:active>a:only-child,.ant-btn-danger.disabled:focus>a:only-child,.ant-btn-danger.disabled:hover>a:only-child,.ant-btn-danger.disabled>a:only-child,.ant-btn-danger[disabled].active>a:only-child,.ant-btn-danger[disabled]:active>a:only-child,.ant-btn-danger[disabled]:focus>a:only-child,.ant-btn-danger[disabled]:hover>a:only-child,.ant-btn-danger[disabled]>a:only-child{color:currentColor}
.ant-btn-danger-disabled.active>a:only-child:after,.ant-btn-danger-disabled:active>a:only-child:after,.ant-btn-danger-disabled:focus>a:only-child:after,.ant-btn-danger-disabled:hover>a:only-child:after,.ant-btn-danger-disabled>a:only-child:after,.ant-btn-danger.disabled.active>a:only-child:after,.ant-btn-danger.disabled:active>a:only-child:after,.ant-btn-danger.disabled:focus>a:only-child:after,.ant-btn-danger.disabled:hover>a:only-child:after,.ant-btn-danger.disabled>a:only-child:after,.ant-btn-danger[disabled].active>a:only-child:after,.ant-btn-danger[disabled]:active>a:only-child:after,.ant-btn-danger[disabled]:focus>a:only-child:after,.ant-btn-danger[disabled]:hover>a:only-child:after,.ant-btn-danger[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-btn-link{color:#1890ff;background-color:transparent;border-color:transparent;box-shadow:none}
@@ -992,11 +992,12 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
.ant-menu-item>.ant-badge>a{color:rgba(0,0,0,.65)}
.ant-menu-item>.ant-badge>a:hover{color:#1890ff}
.ant-menu-item-divider{height:1px;overflow:hidden;line-height:0;background-color:#e8e8e8}
.ant-menu-item-active,.ant-menu-item:hover,.ant-menu-submenu-active,.ant-menu-submenu-title:hover,.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open{color:#fff;background-image:linear-gradient(-20deg,#1a61b3 0,#242d81 100%)}
.ant-menu-item-active,.ant-menu-item:hover,.ant-menu-submenu-active,.ant-menu-submenu-title:hover,.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open{color:#2d2d2d;background-image: linear-gradient(90deg,#99999980 0,#8888889e 100%);border-radius: 0.5rem}
.ant-menu-horizontal .ant-menu-item,.ant-menu-horizontal .ant-menu-submenu{margin-top:-1px}
.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu .ant-menu-submenu-title:hover{background-color:transparent}
.ant-menu-item-selected,.ant-menu-item-selected>a,.ant-menu-item-selected>a:hover{color:#1890ff}
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background:linear-gradient(-20deg,#412ef0 0,#0a2d58 100%);color:#fff}
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background-color: #0a7557;background-image: linear-gradient( 270deg, rgba(123, 199, 77, 0) 30%, #00ab80, rgba(123, 199, 77, 0) 100% );background-repeat: no-repeat;animation: ma-bg-move linear 6.6s infinite;/*background: linear-gradient(90deg,#009670 0,#026247 100%);*/color: #fff;border-radius: 0.5rem}
@-webkit-keyframes ma-bg-move {0% {background-position: -500px 0;}100% {background-position: 1000px 0;}}@keyframes ma-bg-move {0% {background-position: -500px 0;}50% {background-position: 1000px 0;}100% {background-position: 1000px 0;}}
.ant-menu-vertical-right{border-left:1px solid #e8e8e8}
.ant-menu-vertical-left.ant-menu-sub,.ant-menu-vertical-right.ant-menu-sub,.ant-menu-vertical.ant-menu-sub{min-width:160px;padding:0;border-right:0;transform-origin:0 0}
.ant-menu-vertical-left.ant-menu-sub .ant-menu-item,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item,.ant-menu-vertical.ant-menu-sub .ant-menu-item{left:0;margin-left:0;border-right:0}
@@ -1032,7 +1033,7 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
.ant-menu-horizontal>.ant-menu-item-selected>a{color:#1890ff}
.ant-menu-horizontal:after{display:block;clear:both;height:0;content:"\20"}
.ant-menu-inline .ant-menu-item,.ant-menu-vertical .ant-menu-item,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-right .ant-menu-item{position:relative}
.ant-menu-inline .ant-menu-item:after,.ant-menu-vertical .ant-menu-item:after,.ant-menu-vertical-left .ant-menu-item:after,.ant-menu-vertical-right .ant-menu-item:after{position:absolute;top:0;right:0;bottom:0;/*! border-right:3px solid #1890ff; */transform:scaleY(.0001);opacity:0;transition:transform .15s cubic-bezier(.215,.61,.355,1),opacity .15s cubic-bezier(.215,.61,.355,1);content:""}
.ant-menu-inline .ant-menu-item:after,.ant-menu-vertical .ant-menu-item:after,.ant-menu-vertical-left .ant-menu-item:after,.ant-menu-vertical-right .ant-menu-item:after{position:absolute;top:0;right:0;bottom:0/*;border-right:3px solid #1890ff*/;transform:scaleY(.0001);opacity:0;transition:transform .15s cubic-bezier(.215,.61,.355,1),opacity .15s cubic-bezier(.215,.61,.355,1);content:""}
.ant-menu-inline .ant-menu-item,.ant-menu-inline .ant-menu-submenu-title,.ant-menu-vertical .ant-menu-item,.ant-menu-vertical .ant-menu-submenu-title,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-left .ant-menu-submenu-title,.ant-menu-vertical-right .ant-menu-item,.ant-menu-vertical-right .ant-menu-submenu-title{height:40px;margin-top:4px;margin-bottom:4px;padding:0 16px;overflow:hidden;font-size:14px;line-height:40px;text-overflow:ellipsis}
.ant-menu-inline .ant-menu-submenu,.ant-menu-vertical .ant-menu-submenu,.ant-menu-vertical-left .ant-menu-submenu,.ant-menu-vertical-right .ant-menu-submenu{padding-bottom:.02px}
.ant-menu-inline .ant-menu-item:not(:last-child),.ant-menu-vertical .ant-menu-item:not(:last-child),.ant-menu-vertical-left .ant-menu-item:not(:last-child),.ant-menu-vertical-right .ant-menu-item:not(:last-child){margin-bottom:8px}
@@ -1080,8 +1081,9 @@ to{transform:scale(0) translate(50%,-50%);opacity:0}
.ant-menu-dark .ant-menu-item:hover{background-color:transparent}
.ant-menu-dark .ant-menu-item-selected{color:#fff;border-right:0}
.ant-menu-dark .ant-menu-item-selected:after{border-right:0}
.ant-menu-dark .ant-menu-item-selected .anticon,.ant-menu-dark .ant-menu-item-selected .anticon+span,.ant-menu-dark .ant-menu-item-selected>a,.ant-menu-dark .ant-menu-item-selected>a:hover{color:#fff}
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#1890ff}
.ant-menu-dark .ant-menu-item-selected .anticon,.ant-menu-dark .ant-menu-item-selected .anticon+span,.ant-menu-dark .ant-menu-item-selected>a,.ant-menu-dark .ant-menu-item-selected>a:hover{color:#ffffff}
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#0a7557}
/*.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#0a7557}*/
.ant-menu-dark .ant-menu-item-disabled,.ant-menu-dark .ant-menu-item-disabled>a,.ant-menu-dark .ant-menu-submenu-disabled,.ant-menu-dark .ant-menu-submenu-disabled>a{color:hsla(0,0%,100%,.35)!important;opacity:.8}
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title{color:hsla(0,0%,100%,.35)!important}
.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:hsla(0,0%,100%,.35)!important}
@@ -1235,18 +1237,18 @@ span.ant-radio+*{padding-right:8px;padding-left:8px}
.ant-radio-button-wrapper:first-child{border-left:1px solid #d9d9d9;border-radius:4px 0 0 4px}
.ant-radio-button-wrapper:last-child{border-radius:0 4px 4px 0}
.ant-radio-button-wrapper:first-child:last-child{border-radius:4px}
.ant-radio-button-wrapper:hover{position:relative;color:#1890ff}
.ant-radio-button-wrapper:hover{position:relative;color:#009670}
.ant-radio-button-wrapper:focus-within{outline:3px solid rgba(24,144,255,.06)}
.ant-radio-button-wrapper .ant-radio-inner,.ant-radio-button-wrapper input[type=checkbox],.ant-radio-button-wrapper input[type=radio]{width:0;height:0;opacity:0;pointer-events:none}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){z-index:1;color:#1890ff;background:#fff;border-color:#1890ff;box-shadow:-1px 0 0 0 #1890ff}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):before{background-color:#1890ff!important;opacity:.1}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):first-child{border-color:#1890ff;box-shadow:none!important}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#40a9ff;border-color:#40a9ff;box-shadow:-1px 0 0 0 #40a9ff}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#096dd9;border-color:#096dd9;box-shadow:-1px 0 0 0 #096dd9}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){z-index:1;color:#009670;background:#fff;border-color:#009670;box-shadow:-1px 0 0 0 #009670}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):before{background-color:#009670!important;opacity:.1}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):first-child{border-color:#009670;box-shadow:none!important}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#009670;border-color:#009670;box-shadow:-1px 0 0 0 #009670}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#076e54;border-color:#076e54;box-shadow:-1px 0 0 0 #076e54}
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{outline:3px solid rgba(24,144,255,.06)}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){color:#fff;background:#1890ff;border-color:#1890ff}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#fff;background:#40a9ff;border-color:#40a9ff}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#fff;background:#096dd9;border-color:#096dd9}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){color:#fff;background:#009670;border-color:#009670}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#fff;background:#009670;border-color:#009670}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#fff;background:#076e54;border-color:#076e54}
.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{outline:3px solid rgba(24,144,255,.06)}
.ant-radio-button-wrapper-disabled{cursor:not-allowed}
.ant-radio-button-wrapper-disabled,.ant-radio-button-wrapper-disabled:first-child,.ant-radio-button-wrapper-disabled:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9}
@@ -1263,11 +1265,11 @@ to{transform:scale(1.6);opacity:0}
@supports (-moz-appearance:meterbar) and (background-blend-mode:difference,normal){
.ant-radio{vertical-align:text-bottom}
}
.ant-card{box-sizing:border-box;margin:auto 5px 3px auto;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;background:#fff;border-radius:2px;transition:all .3s}
.ant-card{box-sizing:border-box;margin:auto 10px 3px auto;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;background:#fff;border-radius:2px;transition:all .3s}
.ant-card-hoverable{cursor:pointer}
.ant-card-hoverable:hover{border-color:rgba(0,0,0,.09);box-shadow:0 2px 8px rgba(0,0,0,.09)}
.ant-card-bordered{/*! border:1px solid #e8e8e8; *//*! box-shadow: 0px 6px 0px 0px rgb(15, 197, 254); *//*! box-shadow: 0 4px 5px 0 rgba(170, 179, 217, 0.33); */backdrop-filter:blur(7px) saturate(200%);background-color:#fff;border:0 solid rgba(255,255,255,0);box-shadow:0 3px 7.985px -.985px #9aafee}
.ant-card-head{min-height:48px;margin-bottom:-1px;padding:0 24px;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;background:0 0;border-bottom:1px solid #e8e8e8;border-radius:2px 2px 0 0;zoom:1}
.ant-card-bordered{/*! border:1px solid #e8e8e8; *//*! box-shadow: 0px 6px 0px 0px rgb(15, 197, 254); *//*! box-shadow: 0 4px 5px 0 rgba(170, 179, 217, 0.33); */backdrop-filter:blur(7px) saturate(200%);background-color:#fff;border:0 solid rgba(255,255,255,0)/*;box-shadow:0 1px 7px -1px #0000005c*/}
.ant-card-head{min-height:48px;margin-bottom:-1px;padding:0 24px;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;background:0 0;border-bottom:2px solid rgba(155, 155, 155, 0.15);border-radius:2px 2px 0 0;zoom:1}
.ant-card-head:after,.ant-card-head:before{display:table;content:""}
.ant-card-head:after{clear:both}
.ant-card-head-wrapper{display:flex;align-items:center}
@@ -1356,11 +1358,12 @@ to{transform:scale(1.6);opacity:0}
.ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab-active{margin-left:-1px;padding-left:18px}
.ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab{height:auto;border-top:0;border-bottom:1px solid #e8e8e8;border-radius:0 0 4px 4px}
.ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab-active{padding-top:1px;padding-bottom:0;color:#1890ff}
.ant-tabs{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;overflow:hidden;zoom:1}
.ant-tabs{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;overflow:hidden;zoom:1;border-radius:1.5rem;/*box-shadow:0 1px 7px -1px #0000005c;*/transition:all .3s;background-color: white}
.ant-tabs:hover{box-shadow:0 3px 12px -.8px #0000005c}
.ant-tabs:after,.ant-tabs:before{display:table;content:""}
.ant-tabs:after{clear:both}
.ant-tabs-ink-bar{position:absolute;bottom:1px;left:0;z-index:1;box-sizing:border-box;width:0;height:2px;background-color:#1890ff;transform-origin:0 0}
.ant-tabs-bar{margin:0 0 16px;border-bottom:1px solid #e8e8e8;outline:0}
.ant-tabs-ink-bar{position:absolute;bottom:1px;left:0;z-index:1;box-sizing:border-box;width:0;height:2px;background-color:rgb(0 150 112);transform-origin:0 0}
.ant-tabs-bar{margin:1.5rem 1.5rem 0rem 1.5rem!important;border-bottom:2px solid rgb(153 153 153 / 20%);outline:0}
.ant-tabs-bar,.ant-tabs-nav-container{transition:padding .3s cubic-bezier(.645,.045,.355,1)}
.ant-tabs-nav-container{position:relative;box-sizing:border-box;margin-bottom:-1px;overflow:hidden;font-size:14px;line-height:1.5;white-space:nowrap;zoom:1}
.ant-tabs-nav-container:after,.ant-tabs-nav-container:before{display:table;content:""}
@@ -1388,10 +1391,10 @@ to{transform:scale(1.6);opacity:0}
.ant-tabs-nav .ant-tabs-tab{position:relative;display:inline-block;box-sizing:border-box;height:100%;margin:0 32px 0 0;padding:12px 16px;text-decoration:none;cursor:pointer;transition:color .3s cubic-bezier(.645,.045,.355,1)}
.ant-tabs-nav .ant-tabs-tab:before{position:absolute;top:-1px;left:0;width:100%;border-top:2px solid transparent;border-radius:4px 4px 0 0;transition:all .3s;content:"";pointer-events:none}
.ant-tabs-nav .ant-tabs-tab:last-child{margin-right:0}
.ant-tabs-nav .ant-tabs-tab:hover{color:#40a9ff}
.ant-tabs-nav .ant-tabs-tab:active{color:#096dd9}
.ant-tabs-nav .ant-tabs-tab:hover{color:rgb(0 150 112)}
.ant-tabs-nav .ant-tabs-tab:active{color:rgb(0 150 112)}
.ant-tabs-nav .ant-tabs-tab .anticon{margin-right:8px}
.ant-tabs-nav .ant-tabs-tab-active{color:#1890ff;font-weight:500}
.ant-tabs-nav .ant-tabs-tab-active{color:rgb(0 150 112);font-weight:500}
.ant-tabs-nav .ant-tabs-tab-disabled,.ant-tabs-nav .ant-tabs-tab-disabled:hover{color:rgba(0,0,0,.25);cursor:not-allowed}
.ant-tabs .ant-tabs-large-bar .ant-tabs-nav-container{font-size:16px}
.ant-tabs .ant-tabs-large-bar .ant-tabs-tab{padding:16px}
@@ -2436,14 +2439,14 @@ to{transform:scale(1.6);opacity:0}
.ant-cascader-menu-item-disabled.ant-cascader-menu-item-expand .ant-cascader-menu-item-expand-icon,.ant-cascader-menu-item-disabled.ant-cascader-menu-item-loading-icon{color:rgba(0,0,0,.25)}
.ant-cascader-menu-item .ant-cascader-menu-item-keyword{color:#f5222d}
.ant-checkbox{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;top:-.09em;display:inline-block;line-height:1;white-space:nowrap;vertical-align:middle;outline:0;cursor:pointer}
.ant-checkbox-input:focus+.ant-checkbox-inner,.ant-checkbox-wrapper:hover .ant-checkbox-inner,.ant-checkbox:hover .ant-checkbox-inner{border-color:#1890ff}
.ant-checkbox-checked:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid #1890ff;border-radius:2px;visibility:hidden;-webkit-animation:antCheckboxEffect .36s ease-in-out;animation:antCheckboxEffect .36s ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards;content:""}
.ant-checkbox-input:focus+.ant-checkbox-inner,.ant-checkbox-wrapper:hover .ant-checkbox-inner,.ant-checkbox:hover .ant-checkbox-inner{border-color:rgb(0, 150, 112)}
.ant-checkbox-checked:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid rgb(0, 150, 112);border-radius:2px;visibility:hidden;-webkit-animation:antCheckboxEffect .36s ease-in-out;animation:antCheckboxEffect .36s ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards;content:""}
.ant-checkbox-wrapper:hover .ant-checkbox:after,.ant-checkbox:hover:after{visibility:visible}
.ant-checkbox-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:2px;border-collapse:separate;transition:all .3s}
.ant-checkbox-inner:after{position:absolute;top:50%;left:22%;display:table;width:5.71428571px;height:9.14285714px;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(0) translate(-50%,-50%);opacity:0;transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;content:" "}
.ant-checkbox-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;width:100%;height:100%;cursor:pointer;opacity:0}
.ant-checkbox-checked .ant-checkbox-inner:after{position:absolute;display:table;border:2px solid #fff;border-top:0;border-left:0;transform:rotate(45deg) scale(1) translate(-50%,-50%);opacity:1;transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;content:" "}
.ant-checkbox-checked .ant-checkbox-inner{background-color:#1890ff;border-color:#1890ff}
.ant-checkbox-checked .ant-checkbox-inner{background-color:rgb(0, 150, 112);border-color:rgb(0, 150, 112)}
.ant-checkbox-disabled{cursor:not-allowed}
.ant-checkbox-disabled.ant-checkbox-checked .ant-checkbox-inner:after{border-color:rgba(0,0,0,.25);-webkit-animation-name:none;animation-name:none}
.ant-checkbox-disabled .ant-checkbox-input{cursor:not-allowed}
@@ -2462,9 +2465,9 @@ to{transform:scale(1.6);opacity:0}
.ant-checkbox-indeterminate .ant-checkbox-inner{background-color:#fff;border-color:#d9d9d9}
.ant-checkbox-indeterminate .ant-checkbox-inner:after{top:50%;left:50%;width:8px;height:8px;background-color:#1890ff;border:0;transform:translate(-50%,-50%) scale(1);opacity:1;content:" "}
.ant-checkbox-indeterminate.ant-checkbox-disabled .ant-checkbox-inner:after{background-color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)}
.ant-collapse{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";background-color:#fafafa;border:1px solid #d9d9d9;border-bottom:0;border-radius:4px}
.ant-collapse>.ant-collapse-item{border-bottom:1px solid #d9d9d9}
.ant-collapse>.ant-collapse-item:last-child,.ant-collapse>.ant-collapse-item:last-child>.ant-collapse-header{border-radius:0 0 4px 4px}
.ant-collapse{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum"/*;background-color:#fafafa*/;border:1px solid rgba(100, 100, 100, 0.2);/*border-bottom:0;*/border-radius:0.5rem}
.ant-collapse>.ant-collapse-item{border-bottom:1px solid rgb(217 217 217 / 20%)}
.ant-collapse>.ant-collapse-item:last-child,.ant-collapse>.ant-collapse-item:last-child>.ant-collapse-header{border-radius:0.5rem}
.ant-collapse>.ant-collapse-item>.ant-collapse-header{position:relative;padding:12px 16px 12px 40px;color:rgba(0,0,0,.85);line-height:22px;cursor:pointer;transition:all .3s}
.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;top:50%;left:16px;display:inline-block;font-size:12px;transform:translateY(-50%)}
.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow>*{line-height:1}
@@ -2478,7 +2481,7 @@ to{transform:scale(1.6);opacity:0}
.ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header{padding:12px 40px 12px 16px}
.ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{right:16px;left:auto}
.ant-collapse-anim-active{transition:height .2s cubic-bezier(.215,.61,.355,1)}
.ant-collapse-content{overflow:hidden;color:rgba(0,0,0,.65);background-color:#fff;border-top:1px solid #d9d9d9}
.ant-collapse-content{overflow:hidden;color:rgba(0,0,0,.65);background-color:#fff;border-top:1px solid rgb(0 150 112)}
.ant-collapse-content>.ant-collapse-content-box{padding:16px}
.ant-collapse-content-inactive{display:none}
.ant-collapse-item:last-child>.ant-collapse-content{border-radius:0 0 4px 4px}
@@ -2537,7 +2540,7 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar-picker-input{outline:0}
.ant-calendar-picker-input.ant-input{line-height:1.5}
.ant-calendar-picker-input.ant-input-sm{padding-top:0;padding-bottom:0}
.ant-calendar-picker:hover .ant-calendar-picker-input:not(.ant-input-disabled){border-color:#40a9ff}
.ant-calendar-picker:hover .ant-calendar-picker-input:not(.ant-input-disabled){border-color:rgb(0 150 112)}
.ant-calendar-picker:focus .ant-calendar-picker-input:not(.ant-input-disabled){border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
.ant-calendar-picker-clear,.ant-calendar-picker-icon{position:absolute;top:50%;right:12px;z-index:1;width:14px;height:14px;margin-top:-7px;font-size:12px;line-height:14px;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-calendar-picker-clear{z-index:2;color:rgba(0,0,0,.25);font-size:14px;background:#fff;cursor:pointer;opacity:0;pointer-events:none}
@@ -2547,7 +2550,7 @@ to{transform:scale(1.6);opacity:0}
.ant-input-disabled+.ant-calendar-picker-icon{cursor:not-allowed}
.ant-calendar-picker-small .ant-calendar-picker-clear,.ant-calendar-picker-small .ant-calendar-picker-icon{right:8px}
.ant-calendar{position:relative;width:280px;font-size:14px;line-height:1.5;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #fff;border-radius:4px;outline:0;box-shadow:0 2px 8px rgba(0,0,0,.15)}
.ant-calendar-input-wrap{height:34px;padding:6px 10px;border-bottom:1px solid #e8e8e8}
.ant-calendar-input-wrap{height:34px;padding:6px 10px;border-bottom:1px solid rgb(153 153 153 / 25%)}
.ant-calendar-input{width:100%;height:22px;color:rgba(0,0,0,.65);background:#fff;border:0;outline:0;cursor:auto}
.ant-calendar-input::-moz-placeholder{color:#bfbfbf;opacity:1}
.ant-calendar-input:-ms-input-placeholder{color:#bfbfbf}
@@ -2557,7 +2560,7 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar-input:placeholder-shown{text-overflow:ellipsis}
.ant-calendar-week-number{width:286px}
.ant-calendar-week-number-cell{text-align:center}
.ant-calendar-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid #e8e8e8;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-calendar-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid rgb(153 153 153 / 25%);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-calendar-header a:hover{color:#40a9ff}
.ant-calendar-header .ant-calendar-century-select,.ant-calendar-header .ant-calendar-decade-select,.ant-calendar-header .ant-calendar-month-select,.ant-calendar-header .ant-calendar-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px}
.ant-calendar-header .ant-calendar-century-select-arrow,.ant-calendar-header .ant-calendar-decade-select-arrow,.ant-calendar-header .ant-calendar-month-select-arrow,.ant-calendar-header .ant-calendar-year-select-arrow{display:none}
@@ -2593,8 +2596,8 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar-date{display:block;width:24px;height:24px;margin:0 auto;padding:0;color:rgba(0,0,0,.65);line-height:22px;text-align:center;background:0 0;border:1px solid transparent;border-radius:2px;transition:background .3s ease}
.ant-calendar-date-panel{position:relative;outline:0}
.ant-calendar-date:hover{background:#e6f7ff;cursor:pointer}
.ant-calendar-date:active{color:#fff;background:#40a9ff}
.ant-calendar-today .ant-calendar-date{color:#1890ff;font-weight:700;border-color:#1890ff}
.ant-calendar-date:active{color:rgba(0,0,0,.65);background:#bae7ff}
.ant-calendar-today .ant-calendar-date{color:rgb(0 150 112);font-weight:700;border-color:rgb(0 150 112)}
.ant-calendar-selected-day .ant-calendar-date{background:#bae7ff}
.ant-calendar-last-month-cell .ant-calendar-date,.ant-calendar-last-month-cell .ant-calendar-date:hover,.ant-calendar-next-month-btn-day .ant-calendar-date,.ant-calendar-next-month-btn-day .ant-calendar-date:hover{color:rgba(0,0,0,.25);background:0 0;border-color:transparent}
.ant-calendar-disabled-cell .ant-calendar-date{position:relative;width:auto;color:rgba(0,0,0,.25);background:#f5f5f5;border:1px solid transparent;border-radius:0;cursor:not-allowed}
@@ -2604,7 +2607,7 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar-disabled-cell.ant-calendar-today .ant-calendar-date:before{position:absolute;top:-1px;left:5px;width:24px;height:24px;border:1px solid rgba(0,0,0,.25);border-radius:2px;content:" "}
.ant-calendar-disabled-cell-first-of-row .ant-calendar-date{border-top-left-radius:4px;border-bottom-left-radius:4px}
.ant-calendar-disabled-cell-last-of-row .ant-calendar-date{border-top-right-radius:4px;border-bottom-right-radius:4px}
.ant-calendar-footer{padding:0 12px;line-height:38px;border-top:1px solid #e8e8e8}
.ant-calendar-footer{padding:0 12px;line-height:38px;border-top:1px solid rgb(153 153 153 / 25%)}
.ant-calendar-footer:empty{border-top:0}
.ant-calendar-footer-btn{display:block;text-align:center}
.ant-calendar-footer-extra{text-align:left}
@@ -2614,7 +2617,7 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar .ant-calendar-clear-btn{position:absolute;top:7px;right:5px;display:none;width:20px;height:20px;margin:0;overflow:hidden;line-height:20px;text-align:center;text-indent:-76px}
.ant-calendar .ant-calendar-clear-btn:after{display:inline-block;width:20px;color:rgba(0,0,0,.25);font-size:14px;line-height:1;text-indent:43px;transition:color .3s ease}
.ant-calendar .ant-calendar-clear-btn:hover:after{color:rgba(0,0,0,.45)}
.ant-calendar .ant-calendar-ok-btn{position:relative;display:inline-block;font-weight:400;white-space:nowrap;text-align:center;background-image:none;box-shadow:0 2px 0 rgba(0,0,0,.015);cursor:pointer;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;touch-action:manipulation;height:32px;color:#fff;background-color:#1890ff;border:1px solid #1890ff;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045);height:24px;padding:0 7px;font-size:14px;border-radius:4px;line-height:22px}
.ant-calendar .ant-calendar-ok-btn{position:relative;display:inline-block;font-weight:400;white-space:nowrap;text-align:center;background-image:none;box-shadow:0 2px 0 rgba(0,0,0,.015);cursor:pointer;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;touch-action:manipulation;height:32px;color:#fff;background-color:rgb(0 150 112);border:1px solid rgb(0 150 112);text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045);height:24px;padding:0 7px;font-size:14px;border-radius:4px;line-height:22px}
.ant-calendar .ant-calendar-ok-btn>.anticon{line-height:1}
.ant-calendar .ant-calendar-ok-btn,.ant-calendar .ant-calendar-ok-btn:active,.ant-calendar .ant-calendar-ok-btn:focus{outline:0}
.ant-calendar .ant-calendar-ok-btn:not([disabled]):hover{text-decoration:none}
@@ -2625,13 +2628,13 @@ to{transform:scale(1.6);opacity:0}
.ant-calendar .ant-calendar-ok-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:4px}
.ant-calendar .ant-calendar-ok-btn>a:only-child{color:currentColor}
.ant-calendar .ant-calendar-ok-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-calendar .ant-calendar-ok-btn:focus,.ant-calendar .ant-calendar-ok-btn:hover{color:#fff;background-color:#40a9ff;border-color:#40a9ff}
.ant-calendar .ant-calendar-ok-btn:focus,.ant-calendar .ant-calendar-ok-btn:hover{color:#fff;background-color:rgb(0 150 112);border-color:rgb(0 150 112)}
.ant-calendar .ant-calendar-ok-btn:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child{color:currentColor}
.ant-calendar .ant-calendar-ok-btn:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-calendar .ant-calendar-ok-btn.active,.ant-calendar .ant-calendar-ok-btn:active{color:#fff;background-color:#096dd9;border-color:#096dd9}
.ant-calendar .ant-calendar-ok-btn.active>a:only-child,.ant-calendar .ant-calendar-ok-btn:active>a:only-child{color:currentColor}
.ant-calendar .ant-calendar-ok-btn.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-calendar .ant-calendar-ok-btn-disabled,.ant-calendar .ant-calendar-ok-btn-disabled.active,.ant-calendar .ant-calendar-ok-btn-disabled:active,.ant-calendar .ant-calendar-ok-btn-disabled:focus,.ant-calendar .ant-calendar-ok-btn-disabled:hover,.ant-calendar .ant-calendar-ok-btn.disabled,.ant-calendar .ant-calendar-ok-btn.disabled.active,.ant-calendar .ant-calendar-ok-btn.disabled:active,.ant-calendar .ant-calendar-ok-btn.disabled:focus,.ant-calendar .ant-calendar-ok-btn.disabled:hover,.ant-calendar .ant-calendar-ok-btn[disabled],.ant-calendar .ant-calendar-ok-btn[disabled].active,.ant-calendar .ant-calendar-ok-btn[disabled]:active,.ant-calendar .ant-calendar-ok-btn[disabled]:focus,.ant-calendar .ant-calendar-ok-btn[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;box-shadow:none}
.ant-calendar .ant-calendar-ok-btn-disabled,.ant-calendar .ant-calendar-ok-btn-disabled.active,.ant-calendar .ant-calendar-ok-btn-disabled:active,.ant-calendar .ant-calendar-ok-btn-disabled:focus,.ant-calendar .ant-calendar-ok-btn-disabled:hover,.ant-calendar .ant-calendar-ok-btn.disabled,.ant-calendar .ant-calendar-ok-btn.disabled.active,.ant-calendar .ant-calendar-ok-btn.disabled:active,.ant-calendar .ant-calendar-ok-btn.disabled:focus,.ant-calendar .ant-calendar-ok-btn.disabled:hover,.ant-calendar .ant-calendar-ok-btn[disabled],.ant-calendar .ant-calendar-ok-btn[disabled].active,.ant-calendar .ant-calendar-ok-btn[disabled]:active,.ant-calendar .ant-calendar-ok-btn[disabled]:focus,.ant-calendar .ant-calendar-ok-btn[disabled]:hover{color:rgb(189 185 185);background-color:rgb(189 189 189 / 10%);border:1px solid rgb(199 199 199 / 50%);text-shadow:none;box-shadow:none}
.ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child{color:currentColor}
.ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:0 0;content:""}
.ant-calendar-range-picker-input{width:44%;height:99%;text-align:center;background-color:transparent;border:0;outline:0}
@@ -2942,7 +2945,7 @@ textarea.ant-time-picker-input{max-width:100%;height:auto;min-height:32px;line-h
.ant-tag-cyan-inverse{color:#fff;background:#13c2c2;border-color:#13c2c2}
.ant-tag-lime{color:#a0d911;background:#fcffe6;border-color:#eaff8f}
.ant-tag-lime-inverse{color:#fff;background:#a0d911;border-color:#a0d911}
.ant-tag-green{color:#52c41a;background:#f6ffed;border-color:#b7eb8f}
.ant-tag-green{color:#19bf95;background:#ebfffa;border-color:#8ce7d0}
.ant-tag-green-inverse{color:#fff;background:#52c41a;border-color:#52c41a}
.ant-tag-blue{color:#1890ff;background:#e6f7ff;border-color:#91d5ff}
.ant-tag-blue-inverse{color:#fff;background:#1890ff;border-color:#1890ff}
@@ -2977,8 +2980,8 @@ textarea.ant-time-picker-input{max-width:100%;height:auto;min-height:32px;line-h
.ant-divider{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";background:#e8e8e8}
.ant-divider,.ant-divider-vertical{position:relative;top:-.06em;display:inline-block;width:1px;height:.9em;margin:0 8px;vertical-align:middle}
.ant-divider-horizontal{display:block;clear:both;width:100%;min-width:100%;height:1px;margin:24px 0}
.ant-divider-horizontal.ant-divider-with-text-center,.ant-divider-horizontal.ant-divider-with-text-left,.ant-divider-horizontal.ant-divider-with-text-right{display:table;margin:16px 0;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;white-space:nowrap;text-align:center;background:0 0}
.ant-divider-horizontal.ant-divider-with-text-center:after,.ant-divider-horizontal.ant-divider-with-text-center:before,.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-left:before,.ant-divider-horizontal.ant-divider-with-text-right:after,.ant-divider-horizontal.ant-divider-with-text-right:before{position:relative;top:50%;display:table-cell;width:50%;border-top:1px solid #e8e8e8;transform:translateY(50%);content:""}
.ant-divider-horizontal.ant-divider-with-text-center,.ant-divider-horizontal.ant-divider-with-text-left,.ant-divider-horizontal.ant-divider-with-text-right{display:table;margin:0 0;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;white-space:nowrap;text-align:center;background:0 0}
.ant-divider-horizontal.ant-divider-with-text-center:after,.ant-divider-horizontal.ant-divider-with-text-center:before,.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-left:before,.ant-divider-horizontal.ant-divider-with-text-right:after,.ant-divider-horizontal.ant-divider-with-text-right:before{position:relative;top:50%;display:table-cell;width:50%;border-top:1px solid rgb(0 150 112 / 50%);transform:translateY(50%);content:""}
.ant-divider-horizontal.ant-divider-with-text-left .ant-divider-inner-text,.ant-divider-horizontal.ant-divider-with-text-right .ant-divider-inner-text{display:inline-block;padding:0 10px}
.ant-divider-horizontal.ant-divider-with-text-left:before{top:50%;width:5%}
.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-right:before{top:50%;width:95%}
@@ -3214,7 +3217,7 @@ to{transform:scale(1)}
.ant-input-number:-moz-placeholder-shown{text-overflow:ellipsis}
.ant-input-number:-ms-input-placeholder{text-overflow:ellipsis}
.ant-input-number:placeholder-shown{text-overflow:ellipsis}
.ant-input-number:focus{border-color:#40a9ff;border-right-width:1px!important;outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
.ant-input-number:focus{border-color:rgb(0, 150, 112);border-right-width:1px!important;outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px !important}
.ant-input-number[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
.ant-input-number[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}
textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;transition:all .3s,height 0s}
@@ -3228,8 +3231,8 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-input-number-handler-down-inner svg,.ant-input-number-handler-up-inner svg{display:inline-block}
.ant-input-number-handler-down-inner:before,.ant-input-number-handler-up-inner:before{display:none}
.ant-input-number-handler-down-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-down-inner .ant-input-number-handler-up-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-up-inner-icon{display:block}
.ant-input-number-focused,.ant-input-number:hover{border-color:#40a9ff;border-right-width:1px!important}
.ant-input-number-focused{outline:0;box-shadow:0 0 0 2px rgba(24,144,255,.2)}
.ant-input-number-focused,.ant-input-number:hover{border-color:rgb(0, 150, 112) !important;border-right-width:1px!important}
.ant-input-number-focused{outline:0;box-shadow:rgba(0, 150, 112, 0.2) 0px 0px 0px 2px}
.ant-input-number-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}
.ant-input-number-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}
.ant-input-number-disabled .ant-input-number-input{cursor:not-allowed}
@@ -3268,7 +3271,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-layout-footer{padding:24px 50px;color:rgba(0,0,0,.65);font-size:14px;background:#f0f2f5}
.ant-layout-content{flex:auto;min-height:0}
.ant-layout-sider{position:relative;min-width:0;background:#001529;transition:all .2s}
.ant-layout-sider-children{height:100%;margin-top:-.1px;padding-top:.1px}
.ant-layout-sider-children{height:100%;margin-top:-.1px;padding:0.5rem}
.ant-layout-sider-has-trigger{padding-bottom:48px}
.ant-layout-sider-right{order:1}
.ant-layout-sider-trigger{position:fixed;bottom:0;z-index:1;height:48px;color:#fff;line-height:48px;text-align:center;background:#002140;cursor:pointer;transition:all .2s}
@@ -3278,7 +3281,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-layout-sider-zero-width-trigger-right{left:-36px;border-radius:4px 0 0 4px}
.ant-layout-sider-light{background:#fff}
.ant-layout-sider-light .ant-layout-sider-trigger,.ant-layout-sider-light .ant-layout-sider-zero-width-trigger{color:rgba(0,0,0,.65);background:#fff}
.ant-list{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative}
.ant-list{box-sizing:border-box;margin:1.5em;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative}
.ant-list *{outline:0}
.ant-list-pagination{margin-top:24px;text-align:right}
.ant-list-pagination .ant-pagination-options{text-align:left}
@@ -3303,7 +3306,7 @@ textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height
.ant-list-footer,.ant-list-header{background:0 0}
.ant-list-footer,.ant-list-header{padding-top:12px;padding-bottom:12px}
.ant-list-empty{padding:16px 0;color:rgba(0,0,0,.45);font-size:12px;text-align:center}
.ant-list-split .ant-list-item{border-bottom:1px solid #e8e8e8}
.ant-list-split .ant-list-item{border-bottom:1px solid rgb(153 153 153 / 20%)}
.ant-list-split .ant-list-item:last-child{border-bottom:none}
.ant-list-split .ant-list-header{border-bottom:1px solid #e8e8e8}
.ant-list-loading .ant-list-spin-nested-loading{min-height:32px}
@@ -3530,9 +3533,9 @@ to{max-height:0;padding:0;opacity:0}
.ant-modal-close{position:absolute;top:0;right:0;z-index:10;padding:0;color:rgba(0,0,0,.45);font-weight:700;line-height:1;text-decoration:none;background:0 0;border:0;outline:0;cursor:pointer;transition:color .3s}
.ant-modal-close-x{display:block;width:56px;height:56px;font-size:16px;font-style:normal;line-height:56px;text-align:center;text-transform:none;text-rendering:auto}
.ant-modal-close:focus,.ant-modal-close:hover{color:rgba(0,0,0,.75);text-decoration:none}
.ant-modal-header{padding:16px 24px;color:rgba(0,0,0,.65);background:#fff;border-bottom:1px solid #e8e8e8;border-radius:4px 4px 0 0}
.ant-modal-header{padding:16px 24px;color:rgba(0,0,0,.65);background:#fff;border-bottom:1px solid rgba(100, 100, 100, 0.2);border-radius:4px 4px 0 0}
.ant-modal-body{padding:24px;font-size:14px;line-height:1.5;word-wrap:break-word}
.ant-modal-footer{padding:10px 16px;text-align:right;background:0 0;border-top:1px solid #e8e8e8;border-radius:0 0 4px 4px}
.ant-modal-footer{padding:10px 16px;text-align:right;background:0 0;border-top:1px solid rgba(100, 100, 100, 0.2);border-radius:0 0 4px 4px}
.ant-modal-footer button+button{margin-bottom:0;margin-left:8px}
.ant-modal.zoom-appear,.ant-modal.zoom-enter{transform:none;opacity:0;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-modal-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;height:100%;background-color:rgba(0,0,0,.45);filter:alpha(opacity=50)}
@@ -3671,14 +3674,15 @@ to{max-height:0;margin-bottom:0;padding-top:0;padding-bottom:0;opacity:0}
.ant-popover-placement-leftTop>.ant-popover-content>.ant-popover-arrow{top:12px}
.ant-popover-placement-leftBottom>.ant-popover-content>.ant-popover-arrow{bottom:12px}
.ant-progress{box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";display:inline-block}
.ant-progress{box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;}
.ant-progress-line{position:relative;width:100%;font-size:14px}
.ant-progress-small.ant-progress-line,.ant-progress-small.ant-progress-line .ant-progress-text .anticon{font-size:12px}
.ant-progress-outer{display:inline-block;width:100%;margin-right:0;padding-right:0}
.ant-progress-show-info .ant-progress-outer{margin-right:calc(-2em - 8px);padding-right:calc(2em + 8px)}
.ant-progress-inner{position:relative;display:inline-block;width:100%;overflow:hidden;vertical-align:middle;background-color:#f5f5f5;border-radius:100px}
.ant-progress-circle-trail{stroke:#f5f5f5}
.ant-progress-circle-trail{stroke:rgb(100 100 100 / 15%) !important}
.ant-progress-circle-path{-webkit-animation:ant-progress-appear .3s;animation:ant-progress-appear .3s}
.ant-progress-inner:not(.ant-progress-circle-gradient) .ant-progress-circle-path{stroke:#1890ff}
.ant-progress-inner:not(.ant-progress-circle-gradient) .ant-progress-circle-path{stroke:rgb(0 150 112) !important}
.ant-progress-bg,.ant-progress-success-bg{position:relative;background-color:#1890ff;border-radius:100px;transition:all .4s cubic-bezier(.08,.82,.17,1) 0s}
.ant-progress-success-bg{position:absolute;top:0;left:0;background-color:#52c41a}
.ant-progress-text{display:inline-block;width:2em;margin-left:8px;color:rgba(0,0,0,.45);font-size:1em;line-height:1;white-space:nowrap;text-align:left;vertical-align:middle;word-break:normal}
@@ -3966,7 +3970,7 @@ to{background-position:0 50%}
.ant-switch-small.ant-switch-checked .ant-switch-inner{margin-right:18px;margin-left:3px}
.ant-switch-small.ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-13px}
.ant-switch-small.ant-switch-loading .ant-switch-loading-icon{font-weight:700;transform:scale(.66667)}
.ant-switch-checked{background-color:#1890ff}
.ant-switch-checked{background-color:rgb(0 150 112)}
.ant-switch-checked .ant-switch-inner{margin-right:24px;margin-left:6px}
.ant-switch-checked:after{left:100%;margin-left:-1px;transform:translateX(-100%)}
.ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-19px}
@@ -3988,7 +3992,7 @@ to{transform:rotate(1turn) scale(.66667);transform-origin:50% 50%}
.ant-table-empty .ant-table-body{overflow-x:auto!important;overflow-y:hidden!important}
.ant-table table{width:100%;text-align:left;border-radius:4px 4px 0 0;border-collapse:separate;border-spacing:0}
.ant-table-layout-fixed table{table-layout:fixed}
.ant-table-thead>tr>th{color:rgba(0,0,0,.85);font-weight:500;text-align:left;background:#fafafa;border-bottom:1px solid #e8e8e8;transition:background .3s ease}
.ant-table-thead>tr>th{color:rgba(0,0,0,.85);font-weight:500;text-align:left;background:#fafafa;border-bottom:2px solid rgba(155, 155, 155, 0.15);transition:background .3s ease}
.ant-table-thead>tr>th[colspan]:not([colspan="1"]){text-align:center}
.ant-table-thead>tr>th .ant-table-filter-icon,.ant-table-thead>tr>th .anticon-filter{position:absolute;top:0;right:0;width:28px;height:100%;color:#bfbfbf;font-size:12px;text-align:center;cursor:pointer;transition:all .3s}
.ant-table-thead>tr>th .ant-table-filter-icon>svg,.ant-table-thead>tr>th .anticon-filter>svg{position:absolute;top:50%;left:50%;margin-top:-5px;margin-left:-6px}
@@ -4049,7 +4053,7 @@ to{transform:rotate(1turn) scale(.66667);transform-origin:50% 50%}
.ant-table-bordered.ant-table-fixed-header .ant-table-body-inner>table,.ant-table-bordered.ant-table-fixed-header .ant-table-header+.ant-table-body>table{border-top:0}
.ant-table-bordered .ant-table-thead>tr:not(:last-child)>th{border-bottom:1px solid #e8e8e8}
.ant-table-bordered .ant-table-tbody>tr>td,.ant-table-bordered .ant-table-thead>tr>th{border-right:1px solid #e8e8e8}
.ant-table-placeholder{position:relative;z-index:1;margin-top:-1px;padding:16px;color:rgba(0,0,0,.25);font-size:14px;text-align:center;background:#fff;border-top:1px solid #e8e8e8;border-bottom:1px solid #e8e8e8;border-radius:0 0 4px 4px}
.ant-table-placeholder{position:relative;z-index:1;margin-top:-1px;padding:16px;color:rgba(0,0,0,.25);font-size:14px;text-align:center;background:#fff;border-top:2px solid rgba(155, 155, 155, 0.15);border-bottom:2px solid rgba(155, 155, 155, 0.15);border-radius:0 0 4px 4px}
.ant-table-pagination.ant-pagination{float:right;margin:16px 0}
.ant-table-filter-dropdown{position:relative;min-width:96px;margin-left:-8px;background:#fff;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.15)}
.ant-table-filter-dropdown .ant-dropdown-menu{max-height:calc(100vh - 130px);overflow-x:hidden;border:0;border-radius:4px 4px 0 0;box-shadow:none}
@@ -4072,8 +4076,8 @@ to{transform:rotate(1turn) scale(.66667);transform-origin:50% 50%}
.ant-table-selection-down{display:inline-block;padding:0;line-height:1;cursor:pointer}
.ant-table-selection-down:hover .anticon-down{color:rgba(0,0,0,.6)}
.ant-table-row-expand-icon{color:#1890ff;text-decoration:none;cursor:pointer;transition:color .3s;display:inline-block;width:17px;height:17px;color:inherit;line-height:13px;text-align:center;background:#fff;border:1px solid #e8e8e8;border-radius:2px;outline:0;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{color:#40a9ff}
.ant-table-row-expand-icon:active{color:#096dd9}
.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{color:rgb(0 150 112)}
.ant-table-row-expand-icon:active{color:rgb(0 150 112)}
.ant-table-row-expand-icon:active,.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{border-color:currentColor}
.ant-table-row-expanded:after{content:"-"}
.ant-table-row-collapsed:after{content:"+"}
@@ -4491,4 +4495,4 @@ to{width:0;height:0;margin:0;padding:0;opacity:0}
}
@keyframes uploadAnimateInlineOut{
to{width:0;height:0;margin:0;padding:0;opacity:0}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,22 @@
html,
body {
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
overflow: hidden;
}
#app {
height: 100%;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
padding: 0;
overflow: auto;
}
.ant-space {
@@ -10,8 +27,14 @@
display: none;
}
@media (max-width: 768px) {
.ant-layout-sider {
display: none;
}
}
.ant-card {
border-radius: 30px;
border-radius: 1.5rem;
}
.ant-card-hoverable {
@@ -169,7 +192,7 @@
.ant-layout-sider-zero-width-trigger,
.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu,
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
background:#161b22
background:#1A202B
}
.ant-card-dark {
@@ -180,9 +203,36 @@
.ant-card-dark:hover {
border-color: #e8e8e8;
box-shadow: 0 2px 8px rgba(255,255,255,.15);
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 70%);
}
.ant-setting-textarea {
margin-top: 1.5rem;
min-height: 300px !important;
/*max-height: 800px !important;*/
}
.ant-card-dark-box-nohover{
margin-top: .5rem;
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;
/*background-color: rgb(36 44 58 / 50%);*/
}
.ant-card-dark-securitybox-nohover{
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
.ant-card-dark-securitybox-nohover:hover{
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 0%) !important;
}
/* .ant-card-bordered:hover {
box-shadow: 0 3px 12px -0.8px #0000005c;
} */
.ant-card-dark .ant-table-thead th {
color: hsla(0,0%,100%,.65);
background-color: #161b22;
@@ -199,6 +249,7 @@
.ant-card-dark .ant-input-group-addon {
color: hsla(0,0%,100%,.65);
background-color: #262f3d;
border: 1px solid rgb(149 149 149 / 30%);
}
.ant-card-dark .ant-list-item-meta-title,
@@ -212,6 +263,7 @@
.ant-card-dark .ant-modal-close,
.ant-card-dark i,
.ant-card-dark .ant-select-dropdown-menu-item,
.ant-card-dark .ant-calendar-day-select,
.ant-card-dark .ant-calendar-month-select,
.ant-card-dark .ant-calendar-year-select,
.ant-card-dark .ant-calendar-date,
@@ -221,12 +273,15 @@
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td {
background-color: #11314d;
}
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
.ant-card-dark .ant-calendar-date:hover,
.ant-card-dark .ant-select-dropdown-menu-item-active,
.ant-card-dark .ant-calendar-date:hover,
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
background-color: #004488;
background-color: rgb(4, 119, 90);
}
.ant-card-dark tbody .ant-table-expanded-row,
@@ -235,6 +290,10 @@
background-color: #1a212a;
}
.ant-input-number {
min-width: 100px;
}
.ant-card-dark .ant-input,
.ant-card-dark .ant-input-number,
.ant-card-dark .ant-input-number-handler-wrap,
@@ -243,7 +302,17 @@
.ant-card-dark .ant-select-selection,
.ant-card-dark .ant-calendar-picker-clear {
color: hsla(0,0%,100%,.65);
background-color: #2e3b52;
background-color: rgb(46 59 82 / 50%);
border: 1px solid rgb(0 150 112 / 0%);
}
.ant-layout:not(.login) .ant-input:focus,
.ant-layout:not(.login) .ant-input:hover,
.ant-layout:not(.login) .ant-input-number:focus,
.ant-layout:not(.login) .ant-input-number:hover,
.ant-layout:not(.login) .ant-calendar-input:focus,
.ant-layout:not(.login) .ant-calendar-input:hover {
background-color: rgba(0, 149, 111, 0.1);
}
.ant-card-dark .ant-select-disabled .ant-select-selection {
@@ -254,22 +323,30 @@
.ant-card-dark .ant-collapse-item {
color: hsla(0,0%,100%,.65);
background-color: #161b22;
border-radius: 0.5rem 0.5rem 0 0;
}
.ant-dropdown-menu-dark,
.ant-card-dark .ant-modal-content {
border: 1px solid rgba(255, 255, 255, 0.65);
border:1px solid rgb(100 100 100 / 20%);
box-shadow: 0 2px 8px rgba(255,255,255,.15);
}
.ant-card-dark .ant-modal-content,
.ant-card-dark .ant-modal-body,
.ant-card-dark .ant-modal-header,
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
.ant-card-dark .ant-modal-header {
color: hsla(0,0%,100%,.65);
background-color: #222a37;
}
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
background-color: rgb(0 150 112);
}
.ant-card-dark .ant-calendar-time-picker-select li:hover {
background: #1668dc;
}
.client-table-header {
background-color: #f0f2f5;
}
@@ -316,9 +393,9 @@
}
.ant-card-dark .ant-tag-green {
color: #6abe39;
background: #162312;
border-color: #274916;
color: #37b998;
background: #101e1a;
border-color: #144237;
}
.ant-card-dark .ant-tag-cyan {
@@ -345,7 +422,7 @@
}
.ant-card-dark .ant-switch-checked {
background-color: #0c61b0;
background-color: #009670;
}
.ant-card-dark .ant-btn,
@@ -356,19 +433,19 @@
}
.ant-card-dark .ant-radio-button-wrapper:hover {
color: #177ddc;
color: #009670;
}
.ant-card-dark .ant-btn-primary {
color: hsla(0,0%,100%,.65);
background-color: #073763;
border-color: #1890ff;
background-color: rgb(7 98 75 / 50%);
border-color: #009670;
text-shadow: 0 -1px 0 rgba(255,255,255,.12);
box-shadow: 0 2px 0 rgba(255,255,255,.045);
}
.ant-card-dark .ant-btn-primary:hover {
background-color: #40a9ff;
border-color: #40a9ff;
background-color: #009670;
border-color: #40a9ff00;
}
.ant-dark .ant-popover-content {
@@ -388,4 +465,21 @@
.ant-dark .ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow {
border-color: transparent #2e3b52 #2e3b52 transparent;
}
}
::-webkit-scrollbar {
width: 0.7em;
}
::-webkit-scrollbar-track {
background: rgb(50 62 82 / 25%);
}
::-webkit-scrollbar-thumb {
background: rgb(133 133 133 / 80%);
border-radius: 100vw;
}
::-webkit-scrollbar-thumb:hover {
background: #919191;
}

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.interceptors.request.use(
config => {
config.data = Qs.stringify(config.data, {
arrayFormat: 'repeat'
});
(config) => {
if (config.data instanceof FormData) {
config.headers['Content-Type'] = 'multipart/form-data';
} else {
config.data = Qs.stringify(config.data, {
arrayFormat: 'repeat',
});
}
return config;
},
error => Promise.reject(error)
);
(error) => Promise.reject(error),
);

View File

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

View File

@@ -153,9 +153,9 @@ class DBInbound {
}
}
genLink(clientIndex) {
genLink(address=this.address, remark=this.remark, clientIndex=0) {
const inbound = this.toInbound();
return inbound.genLink(this.address, this.remark, clientIndex);
return inbound.genLink(address, remark, clientIndex);
}
get genInboundLinks() {
@@ -172,6 +172,7 @@ class AllSetting {
this.webCertFile = "";
this.webKeyFile = "";
this.webBasePath = "/";
this.sessionMaxAge = "";
this.expireDiff = "";
this.trafficDiff = "";
this.tgBotEnable = false;
@@ -180,8 +181,17 @@ class AllSetting {
this.tgRunTime = "@daily";
this.tgBotBackup = false;
this.tgCpu = "";
this.tgLang = "en-US";
this.xrayTemplateConfig = "";
this.secretEnable = false;
this.subEnable = false;
this.subListen = "";
this.subPort = "2096";
this.subPath = "sub/";
this.subDomain = "";
this.subCertFile = "";
this.subKeyFile = "";
this.subUpdates = 0;
this.timeLocation = "Asia/Tehran";

View File

@@ -17,29 +17,8 @@ const VmessMethods = {
};
const SSMethods = {
AES_128_GCM: 'aes-128-gcm',
AES_256_GCM: 'aes-256-gcm',
CHACHA20_POLY1305: 'chacha20-poly1305',
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
XCHACHA20_POLY1305: 'xchacha20-poly1305',
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
};
const RULE_IP = {
PRIVATE: 'geoip:private',
CN: 'geoip:cn',
};
const RULE_DOMAIN = {
ADS: 'geosite:category-ads',
ADS_ALL: 'geosite:category-ads-all',
CN: 'geosite:cn',
GOOGLE: 'geosite:google',
FACEBOOK: 'geosite:facebook',
SPEEDTEST: 'geosite:speedtest',
};
const XTLS_FLOW_CONTROL = {
@@ -93,21 +72,26 @@ const UTLS_FINGERPRINT = {
};
const ALPN_OPTION = {
H3: "h3",
H2: "h2",
HTTP1: "http/1.1",
H2: "h2",
H3: "h3",
};
const SNIFFING_OPTION = {
HTTP: "http",
TLS: "tls",
QUIC: "quic",
};
Object.freeze(Protocols);
Object.freeze(VmessMethods);
Object.freeze(SSMethods);
Object.freeze(RULE_IP);
Object.freeze(RULE_DOMAIN);
Object.freeze(XTLS_FLOW_CONTROL);
Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(TLS_VERSION_OPTION);
Object.freeze(TLS_CIPHER_OPTION);
Object.freeze(ALPN_OPTION);
Object.freeze(SNIFFING_OPTION);
class XrayCommonClass {
@@ -456,18 +440,26 @@ class QuicStreamSettings extends XrayCommonClass {
}
class GrpcStreamSettings extends XrayCommonClass {
constructor(serviceName="") {
constructor(
serviceName="",
multiMode=false
) {
super();
this.serviceName = serviceName;
this.multiMode = multiMode;
}
static fromJson(json={}) {
return new GrpcStreamSettings(json.serviceName);
return new GrpcStreamSettings(
json.serviceName,
json.multiMode
);
}
toJson() {
return {
serviceName: this.serviceName,
multiMode: this.multiMode
}
}
}
@@ -490,8 +482,8 @@ class TlsStreamSettings extends XrayCommonClass {
this.settings = settings;
}
addCert(cert) {
this.certs.push(cert);
addCert() {
this.certs.push(new TlsStreamSettings.Cert());
}
removeCert(index) {
@@ -506,8 +498,7 @@ class TlsStreamSettings extends XrayCommonClass {
}
if (!ObjectUtil.isEmpty(json.settings)) {
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName);
}
settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains); }
return new TlsStreamSettings(
json.serverName,
json.minVersion,
@@ -574,17 +565,19 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
};
TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor(allowInsecure = false, fingerprint = '', serverName = '') {
constructor(allowInsecure = false, fingerprint = '', serverName = '', domains = []) {
super();
this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint;
this.serverName = serverName;
this.domains = domains;
}
static fromJson(json = {}) {
return new TlsStreamSettings.Settings(
json.allowInsecure,
json.fingerprint,
json.servername,
json.serverName,
json.domains,
);
}
toJson() {
@@ -592,6 +585,7 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint,
serverName: this.serverName,
domains: this.domains,
};
}
};
@@ -607,8 +601,8 @@ class XtlsStreamSettings extends XrayCommonClass {
this.settings = settings;
}
addCert(cert) {
this.certs.push(cert);
addCert() {
this.certs.push(new XtlsStreamSettings.Cert());
}
removeCert(index) {
@@ -714,7 +708,7 @@ class RealityStreamSettings extends XrayCommonClass {
minClient = '',
maxClient = '',
maxTimediff = 0,
shortIds = RandomUtil.randowShortId(),
shortIds = RandomUtil.randomShortId(),
settings= new RealityStreamSettings.Settings()
){
super();
@@ -733,7 +727,7 @@ class RealityStreamSettings extends XrayCommonClass {
static fromJson(json = {}) {
let settings;
if (!ObjectUtil.isEmpty(json.settings)) {
settings = new RealityStreamSettings.Settings(json.settings.publicKey , json.settings.fingerprint, json.settings.serverName);
settings = new RealityStreamSettings.Settings(json.settings.publicKey , json.settings.fingerprint, json.settings.serverName, json.settings.spiderX);
}
return new RealityStreamSettings(
json.show,
@@ -766,17 +760,19 @@ class RealityStreamSettings extends XrayCommonClass {
}
RealityStreamSettings.Settings = class extends XrayCommonClass {
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '') {
constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '', spiderX= '/') {
super();
this.publicKey = publicKey;
this.fingerprint = fingerprint;
this.serverName = serverName;
this.spiderX = spiderX;
}
static fromJson(json = {}) {
return new RealityStreamSettings.Settings(
json.publicKey,
json.fingerprint,
json.serverName,
json.spiderX,
);
}
toJson() {
@@ -784,6 +780,7 @@ RealityStreamSettings.Settings = class extends XrayCommonClass {
publicKey: this.publicKey,
fingerprint: this.fingerprint,
serverName: this.serverName,
spiderX: this.spiderX,
};
}
};
@@ -888,7 +885,7 @@ class StreamSettings extends XrayCommonClass {
}
class Sniffing extends XrayCommonClass {
constructor(enabled=true, destOverride=['http', 'tls']) {
constructor(enabled=true, destOverride=['http', 'tls', 'quic']) {
super();
this.enabled = enabled;
this.destOverride = destOverride;
@@ -898,7 +895,7 @@ class Sniffing extends XrayCommonClass {
let destOverride = ObjectUtil.clone(json.destOverride);
if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) {
if (ObjectUtil.isEmpty(destOverride[0])) {
destOverride = ['http', 'tls'];
destOverride = ['http', 'tls', 'quic'];
}
}
return new Sniffing(
@@ -940,7 +937,7 @@ class Inbound extends XrayCommonClass {
this._protocol = protocol;
this.settings = Inbound.Settings.getSettings(protocol);
if (protocol === Protocols.TROJAN) {
this.tls = true;
this.tls = false;
}
}
@@ -1013,66 +1010,6 @@ class Inbound extends XrayCommonClass {
return this.network === "http";
}
// VMess & VLess
get uuid() {
switch (this.protocol) {
case Protocols.VMESS:
return this.settings.vmesses[0].id;
case Protocols.VLESS:
return this.settings.vlesses[0].id;
default:
return "";
}
}
// VLess & Trojan
get flow() {
switch (this.protocol) {
case Protocols.VLESS:
return this.settings.vlesses[0].flow;
case Protocols.TROJAN:
return this.settings.trojans[0].flow;
default:
return "";
}
}
// VMess
get alterId() {
switch (this.protocol) {
case Protocols.VMESS:
return this.settings.vmesses[0].alterId;
default:
return "";
}
}
// Socks & HTTP
get username() {
switch (this.protocol) {
case Protocols.SOCKS:
case Protocols.HTTP:
return this.settings.accounts[0].user;
default:
return "";
}
}
// Trojan & Shadowsocks & Socks & HTTP
get password() {
switch (this.protocol) {
case Protocols.TROJAN:
return this.settings.trojans[0].password;
case Protocols.SHADOWSOCKS:
return this.settings.password;
case Protocols.SOCKS:
case Protocols.HTTP:
return this.settings.accounts[0].pass;
default:
return "";
}
}
// Shadowsocks
get method() {
switch (this.protocol) {
@@ -1147,9 +1084,13 @@ class Inbound extends XrayCommonClass {
return this.settings.vlesses[index].expiryTime < new Date().getTime();
return false
case Protocols.TROJAN:
if(this.settings.trojans[index].expiryTime > 0)
return this.settings.trojans[index].expiryTime < new Date().getTime();
return false
if(this.settings.trojans[index].expiryTime > 0)
return this.settings.trojans[index].expiryTime < new Date().getTime();
return false
case Protocols.SHADOWSOCKS:
if(this.settings.shadowsockses[index].expiryTime > 0)
return this.settings.shadowsockses[index].expiryTime < new Date().getTime();
return false
default:
return false;
}
@@ -1228,7 +1169,6 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
return true;
default:
return false;
@@ -1261,50 +1201,6 @@ class Inbound extends XrayCommonClass {
if (this.protocol !== Protocols.VMESS) {
return '';
}
let network = this.stream.network;
let type = 'none';
let host = '';
let path = '';
if (network === 'tcp') {
let tcp = this.stream.tcp;
type = tcp.type;
if (type === 'http') {
let request = tcp.request;
path = request.path.join(',');
let index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
host = request.headers[index].value;
}
}
} else if (network === 'kcp') {
let kcp = this.stream.kcp;
type = kcp.type;
path = kcp.seed;
} else if (network === 'ws') {
let ws = this.stream.ws;
path = ws.path;
let index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
host = ws.headers[index].value;
}
} else if (network === 'http') {
network = 'h2';
path = this.stream.http.path;
host = this.stream.http.host.join(',');
} else if (network === 'quic') {
type = this.stream.quic.type;
host = this.stream.quic.security;
path = this.stream.quic.key;
} else if (network === 'grpc') {
path = this.stream.grpc.serviceName;
}
if (this.stream.security === 'tls') {
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
}
}
let obj = {
v: '2',
ps: remark,
@@ -1312,16 +1208,66 @@ class Inbound extends XrayCommonClass {
port: this.port,
id: this.settings.vmesses[clientIndex].id,
aid: this.settings.vmesses[clientIndex].alterId,
net: network,
type: type,
host: host,
path: path,
net: this.stream.network,
type: 'none',
tls: this.stream.security,
sni: this.stream.tls.settings.serverName,
fp: this.stream.tls.settings.fingerprint,
alpn: this.stream.tls.alpn.join(','),
allowInsecure: this.stream.tls.settings.allowInsecure,
};
let network = this.stream.network;
if (network === 'tcp') {
let tcp = this.stream.tcp;
obj.type = tcp.type;
if (tcp.type === 'http') {
let request = tcp.request;
obj.path = request.path.join(',');
let index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
obj.host = request.headers[index].value;
}
}
} else if (network === 'kcp') {
let kcp = this.stream.kcp;
obj.type = kcp.type;
obj.path = kcp.seed;
} else if (network === 'ws') {
let ws = this.stream.ws;
obj.path = ws.path;
let index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
obj.host = ws.headers[index].value;
}
} else if (network === 'http') {
obj.net = 'h2';
obj.path = this.stream.http.path;
obj.host = this.stream.http.host.join(',');
} else if (network === 'quic') {
obj.type = this.stream.quic.type;
obj.host = this.stream.quic.security;
obj.path = this.stream.quic.key;
} else if (network === 'grpc') {
obj.path = this.stream.grpc.serviceName;
if (this.stream.grpc.multiMode){
obj.type = 'multi'
}
}
if (this.stream.security === 'tls') {
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
obj.add = this.stream.tls.server;
}
if (!ObjectUtil.isEmpty(this.stream.tls.settings.serverName)){
obj.sni = this.stream.tls.settings.serverName;
}
if (!ObjectUtil.isEmpty(this.stream.tls.settings.fingerprint)){
obj.fp = this.stream.tls.settings.fingerprint;
}
if (this.stream.tls.alpn.length>0){
obj.alpn = this.stream.tls.alpn.join(',');
}
if (this.stream.tls.settings.allowInsecure){
obj.allowInsecure = this.stream.tls.settings.allowInsecure;
}
}
return 'vmess://' + base64(JSON.stringify(obj, null, 2));
}
@@ -1374,6 +1320,9 @@ class Inbound extends XrayCommonClass {
case "grpc":
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
if(grpc.multiMode){
params.set("mode", "multi");
}
break;
}
@@ -1404,6 +1353,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.xtls.server)) {
address = this.stream.xtls.server;
}
if (this.stream.xtls.settings.serverName !== ''){
params.set("sni", this.stream.xtls.settings.serverName);
}
params.set("flow", this.settings.vlesses[clientIndex].flow);
}
@@ -1423,6 +1375,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
}
const link = `vless://${uuid}@${address}:${port}`;
@@ -1434,13 +1389,11 @@ class Inbound extends XrayCommonClass {
return url.toString();
}
genSSLink(address='', remark='') {
genSSLink(address='', remark='', clientIndex = 0) {
let settings = this.settings;
const server = this.stream.tls.server;
if (!ObjectUtil.isEmpty(server)) {
address = server;
}
return 'ss://' + safeBase64(settings.method + ':' + settings.password) + `@${address}:${this.port}#${encodeURIComponent(remark)}`;
const port = this.port;
return 'ss://' + safeBase64(settings.method + ':' + settings.password + ':' +settings.shadowsockses[clientIndex].password) + '@' + address + ':' + this.port + '#' + encodeURIComponent(remark);
}
genTrojanLink(address = '', remark = '', clientIndex = 0) {
@@ -1491,6 +1444,9 @@ class Inbound extends XrayCommonClass {
case "grpc":
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
if(grpc.multiMode){
params.set("mode", "multi");
}
break;
}
@@ -1522,6 +1478,9 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
params.set("spx", this.stream.reality.settings.spiderX);
}
}
if (this.xtls) {
@@ -1533,10 +1492,13 @@ class Inbound extends XrayCommonClass {
if (!ObjectUtil.isEmpty(this.stream.xtls.server)) {
address = this.stream.xtls.server;
}
if (this.stream.xtls.settings.serverName !== ''){
params.set("sni", this.stream.xtls.settings.serverName);
}
params.set("flow", this.settings.trojans[clientIndex].flow);
}
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);
for (const [key, value] of params) {
url.searchParams.set(key, value)
@@ -1547,21 +1509,13 @@ class Inbound extends XrayCommonClass {
genLink(address='', remark='', clientIndex=0) {
switch (this.protocol) {
case Protocols.VMESS:
if (this.settings.vmesses[clientIndex].email != ""){
remark += '-' + this.settings.vmesses[clientIndex].email
}
case Protocols.VMESS:
return this.genVmessLink(address, remark, clientIndex);
case Protocols.VLESS:
if (this.settings.vlesses[clientIndex].email != ""){
remark += '-' + this.settings.vlesses[clientIndex].email
}
return this.genVLESSLink(address, remark, clientIndex);
case Protocols.SHADOWSOCKS: return this.genSSLink(address, remark);
case Protocols.SHADOWSOCKS:
return this.genSSLink(address, remark, clientIndex);
case Protocols.TROJAN:
if (this.settings.trojans[clientIndex].email != ""){
remark += '-' + this.settings.trojans[clientIndex].email
}
return this.genTrojanLink(address, remark, clientIndex);
default: return '';
}
@@ -1573,12 +1527,17 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
JSON.parse(this.settings).clients.forEach((_,index) => {
link += this.genLink(address, remark, index) + '\r\n';
case Protocols.SHADOWSOCKS:
JSON.parse(this.settings).clients.forEach((client,index) => {
if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){
this.stream.tls.settings.domains.forEach((domain) => {
link += this.genLink(domain.domain, remark + '-' + client.email + '-' + domain.remark, index) + '\r\n';
});
} else {
link += this.genLink(address, remark + '-' + client.email, index) + '\r\n';
}
});
return link;
case Protocols.SHADOWSOCKS:
return (this.genSSLink(address, remark) + '\r\n');
default: return '';
}
}
@@ -2021,13 +1980,15 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
Inbound.ShadowsocksSettings = class extends Inbound.Settings {
constructor(protocol,
method=SSMethods.BLAKE3_AES_256_GCM,
password=RandomUtil.randomSeq(44),
network='tcp,udp'
password=RandomUtil.randomShadowsocksPassword(),
network='tcp,udp',
shadowsockses=[new Inbound.ShadowsocksSettings.Shadowsocks()]
) {
super(protocol);
this.method = method;
this.password = password;
this.network = network;
this.shadowsockses = shadowsockses;
}
static fromJson(json={}) {
@@ -2036,6 +1997,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
json.method,
json.password,
json.network,
json.clients.map(client => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)),
);
}
@@ -2044,10 +2006,77 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
method: this.method,
password: this.password,
network: this.network,
clients: Inbound.ShadowsocksSettings.toJsonArray(this.shadowsockses)
};
}
};
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
constructor(password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
super();
this.password = password;
this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
}
toJson() {
return {
password: this.password,
email: this.email,
limitIp: this.limitIp,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
enable: this.enable,
tgId: this.tgId,
subId: this.subId,
};
}
static fromJson(json = {}) {
return new Inbound.ShadowsocksSettings.Shadowsocks(
json.password,
json.email,
json.limitIp,
json.totalGB,
json.expiryTime,
json.enable,
json.tgId,
json.subId,
);
}
get _expiryTime() {
if (this.expiryTime === 0 || this.expiryTime === "") {
return null;
}
if (this.expiryTime < 0){
return this.expiryTime / -86400000;
}
return moment(this.expiryTime);
}
set _expiryTime(t) {
if (t == null || t === "") {
this.expiryTime = 0;
} else {
this.expiryTime = t.valueOf();
}
}
get _totalGB() {
return toFixed(this.totalGB / ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = toFixed(gb * ONE_GB, 0);
}
};
Inbound.DokodemoSettings = class extends Inbound.Settings {
constructor(protocol, address, port, network='tcp,udp', followRedirect=false) {
super(protocol);

View File

@@ -5,7 +5,9 @@ const ONE_TB = ONE_GB * 1024;
const ONE_PB = ONE_TB * 1024;
function sizeFormat(size) {
if (size < ONE_KB) {
if (size < 0) {
return "0 B";
} else if (size < ONE_KB) {
return size.toFixed(0) + " B";
} else if (size < ONE_MB) {
return (size / ONE_KB).toFixed(2) + " KB";
@@ -20,6 +22,23 @@ function sizeFormat(size) {
}
}
function cpuSpeedFormat(speed) {
if (speed > 1000) {
const GHz = speed / 1000;
return GHz.toFixed(2) + " GHz";
} else {
return speed.toFixed(2) + " MHz";
}
}
function cpuCoreFormat(cores) {
if (cores === 1) {
return "1 Core";
} else {
return cores + " Cores";
}
}
function base64(str) {
return Base64.encode(str);
}
@@ -56,14 +75,63 @@ function toFixed(num, n) {
return Math.round(num * n) / n;
}
function debounce (fn, delay) {
var timeoutID = null
function debounce(fn, delay) {
var timeoutID = null;
return function () {
clearTimeout(timeoutID)
var args = arguments
var that = this
timeoutID = setTimeout(function () {
fn.apply(that, args)
}, delay)
clearTimeout(timeoutID);
var args = arguments;
var that = this;
timeoutID = setTimeout(function () {
fn.apply(that, args);
}, delay);
};
}
function getCookie(cname) {
let name = cname + '=';
let ca = document.cookie.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) {
// decode cookie value only
return decodeURIComponent(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();
// encode cookie value
document.cookie = cname + '=' + encodeURIComponent(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 oneHour = oneMinute * 60; // 一小时的毫秒数
const oneDay = oneHour * 24; // 一天的毫秒数
const oneWeek = oneDay * 7; // 一星期的毫秒数
const oneMonth = oneDay * 30; // 一个月的毫秒数
const oneMinute = 1000 * 60; // MilliseConds in a Minute
const oneHour = oneMinute * 60; // The milliseconds of one hour
const oneDay = oneHour * 24; // The Number of MilliseConds A Day
const oneWeek = oneDay * 7; // The milliseconds per week
const oneMonth = oneDay * 30; // The milliseconds of a month
/**
* 按天数减少
* Decrease according to the number of days
*
* @param days 要减少的天数
* @param days to reduce the number of days to be reduced
*/
Date.prototype.minusDays = function (days) {
return this.minusMillis(oneDay * days);
};
/**
* 按天数增加
* Increase according to the number of days
*
* @param days 要增加的天数
* @param days The number of days to be increased
*/
Date.prototype.plusDays = function (days) {
return this.plusMillis(oneDay * days);
};
/**
* 按小时减少
* A few
*
* @param hours 要减少的小时数
* @param hours to be reduced
*/
Date.prototype.minusHours = function (hours) {
return this.minusMillis(oneHour * hours);
};
/**
* 按小时增加
* Increase hourly
*
* @param hours 要增加的小时数
* @param hours to increase the number of hours
*/
Date.prototype.plusHours = function (hours) {
return this.plusMillis(oneHour * hours);
};
/**
* 按分钟减少
* Make reduction in minutes
*
* @param minutes 要减少的分钟数
* @param minutes to reduce the number of minutes
*/
Date.prototype.minusMinutes = function (minutes) {
return this.minusMillis(oneMinute * minutes);
};
/**
* 按分钟增加
* Add in minutes
*
* @param minutes 要增加的分钟数
* @param minutes to increase the number of minutes
*/
Date.prototype.plusMinutes = function (minutes) {
return this.plusMillis(oneMinute * minutes);
};
/**
* 按毫秒减少
* Decrease in milliseconds
*
* @param millis 要减少的毫秒数
* @param millis to reduce the milliseconds
*/
Date.prototype.minusMillis = function(millis) {
let time = this.getTime() - millis;
@@ -71,9 +71,9 @@ Date.prototype.minusMillis = function(millis) {
};
/**
* 按毫秒增加
* Add in milliseconds to increase
*
* @param millis 要增加的毫秒数
* @param millis to increase the milliseconds to increase
*/
Date.prototype.plusMillis = function(millis) {
let time = this.getTime() + millis;
@@ -83,7 +83,7 @@ Date.prototype.plusMillis = function(millis) {
};
/**
* 设置时间为当天的 00:00:00.000
* Setting time is 00: 00: 00.000 on the day
*/
Date.prototype.setMinTime = function () {
this.setHours(0);
@@ -94,7 +94,7 @@ Date.prototype.setMinTime = function () {
};
/**
* 设置时间为当天的 23:59:59.999
* Setting time is 23: 59: 59.999 on the same day
*/
Date.prototype.setMaxTime = function () {
this.setHours(23);
@@ -105,37 +105,36 @@ Date.prototype.setMaxTime = function () {
};
/**
* 格式化日期
* Formatting date
*/
Date.prototype.formatDate = function () {
return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate());
};
/**
* 格式化时间
* Format time
*/
Date.prototype.formatTime = function () {
return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds());
};
/**
* 格式化日期加时间
* Formatting date plus time
*
* @param split 日期和时间之间的分隔符,默认是一个空格
* @param split Date and time separation symbols, default is a space
*/
Date.prototype.formatDateTime = function (split = ' ') {
return this.formatDate() + split + this.formatTime();
};
class DateUtil {
// 字符串转 Date 对象
// String string to date object
static parseDate(str) {
return new Date(str.replace(/-/g, '/'));
}
static formatMillis(millis) {
return moment(millis).format('YYYY-M-D H:m:s')
return moment(millis).format('YYYY-M-D H:m:s');
}
static firstDayOfMonth() {
@@ -144,4 +143,4 @@ class DateUtil {
date.setMinTime();
return date;
}
}
}

View File

@@ -68,36 +68,20 @@ class HttpUtil {
}
class PromiseUtil {
static async sleep(timeout) {
await new Promise(resolve => {
setTimeout(resolve, timeout)
});
}
}
const seq = [
'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z'
];
const seq = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const shortIdSeq = [
'a', 'b', 'c', 'd', 'e', 'f',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
];
const shortIdSeq = 'abcdef0123456789'.split('');
class RandomUtil {
static randomIntRange(min, max) {
return parseInt(Math.random() * (max - min) + min, 10);
return Math.floor(Math.random() * (max - min) + min);
}
static randomInt(n) {
@@ -119,6 +103,10 @@ class RandomUtil {
}
return str;
}
static randomShortId() {
return this.randomShortIdSeq(8);
}
static randomLowerAndNum(count) {
let str = '';
@@ -153,22 +141,20 @@ class RandomUtil {
static randomText() {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5)
for(var ii=0; ii<len; ii++){
for (var ii = 0; ii < 8; ii++) {
string += chars[Math.floor(Math.random() * chars.length)];
}
return string;
}
static randowShortId() {
let str = '';
str += this.randomShortIdSeq(8)
return str;
static randomShadowsocksPassword() {
let array = new Uint8Array(32);
window.crypto.getRandomValues(array);
return btoa(String.fromCharCode.apply(null, array));
}
}
class ObjectUtil {
static getPropIgnoreCase(obj, prop) {
for (const name in obj) {
if (!obj.hasOwnProperty(name)) {
@@ -316,5 +302,4 @@ class ObjectUtil {
}
return true;
}
}

View File

@@ -1,10 +1,15 @@
package controller
import "github.com/gin-gonic/gin"
import (
"x-ui/web/service"
"github.com/gin-gonic/gin"
)
type APIController struct {
BaseController
inboundController *InboundController
Tgbot service.Tgbot
}
func NewAPIController(g *gin.RouterGroup) *APIController {
@@ -14,7 +19,7 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
}
func (a *APIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/xui/API/inbounds")
g = g.Group("/panel/api/inbounds")
g.Use(a.checkLogin)
g.GET("/list", a.getAllInbounds)
@@ -25,30 +30,37 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
g.POST("/update/:id", a.updateInbound)
g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/addClient/", a.addInboundClient)
g.POST("/addClient", a.addInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
g.GET("/createbackup", a.createBackup)
a.inboundController = NewInboundController(g)
}
func (a *APIController) getAllInbounds(c *gin.Context) {
a.inboundController.getInbounds(c)
}
func (a *APIController) getSingleInbound(c *gin.Context) {
a.inboundController.getInbound(c)
}
func (a *APIController) getClientTraffics(c *gin.Context) {
a.inboundController.getClientTraffics(c)
}
func (a *APIController) addInbound(c *gin.Context) {
a.inboundController.addInbound(c)
}
func (a *APIController) delInbound(c *gin.Context) {
a.inboundController.delInbound(c)
}
func (a *APIController) updateInbound(c *gin.Context) {
a.inboundController.updateInbound(c)
}
@@ -60,21 +72,35 @@ func (a *APIController) getClientIps(c *gin.Context) {
func (a *APIController) clearClientIps(c *gin.Context) {
a.inboundController.clearClientIps(c)
}
func (a *APIController) addInboundClient(c *gin.Context) {
a.inboundController.addInboundClient(c)
}
func (a *APIController) delInboundClient(c *gin.Context) {
a.inboundController.delInboundClient(c)
}
func (a *APIController) updateInboundClient(c *gin.Context) {
a.inboundController.updateInboundClient(c)
}
func (a *APIController) resetClientTraffic(c *gin.Context) {
a.inboundController.resetClientTraffic(c)
}
func (a *APIController) resetAllTraffics(c *gin.Context) {
a.inboundController.resetAllTraffics(c)
}
func (a *APIController) resetAllClientTraffics(c *gin.Context) {
a.inboundController.resetAllClientTraffics(c)
}
func (a *APIController) delDepletedClients(c *gin.Context) {
a.inboundController.delDepletedClients(c)
}
func (a *APIController) createBackup(c *gin.Context) {
a.Tgbot.SendBackupToAdmins()
}

View File

@@ -1,9 +1,12 @@
package controller
import (
"github.com/gin-gonic/gin"
"net/http"
"x-ui/logger"
"x-ui/web/locale"
"x-ui/web/session"
"github.com/gin-gonic/gin"
)
type BaseController struct {
@@ -12,7 +15,7 @@ type BaseController struct {
func (a *BaseController) checkLogin(c *gin.Context) {
if !session.IsLogin(c) {
if isAjax(c) {
pureJsonMsg(c, false, I18n(c, "pages.login.loginAgain"))
pureJsonMsg(c, false, I18nWeb(c, "pages.login.loginAgain"))
} else {
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}
@@ -22,11 +25,13 @@ func (a *BaseController) checkLogin(c *gin.Context) {
}
}
func I18n(c *gin.Context, name string) string {
anyfunc, _ := c.Get("I18n")
i18n, _ := anyfunc.(func(key string, params ...string) (string, error))
message, _ := i18n(name)
return message
func I18nWeb(c *gin.Context, name string, params ...string) string {
anyfunc, funcExists := c.Get("I18n")
if !funcExists {
logger.Warning("I18n function not exists in gin context!")
return ""
}
i18nFunc, _ := anyfunc.(func(i18nType locale.I18nType, key string, keyParams ...string) string)
msg := i18nFunc(locale.Web, name, params...)
return msg
}

View File

@@ -35,10 +35,11 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/addClient", a.addInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
}
@@ -59,7 +60,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
user := session.GetLoginUser(c)
inbounds, err := a.inboundService.GetInbounds(user.Id)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
return
}
jsonObj(c, inbounds, nil)
@@ -67,12 +68,12 @@ func (a *InboundController) getInbounds(c *gin.Context) {
func (a *InboundController) getInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "get"), err)
jsonMsg(c, I18nWeb(c, "get"), err)
return
}
inbound, err := a.inboundService.GetInbound(id)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.toasts.obtain"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
return
}
jsonObj(c, inbound, nil)
@@ -92,7 +93,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.addTo"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.create"), err)
return
}
user := session.GetLoginUser(c)
@@ -100,7 +101,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
inbound.Enable = true
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
inbound, err = a.inboundService.AddInbound(inbound)
jsonMsgObj(c, I18n(c, "pages.inbounds.addTo"), inbound, err)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
@@ -109,11 +110,11 @@ func (a *InboundController) addInbound(c *gin.Context) {
func (a *InboundController) delInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "delete"), err)
jsonMsg(c, I18nWeb(c, "delete"), err)
return
}
err = a.inboundService.DelInbound(id)
jsonMsgObj(c, I18n(c, "delete"), id, err)
jsonMsgObj(c, I18nWeb(c, "delete"), id, err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
@@ -122,7 +123,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
func (a *InboundController) updateInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
inbound := &model.Inbound{
@@ -130,11 +131,11 @@ func (a *InboundController) updateInbound(c *gin.Context) {
}
err = c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
inbound, err = a.inboundService.UpdateInbound(inbound)
jsonMsgObj(c, I18n(c, "pages.inbounds.revise"), inbound, err)
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.update"), inbound, err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
@@ -145,17 +146,24 @@ func (a *InboundController) getClientIps(c *gin.Context) {
ips, err := a.inboundService.GetInboundClientIps(email)
if err != nil {
jsonObj(c, "Failed to get client IPs", nil)
return
}
if ips == "" {
jsonObj(c, "No IP Record", nil)
return
}
jsonObj(c, ips, nil)
}
func (a *InboundController) clearClientIps(c *gin.Context) {
email := c.Param("email")
err := a.inboundService.ClearClientIps(email)
if err != nil {
jsonMsg(c, "Revise", err)
jsonMsg(c, "Update", err)
return
}
jsonMsg(c, "Log Cleared", nil)
@@ -164,13 +172,13 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
data := &model.Inbound{}
err := c.ShouldBind(data)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
err = a.inboundService.AddInboundClient(data)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client(s) added", nil)
@@ -182,14 +190,14 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
clientId := c.Param("clientId")
err = a.inboundService.DelInboundClient(id, clientId)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client deleted", nil)
@@ -199,22 +207,18 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
}
func (a *InboundController) updateInboundClient(c *gin.Context) {
index, err := strconv.Atoi(c.Param("index"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
clientId := c.Param("clientId")
inbound := &model.Inbound{}
err = c.ShouldBind(inbound)
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
err = a.inboundService.UpdateInboundClient(inbound, index)
err = a.inboundService.UpdateInboundClient(inbound, clientId)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client updated", nil)
@@ -226,14 +230,14 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
email := c.Param("email")
err = a.inboundService.ResetClientTraffic(id, email)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "traffic reseted", nil)
@@ -245,7 +249,7 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
func (a *InboundController) resetAllTraffics(c *gin.Context) {
err := a.inboundService.ResetAllTraffics()
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "All traffics reseted", nil)
@@ -254,14 +258,28 @@ func (a *InboundController) resetAllTraffics(c *gin.Context) {
func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
err = a.inboundService.ResetAllClientTraffics(id)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "All traffics of client reseted", nil)
}
func (a *InboundController) delDepletedClients(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}
err = a.inboundService.DelDepletedClients(id)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "All delpeted clients are deleted", nil)
}

View File

@@ -39,7 +39,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
func (a *IndexController) index(c *gin.Context) {
if session.IsLogin(c) {
c.Redirect(http.StatusTemporaryRedirect, "xui/")
c.Redirect(http.StatusTemporaryRedirect, "panel/")
return
}
html(c, "login.html", "pages.login.title", nil)
@@ -49,32 +49,45 @@ func (a *IndexController) login(c *gin.Context) {
var form LoginForm
err := c.ShouldBind(&form)
if err != nil {
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.invalidFormData"))
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
return
}
if form.Username == "" {
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyUsername"))
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyUsername"))
return
}
if form.Password == "" {
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword"))
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.emptyPassword"))
return
}
user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret)
timeStr := time.Now().Format("2006-01-02 15:04:05")
if user == nil {
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
pureJsonMsg(c, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
return
} else {
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
logger.Infof("%s login success, Ip Address: %s\n", form.Username, getRemoteIp(c))
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
}
sessionMaxAge, err := a.settingService.GetSessionMaxAge()
if err != nil {
logger.Infof("Unable to get session's max age from DB")
}
if sessionMaxAge > 0 {
err = session.SetMaxAge(c, sessionMaxAge*60)
if err != nil {
logger.Infof("Unable to set session's max age")
}
}
err = session.SetLoginUser(c, user)
logger.Info("user", user.Id, "login success")
jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err)
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), err)
}
func (a *IndexController) logout(c *gin.Context) {
@@ -91,5 +104,4 @@ func (a *IndexController) getSecretStatus(c *gin.Context) {
if err == nil {
jsonObj(c, status, nil)
}
}

View File

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

View File

@@ -49,7 +49,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
func (a *SettingController) getAllSetting(c *gin.Context) {
allSetting, err := a.settingService.GetAllSetting()
if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, allSetting, nil)
@@ -58,7 +58,7 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultJsonConfig()
if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, defaultJsonConfig, nil)
@@ -67,29 +67,74 @@ func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
func (a *SettingController) getDefaultSettings(c *gin.Context) {
expireDiff, err := a.settingService.GetExpireDiff()
if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
trafficDiff, err := a.settingService.GetTrafficDiff()
if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
defaultCert, err := a.settingService.GetCertFile()
if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
defaultKey, err := a.settingService.GetKeyFile()
if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
tgBotEnable, err := a.settingService.GetTgbotenabled()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subEnable, err := a.settingService.GetSubEnable()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subPort, err := a.settingService.GetSubPort()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subPath, err := a.settingService.GetSubPath()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subDomain, err := a.settingService.GetSubDomain()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subKeyFile, err := a.settingService.GetSubKeyFile()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subCertFile, err := a.settingService.GetSubCertFile()
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return
}
subTLS := false
if subKeyFile != "" || subCertFile != "" {
subTLS = true
}
result := map[string]interface{}{
"expireDiff": expireDiff,
"trafficDiff": trafficDiff,
"defaultCert": defaultCert,
"defaultKey": defaultKey,
"tgBotEnable": tgBotEnable,
"subEnable": subEnable,
"subPort": subPort,
"subPath": subPath,
"subDomain": subDomain,
"subTLS": subTLS,
}
jsonObj(c, result, nil)
}
@@ -98,27 +143,27 @@ func (a *SettingController) updateSetting(c *gin.Context) {
allSetting := &entity.AllSetting{}
err := c.ShouldBind(allSetting)
if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
return
}
err = a.settingService.UpdateAllSetting(allSetting)
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
}
func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{}
err := c.ShouldBind(form)
if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
return
}
user := session.GetLoginUser(c)
if user.Username != form.OldUsername || user.Password != form.OldPassword {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.originalUserPassIncorrect")))
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
return
}
if form.NewUsername == "" || form.NewPassword == "" {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), errors.New(I18n(c, "pages.setting.toasts.userPassMustBeNotEmpty")))
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
return
}
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
@@ -127,19 +172,19 @@ func (a *SettingController) updateUser(c *gin.Context) {
user.Password = form.NewPassword
session.SetLoginUser(c, user)
}
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
}
func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, I18n(c, "pages.setting.restartPanel"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.restartPanel"), err)
}
func (a *SettingController) updateSecret(c *gin.Context) {
form := &updateSecretForm{}
err := c.ShouldBind(form)
if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
}
user := session.GetLoginUser(c)
err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
@@ -147,8 +192,9 @@ func (a *SettingController) updateSecret(c *gin.Context) {
user.LoginSecret = form.LoginSecret
session.SetLoginUser(c, user)
}
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), err)
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
}
func (a *SettingController) getUserSecret(c *gin.Context) {
loginUser := session.GetLoginUser(c)
user := a.userService.GetUserSecret(loginUser.Id)

View File

@@ -1,24 +1,16 @@
package controller
import (
"github.com/gin-gonic/gin"
"net"
"net/http"
"strings"
"x-ui/config"
"x-ui/logger"
"x-ui/web/entity"
"github.com/gin-gonic/gin"
)
func getUriId(c *gin.Context) int64 {
s := struct {
Id int64 `uri:"id"`
}{}
_ = c.BindUri(&s)
return s.Id
}
func getRemoteIp(c *gin.Context) string {
value := c.GetHeader("X-Forwarded-For")
if value != "" {
@@ -46,12 +38,12 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
if err == nil {
m.Success = true
if msg != "" {
m.Msg = msg + I18n(c, "success")
m.Msg = msg + I18nWeb(c, "success")
}
} else {
m.Success = false
m.Msg = msg + I18n(c, "fail") + ": " + err.Error()
logger.Warning(msg+I18n(c, "fail")+": ", err)
m.Msg = msg + I18nWeb(c, "fail") + ": " + err.Error()
logger.Warning(msg+I18nWeb(c, "fail")+": ", err)
}
c.JSON(http.StatusOK, m)
}
@@ -75,6 +67,7 @@ func html(c *gin.Context, name string, title string, data gin.H) {
data = gin.H{}
}
data["title"] = title
data["host"] = strings.Split(c.Request.Host, ":")[0]
data["request_uri"] = c.Request.RequestURI
data["base_path"] = c.GetString("base_path")
c.HTML(http.StatusOK, name, getContext(data))
@@ -84,10 +77,8 @@ func getContext(h gin.H) gin.H {
a := gin.H{
"cur_ver": config.GetVersion(),
}
if h != nil {
for key, value := range h {
a[key] = value
}
for key, value := range h {
a[key] = value
}
return a
}

View File

@@ -18,12 +18,12 @@ func NewXUIController(g *gin.RouterGroup) *XUIController {
}
func (a *XUIController) initRouter(g *gin.RouterGroup) {
g = g.Group("/xui")
g = g.Group("/panel")
g.Use(a.checkLogin)
g.GET("/", a.index)
g.GET("/inbounds", a.inbounds)
g.GET("/setting", a.setting)
g.GET("/settings", a.settings)
a.inboundController = NewInboundController(g)
a.settingController = NewSettingController(g)
@@ -37,6 +37,6 @@ func (a *XUIController) inbounds(c *gin.Context) {
html(c, "inbounds.html", "pages.inbounds.title", nil)
}
func (a *XUIController) setting(c *gin.Context) {
html(c, "setting.html", "pages.setting.title", nil)
func (a *XUIController) settings(c *gin.Context) {
html(c, "settings.html", "pages.settings.title", nil)
}

View File

@@ -32,6 +32,7 @@ type AllSetting struct {
WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebBasePath string `json:"webBasePath" form:"webBasePath"`
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
@@ -40,9 +41,18 @@ type AllSetting struct {
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgCpu int `json:"tgCpu" form:"tgCpu"`
TgLang string `json:"tgLang" form:"tgLang"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"`
SubPath string `json:"subPath" form:"subPath"`
SubDomain string `json:"subDomain" form:"subDomain"`
SubCertFile string `json:"subCertFile" form:"subCertFile"`
SubKeyFile string `json:"subKeyFile" form:"subKeyFile"`
SubUpdates int `json:"subUpdates" form:"subUpdates"`
}
func (s *AllSetting) CheckValid() error {
@@ -53,10 +63,25 @@ func (s *AllSetting) CheckValid() error {
}
}
if s.SubListen != "" {
ip := net.ParseIP(s.SubListen)
if ip == nil {
return common.NewError("Sub listen is not valid ip:", s.SubListen)
}
}
if s.WebPort <= 0 || s.WebPort > 65535 {
return common.NewError("web port is not a valid port:", s.WebPort)
}
if s.SubPort <= 0 || s.SubPort > 65535 {
return common.NewError("Sub port is not a valid port:", s.SubPort)
}
if s.SubPort == s.WebPort {
return common.NewError("Sub and Web could not use same port:", s.SubPort)
}
if s.WebCertFile != "" || s.WebKeyFile != "" {
_, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)
if err != nil {
@@ -64,6 +89,13 @@ func (s *AllSetting) CheckValid() error {
}
}
if s.SubCertFile != "" || s.SubKeyFile != "" {
_, err := tls.LoadX509KeyPair(s.SubCertFile, s.SubKeyFile)
if err != nil {
return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.SubCertFile, s.SubKeyFile, err)
}
}
if !strings.HasPrefix(s.WebBasePath, "/") {
s.WebBasePath = "/" + s.WebBasePath
}

View File

@@ -2,17 +2,23 @@ package global
import (
"context"
"github.com/robfig/cron/v3"
_ "unsafe"
"github.com/robfig/cron/v3"
)
var webServer WebServer
var subServer SubServer
type WebServer interface {
GetCron() *cron.Cron
GetCtx() context.Context
}
type SubServer interface {
GetCtx() context.Context
}
func SetWebServer(s WebServer) {
webServer = s
}
@@ -20,3 +26,11 @@ func SetWebServer(s WebServer) {
func GetWebServer() WebServer {
return webServer
}
func SetSubServer(s SubServer) {
subServer = s
}
func GetSubServer() SubServer {
return subServer
}

82
web/global/hashStorage.go Normal file
View File

@@ -0,0 +1,82 @@
package global
import (
"crypto/md5"
"encoding/hex"
"regexp"
"sync"
"time"
)
type HashEntry struct {
Hash string
Value string
Timestamp time.Time
}
type HashStorage struct {
sync.RWMutex
Data map[string]HashEntry
Expiration time.Duration
}
func NewHashStorage(expiration time.Duration) *HashStorage {
return &HashStorage{
Data: make(map[string]HashEntry),
Expiration: expiration,
}
}
func (h *HashStorage) SaveHash(query string) string {
h.Lock()
defer h.Unlock()
md5Hash := md5.Sum([]byte(query))
md5HashString := hex.EncodeToString(md5Hash[:])
entry := HashEntry{
Hash: md5HashString,
Value: query,
Timestamp: time.Now(),
}
h.Data[md5HashString] = entry
return md5HashString
}
func (h *HashStorage) GetValue(hash string) (string, bool) {
h.RLock()
defer h.RUnlock()
entry, exists := h.Data[hash]
return entry.Value, exists
}
func (h *HashStorage) IsMD5(hash string) bool {
match, _ := regexp.MatchString("^[a-f0-9]{32}$", hash)
return match
}
func (h *HashStorage) RemoveExpiredHashes() {
h.Lock()
defer h.Unlock()
now := time.Now()
for hash, entry := range h.Data {
if now.Sub(entry.Timestamp) > h.Expiration {
delete(h.Data, hash)
}
}
}
func (h *HashStorage) Reset() {
h.Lock()
defer h.Unlock()
h.Data = make(map[string]HashEntry)
}

View File

@@ -7,12 +7,13 @@
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel=icon type=”image/x-icon” href="{{ .base_path }}assets/favicon.ico">
<link rel="shortcut icon" type="image/x-icon" href="{{ .base_path }}assets/favicon.ico">
<style>
[v-cloak] {
display: none;
}
</style>
<title>{{ i18n .title}}</title>
<title>{{ .host }}-{{ i18n .title}}</title>
</head>
{{end}}

View File

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

View File

@@ -1,46 +1,60 @@
{{define "qrcodeModal"}}
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
:closable="true"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:class="themeSwitcher.darkCardClass"
:footer="null"
width="300px">
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
<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>
<template v-if="app.subSettings.enable && qrModal.subId">
<a-divider>Subscription</a-divider>
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas>
</template>
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<template v-for="(row, index) in qrModal.qrcodes">
<a-tag color="orange" style="margin-top: 10px;display: block;text-align: center;">[[ row.remark ]]</a-tag>
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas>
</template>
</a-modal>
<script>
const qrModal = {
title: '',
content: '',
clientIndex: 0,
inbound: new Inbound(),
dbInbound: new DBInbound(),
copyText: '',
qrcode: null,
client: null,
qrcodes: [],
clipboard: null,
visible: false,
show: function (title='', content='', dbInbound=new DBInbound(), copyText='') {
subId: '',
show: function (title = '', dbInbound = new DBInbound(), clientIndex = 0) {
this.title = title;
this.content = content;
this.clientIndex = clientIndex;
this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound();
if (ObjectUtil.isEmpty(copyText)) {
this.copyText = content;
settings = JSON.parse(this.inbound.settings);
this.client = settings.clients[clientIndex];
remark = this.dbInbound.remark + "-" + this.client.email;
address = this.dbInbound.address;
this.subId = '';
this.qrcodes = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => {
this.qrcodes.push({
remark: remark + "-" + domain.remark,
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, clientIndex)
});
});
} else {
this.copyText = copyText;
this.qrcodes.push({
remark: remark,
link: this.inbound.genLink(address, remark, clientIndex)
});
}
this.visible = true;
qrModalApp.$nextTick(() => {
if (this.qrcode === null) {
this.qrcode = new QRious({
element: document.querySelector('#qrCode'),
size: 260,
value: content,
});
} else {
this.qrcode.value = content;
}
});
},
close: function () {
this.visible = false;
@@ -48,21 +62,46 @@
};
const qrModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#qrcode-modal',
data: {
qrModal: qrModal,
},
methods: {
copyToClipboard() {
this.qrModal.clipboard = new ClipboardJS('#qrCode', {
text: () => this.qrModal.copyText,
copyToClipboard(elmentId,content) {
this.qrModal.clipboard = new ClipboardJS('#'+elmentId, {
text: () => content,
});
this.qrModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.qrModal.clipboard.destroy();
});
},
setQrCode(elmentId,content) {
new QRious({
element: document.querySelector('#'+elmentId),
size: 260,
value: content,
});
},
genSubLink(subID) {
protocol = app.subSettings.tls ? "https://" : "http://";
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
subPort = app.subSettings.port;
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
subPath = app.subSettings.path;
return protocol + hostName + port + subPath + subID;
}
},
updated() {
if (qrModal.client.subId){
qrModal.subId = qrModal.client.subId;
this.setQrCode("qrCode-sub",this.genSubLink(qrModal.subId));
}
qrModal.qrcodes.forEach((element,index) => {
this.setQrCode("qrCode-"+index, element.link);
});
}
});
</script>

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
{{define "clientsBulkModal"}}
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:class="themeSwitcher.darkCardClass"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.client.method" }}'>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</a-select-option>
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
@@ -33,6 +33,30 @@
<span slot="label">{{ i18n "pages.client.clientCount" }}</span>
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item>
<a-form-item v-if="app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
</a-form-item>
<a-form-item v-if="app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="clientsBulkModal.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
@@ -43,29 +67,24 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input type="number" v-model.number="clientsBulkModal.limitIp" min="0" style="width: 70px;" ></a-input>
<a-input-number v-model="clientsBulkModal.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Subscription">
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
</a-form-item>
<a-form-item label="Telegram ID">
<a-input v-model.trim="clientsBulkModal.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -75,15 +94,17 @@
</span>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
<a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
<a-input-number v-model="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -91,8 +112,8 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
</a-form>
@@ -122,37 +143,42 @@
delayedStart: false,
ok() {
clients = [];
method=clientsBulkModal.emailMethod;
if(method>1){
start=clientsBulkModal.firstNum;
end=clientsBulkModal.lastNum + 1;
method = clientsBulkModal.emailMethod;
if (method > 1) {
start = clientsBulkModal.firstNum;
end = clientsBulkModal.lastNum + 1;
} else {
start=0;
end=clientsBulkModal.quantity;
start = 0;
end = clientsBulkModal.quantity;
}
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : "";
useNum=(method>1);
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? clientsBulkModal.emailPostfix : "";
prefix = (method > 0 && clientsBulkModal.emailPrefix.length > 0) ? clientsBulkModal.emailPrefix : "";
useNum = (method > 1);
postfix = (method > 2 && clientsBulkModal.emailPostfix.length > 0) ? clientsBulkModal.emailPostfix : "";
for (let i = start; i < end; i++) {
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
if(method==4) newClient.email = "";
if (method == 4) newClient.email = "";
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
newClient.subId = clientsBulkModal.subId;
newClient.tgId = clientsBulkModal.tgId;
if (clientsBulkModal.subId.length > 0) newClient.subId = clientsBulkModal.subId;
if (clientsBulkModal.tgId.length > 0) newClient.tgId = clientsBulkModal.tgId;
newClient.limitIp = clientsBulkModal.limitIp;
newClient._totalGB = clientsBulkModal.totalGB;
newClient._expiryTime = clientsBulkModal.expiryTime;
if(clientsBulkModal.inbound.canEnableTlsFlow()){
if (clientsBulkModal.inbound.canEnableTlsFlow()) {
newClient.flow = clientsBulkModal.flow;
}
if(clientsBulkModal.inbound.xtls){
if (clientsBulkModal.inbound.xtls) {
newClient.flow = clientsBulkModal.flow;
}
clients.push(newClient);
}
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
},
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
show({
title = '',
okText = '{{ i18n "sure" }}',
dbInbound = null,
confirm = (inbound, dbInbound) => { }
}) {
this.visible = true;
this.title = title;
this.okText = okText;
@@ -160,24 +186,25 @@
this.quantity = 1;
this.totalGB = 0;
this.expiryTime = 0;
this.emailMethod= 0;
this.limitIp= 0;
this.firstNum= 1;
this.lastNum= 1;
this.emailPrefix= "";
this.emailPostfix= "";
this.subId= "";
this.tgId= "";
this.flow= "";
this.emailMethod = 0;
this.limitIp = 0;
this.firstNum = 1;
this.lastNum = 1;
this.emailPrefix = "";
this.emailPostfix = "";
this.subId = "";
this.tgId = "";
this.flow = "";
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.delayedStart = false;
},
getClients(protocol, clientSettings) {
switch(protocol){
switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
@@ -186,6 +213,7 @@
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks();
default: return null;
}
},
@@ -209,10 +237,11 @@
get delayedExpireDays() {
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
},
set delayedExpireDays(days){
set delayedExpireDays(days) {
this.clientsBulkModal.expiryTime = -86400000 * days;
},
},
});
</script>
{{end}}

View File

@@ -1,7 +1,7 @@
{{define "clientsModal"}}
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:class="themeSwitcher.darkCardClass"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/client"}}
</a-modal>
@@ -17,18 +17,18 @@
inbound: new Inbound(),
clients: [],
clientStats: [],
oldClientId: "",
index: null,
clientIps: null,
isExpired: false,
delayedStart: false,
ok() {
if(clientModal.isEdit){
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.index);
if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
} else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
}
},
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=()=>{}, isEdit=false }) {
show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {
this.visible = true;
this.title = title;
this.okText = okText;
@@ -37,31 +37,40 @@
this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
this.index = index === null ? this.clients.length : index;
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
this.delayedStart = false;
if (!isEdit){
this.addClient(this.inbound.protocol, this.clients);
} else {
if (this.clients[index].expiryTime < 0){
if (isEdit) {
if (this.clients[index].expiryTime < 0) {
this.delayedStart = true;
}
this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]);
} else {
this.addClient(this.inbound.protocol, this.clients);
}
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm;
},
getClients(protocol, clientSettings) {
switch(protocol){
switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
getClientId(protocol, client) {
switch (protocol) {
case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email;
default: return client.id;
}
},
addClient(protocol, clients) {
switch (protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks());
default: return null;
}
},
@@ -92,39 +101,27 @@
return this.clientModal.isEdit;
},
get isTrafficExhausted() {
if(!clientStats) return false
if(clientStats.total <= 0) return false
if(clientStats.up + clientStats.down < clientStats.total) return false
if (!clientStats) return false
if (clientStats.total <= 0) return false
if (clientStats.up + clientStats.down < clientStats.total) return false
return true
},
get isExpiry() {
return this.clientModal.isExpired
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
},
get statsColor() {
if(!clientStats) return 'blue'
if(clientStats.total <= 0) return 'blue'
else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan'
else return 'red'
return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
},
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
},
set delayedExpireDays(days){
set delayedExpireDays(days) {
this.client.expiryTime = -86400000 * days;
},
},
methods: {
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string;
},
async getDBClientIps(email,event) {
const msg = await HttpUtil.post('/xui/inbound/clientIps/'+ email);
async getDBClientIps(email, event) {
const msg = await HttpUtil.post('/panel/inbound/clientIps/' + email);
if (!msg.success) {
return;
}
@@ -138,22 +135,22 @@
}
},
async clearDBClientIps(email) {
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email);
const msg = await HttpUtil.post('/panel/inbound/clearClientIps/' + email);
if (!msg.success) {
return;
}
document.getElementById("clientIPs").value = ""
},
resetClientTraffic(email,dbInboundId,iconElement) {
resetClientTraffic(email, dbInboundId, iconElement) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ email);
const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email);
if (msg.success) {
this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0;
@@ -164,5 +161,6 @@
},
},
});
</script>
{{end}}

View File

@@ -1,17 +1,17 @@
{{define "menuItems"}}
<a-menu-item key="{{ .base_path }}xui/">
<a-menu-item key="{{ .base_path }}panel/">
<a-icon type="dashboard"></a-icon>
<span>{{ i18n "menu.dashboard"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}xui/inbounds">
<a-menu-item key="{{ .base_path }}panel/inbounds">
<a-icon type="user"></a-icon>
<span>{{ i18n "menu.inbounds"}}</span>
</a-menu-item>
<a-menu-item key="{{ .base_path }}xui/setting">
<a-menu-item key="{{ .base_path }}panel/settings">
<a-icon type="setting"></a-icon>
<span>{{ i18n "menu.setting"}}</span>
<span>{{ i18n "menu.settings"}}</span>
</a-menu-item>
<!--<a-menu-item key="{{ .base_path }}xui/clients">-->
<!--<a-menu-item key="{{ .base_path }}panel/clients">-->
<!-- <a-icon type="laptop"></a-icon>-->
<!-- <span>Client</span>-->
<!--</a-menu-item>-->
@@ -23,17 +23,14 @@
{{define "commonSider"}}
<a-layout-sider :theme="siderDrawer.theme" id="sider" collapsible breakpoint="md" collapsed-width="0">
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
<a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0">
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
<theme-switch />
</a-menu-item>
</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">
{{template "menuItems" .}}
</a-menu>
@@ -41,32 +38,25 @@
<a-drawer id="sider-drawer" placement="left" :closable="false"
@close="siderDrawer.close()"
:visible="siderDrawer.visible"
:wrap-class-name="siderDrawer.isDarkTheme ? 'ant-drawer-dark' : ''"
:wrap-class-name="themeSwitcher.darkDrawerClass"
:wrap-style="{ padding: 0 }">
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
</div>
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
<a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
<theme-switch />
</a-menu-item>
</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">
{{template "menuItems" .}}
</a-menu>
</a-drawer>
<script>
const darkClass = "ant-card-dark";
const bgDarkStyle = "background-color: #242c3a";
const siderDrawer = {
visible: false,
collapsed: false,
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
show() {
this.visible = true;
},
@@ -76,17 +66,6 @@
change() {
this.visible = !this.visible;
},
toggleCollapsed() {
this.collapsed = !this.collapsed;
},
changeTheme() {
this.isDarkTheme = ! this.isDarkTheme;
localStorage.setItem("dark-mode", this.isDarkTheme);
},
get theme() {
return this.isDarkTheme ? 'dark' : 'light';
}
};
</script>
{{end}}

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,13 @@
{{define "component/settingListItem"}}
<a-list-item style="padding: 20px">
<a-row>
<a-row v-if="type === 'textarea'">
<a-col>
<a-list-item-meta :title="title" :description="desc"/>
<a-textarea class="ant-setting-textarea" :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10 }"></a-textarea>
<!--a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 30 }"></a-textarea-->
</a-col>
</a-row>
<a-row v-else>
<a-col :lg="24" :xl="12">
<a-list-item-meta :title="title" :description="desc"/>
</a-col>
@@ -9,10 +16,7 @@
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template>
<template v-else-if="type === 'number'">
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template>
<template v-else-if="type === 'textarea'">
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
<a-input-number :value="value" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number>
</template>
<template v-else-if="type === 'switch'">
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
@@ -25,8 +29,8 @@
{{define "component/setting"}}
<script>
Vue.component('setting-list-item', {
props: ["type", "title", "desc", "value"],
props: ["type", "title", "desc", "value", "min"],
template: `{{template "component/settingListItem"}}`,
});
</script>
{{end}}
{{end}}

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

@@ -1,36 +1,62 @@
{{define "form/client"}}
<a-form layout="inline" v-if="client">
<template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">
Account is (Expired|Traffic Ended) And Disabled
</a-tag>
</template>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
</template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
</a-form-item>
<a-form-item label="{{ i18n "pages.inbounds.enable" }}">
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
<a-switch v-model="client.enable"></a-switch>
</a-form-item>
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN">
<a-input v-model.trim="client.password" style="width: 150px;" ></a-input>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 300px;"></a-input>
</a-form-item>
<br>
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
<a-input-number v-model="client.alterId"></a-input-number>
</a-form-item>
<a-form-item label="ID" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
<a-input type="number" v-model.number="client.alterId" style="width: 70px;"></a-input>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item label="Subscription" v-if="client.email">
<a-input v-model.trim="client.subId"></a-input>
</a-form-item>
<a-form-item label="Telegram Username" v-if="client.email">
<a-form-item v-if="client.email && app.tgBotEnable" >
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
@@ -43,7 +69,7 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="client.email && client.limitIp > 0 && isEdit">
<span slot="label">
@@ -64,25 +90,29 @@
</a-tooltip>
</span>
<a-form layout="block">
<a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
<a-textarea id="clientIPs" readonly
@click="getDBClientIps(client.email,$event)"
placeholder="Click To Get IPs"
:auto-size="{ minRows: 2, maxRows: 10 }">
</a-textarea>
</a-form>
</a-form-item>
<br>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -90,9 +120,10 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB":min="0" style="width: 70px;"></a-input-number>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
<template v-if="isEdit && clientStats">
<span>{{ i18n "usage" }}:</span>
<br>
<span> {{ i18n "usage" }}:</span>
<a-tag :color="statsColor">
[[ sizeFormat(clientStats.up) ]] /
[[ sizeFormat(clientStats.down) ]]
@@ -100,19 +131,22 @@
</a-tag>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" v-if="client.email.length > 0"></a-icon>
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"></a-icon>
</a-tooltip>
</template>
</template>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientModal.delayedStart">
<a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
<a-input-number v-model="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -120,8 +154,8 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
</a-form-item>

View File

@@ -8,7 +8,7 @@
<a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
</a-select>
</a-form-item>
@@ -24,12 +24,14 @@
</span>
<a-input v-model.trim="inbound.listen"></a-input>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
<a-input type="number" v-model.number="inbound.port"></a-input>
<a-input-number v-model="inbound.port"></a-input-number>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -41,7 +43,7 @@
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
@@ -49,9 +51,9 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
</a-form-item>
</a-form>

View File

@@ -4,15 +4,17 @@
<a-input v-model.trim="inbound.settings.address"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
<a-input type="number" v-model.number="inbound.settings.port"></a-input>
<a-input-number v-model="inbound.settings.port"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<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">TCP</a-select-option>
<a-select-option value="udp">UDP</a-select-option>
</a-select>
</a-form-item>
<br>
<a-form-item label="FollowRedirect">
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item>

View File

@@ -1,7 +1,113 @@
{{define "form/shadowsocks"}}
<a-form layout="inline" style="padding: 10px 0px;">
<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-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
</a-tooltip>
</span>
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
<a-form-item label="Password">
<a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 250px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.shadowsockses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.shadowsockses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.shadowsockses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<a-form layout="inline">
<a-form-item label='{{ i18n "encryption" }}'>
<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>
</a-form-item>
@@ -9,7 +115,7 @@
<a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<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">TCP</a-select-option>
<a-select-option value="udp">UDP</a-select-option>

View File

@@ -1,10 +1,10 @@
{{define "form/socks"}}
<a-form layout="inline">
<!-- <a-form-item label="Password authentication">-->
<a-form-item label='{{ i18n "password" }}'>
<a-switch :checked="inbound.settings.auth === 'password'"
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
</a-form-item>
<br>
<template v-if="inbound.settings.auth === 'password'">
<a-form-item label='{{ i18n "username" }}'>
<a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
@@ -13,11 +13,11 @@
<a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
</a-form-item>
</template>
<br>
<a-form-item label='{{ i18n "pages.inbounds.enable" }} udp'>
<a-switch v-model="inbound.settings.udp"></a-switch>
</a-form-item>
<a-form-item v-if="inbound.settings.udp"
label="IP">
<a-form-item v-if="inbound.settings.udp" label="IP">
<a-input v-model.trim="inbound.settings.ip"></a-input>
</a-form-item>
</a-form>

View File

@@ -1,31 +1,49 @@
{{define "form/trojan"}}
<a-form layout="inline">
<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-form layout="inline">
<a-form layout="inline" style="padding: 10px 0px;">
<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-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span>
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
</a-form>
<a-form-item label="Password">
<a-input v-model.trim="client.password" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item label="Subscription" v-if="client.email">
<a-input v-model.trim="client.subId"></a-input>
</a-form-item>
<a-form-item label="Telegram Username" v-if="client.email">
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<a-form-item label="Password">
<a-icon @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
<a-input v-model.trim="client.password" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
@@ -35,60 +53,69 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<template v-if="inbound.isTcp">
<a-form layout="inline">
<a-form-item label="Fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addTrojanFallback()">
<a-button type="primary" size="small" @click="inbound.settings.addTrojanFallback()">
+
</a-button>
</a-row>
@@ -115,7 +142,7 @@
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xVer">
<a-input type="number" v-model.number="fallback.xver"></a-input>
<a-input-number v-model="fallback.xver"></a-input-number>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>

View File

@@ -1,106 +1,133 @@
{{define "form/vless"}}
<a-form layout="inline">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form layout="inline">
<a-form layout="inline" style="padding: 10px 0px;">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span>
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
</a-form>
<a-form-item label="ID">
<a-input v-model.trim="client.id" style="width: 300px;" ></a-input>
</a-form-item>
<a-form-item label="Subscription" v-if="client.email">
<a-input v-model.trim="client.subId"></a-input>
</a-form-item>
<a-form-item label="Telegram Username" v-if="client.email">
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<a-form-item label="ID">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<template v-if="inbound.isTcp">
<a-form layout="inline">
<a-form-item label="Fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addFallback()">
<a-button type="primary" size="small" @click="inbound.settings.addFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
@@ -121,7 +148,7 @@
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xVer">
<a-input type="number" v-model.number="fallback.xver"></a-input>
<a-input-number v-model="fallback.xver"></a-input-number>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>

View File

@@ -1,34 +1,54 @@
{{define "form/vmess"}}
<a-form layout="inline">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form layout="inline">
<a-form layout="inline" style="padding: 10px 0px;">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span>
<span>{{ i18n "pages.inbounds.email" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.email" style="width: 150px;"></a-input>
<a-icon @click="client.email = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.email" style="width: 200px;"></a-input>
</a-form-item>
</a-form>
<a-form-item label="ID">
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "additional" }} ID'>
<a-input type="number" v-model.number="client.alterId" style="width: 70px;"></a-input>
</a-form-item>
<a-form-item label="Subscription" v-if="client.email">
<a-input v-model.trim="client.subId"></a-input>
</a-form-item>
<a-form-item label="Telegram Username" v-if="client.email">
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<br>
<a-form-item label='{{ i18n "additional" }} ID'>
<a-input-number v-model="client.alterId"></a-input-number>
</a-form-item>
<br>
<a-form-item label="ID">
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.subSettings.enable">
<span slot="label">
Subscription
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-icon @click="client.subId = RandomUtil.randomText()" type="sync"> </a-icon>
<a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email && app.tgBotEnable">
<span slot="label">
Telegram ID
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.IPLimit" }}</span>
<a-tooltip>
@@ -38,52 +58,61 @@
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vmesses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item>
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<br>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.darkCardClass"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount" }}: ' + inbound.settings.vmesses.length">
<table width="100%">
<tr class="client-table-header">
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
</a-form>
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
</a-form-item>
</a-form>
{{end}}

View File

@@ -1,16 +1,21 @@
{{define "form/sniffing"}}
<a-form layout="inline">
<a-form-item>
<span slot="label">
Sniffing
<a-tooltip>
<template slot="title">
<span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
</a-form-item>
<a-form-item>
<span slot="label">
Sniffing
<a-tooltip>
<template slot="title">
<span >{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
</a-form-item>
<a-form-item>
<a-checkbox-group v-model="inbound.sniffing.destOverride" v-if="inbound.sniffing.enabled">
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
</a-form>
{{end}}

View File

@@ -3,5 +3,8 @@
<a-form-item label="ServiceName">
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
</a-form-item>
<a-form-item label="Multi Mode">
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
</a-form-item>
</a-form>
{{end}}

View File

@@ -1,38 +1,46 @@
{{define "form/streamKCP"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;">
<a-select-option value="none">NoneNot Camouflage</a-select-option>
<a-select-option value="srtp">SRTPCamouflage Video Call</a-select-option>
<a-select-option value="utp">UTPCamouflage BT Download</a-select-option>
<a-select-option value="wechat-video">Wechat-VideoCamouflage WeChat Video</a-select-option>
<a-select-option value="dtls">DTLSCamouflage DTLS 1.2 Packages</a-select-option>
<a-select-option value="wireguard">WireguardCamouflage Wireguard Packages</a-select-option>
<a-select v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="none">None (Not Camouflage)</a-select-option>
<a-select-option value="srtp">SRTP (Camouflage Video Call)</a-select-option>
<a-select-option value="utp">UTP (Camouflage BT Download)</a-select-option>
<a-select-option value="wechat-video">Wechat-Video (Camouflage WeChat Video)</a-select-option>
<a-select-option value="dtls">DTLS (Camouflage DTLS 1.2 Packages)</a-select-option>
<a-select-option value="wireguard">Wireguard (Camouflage Wireguard Packages)</a-select-option>
</a-select>
</a-form-item>
<br>
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.number="inbound.stream.kcp.seed"></a-input>
<a-input v-model="inbound.stream.kcp.seed"></a-input>
</a-form-item>
<br>
<a-form-item label="MTU">
<a-input type="number" v-model.number="inbound.stream.kcp.mtu"></a-input>
<a-input-number v-model="inbound.stream.kcp.mtu"></a-input-number>
</a-form-item>
<br>
<a-form-item label="TTI (ms)">
<a-input type="number" v-model.number="inbound.stream.kcp.tti"></a-input>
<a-input-number v-model="inbound.stream.kcp.tti"></a-input-number>
</a-form-item>
<br>
<a-form-item label="Uplink Capacity (MB/S)">
<a-input type="number" v-model.number="inbound.stream.kcp.upCap"></a-input>
<a-input-number v-model="inbound.stream.kcp.upCap"></a-input-number>
</a-form-item>
<br>
<a-form-item label="Downlink Capacity (MB/S)">
<a-input type="number" v-model.number="inbound.stream.kcp.downCap"></a-input>
<a-input-number v-model="inbound.stream.kcp.downCap"></a-input-number>
</a-form-item>
<br>
<a-form-item label="Congestion">
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
</a-form-item>
<br>
<a-form-item label="Read Buffer Size (MB)">
<a-input type="number" v-model.number="inbound.stream.kcp.readBuffer"></a-input>
<a-input-number v-model="inbound.stream.kcp.readBuffer"></a-input-number>
</a-form-item>
<br>
<a-form-item label="Write Buffer Size (MB)">
<a-input type="number" v-model.number="inbound.stream.kcp.writeBuffer"></a-input>
<a-input-number v-model="inbound.stream.kcp.writeBuffer"></a-input-number>
</a-form-item>
</a-form>
{{end}}

View File

@@ -1,7 +1,7 @@
{{define "form/streamQUIC"}}
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
@@ -11,13 +11,13 @@
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="none">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option>
<a-select-option value="wechat-video">wechat-videocamouflage WeChat video</a-select-option>
<a-select-option value="dtls">dtlscamouflage DTLS 1.2 packages</a-select-option>
<a-select-option value="wireguard">wireguardcamouflage wireguard packages</a-select-option>
<a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="none">none (not camouflage)</a-select-option>
<a-select-option value="srtp">srtp (camouflage video call)</a-select-option>
<a-select-option value="utp">utp (camouflage BT download)</a-select-option>
<a-select-option value="wechat-video">wechat-video (camouflage WeChat video)</a-select-option>
<a-select-option value="dtls">dtls (camouflage DTLS 1.2 packages)</a-select-option>
<a-select-option value="wireguard">wireguard (camouflage wireguard packages)</a-select-option>
</a-select>
</a-form-item>
</a-form>

View File

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

View File

@@ -4,7 +4,7 @@
<a-form-item label="AcceptProxyProtocol">
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
</a-form-item>
<a-form-item label="HTTP {{ i18n "camouflage" }}">
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
<a-switch
:checked="inbound.stream.tcp.type === 'http'"
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
@@ -13,8 +13,7 @@
</a-form>
<!-- tcp request -->
<a-form v-if="inbound.stream.tcp.type === 'http'"
layout="inline">
<a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
</a-form-item>
@@ -26,12 +25,11 @@
<a-input v-model.trim="inbound.stream.tcp.request.path[index]"></a-input>
</a-row>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<br>
<a-form-item>
<a-row>
<a-button size="small"
@click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
+
</a-button>
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">+</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
<a-input style="width: 50%" v-model.trim="header.name"
@@ -39,19 +37,16 @@
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small"
@click="inbound.stream.tcp.request.removeHeader(index)">
-
</a-button>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
<!-- tcp response -->
<a-form v-if="inbound.stream.tcp.type === 'http'"
layout="inline">
<a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
</a-form-item>
@@ -61,12 +56,10 @@
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
<a-form-item>
<a-row>
<a-button size="small"
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
+
</a-button>
<span>{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}:</span>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">+</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
<a-input style="width: 50%" v-model.trim="header.name"
@@ -74,10 +67,7 @@
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small"
@click="inbound.stream.tcp.response.removeHeader(index)">
-
</a-button>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button>
</template>
</a-input>
</a-input-group>

View File

@@ -8,12 +8,11 @@
<a-form-item label='{{ i18n "path" }}'>
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
<br>
<a-form-item>
<a-row>
<a-button size="small"
@click="inbound.stream.ws.addHeader('Host', '')">
+
</a-button>
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.ws.addHeader('Host', '')">+</a-button>
</a-row>
<a-input-group v-for="(header, index) in inbound.stream.ws.headers">
<a-input style="width: 50%" v-model.trim="header.name"
@@ -21,10 +20,7 @@
<a-input style="width: 50%" v-model.trim="header.value"
addon-before='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button size="small"
@click="inbound.stream.ws.removeHeader(index)">
-
</a-button>
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.ws.removeHeader(index)">-</a-button>
</template>
</a-input>
</a-input-group>

View File

@@ -10,7 +10,7 @@
Reality
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.Realitydec" }}</span>
<span>{{ i18n "pages.inbounds.realityDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
@@ -22,7 +22,7 @@
XTLS
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.XTLSdec" }}</span>
<span>{{ i18n "pages.inbounds.xtlsDesc" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
@@ -33,22 +33,40 @@
<!-- tls settings -->
<a-form v-if="inbound.tls" layout="inline">
<a-form-item label='{{ i18n "domainName" }}'>
<a-form-item label='Multi Domain'>
<a-switch v-model="multiDomain"></a-switch>
</a-form-item>
<a-form-item v-if="multiDomain">
<a-row>
<span>Domains:</span>
<a-button v-if="multiDomain" type="primary" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})" style="margin-left: 10px">+</a-button>
</a-row>
<a-input-group v-for="(row, index) in inbound.stream.tls.settings.domains">
<a-input style="width: 40%" v-model.trim="row.remark" addon-before='{{ i18n "remark" }}'></a-input>
<a-input style="width: 60%" v-model.trim="row.domain" addon-before='{{ i18n "host" }}'>
<template slot="addonAfter">
<a-button type="primary" size="small" style="margin-left: 10px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item v-else label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="CipherSuites">
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px">
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value="">auto</a-select-option>
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="MinVersion">
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="MaxVersion">
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
@@ -56,49 +74,57 @@
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 170px">
<a-select v-model="inbound.stream.tls.settings.fingerprint"
style="width: 170px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Alpn">
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
<a-checkbox v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
</a-form-item>
<template v-if="inbound.stream.tls.certs[0].useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].key"></a-input>
<template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="cert.certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="cert.keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
</a-form-item>
</template>
</template>
</a-form>
<!-- xtls settings -->
<a-form v-if="inbound.xtls" layout="inline">
<a-form v-else-if="inbound.xtls" layout="inline">
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.xtls.server"></a-input>
</a-form-item>
<a-form-item label="SNI" placeholder="Server Name Indication">
<a-input v-model.trim="inbound.stream.xtls.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="Alpn">
<a-checkbox-group v-model="inbound.stream.xtls.alpn" style="width:200px">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
@@ -107,28 +133,32 @@
<a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.xtls.settings.allowInsecure"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="inbound.stream.xtls.certs[0].useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
</a-form-item>
<template v-if="inbound.stream.xtls.certs[0].useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="inbound.stream.xtls.certs[0].certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.xtls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.xtls.certs[0].cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.xtls.certs[0].key"></a-input>
<template v-for="cert,index in inbound.stream.xtls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid">
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.xtls.addCert()" style="margin-left: 10px">+</a-button>
<a-button v-if="inbound.stream.xtls.certs.length>1" type="primary" size="small" @click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px">-</a-button>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="cert.certFile" style="width:300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="cert.keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button type="primary" icon="import" @click="setDefaultCertXtls(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.cert"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="3" style="width:300px;" v-model="cert.key"></a-input>
</a-form-item>
</template>
</template>
</a-form>
@@ -139,24 +169,30 @@
</a-switch>
</a-form-item>
<a-form-item label="xVer">
<a-input type="number" v-model.number="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input>
<a-input-number v-model="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input-number>
</a-form-item>
<a-form-item label="uTLS" >
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 135px">
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.reality.settings.fingerprint"
style="width: 135px" :dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="dest">
<a-form-item label="Dest">
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 300px"></a-input>
</a-form-item>
<a-form-item label="Server Names">
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 300px"></a-input>
</a-form-item>
<a-form-item label="ShortIds">
<a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
<a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId()" type="sync"> </a-icon>
<a-input v-model.trim="inbound.stream.reality.shortIds" style="width: 150px;"></a-input>
</a-form-item>
<br>
<a-form-item label="SpiderX">
<a-input v-model.trim="inbound.stream.reality.settings.spiderX" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item label="Private Key">
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 300px"></a-input>
@@ -164,7 +200,7 @@
<a-form-item label="Public Key">
<a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 300px"></a-input>
</a-form-item>
<a-form-item >
<a-form-item>
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Key</a-button>
</a-form-item>
</a-form>

View File

@@ -29,20 +29,21 @@
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
</template>
<template slot="traffic" slot-scope="text, client">
<a-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
<a-tag :color="statsColor(record, client.email)">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
<template v-if="client._totalGB > 0">
<a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]]GB</a-tag>
<a-tag v-else color="cyan">[[client._totalGB]]GB</a-tag>
<a-tag :color="statsColor(record, client.email)">[[client._totalGB]]GB</a-tag>
</template>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</template>
<template slot="expiryTime" slot-scope="text, client, index">
<template v-if="client.expiryTime > 0">
<a-tag :color="isExpiry(record, index)? 'red' : 'blue'">
<a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)">
[[ DateUtil.formatMillis(client._expiryTime) ]]
</a-tag>
</template>
<a-tag v-else-if="client.expiryTime < 0" color="cyan">[[ client._expiryTime ]] {{ i18n "pages.client.days" }}</a-tag>
<a-tag v-else-if="client.expiryTime < 0" color="cyan">
[[ client._expiryTime ]] {{ i18n "pages.client.days" }}
</a-tag>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</template>
{{end}}

View File

@@ -3,61 +3,65 @@
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
:closable="true"
:mask-closable="true"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:class="themeSwitcher.darkCardClass"
:footer="null"
width="600px"
>
<table style="margin-bottom: 10px; width: 100%;">
<tr><td>
<table>
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag color="green">[[ dbInbound.port ]]</a-tag></td></tr>
</table>
</td>
<td v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<table>
<tr>
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
</tr>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
<tr v-if="inbound.path"><td>{{ i18n "path" }}</td><td><a-tag color="green">[[ inbound.path ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "path" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
</template>
<template v-if="inbound.isQuic">
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.quicKey ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag color="green">[[ inbound.quicType ]]</a-tag></td></tr>
</template>
<template v-if="inbound.isKcp">
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.kcpType ]]</a-tag></td></tr>
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></td></tr>
</template>
<template v-if="inbound.isGrpc">
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
</template>
</table>
</td></tr>
<tr colspan="2" v-if="dbInbound.hasLink()">
<td v-if="inbound.tls">
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else-if="inbound.xtls">
xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else-if="inbound.reality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
<tr>
<td>
<table>
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr>
<tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag color="green">[[ dbInbound.port ]]</a-tag></td></tr>
</table>
</td>
<td v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<table>
<tr>
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
</tr>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
<tr v-if="inbound.path"><td>{{ i18n "path" }}</td><td><a-tag color="green">[[ inbound.path ]]</a-tag></td></tr>
<tr v-else><td>{{ i18n "path" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
</template>
<template v-if="inbound.isQuic">
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.quicKey ]]</a-tag></td></tr>
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag color="green">[[ inbound.quicType ]]</a-tag></td></tr>
</template>
<template v-if="inbound.isKcp">
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.kcpType ]]</a-tag></td></tr>
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></td></tr>
</template>
<template v-if="inbound.isGrpc">
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
<tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
</template>
</table>
</td>
</tr>
<tr colspan="2" v-if="dbInbound.hasLink()">
<td v-if="inbound.tls">
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else-if="inbound.xtls">
xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</td>
<td v-else-if="inbound.reality">
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
</td>
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag>
</td>
</tr>
</table>
@@ -84,7 +88,7 @@
<th>{{ i18n "pages.inbounds.expireDate" }}</th>
<tr>
<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['down']) ]]
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
@@ -96,7 +100,7 @@
</td>
<td>
<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) ]]
</a-tag>
</template>
@@ -105,16 +109,16 @@
</td>
</tr>
</table>
<table v-if="infoModal.clientSettings.subId + infoModal.clientSettings.tgId" style="margin-bottom: 10px;">
<tr v-if="infoModal.clientSettings.subId">
<td>Subscription link</td>
<td><a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a></td>
</tr>
<tr v-if="infoModal.clientSettings.tgId">
<td>Telegram Username</td>
<td><a :href="[[ tgBase + infoModal.clientSettings.tgId ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></td>
</tr>
</table>
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
<a-divider>Subscription link</a-divider>
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
<a-icon id="copy-sub-link" type="snippets" @click="copyToClipboard('copy-sub-link', infoModal.subLink)"></a-icon>
</template>
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
<a-divider>Telegram Username</a-divider>
<a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a>
<a-icon id="copy-tg-link" type="snippets" @click="copyToClipboard('copy-tg-link', '@' + infoModal.clientSettings.tgId)"></a-icon>
</template>
</template>
<template v-else>
<a-divider></a-divider>
@@ -123,7 +127,8 @@
<th>{{ i18n "encryption" }}</th>
<th>{{ i18n "password" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
</tr><tr>
</tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
@@ -135,7 +140,8 @@
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
<th>FollowRedirect</th>
</tr><tr>
</tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
@@ -148,15 +154,18 @@
<th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
<th>IP</th>
</tr><tr>
</tr>
<tr>
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
</tr><tr v-if="inbound.settings.auth == 'password'">
</tr>
<tr v-if="inbound.settings.auth == 'password'">
<td> </td>
<td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td>
</tr><tr v-for="account,index in inbound.settings.accounts">
</tr>
<tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
@@ -168,7 +177,8 @@
<th> </th>
<th>{{ i18n "username" }}</th>
<th>{{ i18n "password" }}</th>
</tr><tr v-for="account,index in inbound.settings.accounts">
</tr>
<tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
@@ -178,11 +188,18 @@
</template>
<div v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider>
<p>[[ infoModal.link ]]</p>
<button class="ant-btn ant-btn-primary" id="copy-url-link"><a-icon type="snippets"></a-icon>{{ i18n "copy" }}</button>
<a-row v-for="(link,index) in infoModal.links">
<a-col :span="21"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
<a-col :span="3" style="text-align: right;">
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
<a-icon type="snippets"></a-icon>{{ i18n "copy" }}
</button>
</a-col>
</a-row>
</div>
</a-modal>
<script>
const infoModal = {
visible: false,
inbound: new Inbound(),
@@ -193,31 +210,56 @@
upStats: 0,
downStats: 0,
clipboard: null,
link: null,
links: [],
index: null,
isExpired: false,
subLink: '',
tgLink: '',
show(dbInbound, index) {
this.index = index;
this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound);
this.link = dbInbound.genLink(index);
this.settings = JSON.parse(this.inbound.settings);
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
this.isExpired = this.inbound.isExpiry(index);
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
this.visible = true;
infoModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#copy-url-link', {
text: () => this.link,
remark = this.dbInbound.remark + "-" + this.clientSettings.email;
address = this.dbInbound.address;
this.links = [];
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
this.inbound.stream.tls.settings.domains.forEach((domain) => {
this.links.push({
remark: remark + "-" + domain.remark,
link: this.inbound.genLink(domain.domain, remark + "-" + domain.remark, index)
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
});
} else {
this.links.push({
remark: remark,
link: this.inbound.genLink(address, remark, index)
});
}
if (this.clientSettings) {
if (this.clientSettings.subId) {
this.subLink = this.genSubLink(this.clientSettings.subId);
}
});
if (this.clientSettings.tgId) {
this.tgLink = "https://t.me/" + this.clientSettings.tgId;
}
}
this.visible = true;
},
close() {
infoModal.visible = false;
},
genSubLink(subID) {
protocol = app.subSettings.tls ? "https://" : "http://";
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
subPort = app.subSettings.port;
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
subPath = app.subSettings.path;
return protocol + hostName + port + subPath + subID;
}
};
const infoModalApp = new Vue({
@@ -232,42 +274,32 @@
return this.infoModal.inbound;
},
get isActive() {
if(infoModal.clientStats){
if (infoModal.clientStats) {
return infoModal.clientStats.enable;
}
return infoModal.dbInbound.isEnable;
},
get isEnable() {
if(infoModal.clientSettings){
if (infoModal.clientSettings) {
return infoModal.clientSettings.enable;
}
return infoModal.dbInbound.isEnable;
},
get subBase() {
return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port:"") + basePath + "sub/";
},
get tgBase() {
return "https://t.me/"
},
},
methods: {
copyTextToClipboard(elmentId,content) {
copyToClipboard(elmentId, content) {
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content,
});
this.infoModal.clipboard.on('success', () => {
text: () => content,
});
this.infoModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.infoModal.clipboard.destroy();
});
},
statsColor(stats) {
if(!stats) return 'blue'
if(stats['total'] === 0) return 'blue'
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
else return 'red'
return usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
}
},
});
</script>

View File

@@ -1,7 +1,7 @@
{{define "inboundModal"}}
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:class="themeSwitcher.darkCardClass"
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}}
</a-modal>
@@ -19,7 +19,7 @@
ok() {
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
},
show({ title='', okText='{{ i18n "sure" }}', inbound=null, dbInbound=null, confirm=(inbound, dbInbound)=>{}, isEdit=false }) {
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => {}, isEdit = false }) {
this.title = title;
this.okText = okText;
if (inbound) {
@@ -44,10 +44,11 @@
inModal.confirmLoading = loading;
},
getClients(protocol, clientSettings) {
switch(protocol){
switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
@@ -86,9 +87,21 @@
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
},
set delayedExpireDays(days){
set delayedExpireDays(days) {
this.client.expiryTime = -86400000 * days;
},
get multiDomain() {
return this.inbound.stream.tls.settings.domains.length > 0;
},
set multiDomain(value) {
if (value) {
inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = [{remark: "", domain: window.location.host.split(":")[0]}];
} else {
inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = [];
}
}
},
methods: {
streamNetworkChange() {
@@ -99,11 +112,15 @@
this.inModal.inbound.reality = false;
}
},
setDefaultCertData(){
inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey;
setDefaultCertData(index) {
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;
},
async getNewX25519Cert(){
setDefaultCertXtls(index) {
inModal.inbound.stream.xtls.certs[index].certFile = app.defaultCert;
inModal.inbound.stream.xtls.certs[index].keyFile = app.defaultKey;
},
async getNewX25519Cert() {
inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewX25519Cert');
inModal.loading(false);
@@ -112,15 +129,6 @@
}
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
},
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string;
}
},
});

View File

@@ -12,10 +12,11 @@
margin-top: 10px;
}
</style>
<body>
<a-layout id="app" v-cloak>
{{ template "commonSider" . }}
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading">
<transition name="list" appear>
@@ -24,7 +25,7 @@
</a-tag>
</transition>
<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-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.totalDownUp" }}:
@@ -41,19 +42,19 @@
<a-col :xs="24" :sm="24" :lg="12">
{{ i18n "clients" }}:
<a-tag color="green">[[ total.clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content">
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
</template>
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content">
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
</template>
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content">
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
</template>
@@ -64,13 +65,57 @@
</a-card>
</transition>
<transition name="list" appear>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
<a-button type="primary" icon="reload" @click="resetAllTraffic">{{ i18n "pages.inbounds.resetAllTraffic" }}</a-button>
<a-row>
<a-col :xs="24" :sm="24" :lg="12">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-dropdown :trigger="['click']">
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }}
</a-menu-item>
<a-menu-item key="resetInbounds">
<a-icon type="reload"></a-icon>
{{ i18n "pages.inbounds.resetAllTraffic" }}
</a-menu-item>
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
</a-menu-item>
<a-menu-item key="delDepletedClients">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</a-menu>
</a-dropdown>
</a-col>
<a-col :xs="24" :sm="24" :lg="12" style="text-align: right;">
<a-select v-model="refreshInterval"
style="width: 65px;"
v-if="isRefreshEnabled"
@change="changeRefreshInterval"
:dropdown-class-name="themeSwitcher.darkCardClass">
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
</a-select>
<a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon>
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
</a-col>
</a-row>
</div>
<a-input v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
<a-switch v-model="enableFilter"
checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}'
@change="toggleFilter" style="margin-right: 10px;">
</a-switch>
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
<a-radio-button value="">{{ i18n "none" }}</a-radio-button>
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
</a-radio-group>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds"
:loading="spinning" :scroll="{ x: 1300 }"
@@ -78,19 +123,15 @@
style="margin-top: 20px"
@change="() => getDBInbounds()">
<template slot="action" slot-scope="text, dbInbound">
<a-icon type="edit" style="font-size: 25px" @click="openEditInbound(dbInbound.id);"></a-icon>
<a-icon type="edit" style="font-size: 22px" @click="openEditInbound(dbInbound.id);"></a-icon>
<a-dropdown :trigger="['click']">
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme">
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
<a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }}
</a-menu-item>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="themeSwitcher.currentTheme">
<a-menu-item key="edit">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS">
<a-menu-item key="addClient">
<a-icon type="user-add"></a-icon>
{{ i18n "pages.client.add"}}
@@ -101,12 +142,16 @@
</a-menu-item>
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics"}}
{{ i18n "pages.inbounds.resetInboundClientTraffics"}}
</a-menu-item>
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}}
</a-menu-item>
<a-menu-item key="delDepletedClients">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</template>
<template v-else>
<a-menu-item key="showInfo">
@@ -118,7 +163,7 @@
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
</a-menu-item>
<a-menu-item key="clone">
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.Clone"}}
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
</a-menu-item>
<a-menu-item key="delete">
<span style="color: #FF4D4F">
@@ -140,19 +185,19 @@
<template slot="clients" slot-scope="text, dbInbound">
<template v-if="clientCount[dbInbound.id]">
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template>
@@ -193,7 +238,7 @@
{{template "client_table"}}
</a-table>
<a-table
v-else-if="record.protocol === Protocols.TROJAN"
v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS"
:row-key="client => client.id"
:columns="innerTrojanColumns"
:data-source="getInboundClients(record)"
@@ -210,6 +255,7 @@
</a-layout>
</a-layout>
{{template "js" .}}
{{template "component/themeSwitcher" .}}
<script>
const columns = [{
@@ -226,7 +272,7 @@
title: "ID",
align: 'center',
dataIndex: "id",
width: 30,
width: 40,
}, {
title: '{{ i18n "pages.inbounds.remark" }}',
align: 'center',
@@ -240,7 +286,7 @@
}, {
title: '{{ i18n "pages.inbounds.protocol" }}',
align: 'left',
width: 80,
width: 90,
scopedSlots: { customRender: 'protocol' },
}, {
title: '{{ i18n "clients" }}',
@@ -261,7 +307,7 @@
const innerColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
@@ -270,11 +316,11 @@
const innerTrojanColumns = [
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'Password', width: 120, dataIndex: "password" },
{ title: 'Password', width: 170, dataIndex: "password" },
];
const app = new Vue({
@@ -282,75 +328,102 @@
el: '#app',
data: {
siderDrawer,
themeSwitcher,
spinning: false,
inbounds: [],
dbInbounds: [],
searchKey: '',
enableFilter: false,
filterBy: '',
searchedInbounds: [],
expireDiff: 0,
trafficDiff: 0,
defaultCert: '',
defaultKey: '',
clientCount: {},
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
subSettings: {
enable : false,
port: 0,
path: '',
domain: '',
tls: false
},
tgBotEnable: false
},
methods: {
loading(spinning=true) {
loading(spinning = true) {
this.spinning = spinning;
},
async getDBInbounds() {
this.loading();
const msg = await HttpUtil.post('/xui/inbound/list');
this.loading(false);
this.refreshing = true;
const msg = await HttpUtil.post('/panel/inbound/list');
if (!msg.success) {
this.refreshing = false;
return;
}
this.setInbounds(msg.obj);
this.searchKey = '';
setTimeout(() => {
this.refreshing = false;
}, 500);
},
async getDefaultSettings() {
this.loading();
const msg = await HttpUtil.post('/xui/setting/defaultSettings');
this.loading(false);
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
if (!msg.success) {
return;
}
this.expireDiff = msg.obj.expireDiff * 86400000;
this.trafficDiff = msg.obj.trafficDiff * 1073741824;
this.defaultCert = msg.obj.defaultCert;
this.defaultKey = msg.obj.defaultKey;
with(msg.obj){
this.expireDiff = expireDiff * 86400000;
this.trafficDiff = trafficDiff * 1073741824;
this.defaultCert = defaultCert;
this.defaultKey = defaultKey;
this.tgBotEnable = tgBotEnable;
this.subSettings = {
enable : subEnable,
port: subPort,
path: subPath,
domain: subDomain,
tls: subTLS
};
}
},
setInbounds(dbInbounds) {
this.inbounds.splice(0);
this.dbInbounds.splice(0);
this.searchedInbounds.splice(0);
for (const inbound of dbInbounds) {
const dbInbound = new DBInbound(inbound);
to_inbound = dbInbound.toInbound()
this.inbounds.push(to_inbound);
this.dbInbounds.push(dbInbound);
this.searchedInbounds.push(dbInbound);
if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){
this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound);
if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
}
}
if(this.enableFilter){
this.filterInbounds();
} else {
this.searchInbounds(this.searchKey);
}
},
getClientCounts(dbInbound,inbound){
let clientCount = 0,active = [], deactive = [], depleted = [], expiring = [];
getClientCounts(dbInbound, inbound) {
let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [];
clients = this.getClients(dbInbound.protocol, inbound.settings);
clientStats = dbInbound.clientStats
now = new Date().getTime()
if(clients){
if (clients) {
clientCount = clients.length;
if(dbInbound.enable){
if (dbInbound.enable) {
clients.forEach(client => {
client.enable ? active.push(client.email) : deactive.push(client.email);
});
clientStats.forEach(client => {
if(!client.enable) {
if (!client.enable) {
depleted.push(client.email);
} else {
if ((client.expiryTime > 0 && (client.expiryTime-now < this.expireDiff)) ||
(client.total > 0 && (client.total-(client.up+client.down) < this.trafficDiff ))) expiring.push(client.email);
if ((client.expiryTime > 0 && (client.expiryTime - now < this.expireDiff)) ||
(client.total > 0 && (client.total - (client.up + client.down) < this.trafficDiff))) expiring.push(client.email);
}
});
} else {
@@ -376,10 +449,10 @@
if (ObjectUtil.deepSearch(inbound, key)) {
const newInbound = new DBInbound(inbound);
const inboundSettings = JSON.parse(inbound.settings);
if (inboundSettings.hasOwnProperty('clients')){
if (inboundSettings.hasOwnProperty('clients')) {
const searchedSettings = { "clients": [] };
inboundSettings.clients.forEach(client => {
if (ObjectUtil.deepSearch(client, key)){
if (ObjectUtil.deepSearch(client, key)) {
searchedSettings.clients.push(client);
}
});
@@ -390,6 +463,54 @@
});
}
},
filterInbounds() {
if (ObjectUtil.isEmpty(this.filterBy)) {
this.searchedInbounds = this.dbInbounds.slice();
} else {
this.searchedInbounds.splice(0, this.searchedInbounds.length);
this.dbInbounds.forEach(inbound => {
const newInbound = new DBInbound(inbound);
const inboundSettings = JSON.parse(inbound.settings);
if (this.clientCount[inbound.id] && this.clientCount[inbound.id].hasOwnProperty(this.filterBy)){
const list = this.clientCount[inbound.id][this.filterBy];
if (list.length > 0) {
const filteredSettings = { "clients": [] };
inboundSettings.clients.forEach(client => {
if (list.includes(client.email)) {
filteredSettings.clients.push(client);
}
});
newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
this.searchedInbounds.push(newInbound);
}
}
});
}
},
toggleFilter(){
if(this.enableFilter) {
this.searchKey = '';
} else {
this.filterBy = '';
this.searchedInbounds = this.dbInbounds.slice();
}
},
generalActions(action) {
switch (action.key) {
case "export":
this.exportAllLinks();
break;
case "resetInbounds":
this.resetAllTraffic();
break;
case "resetClients":
this.resetAllClientTraffics(-1);
break;
case "delDepletedClients":
this.delDepletedClients(-1)
break;
}
},
clickAction(action, dbInbound) {
switch (action.key) {
case "qrcode":
@@ -422,11 +543,14 @@
case "delete":
this.delInbound(dbInbound.id);
break;
case "delDepletedClients":
this.delDepletedClients(dbInbound.id)
break;
}
},
openCloneInbound(dbInbound) {
openCloneInbound(dbInbound) {
this.$confirm({
title: '{{ i18n "pages.inbounds.cloneInbound"}}' + dbInbound.remark,
title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
cancelText: '{{ i18n "cancel" }}',
@@ -439,7 +563,6 @@
});
},
async cloneInbound(baseInbound, dbInbound) {
const inbound = new Inbound();
const data = {
up: dbInbound.up,
down: dbInbound.down,
@@ -448,19 +571,19 @@
enable: dbInbound.enable,
expiryTime: dbInbound.expiryTime,
listen: inbound.listen,
port: inbound.port,
listen: '',
port: RandomUtil.randomIntRange(10000, 60000),
protocol: baseInbound.protocol,
settings: inbound.settings.toString(),
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
streamSettings: baseInbound.stream.toString(),
sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
};
await this.submit('/xui/inbound/add', data, inModal);
await this.submit('/panel/inbound/add', data, inModal);
},
openAddInbound() {
inModal.show({
title: '{{ i18n "pages.inbounds.addInbound"}}',
okText: '{{ i18n "pages.inbounds.addTo"}}',
okText: '{{ i18n "pages.inbounds.create"}}',
cancelText: '{{ i18n "close" }}',
confirm: async (inbound, dbInbound) => {
inModal.loading();
@@ -475,7 +598,7 @@
const inbound = dbInbound.toInbound();
inModal.show({
title: '{{ i18n "pages.inbounds.modifyInbound"}}',
okText: '{{ i18n "pages.inbounds.revise"}}',
okText: '{{ i18n "pages.inbounds.update"}}',
cancelText: '{{ i18n "close" }}',
inbound: inbound,
dbInbound: dbInbound,
@@ -504,7 +627,7 @@
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit('/xui/inbound/add', data, inModal);
await this.submit('/panel/inbound/add', data, inModal);
},
async updateInbound(inbound, dbInbound) {
const data = {
@@ -523,7 +646,7 @@
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
},
openAddClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -561,38 +684,38 @@
okText: '{{ i18n "pages.client.submitEdit"}}',
dbInbound: dbInbound,
index: index,
confirm: async (client, dbInboundId, index) => {
confirm: async (client, dbInboundId, clientId) => {
clientModal.loading();
await this.updateClient(client, dbInboundId, index);
await this.updateClient(client, dbInboundId, clientId);
clientModal.close();
},
isEdit: true
});
},
findIndexOfClient(clients,client) {
findIndexOfClient(clients, client) {
firstKey = Object.keys(client)[0];
return clients.findIndex(c => c[firstKey] === client[firstKey]);
},
async addClient(clients, dbInboundId) {
const data = {
id: dbInboundId,
settings: '{"clients": [' + clients.toString() +']}',
settings: '{"clients": [' + clients.toString() + ']}',
};
await this.submit(`/xui/inbound/addClient`, data);
await this.submit(`/panel/inbound/addClient`, data);
},
async updateClient(client, dbInboundId, index) {
async updateClient(client, dbInboundId, clientId) {
const data = {
id: dbInboundId,
settings: '{"clients": [' + client.toString() +']}',
settings: '{"clients": [' + client.toString() + ']}',
};
await this.submit(`/xui/inbound/updateClient/${index}`, data);
await this.submit(`/panel/inbound/updateClient/${clientId}`, data);
},
resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => {
@@ -607,42 +730,70 @@
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
class: themeSwitcher.darkCardClass,
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
});
},
delClient(dbInboundId,client) {
delClient(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientId = dbInbound.protocol == "trojan" ? client.password : client.id;
clientId = this.getClientId(dbInbound.protocol, client);
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
class: themeSwitcher.darkCardClass,
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`),
onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
});
},
getClients(protocol, clientSettings) {
switch(protocol){
switch (protocol) {
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
default: return null;
}
},
getClientId(protocol, client) {
switch (protocol) {
case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email;
default: return client.id;
}
},
checkFallback(dbInbound) {
newDbInbound = new DBInbound(dbInbound);
if (dbInbound.listen.startsWith("@")){
rootInbound = this.inbounds.find((i) =>
i.tls &&
['trojan','vless'].includes(i.protocol) &&
i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
);
if (rootInbound) {
newDbInbound.listen = rootInbound.listen;
newDbInbound.port = rootInbound.port;
newInbound = newDbInbound.toInbound();
newInbound.stream.security = 'tls';
newInbound.stream.tls = rootInbound.stream.tls;
newDbInbound.streamSettings = newInbound.stream.toString();
}
}
return newDbInbound;
},
showQrcode(dbInbound, clientIndex) {
const link = dbInbound.genLink(clientIndex);
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
newDbInbound = this.checkFallback(dbInbound);
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, clientIndex);
},
showInfo(dbInbound, index) {
infoModal.show(dbInbound, index);
newDbInbound = this.checkFallback(dbInbound);
infoModal.show(newDbInbound, index);
},
switchEnable(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound);
this.submit(`/panel/inbound/update/${dbInboundId}`, dbInbound);
},
async switchEnableClient(dbInboundId, client) {
this.loading()
@@ -651,7 +802,8 @@
clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client);
clients[index].enable = !clients[index].enable;
await this.updateClient(clients[index],dbInboundId, index);
clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index], dbInboundId, clientId);
this.loading(false);
},
async submit(url, data) {
@@ -661,79 +813,118 @@
}
},
getInboundClients(dbInbound) {
if(dbInbound.protocol == Protocols.VLESS) {
return dbInbound.toInbound().settings.vlesses
} else if(dbInbound.protocol == Protocols.VMESS) {
return dbInbound.toInbound().settings.vmesses
} else if(dbInbound.protocol == Protocols.TROJAN) {
return dbInbound.toInbound().settings.trojans
if (dbInbound.protocol == Protocols.VLESS) {
return dbInbound.toInbound().settings.vlesses;
} else if (dbInbound.protocol == Protocols.VMESS) {
return dbInbound.toInbound().settings.vmesses;
} else if (dbInbound.protocol == Protocols.TROJAN) {
return dbInbound.toInbound().settings.trojans;
} else if (dbInbound.protocol == Protocols.SHADOWSOCKS) {
return dbInbound.toInbound().settings.shadowsockses;
}
},
resetClientTraffic(client,dbInboundId) {
resetClientTraffic(client, dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email),
onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
})
},
resetAllTraffic() {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllTraffics'),
onOk: () => this.submit('/panel/inbound/resetAllTraffics'),
});
},
resetAllClientTraffics(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
onOk: () => this.submit('/panel/inbound/resetAllClientTraffics/' + dbInboundId),
})
},
delDepletedClients(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
class: themeSwitcher.darkCardClass,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/panel/inbound/delDepletedClients/' + dbInboundId),
})
},
isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index)
},
getUpStats(dbInbound, email) {
if(email.length == 0) return 0
if (email.length == 0) return 0
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.up : 0
},
getDownStats(dbInbound, email) {
if(email.length == 0) return 0
if (email.length == 0) return 0
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.down : 0
},
isTrafficExhausted(dbInbound, email) {
if(email.length == 0) return false
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
statsColor(dbInbound, email) {
if(email.length == 0) return 'blue';
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
},
isClientEnabled(dbInbound, email) {
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
return clientStats ? clientStats['enable'] : true
},
isRemovable(dbInbound_id){
isRemovable(dbInbound_id) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
},
inboundLinks(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark);
newDbInbound = this.checkFallback(dbInbound);
txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks, newDbInbound.remark);
},
exportAllLinks() {
exportAllLinks() {
let copyText = '';
for (const dbInbound of this.dbInbounds) {
copyText += dbInbound.genInboundLinks
}
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds');
},
async startDataRefreshLoop() {
while (this.isRefreshEnabled) {
try {
await this.getDBInbounds();
} catch (e) {
console.error(e);
}
await PromiseUtil.sleep(this.refreshInterval);
}
},
toggleRefresh() {
localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
},
changeRefreshInterval() {
localStorage.setItem("refreshInterval", this.refreshInterval);
},
async manualRefresh() {
if (!this.refreshing) {
this.spinning = true;
await this.getDBInbounds();
this.spinning = false;
}
},
},
watch: {
@@ -742,8 +933,15 @@
}, 500)
},
mounted() {
this.loading();
this.getDefaultSettings();
this.getDBInbounds();
if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
else {
this.getDBInbounds();
}
this.loading(false);
},
computed: {
total() {
@@ -780,5 +978,6 @@
{{template "inboundInfoModal"}}
{{template "clientsModal"}}
{{template "clientsBulkModal"}}
</body>
</html>

View File

@@ -13,32 +13,34 @@
}
.ant-card-dark h2 {
color: hsla(0,0%,100%,.65);
color: hsla(0, 0%, 100%, .65);
}
</style>
<body>
<a-layout id="app" v-cloak>
{{ template "commonSider" . }}
<a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout id="content-layout" :style="themeSwitcher.bgStyle">
<a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
<transition name="list" appear>
<a-row>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row>
<a-col :sm="24" :md="12">
<a-row>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:class="themeSwitcher.darkCardClass"
:percent="status.cpu.percent"></a-progress>
<div>CPU</div>
<div>CPU: [[ cpuCoreFormat(status.cpuCores) ]]</div>
<div>Speed: [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
</a-col>
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:class="themeSwitcher.darkCardClass"
:percent="status.mem.percent"></a-progress>
<div>
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
@@ -51,7 +53,7 @@
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:class="themeSwitcher.darkCardClass"
:percent="status.swap.percent"></a-progress>
<div>
Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
@@ -60,7 +62,7 @@
<a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:class="themeSwitcher.darkCardClass"
:percent="status.disk.percent"></a-progress>
<div>
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
@@ -75,26 +77,22 @@
<transition name="list" appear>
<a-row>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-card hoverable :class="themeSwitcher.darkCardClass">
3x-ui: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
Telegram: <a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "pages.index.operationHours" }}:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.operationHoursDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "menu.link" }}:
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-card hoverable :class="themeSwitcher.darkCardClass">
{{ i18n "pages.index.xrayStatus" }}:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.state === State.Error">
@@ -109,35 +107,78 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "menu.link" }}:
<a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">Log Reports</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openConfig">Config</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="getBackup">Backup</a-tag>
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row>
<a-col :span="12">
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.systemLoadDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
<a-col :span="12">
{{ i18n "pages.index.operationHours" }}:
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row>
<a-col :span="12">
IPv4:
<a-tooltip>
<template slot="title">
[[ status.publicIP.ipv4 ]]
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
<a-col :span="12">
IPv6:
<a-tooltip>
<template slot="title">
[[ status.publicIP.ipv6 ]]
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row>
<a-col :span="12">
TCP: [[ status.tcpCount ]]
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.connectionTcpCountDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
<a-col :span="12">
UDP: [[ status.udpCount ]]
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.connectionUdpCountDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
TCP / UDP {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.connectionCountDesc" }}
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row>
<a-col :span="12">
<a-icon type="arrow-up"></a-icon>
[[ sizeFormat(status.netIO.up) ]] / S
[[ sizeFormat(status.netIO.up) ]]/S
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.upSpeed" }}
@@ -147,7 +188,7 @@
</a-col>
<a-col :span="12">
<a-icon type="arrow-down"></a-icon>
[[ sizeFormat(status.netIO.down) ]] / S
[[ sizeFormat(status.netIO.down) ]]/S
<a-tooltip>
<template slot="title">
{{ i18n "pages.index.downSpeed" }}
@@ -159,7 +200,7 @@
</a-card>
</a-col>
<a-col :sm="24" :md="12">
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-card hoverable :class="themeSwitcher.darkCardClass">
<a-row>
<a-col :span="12">
<a-icon type="cloud-upload"></a-icon>
@@ -188,9 +229,10 @@
</transition>
</a-layout-content>
</a-layout>
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
:closable="true" @ok="() => versionModal.visible = false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:class="themeSwitcher.darkCardClass"
footer="">
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
@@ -201,9 +243,10 @@
</a-tag>
</template>
</a-modal>
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:class="themeSwitcher.darkCardClass"
width="800px"
footer="">
<a-form layout="inline">
@@ -211,7 +254,7 @@
<a-select v-model="logModal.rows"
style="width: 80px"
@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="20">20</a-select-option>
<a-select-option value="50">50</a-select-option>
@@ -227,12 +270,31 @@
{{ i18n "download" }} x-ui.log
</a-button>
</a-form-item>
</a-form>
</a-form>
<a-input type="textarea" v-model="logModal.logs" disabled="true"
:autosize="{ minRows: 10, maxRows: 22}"></a-input>
</a-modal>
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
:closable="true" :class="themeSwitcher.darkCardClass"
@ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
<p style="color: inherit; font-size: 16px; padding: 4px 2px;">
<a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
[[ backupModal.description ]]
</p>
<a-space direction="horizontal" style="text-align: center" style="margin-bottom: 10px;">
<a-button type="primary" @click="exportDatabase()">
[[ backupModal.exportText ]]
</a-button>
<a-button type="primary" @click="importDatabase()">
[[ backupModal.importText ]]
</a-button>
</a-space>
</a-modal>
</a-layout>
{{template "js" .}}
{{template "component/themeSwitcher" .}}
{{template "textModal"}}
<script>
@@ -272,26 +334,32 @@
class Status {
constructor(data) {
this.cpu = new CurTotal(0, 0);
this.cpuCores = 0;
this.cpuSpeedMhz = 0;
this.disk = new CurTotal(0, 0);
this.loads = [0, 0, 0];
this.mem = new CurTotal(0, 0);
this.netIO = {up: 0, down: 0};
this.netTraffic = {sent: 0, recv: 0};
this.netIO = { up: 0, down: 0 };
this.netTraffic = { sent: 0, recv: 0 };
this.publicIP = { ipv4: 0, ipv6: 0 };
this.swap = new CurTotal(0, 0);
this.tcpCount = 0;
this.udpCount = 0;
this.uptime = 0;
this.xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
this.xray = { state: State.Stop, errorMsg: "", version: "", color: "" };
if (data == null) {
return;
}
this.cpu = new CurTotal(data.cpu, 100);
this.cpuCores = data.cpuCores;
this.cpuSpeedMhz = data.cpuSpeedMhz;
this.disk = new CurTotal(data.disk.current, data.disk.total);
this.loads = data.loads.map(load => toFixed(load, 2));
this.mem = new CurTotal(data.mem.current, data.mem.total);
this.netIO = data.netIO;
this.netTraffic = data.netTraffic;
this.publicIP = data.publicIP;
this.swap = new CurTotal(data.swap.current, data.swap.total);
this.tcpCount = data.tcpCount;
this.udpCount = data.udpCount;
@@ -339,14 +407,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({
delimiters: ['[[', ']]'],
el: '#app',
data: {
siderDrawer,
themeSwitcher,
status: new Status(),
versionModal,
logModal,
backupModal,
spinning: false,
loadingTip: '{{ i18n "loading"}}',
},
@@ -378,17 +471,16 @@
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
okText: '{{ i18n "confirm"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
class: themeSwitcher.darkCardClass,
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefreshh"}}');
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
await HttpUtil.post(`/server/installXray/${version}`);
this.loading(false);
},
});
},
//here add stop xray function
async stopXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService');
@@ -397,7 +489,6 @@
return;
}
},
//here add restart xray function
async restartXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService');
@@ -406,27 +497,67 @@
return;
}
},
async openLogs(rows){
async openLogs(rows) {
this.loading(true);
const msg = await HttpUtil.post('server/logs/'+rows);
const msg = await HttpUtil.post('server/logs/' + rows);
this.loading(false);
if (!msg.success) {
return;
}
logModal.show(msg.obj,rows);
logModal.show(msg.obj, rows);
},
async openConfig(){
async openConfig() {
this.loading(true);
const msg = await HttpUtil.post('server/getConfigJson');
this.loading(false);
if (!msg.success) {
return;
}
txtModal.show('config.json',JSON.stringify(msg.obj, null, 2),'config.json');
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';
}
},
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("/panel/setting/restartPanel");
this.loading(false);
if (restartMsg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
location.reload();
}
}
});
fileInput.click();
},
},
async mounted() {
while (true) {

View File

@@ -1,772 +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.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">{{ i18n "confirm" }}</a-button>
</a-form-item>
</a-form>
<a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'">
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.setting.loginSecurity" }}' description='{{ i18n "pages.setting.loginSecurityDesc" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-switch @change="toggleToken(allSetting.secretEnable)" v-model="allSetting.secretEnable"></a-switch>
</template>
</a-col>
</a-row>
</a-list-item>
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.setting.secretToken" }}' description='{{ i18n "pages.setting.secretTokenDesc" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<svg
@click="getNewSecret"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/>
</svg>
<template>
<a-textarea type="text" id='token' :disabled="!allSetting.secretEnable" v-model="user.loginSecret"></a-textarea>
</template>
</a-col>
</a-row>
</a-list-item>
<a-button type="primary" @click="updateSecret">{{ i18n "confirm" }}</a-button>
</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;'">
<a-divider>{{ i18n "pages.setting.actions"}}</a-divider>
<a-space direction="horizontal" style="padding: 0 20px">
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.setting.resetDefaultConfig" }}</a-button>
</a-space>
<a-divider>{{ i18n "pages.setting.basicTemplate"}}</a-divider>
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.setting.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.setting.generalConfigsDesc" }}
</h2>
</a-row>
<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>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigAds"}}' desc='{{ i18n "pages.setting.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPorn"}}' desc='{{ i18n "pages.setting.xrayConfigPornDesc"}}' v-model="PornSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.setting.countryConfigs"}}'>
<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.setting.countryConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigIRIp"}}' desc='{{ i18n "pages.setting.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigIRDomain"}}' desc='{{ i18n "pages.setting.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigChinaIp"}}' desc='{{ i18n "pages.setting.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigChinaDomain"}}' desc='{{ i18n "pages.setting.xrayConfigChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigRussiaIp"}}' desc='{{ i18n "pages.setting.xrayConfigRussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.setting.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.setting.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.setting.ipv4ConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.setting.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.setting.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.setting.warpConfigs"}}'>
<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.setting.warpConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigGoogleWARP"}}' desc='{{ i18n "pages.setting.xrayConfigGoogleWARPDesc"}}' v-model="GoogleWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigOpenAIWARP"}}' desc='{{ i18n "pages.setting.xrayConfigOpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.setting.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.setting.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
<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>
<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: new User(),
lang: getLang(),
ipv4Settings: {
tag: "IPv4",
protocol: "freedom",
settings: {
domainStrategy: "UseIPv4"
}
},
warpSettings: {
tag: "WARP",
protocol: "socks",
settings: {
servers: [
{
address: "127.0.0.1",
port: 40000
}
]
}
},
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"],
ru: ["geosite:category-ru-gov"],
ir: [
"regexp:.*\\.ir$",
"ext:iran.dat:ir",
"ext:iran.dat:other",
"ext:iran.dat:ads",
"geosite:category-ir"
]
},
}
},
methods: {
loading(spinning = true , obj) {
if(obj == null)
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;
}
await this.getUserSecret();
},
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 getUserSecret(){
const user_msg = await HttpUtil.post("/xui/setting/getUserSecret", this.user);
if (user_msg.success){
this.user = user_msg.obj;
}
this.loading(false);
},
async updateSecret(){
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/updateUserSecret", this.user);
if (msg.success){
this.user = msg.obj;
}
this.loading(false);
await this.updateAllSetting();
},
async getNewSecret(){
this.loading(true);
await PromiseUtil.sleep(1000);
var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
var string = '';
var len = 64;
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
this.user.loginSecret = string;
document.getElementById('token').value =this.user.loginSecret;
this.loading(false);
},
async toggleToken(value){
if(value)
this.getNewSecret();
else
this.user.loginSecret = "";
},
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;
}
},
checkRequiredOutbounds() {
const newTemplateSettings = this.templateSettings;
const haveIPv4Outbounds = newTemplateSettings.outbounds.some((o) => o?.tag === "IPv4");
const haveIPv4Rules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === "IPv4");
const haveWARPOutbounds = newTemplateSettings.outbounds.some((o) => o?.tag === "WARP");
const haveWARPRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === "WARP");
if (haveWARPRules && !haveWARPOutbounds) {
newTemplateSettings.outbounds.push(this.warpSettings);
}
if (haveIPv4Rules && !haveIPv4Outbounds) {
newTemplateSettings.outbounds.push(this.ipv4Settings);
}
this.templateSettings = newTemplateSettings;
},
templateRuleGetter(routeSettings) {
const { data, property, outboundTag } = routeSettings;
let result = false;
if (this.templateSettings != null) {
this.templateSettings.routing.rules.forEach(
(routingRule) => {
if (
routingRule.hasOwnProperty(property) &&
routingRule.hasOwnProperty("outboundTag") &&
routingRule.outboundTag === outboundTag
) {
if (data.includes(routingRule[property][0])) {
result = true;
}
}
}
);
}
return result;
},
templateRuleSetter(routeSettings) {
const { newValue, data, property, outboundTag } = routeSettings;
const oldTemplateSettings = this.templateSettings;
const newTemplateSettings = oldTemplateSettings;
if (newValue) {
const propertyRule = {
type: "field",
outboundTag,
[property]: data
};
newTemplateSettings.routing.rules.push(propertyRule);
}
else {
const newRules = [];
newTemplateSettings.routing.rules.forEach(
(routingRule) => {
if (
routingRule.hasOwnProperty(property) &&
routingRule.hasOwnProperty("outboundTag") &&
routingRule.outboundTag === outboundTag
) {
if (data.includes(routingRule[property][0])) {
return;
}
}
newRules.push(routingRule);
}
);
newTemplateSettings.routing.rules = newRules;
}
this.templateSettings = newTemplateSettings;
this.checkRequiredOutbounds();
}
},
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 () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "protocol",
data: this.settingsData.protocols.bittorrent
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "protocol",
data: this.settingsData.protocols.bittorrent
});
},
},
privateIpSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.local
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.local
});
},
},
AdsSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ads
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ads
});
},
},
PornSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.porn
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.porn
});
},
},
GoogleIPv4Settings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "IPv4",
property: "domain",
data: this.settingsData.domains.google
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "IPv4",
property: "domain",
data: this.settingsData.domains.google
});
},
},
NetflixIPv4Settings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "IPv4",
property: "domain",
data: this.settingsData.domains.netflix
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "IPv4",
property: "domain",
data: this.settingsData.domains.netflix
});
},
},
IRIpSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.ir
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.ir
});
},
},
IRDomainSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ir
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ir
});
},
},
ChinaIpSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.cn
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.cn
});
},
},
ChinaDomainSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.cn
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.cn
});
},
},
RussiaIpSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.ru
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "ip",
data: this.settingsData.ips.ru
});
},
},
RussiaDomainSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ru
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "blocked",
property: "domain",
data: this.settingsData.domains.ru
});
},
},
GoogleWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.google
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.google
});
},
},
OpenAIWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.openai
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.openai
});
},
},
NetflixWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.netflix
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.netflix
});
},
},
SpotifyWARPSettings: {
get: function () {
return this.templateRuleGetter({
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.spotify
});
},
set: function (newValue) {
this.templateRuleSetter({
newValue,
outboundTag: "WARP",
property: "domain",
data: this.settingsData.domains.spotify
});
},
},
}
});
</script>
</body>
</html>

1104
web/html/xui/settings.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,48 +4,58 @@ import (
"encoding/json"
"os"
"regexp"
ss "strings"
"sync"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/web/service"
"x-ui/xray"
// "strconv"
"github.com/go-cmd/cmd"
"net"
"sort"
"strings"
"time"
"github.com/go-cmd/cmd"
)
type CheckClientIpJob struct {
xrayService service.XrayService
inboundService service.InboundService
xrayService service.XrayService
AllowedIps []string
mutex sync.Mutex
}
var job *CheckClientIpJob
var disAllowedIps []string
var AllowedIps []string
var ipRegx *regexp.Regexp = regexp.MustCompile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
var emailRegx *regexp.Regexp = regexp.MustCompile(`email:.+`)
func NewCheckClientIpJob() *CheckClientIpJob {
job = new(CheckClientIpJob)
job := &CheckClientIpJob{}
return job
}
func (j *CheckClientIpJob) Run() {
logger.Debug("Check Client IP Job...")
processLogFile()
j.processLogFile()
// disAllowedIps = []string{"192.168.1.183","192.168.1.197"}
blockedIps := []byte(ss.Join(disAllowedIps, ","))
err := os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755)
// AllowedIps = []string{"192.168.1.183","192.168.1.197"}
allowedIps := []byte(strings.Join(j.getAllowedIps(), ","))
// check if file exists, if not create one
_, err := os.Stat(xray.GetAllowedIPsPath())
if os.IsNotExist(err) {
_, err = os.OpenFile(xray.GetAllowedIPsPath(), os.O_RDWR|os.O_CREATE, 0755)
checkError(err)
}
err = os.WriteFile(xray.GetAllowedIPsPath(), allowedIps, 0755)
checkError(err)
}
func processLogFile() {
func (j *CheckClientIpJob) processLogFile() {
accessLogPath := GetAccessLogPath()
if accessLogPath == "" {
logger.Warning("xray log not init in config.json")
logger.Warning("access.log doesn't exist in your config.json")
return
}
@@ -58,10 +68,8 @@ func processLogFile() {
checkError(err)
}
lines := ss.Split(string(data), "\n")
lines := strings.Split(string(data), "\n")
for _, line := range lines {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
emailRegx, _ := regexp.Compile(`email:.+`)
matchesIp := ipRegx.FindString(line)
if len(matchesIp) > 0 {
@@ -74,30 +82,24 @@ func processLogFile() {
if matchesEmail == "" {
continue
}
matchesEmail = ss.Split(matchesEmail, "email: ")[1]
matchesEmail = strings.TrimSpace(strings.Split(matchesEmail, "email: ")[1])
if InboundClientIps[matchesEmail] != nil {
if contains(InboundClientIps[matchesEmail], ip) {
continue
}
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
} else {
if !contains(InboundClientIps[matchesEmail], ip) {
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
}
}
}
disAllowedIps = []string{}
j.setAllowedIps([]string{})
for clientEmail, ips := range InboundClientIps {
inboundClientIps, err := GetInboundClientIps(clientEmail)
sort.Sort(sort.StringSlice(ips))
sort.Strings(ips)
if err != nil {
addInboundClientIps(clientEmail, ips)
} else {
updateInboundClientIps(inboundClientIps, clientEmail, ips)
j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
}
}
@@ -110,6 +112,7 @@ func processLogFile() {
stop <- true
}
func GetAccessLogPath() string {
config, err := os.ReadFile(xray.GetConfigPath())
@@ -130,20 +133,22 @@ func GetAccessLogPath() string {
return ""
}
func checkError(e error) {
if e != nil {
logger.Warning("client ip job err:", e)
}
}
func contains(s []string, str string) bool {
for _, v := range s {
if v == str {
return true
}
}
return false
}
func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
db := database.GetDB()
InboundClientIps := &model.InboundClientIps{}
@@ -153,14 +158,12 @@ func GetInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
}
return InboundClientIps, nil
}
func addInboundClientIps(clientEmail string, ips []string) error {
inboundClientIps := &model.InboundClientIps{}
jsonIps, err := json.Marshal(ips)
checkError(err)
// Trim any leading/trailing whitespace from clientEmail
clientEmail = strings.TrimSpace(clientEmail)
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
@@ -168,10 +171,10 @@ func addInboundClientIps(clientEmail string, ips []string) error {
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
} else {
if r := recover(); r != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
@@ -181,13 +184,7 @@ func addInboundClientIps(clientEmail string, ips []string) error {
}
return nil
}
func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) error {
jsonIps, err := json.Marshal(ips)
checkError(err)
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) error {
// check inbound limitation
inbound, err := GetInboundByEmail(clientEmail)
@@ -204,17 +201,22 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
for _, client := range clients {
if client.Email == clientEmail {
limitIp := client.LimitIP
if limitIp < len(ips) && limitIp != 0 && inbound.Enable {
disAllowedIps = append(disAllowedIps, ips[limitIp:]...)
for _, ip := range ips[:limitIp] {
j.addAllowedIp(ip)
}
}
}
}
logger.Debug("disAllowedIps ", disAllowedIps)
sort.Sort(sort.StringSlice(disAllowedIps))
jsonIps, err := json.Marshal(ips) // marshal the possibly truncated list of IPs
checkError(err)
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
logger.Debug("Allowed IPs: ", ips)
db := database.GetDB()
err = db.Save(inboundClientIps).Error
@@ -223,6 +225,25 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
}
return nil
}
func (j *CheckClientIpJob) setAllowedIps(ips []string) {
j.mutex.Lock()
defer j.mutex.Unlock()
j.AllowedIps = ips
}
func (j *CheckClientIpJob) addAllowedIp(ip string) {
j.mutex.Lock()
defer j.mutex.Unlock()
j.AllowedIps = append(j.AllowedIps, ip)
}
func (j *CheckClientIpJob) getAllowedIps() []string {
j.mutex.Lock()
defer j.mutex.Unlock()
return j.AllowedIps
}
func DisableInbound(id int) error {
db := database.GetDB()
result := db.Model(model.Inbound{}).
@@ -249,7 +270,6 @@ func GetInboundByEmail(clientEmail string) (*model.Inbound, error) {
}
func LimitDevice() {
var destIp, destPort, srcIp, srcPort string
localIp, err := LocalIP()
checkError(err)
@@ -265,19 +285,19 @@ func LimitDevice() {
data := strings.Split(row, " ")
if len(data) < 2 {
continue // Skip this row if it doesn't have at least two elements
}
destIp, destPort, srcIp, srcPort := "", "", "", ""
destIp = string(ipRegx.FindString(data[0]))
destPort = portRegx.FindString(data[0])
destPort = strings.Replace(destPort, ":", "", -1)
srcIp = string(ipRegx.FindString(data[1]))
srcPort = portRegx.FindString(data[1])
srcPort = strings.Replace(srcPort, ":", "", -1)
if contains(disAllowedIps, srcIp) {
if contains(AllowedIps, srcIp) {
dropCmd := cmd.NewCmd("bash", "-c", "ss -K dport = "+srcPort)
dropCmd.Start()
@@ -285,6 +305,7 @@ func LimitDevice() {
}
}
}
}
func LocalIP() ([]string, error) {

View File

@@ -1,7 +1,7 @@
package job
import (
"fmt"
"strconv"
"time"
"x-ui/web/service"
@@ -24,7 +24,10 @@ func (j *CheckCpuJob) Run() {
// get latest status of server
percent, err := cpu.Percent(1*time.Second, false)
if err == nil && percent[0] > float64(threshold) {
msg := fmt.Sprintf("🔴 CPU usage %.2f%% is more than threshold %d%%", percent[0], threshold)
msg := j.tgbotService.I18nBot("tgbot.messages.cpuThreshold",
"Percent=="+strconv.FormatFloat(percent[0], 'f', 2, 64),
"Threshold=="+strconv.Itoa(threshold))
j.tgbotService.SendMsgToTgbotAdmins(msg)
}
}

View File

@@ -0,0 +1,19 @@
package job
import (
"x-ui/web/service"
)
type CheckHashStorageJob struct {
tgbotService service.Tgbot
}
func NewCheckHashStorageJob() *CheckHashStorageJob {
return new(CheckHashStorageJob)
}
// Here Run is an interface method of the Job interface
func (j *CheckHashStorageJob) Run() {
// Remove expired hashes from storage
j.tgbotService.GetHashStorage().RemoveExpiredHashes()
}

144
web/locale/locale.go Normal file
View File

@@ -0,0 +1,144 @@
package locale
import (
"embed"
"io/fs"
"strings"
"x-ui/logger"
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/pelletier/go-toml/v2"
"golang.org/x/text/language"
)
var i18nBundle *i18n.Bundle
var LocalizerWeb *i18n.Localizer
var LocalizerBot *i18n.Localizer
type I18nType string
const (
Bot I18nType = "bot"
Web I18nType = "web"
)
type SettingService interface {
GetTgLang() (string, error)
}
func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
// set default bundle to english
i18nBundle = i18n.NewBundle(language.English)
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
// parse files
if err := parseTranslationFiles(i18nFS, i18nBundle); err != nil {
return err
}
// setup bot locale
if err := initTGBotLocalizer(settingService); err != nil {
return err
}
return nil
}
func createTemplateData(params []string, seperator ...string) map[string]interface{} {
var sep string = "=="
if len(seperator) > 0 {
sep = seperator[0]
}
templateData := make(map[string]interface{})
for _, param := range params {
parts := strings.SplitN(param, sep, 2)
templateData[parts[0]] = parts[1]
}
return templateData
}
func I18n(i18nType I18nType, key string, params ...string) string {
var localizer *i18n.Localizer
switch i18nType {
case "bot":
localizer = LocalizerBot
case "web":
localizer = LocalizerWeb
default:
logger.Errorf("Invalid type for I18n: %s", i18nType)
return ""
}
templateData := createTemplateData(params)
msg, err := localizer.Localize(&i18n.LocalizeConfig{
MessageID: key,
TemplateData: templateData,
})
if err != nil {
logger.Errorf("Failed to localize message: %v", err)
return ""
}
return msg
}
func initTGBotLocalizer(settingService SettingService) error {
botLang, err := settingService.GetTgLang()
if err != nil {
return err
}
LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang)
return nil
}
func LocalizerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
var lang string
if cookie, err := c.Request.Cookie("lang"); err == nil {
lang = cookie.Value
} else {
lang = c.GetHeader("Accept-Language")
}
LocalizerWeb = i18n.NewLocalizer(i18nBundle, lang)
c.Set("localizer", LocalizerWeb)
c.Set("I18n", I18n)
c.Next()
}
}
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
err := fs.WalkDir(i18nFS, "translation",
func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
data, err := i18nFS.ReadFile(path)
if err != nil {
return err
}
_, err = i18nBundle.ParseMessageFileBytes(data, path)
return err
})
if err != nil {
return err
}
return nil
}

View File

@@ -6,7 +6,11 @@
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
"services": [
"HandlerService",
"LoggerService",
"StatsService"
]
},
"inbounds": [
{
@@ -47,20 +51,26 @@
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"inboundTag": [
"api"
],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
"ip": [
"geoip:private"
]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
"protocol": [
"bittorrent"
]
}
]
},
"stats": {}
}
}

View File

@@ -3,6 +3,7 @@ package service
import (
"encoding/json"
"fmt"
"strings"
"time"
"x-ui/database"
"x-ui/database/model"
@@ -50,7 +51,7 @@ func (s *InboundService) checkPortExist(port int, ignoreId int) (bool, error) {
return count > 0, nil
}
func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, error) {
func (s *InboundService) GetClients(inbound *model.Inbound) ([]model.Client, error) {
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings)
if settings == nil {
@@ -109,7 +110,7 @@ func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (str
}
func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (string, error) {
clients, err := s.getClients(inbound)
clients, err := s.GetClients(inbound)
if err != nil {
return "", err
}
@@ -149,7 +150,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, err
return inbound, common.NewError("Duplicate email:", existEmail)
}
clients, err := s.getClients(inbound)
clients, err := s.GetClients(inbound)
if err != nil {
return inbound, err
}
@@ -207,7 +208,7 @@ func (s *InboundService) DelInbound(id int) error {
if err != nil {
return err
}
clients, err := s.getClients(inbound)
clients, err := s.GetClients(inbound)
if err != nil {
return err
}
@@ -262,7 +263,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
}
func (s *InboundService) AddInboundClient(data *model.Inbound) error {
clients, err := s.getClients(data)
clients, err := s.GetClients(data)
if err != nil {
return err
}
@@ -331,6 +332,9 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
if oldInbound.Protocol == "trojan" {
client_key = "password"
}
if oldInbound.Protocol == "shadowsocks" {
client_key = "email"
}
inerfaceClients := settings["clients"].([]interface{})
var newClients []interface{}
@@ -367,8 +371,8 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
return db.Save(oldInbound).Error
}
func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) error {
clients, err := s.getClients(data)
func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) error {
clients, err := s.GetClients(data)
if err != nil {
return err
}
@@ -386,12 +390,30 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
return err
}
oldClients, err := s.getClients(oldInbound)
oldClients, err := s.GetClients(oldInbound)
if err != nil {
return err
}
if len(clients[0].Email) > 0 && clients[0].Email != oldClients[index].Email {
oldEmail := ""
clientIndex := 0
for index, oldClient := range oldClients {
oldClientId := ""
if oldInbound.Protocol == "trojan" {
oldClientId = oldClient.Password
} else if oldInbound.Protocol == "shadowsocks" {
oldClientId = oldClient.Email
} else {
oldClientId = oldClient.ID
}
if clientId == oldClientId {
oldEmail = oldClient.Email
clientIndex = index
break
}
}
if len(clients[0].Email) > 0 && clients[0].Email != oldEmail {
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return err
@@ -406,10 +428,8 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
if err != nil {
return err
}
settingsClients := oldSettings["clients"].([]interface{})
settingsClients[index] = inerfaceClients[0]
settingsClients[clientIndex] = inerfaceClients[0]
oldSettings["clients"] = settingsClients
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
@@ -421,12 +441,12 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
db := database.GetDB()
if len(clients[0].Email) > 0 {
if len(oldClients[index].Email) > 0 {
err = s.UpdateClientStat(oldClients[index].Email, &clients[0])
if len(oldEmail) > 0 {
err = s.UpdateClientStat(oldEmail, &clients[0])
if err != nil {
return err
}
err = s.UpdateClientIPs(db, oldClients[index].Email, clients[0].Email)
err = s.UpdateClientIPs(db, oldEmail, clients[0].Email)
if err != nil {
return err
}
@@ -434,11 +454,11 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
s.AddClientStat(data.Id, &clients[0])
}
} else {
err = s.DelClientStat(db, oldClients[index].Email)
err = s.DelClientStat(db, oldEmail)
if err != nil {
return err
}
err = s.DelClientIPs(db, oldClients[index].Email)
err = s.DelClientIPs(db, oldEmail)
if err != nil {
return err
}
@@ -541,7 +561,7 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
for traffic_index := range dbClientTraffics {
if c["email"] == dbClientTraffics[traffic_index].Email {
if dbClientTraffics[traffic_index].ExpiryTime < 0 && c["email"] == dbClientTraffics[traffic_index].Email {
oldExpiryTime := c["expiryTime"].(float64)
newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime)
c["expiryTime"] = newExpiryTime
@@ -580,6 +600,7 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
count := result.RowsAffected
return count, err
}
func (s *InboundService) DisableInvalidClients() (int64, error) {
db := database.GetDB()
now := time.Now().Unix() * 1000
@@ -590,7 +611,8 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
count := result.RowsAffected
return count, err
}
func (s *InboundService) RemoveOrphanedTraffics() {
func (s *InboundService) MigrationRemoveOrphanedTraffics() {
db := database.GetDB()
db.Exec(`
DELETE FROM client_traffics
@@ -601,6 +623,7 @@ func (s *InboundService) RemoveOrphanedTraffics() {
)
`)
}
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
db := database.GetDB()
@@ -619,6 +642,7 @@ func (s *InboundService) AddClientStat(inboundId int, client *model.Client) erro
}
return nil
}
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
db := database.GetDB()
@@ -649,6 +673,294 @@ func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error {
return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error
}
func (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("id = ?", trafficId).Find(&traffics).Error
if err != nil {
logger.Warning(err)
return nil, nil, err
}
if len(traffics) > 0 {
inbound, err = s.GetInbound(traffics[0].InboundId)
return traffics[0], inbound, err
}
return nil, nil, nil
}
func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
if err != nil {
logger.Warning(err)
return nil, nil, err
}
if len(traffics) > 0 {
inbound, err = s.GetInbound(traffics[0].InboundId)
return traffics[0], inbound, err
}
return nil, nil, nil
}
func (s *InboundService) GetClientByEmail(clientEmail string) (*xray.ClientTraffic, *model.Client, error) {
traffic, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
return nil, nil, err
}
if inbound == nil {
return nil, nil, common.NewError("Inbound Not Found For Email:", clientEmail)
}
clients, err := s.GetClients(inbound)
if err != nil {
return nil, nil, err
}
for _, client := range clients {
if client.Email == clientEmail {
return traffic, &client, nil
}
}
return nil, nil, common.NewError("Client Not Found In Inbound For Email:", clientEmail)
}
func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) error {
traffic, inbound, err := s.GetClientInboundByTrafficID(trafficId)
if err != nil {
return err
}
if inbound == nil {
return common.NewError("Inbound Not Found For Traffic ID:", trafficId)
}
clientEmail := traffic.Email
oldClients, err := s.GetClients(inbound)
if err != nil {
return err
}
clientId := ""
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else {
clientId = oldClient.ID
}
break
}
}
if len(clientId) == 0 {
return common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
if c["email"] == clientEmail {
c["tgId"] = tgId
newClients = append(newClients, interface{}(c))
}
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
inbound.Settings = string(modifiedSettings)
return s.UpdateInboundClient(inbound, clientId)
}
func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, error) {
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
return false, err
}
if inbound == nil {
return false, common.NewError("Inbound Not Found For Email:", clientEmail)
}
oldClients, err := s.GetClients(inbound)
if err != nil {
return false, err
}
clientId := ""
clientOldEnabled := false
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else {
clientId = oldClient.ID
}
clientOldEnabled = oldClient.Enable
break
}
}
if len(clientId) == 0 {
return false, common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return false, err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
if c["email"] == clientEmail {
c["enable"] = !clientOldEnabled
newClients = append(newClients, interface{}(c))
}
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return false, err
}
inbound.Settings = string(modifiedSettings)
return !clientOldEnabled, s.UpdateInboundClient(inbound, clientId)
}
func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) error {
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
return err
}
if inbound == nil {
return common.NewError("Inbound Not Found For Email:", clientEmail)
}
oldClients, err := s.GetClients(inbound)
if err != nil {
return err
}
clientId := ""
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else {
clientId = oldClient.ID
}
break
}
}
if len(clientId) == 0 {
return common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
if c["email"] == clientEmail {
c["limitIp"] = count
newClients = append(newClients, interface{}(c))
}
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
inbound.Settings = string(modifiedSettings)
return s.UpdateInboundClient(inbound, clientId)
}
func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry_time int64) error {
_, inbound, err := s.GetClientInboundByEmail(clientEmail)
if err != nil {
return err
}
if inbound == nil {
return common.NewError("Inbound Not Found For Email:", clientEmail)
}
oldClients, err := s.GetClients(inbound)
if err != nil {
return err
}
clientId := ""
for _, oldClient := range oldClients {
if oldClient.Email == clientEmail {
if inbound.Protocol == "trojan" {
clientId = oldClient.Password
} else {
clientId = oldClient.ID
}
break
}
}
if len(clientId) == 0 {
return common.NewError("Client Not Found For Email:", clientEmail)
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(inbound.Settings), &settings)
if err != nil {
return err
}
clients := settings["clients"].([]interface{})
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
if c["email"] == clientEmail {
c["expiryTime"] = expiry_time
newClients = append(newClients, interface{}(c))
}
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
inbound.Settings = string(modifiedSettings)
return s.UpdateInboundClient(inbound, clientId)
}
func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
Where("email = ?", clientEmail).
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
err := result.Error
if err != nil {
return err
}
return nil
}
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
db := database.GetDB()
@@ -667,8 +979,15 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
func (s *InboundService) ResetAllClientTraffics(id int) error {
db := database.GetDB()
whereText := "inbound_id "
if id == -1 {
whereText += " > ?"
} else {
whereText += " = ?"
}
result := db.Model(xray.ClientTraffic{}).
Where("inbound_id = ?", id).
Where(whereText, id).
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
err := result.Error
@@ -694,6 +1013,84 @@ func (s *InboundService) ResetAllTraffics() error {
return nil
}
func (s *InboundService) DelDepletedClients(id int) (err error) {
db := database.GetDB()
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
whereText := "inbound_id "
if id < 0 {
whereText += "> ?"
} else {
whereText += "= ?"
}
depletedClients := []xray.ClientTraffic{}
err = db.Model(xray.ClientTraffic{}).Where(whereText+" and enable = ?", id, false).Select("inbound_id, GROUP_CONCAT(email) as email").Group("inbound_id").Find(&depletedClients).Error
if err != nil {
return err
}
for _, depletedClient := range depletedClients {
emails := strings.Split(depletedClient.Email, ",")
oldInbound, err := s.GetInbound(depletedClient.InboundId)
if err != nil {
return err
}
var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return err
}
oldClients := oldSettings["clients"].([]interface{})
var newClients []interface{}
for _, client := range oldClients {
deplete := false
c := client.(map[string]interface{})
for _, email := range emails {
if email == c["email"].(string) {
deplete = true
break
}
}
if !deplete {
newClients = append(newClients, client)
}
}
if len(newClients) > 0 {
oldSettings["clients"] = newClients
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil {
return err
}
oldInbound.Settings = string(newSettings)
err = tx.Save(oldInbound).Error
if err != nil {
return err
}
} else {
// Delete inbound if no client remains
s.DelInbound(depletedClient.InboundId)
}
}
err = tx.Where(whereText+" and enable = ?", id, false).Delete(xray.ClientTraffic{}).Error
if err != nil {
return err
}
return nil
}
func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTraffic, error) {
db := database.GetDB()
var inbounds []*model.Inbound
@@ -703,7 +1100,7 @@ func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTr
}
var emails []string
for _, inbound := range inbounds {
clients, err := s.getClients(inbound)
clients, err := s.GetClients(inbound)
if err != nil {
logger.Error("Unable to get clients from inbound")
}
@@ -724,18 +1121,20 @@ func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTr
return traffics, err
}
func (s *InboundService) GetClientTrafficByEmail(email string) (traffic []*xray.ClientTraffic, err error) {
func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.ClientTraffic, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err)
return nil, err
}
logger.Warning(err)
return nil, err
}
return traffics, err
if len(traffics) > 0 {
return traffics[0], nil
}
return nil, nil
}
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
@@ -809,8 +1208,11 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
}
return inbounds, nil
}
func (s *InboundService) MigrationRequirements() {
db := database.GetDB()
// Fix inbounds based problems
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
@@ -821,6 +1223,7 @@ func (s *InboundService) MigrationRequirements() {
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
if ok {
// Fix Clinet configuration problems
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
@@ -846,6 +1249,28 @@ func (s *InboundService) MigrationRequirements() {
inbounds[inbound_index].Settings = string(modifiedSettings)
}
// Add client traffic row for all clients which has email
modelClients, err := s.GetClients(inbounds[inbound_index])
if err != nil {
return
}
for _, modelClient := range modelClients {
if len(modelClient.Email) > 0 {
var count int64
db.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
if count == 0 {
s.AddClientStat(inbounds[inbound_index].Id, &modelClient)
}
}
}
}
db.Save(inbounds)
// Remove orphaned traffics
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
}
func (s *InboundService) MigrateDB() {
s.MigrationRequirements()
s.MigrationRemoveOrphanedTraffics()
}

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"io/fs"
"mime/multipart"
"net/http"
"os"
"os/exec"
@@ -14,7 +15,9 @@ import (
"strings"
"time"
"x-ui/config"
"x-ui/database"
"x-ui/logger"
"x-ui/util/common"
"x-ui/util/sys"
"x-ui/xray"
@@ -35,9 +38,11 @@ const (
)
type Status struct {
T time.Time `json:"-"`
Cpu float64 `json:"cpu"`
Mem struct {
T time.Time `json:"-"`
Cpu float64 `json:"cpu"`
CpuCores int `json:"cpuCores"`
CpuSpeedMhz float64 `json:"cpuSpeedMhz"`
Mem struct {
Current uint64 `json:"current"`
Total uint64 `json:"total"`
} `json:"mem"`
@@ -66,6 +71,10 @@ type Status struct {
Sent uint64 `json:"sent"`
Recv uint64 `json:"recv"`
} `json:"netTraffic"`
PublicIP struct {
IPv4 string `json:"ipv4"`
IPv6 string `json:"ipv6"`
} `json:"publicIP"`
}
type Release struct {
@@ -73,7 +82,35 @@ type Release struct {
}
type ServerService struct {
xrayService XrayService
xrayService XrayService
inboundService InboundService
}
const DebugMode = false // Set to true during development
func getPublicIP(url string) string {
resp, err := http.Get(url)
if err != nil {
if DebugMode {
logger.Warning("get public IP failed:", err)
}
return "N/A"
}
defer resp.Body.Close()
ip, err := io.ReadAll(resp.Body)
if err != nil {
if DebugMode {
logger.Warning("read public IP failed:", err)
}
return "N/A"
}
if string(ip) == "" {
return "N/A" // default value
}
return string(ip)
}
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
@@ -89,6 +126,21 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
status.Cpu = percents[0]
}
status.CpuCores, err = cpu.Counts(false)
if err != nil {
logger.Warning("get cpu cores count failed:", err)
}
cpuInfos, err := cpu.Info()
if err != nil {
logger.Warning("get cpu info failed:", err)
} else if len(cpuInfos) > 0 {
cpuInfo := cpuInfos[0]
status.CpuSpeedMhz = cpuInfo.Mhz // setting CPU speed in MHz
} else {
logger.Warning("could not find cpu info")
}
upTime, err := host.Uptime()
if err != nil {
logger.Warning("get uptime failed:", err)
@@ -157,6 +209,9 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
logger.Warning("get udp connections failed:", err)
}
status.PublicIP.IPv4 = getPublicIP("https://api.ipify.org")
status.PublicIP.IPv6 = getPublicIP("https://api6.ipify.org")
if s.xrayService.IsXrayRunning() {
status.Xray.State = Running
status.Xray.ErrorMsg = ""
@@ -175,7 +230,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
}
func (s *ServerService) GetXrayVersions() ([]string, error) {
url := "https://api.github.com/repos/mhsanaei/Xray-core/releases"
url := "https://api.github.com/repos/MHSanaei/Xray-core/releases"
resp, err := http.Get(url)
if err != nil {
return nil, err
@@ -242,7 +297,7 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
}
fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
url := fmt.Sprintf("https://github.com/mhsanaei/Xray-core/releases/download/%s/%s", version, fileName)
url := fmt.Sprintf("https://github.com/MHSanaei/Xray-core/releases/download/%s/%s", version, fileName)
resp, err := http.Get(url)
if err != nil {
return "", err
@@ -395,6 +450,106 @@ func (s *ServerService) GetDb() ([]byte, error) {
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) {
// Run the command
cmd := exec.Command(xray.GetBinaryPath(), "x25519")

View File

@@ -29,6 +29,7 @@ var defaultValueMap = map[string]string{
"webKeyFile": "",
"secret": random.Seq(32),
"webBasePath": "/",
"sessionMaxAge": "0",
"expireDiff": "0",
"trafficDiff": "0",
"timeLocation": "Asia/Tehran",
@@ -38,7 +39,16 @@ var defaultValueMap = map[string]string{
"tgRunTime": "@daily",
"tgBotBackup": "false",
"tgCpu": "0",
"tgLang": "en-US",
"secretEnable": "false",
"subEnable": "false",
"subListen": "",
"subPort": "2096",
"subPath": "sub/",
"subDomain": "",
"subCertFile": "",
"subKeyFile": "",
"subUpdates": "12",
}
type SettingService struct {
@@ -251,16 +261,12 @@ func (s *SettingService) GetTgBotBackup() (bool, error) {
return s.getBool("tgBotBackup")
}
func (s *SettingService) SetTgBotBackup(value bool) error {
return s.setBool("tgBotBackup", value)
}
func (s *SettingService) GetTgCpu() (int, error) {
return s.getInt("tgCpu")
}
func (s *SettingService) SetTgCpu(value int) error {
return s.setInt("tgCpu", value)
func (s *SettingService) GetTgLang() (string, error) {
return s.getString("tgLang")
}
func (s *SettingService) GetPort() (int, error) {
@@ -283,16 +289,12 @@ func (s *SettingService) GetExpireDiff() (int, error) {
return s.getInt("expireDiff")
}
func (s *SettingService) SetExpireDiff(value int) error {
return s.setInt("expireDiff", value)
}
func (s *SettingService) GetTrafficDiff() (int, error) {
return s.getInt("trafficDiff")
}
func (s *SettingService) SetgetTrafficDiff(value int) error {
return s.setInt("trafficDiff", value)
func (s *SettingService) GetSessionMaxAge() (int, error) {
return s.getInt("sessionMaxAge")
}
func (s *SettingService) GetSecretStatus() (bool, error) {
@@ -342,6 +344,48 @@ func (s *SettingService) GetTimeLocation() (*time.Location, error) {
return location, nil
}
func (s *SettingService) GetSubEnable() (bool, error) {
return s.getBool("subEnable")
}
func (s *SettingService) GetSubListen() (string, error) {
return s.getString("subListen")
}
func (s *SettingService) GetSubPort() (int, error) {
return s.getInt("subPort")
}
func (s *SettingService) GetSubPath() (string, error) {
subPath, err := s.getString("subPath")
if err != nil {
return "", err
}
if !strings.HasPrefix(subPath, "/") {
subPath = "/" + subPath
}
if !strings.HasSuffix(subPath, "/") {
subPath += "/"
}
return subPath, nil
}
func (s *SettingService) GetSubDomain() (string, error) {
return s.getString("subDomain")
}
func (s *SettingService) GetSubCertFile() (string, error) {
return s.getString("subCertFile")
}
func (s *SettingService) GetSubKeyFile() (string, error) {
return s.getString("subKeyFile")
}
func (s *SettingService) GetSubUpdates() (int, error) {
return s.getInt("subUpdates")
}
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
if err := allSetting.CheckValid(); err != nil {
return err

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More