Compare commits

...

244 Commits

Author SHA1 Message Date
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
MHSanaei
04d85af40e undo changes 2023-04-24 16:55:29 +03:30
MHSanaei
fca882ee31 sub - tg to inbound 2023-04-24 16:45:22 +03:30
MHSanaei
2832106bc6 v1.2.8 2023-04-24 16:04:01 +03:30
MHSanaei
bf24838939 remove useless 2023-04-24 15:31:25 +03:30
MHSanaei
16e3107d23 Better client delete + api
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-24 15:07:11 +03:30
MHSanaei
262e3c0985 Add database migration
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-24 14:13:25 +03:30
MHSanaei
2b460bac1a Optimize database
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-24 14:04:05 +03:30
MHSanaei
d1c4eb9b4c Update setting.html 2023-04-24 02:48:17 +03:30
MHSanaei
dfa3d39ab3 fix flow view 2023-04-24 02:48:02 +03:30
Ho3ein
2b54d0344e v1.2.7 2023-04-21 19:15:36 +03:30
MHSanaei
f817f922fe add getClientTraffics api
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-21 19:06:59 +03:30
MHSanaei
55f7fcd1b3 typo thanks to @firefoxOnFire 2023-04-21 19:03:23 +03:30
MHSanaei
b0f974a94d secret token thanks to @HarlyquinForest 2023-04-21 19:00:14 +03:30
MHSanaei
6bebde4105 Merge branch 'main' of https://github.com/MHSanaei/3x-ui 2023-04-21 15:34:19 +03:30
Ho3ein
fa4a63c958 Merge pull request #273 from ehsaninuc/patch-1
Update translate.en_US.toml
2023-04-21 02:08:06 +03:30
Ho3ein
11def0a753 Merge pull request #272 from hamid-gh98/main
update iran.dat from xray zip
2023-04-21 02:02:32 +03:30
Ehsan Soltani Azad
513f87550a Update translate.en_US.toml 2023-04-21 01:41:03 +03:30
Hamidreza Ghavami
81838b504c update iran.dat from xray zip 2023-04-21 02:14:00 +04:30
MHSanaei
4980744793 add reset traffic in edit
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-20 15:55:51 +03:30
MHSanaei
5ff6f4094e fix userinfo header
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-20 15:51:38 +03:30
MHSanaei
bbce1eb3f7 fix enabletgbot cli
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-20 15:49:48 +03:30
MHSanaei
204f73a692 main.go enhancements
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-20 15:49:24 +03:30
MHSanaei
641a7d3e57 install warp to menu 2023-04-19 23:18:26 +03:30
Ho3ein
3f7e819a9b Update README.md 2023-04-19 15:41:17 +03:30
Ho3ein
834d003ab6 v1.2.6 2023-04-19 15:40:47 +03:30
MHSanaei
c627227893 bug fixed 2023-04-19 15:37:24 +03:30
MHSanaei
2a8725a7a5 mistake 2023-04-19 13:26:35 +03:30
Ho3ein
ca2d1bb901 Update README.md 2023-04-19 11:59:07 +03:30
Ho3ein
fa19649286 Merge pull request #269 from hamid-gh98/main
Add description for config groups
2023-04-19 11:56:46 +03:30
Ho3ein
56d75f5293 v1.2.5 2023-04-19 11:56:16 +03:30
MHSanaei
e1132a3f41 bug fix
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-19 11:55:38 +03:30
MHSanaei
4d479102ad reality link bug fix
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-19 11:55:31 +03:30
Hamidreza Ghavami
e90c575bfd add description for config groups 2023-04-19 04:02:53 +04:30
Hamidreza
9d02f455cc Merge pull request #268 from hamid-gh98/main
update README.md
2023-04-18 22:41:07 +03:30
Hamidreza Ghavami
13f67f595c update README.md 2023-04-18 23:40:08 +04:30
MHSanaei
25741dcb08 Update README.md 2023-04-18 22:34:43 +03:30
Ho3ein
a7af62162c v1.2.4 2023-04-18 21:36:01 +03:30
MHSanaei
3e0faecaae improve reality setting
split xtls from tls - remove iran warp - remove old setting reality from franzkafka (it was a messy code) -and other improvement

Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-18 21:34:06 +03:30
Ho3ein
dc7dbae14a Merge pull request #266 from hamid-gh98/main
[Support] change settings by different items in panel
2023-04-18 21:10:33 +03:30
Hamidreza Ghavami
dd8c763b21 update translation 2023-04-18 21:25:16 +04:30
Hamidreza Ghavami
e0e7c102b8 update ui of setting.html + add russia setting 2023-04-18 21:24:32 +04:30
Hamidreza Ghavami
f26a7df11b add reset default config function 2023-04-18 21:23:51 +04:30
Hamidreza Ghavami
72a1b1e3f3 update domain list of setting.html 2023-04-18 21:23:12 +04:30
Hamidreza Ghavami
dfdb77c491 add func to setting service 2023-04-18 21:21:37 +04:30
Hamidreza Ghavami
846efe8eb4 add api route setting/getDefaultJsonConfig 2023-04-18 21:21:09 +04:30
Hamidreza Ghavami
8b79b5a315 update README.md 2023-04-18 21:20:28 +04:30
Hamidreza Ghavami
c71041a60d update media/configs 2023-04-18 21:19:25 +04:30
Ho3ein
6865cb108c Merge branch 'main' into main 2023-04-18 15:22:17 +03:30
Ho3ein
ee2089257a Merge pull request #267 from MHSanaei/dependabot/go_modules/github.com/xtls/xray-core-1.8.1
Bump github.com/xtls/xray-core from 1.8.0 to 1.8.1
2023-04-18 14:07:08 +03:30
dependabot[bot]
9f85ec72a8 Bump github.com/xtls/xray-core from 1.8.0 to 1.8.1
Bumps [github.com/xtls/xray-core](https://github.com/xtls/xray-core) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/xtls/xray-core/releases)
- [Commits](https://github.com/xtls/xray-core/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/xtls/xray-core
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-18 10:04:51 +00:00
Ho3ein
c4c266205b buy me a coffee 2023-04-18 13:34:09 +03:30
Hamidreza Ghavami
e4afbcea3b update README.md 2023-04-18 11:33:55 +04:30
Hamidreza Ghavami
44cede41fd update README.md 2023-04-18 11:31:52 +04:30
Hamidreza Ghavami
672fd1da19 update translation 2023-04-18 11:26:51 +04:30
Hamidreza Ghavami
f3fe866af2 change settings to use refactored function in setting.html 2023-04-18 11:23:43 +04:30
Hamidreza Ghavami
25e50aa6f1 Refactor a function for switching settings in setting.html 2023-04-18 11:23:15 +04:30
Hamidreza Ghavami
ff3657e15a add default configs in setting.html 2023-04-18 11:22:44 +04:30
Hamidreza Ghavami
4af626bb4b update html of setting.html 2023-04-18 11:21:12 +04:30
Hamidreza Ghavami
936f2e6ec2 update config.json 2023-04-18 10:22:04 +04:30
Hamidreza Ghavami
3e5984930e update x-ui.sh 2023-04-18 10:21:21 +04:30
Hamidreza Ghavami
40a0297499 update .gitignore 2023-04-18 10:16:43 +04:30
Hamidreza Ghavami
e13015a920 update release.yml 2023-04-18 10:16:10 +04:30
Ho3ein
ec4755952a Merge pull request #265 from MHSanaei/dependabot/github_actions/actions/checkout-3.5.2
Bump actions/checkout from 3.5.1 to 3.5.2
2023-04-17 16:46:36 +03:30
dependabot[bot]
d8fb83b1f2 Bump actions/checkout from 3.5.1 to 3.5.2
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.1 to 3.5.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.5.1...v3.5.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-17 10:08:20 +00:00
MHSanaei
4fb060d25e fix typo 2 2023-04-14 17:22:49 +03:30
MHSanaei
3eff8022f2 fix typo 2023-04-14 16:14:57 +03:30
Ho3ein
19bb28cb26 Merge pull request #262 from MHSanaei/dev
alpn set by default + syntax fix
2023-04-14 15:22:33 +03:30
Ho3ein
b70ecc12b3 Merge pull request #261 from hamid-gh98/main
Support set db and bin folder path from env
2023-04-14 05:28:03 +03:30
MHSanaei
1e72a22c96 syntax fix thanks to @lk29
Co-Authored-By: lk29 <12291632+lk29@users.noreply.github.com>
2023-04-14 01:28:08 +03:30
MHSanaei
d695ea8192 edit name http to h2 2023-04-14 01:26:32 +03:30
MHSanaei
b639a1bbd5 alpn set by default 2023-04-14 01:26:08 +03:30
Hamidreza Ghavami
4f952963ae update get paths functions 2023-04-14 00:10:01 +04:30
Hamidreza Ghavami
7f2ef94c7f update v2-ui.db path 2023-04-14 00:09:14 +04:30
Hamidreza Ghavami
81372de369 update en lang 2023-04-14 00:08:37 +04:30
Hamidreza Ghavami
a3b170d6c4 update and rename client ip job file 2023-04-14 00:07:13 +04:30
Hamidreza Ghavami
4548755375 update db config path 2023-04-14 00:03:46 +04:30
MHSanaei
0c047cf124 v1.2.3 2023-04-13 18:46:28 +03:30
MHSanaei
c6b59f1ee3 [migration] Remove Orphaned Traffics
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-13 18:45:18 +03:30
MHSanaei
b40759fe18 remove traffics of edited emails
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-13 18:44:56 +03:30
MHSanaei
e6f08517a3 [backup] fix db name in we request
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-13 18:42:51 +03:30
Ho3ein
293787f867 Merge pull request #253 from MHSanaei/dependabot/github_actions/actions/checkout-3.5.1
Bump actions/checkout from 3.5.0 to 3.5.1
2023-04-13 13:51:02 +03:30
dependabot[bot]
54bf24f662 Bump actions/checkout from 3.5.0 to 3.5.1
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.0 to 3.5.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.5.0...v3.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-13 10:04:59 +00:00
MHSanaei
b4727cc5f8 update speedtest link
https://www.speedtest.net/apps/cli
2023-04-12 23:41:34 +03:30
Ho3ein
b4fc5a7ab8 arm64 2023-04-12 19:49:09 +03:30
Mohammad Movaghari
d0e53231fe Merge pull request #233 from MHSanaei/revert-232-main
Revert "update branch"
2023-04-12 17:05:27 +03:30
Mohammad Movaghari
cce4b0a449 Revert "update branch" 2023-04-12 06:32:18 -07:00
Mohammad Movaghari
ff90e2a02e Merge pull request #232 from mohammadmovaghari/main
update branch
2023-04-12 17:01:22 +03:30
Mohammad Movaghari
c575425292 Merge branch 'MHSanaei:main' into main 2023-04-12 16:59:17 +03:30
mohammadmovaghari
63f71e527c Merge pull request #2 from mohammadmovaghari/api-fix
Api fix
2023-04-04 17:16:28 +03:30
mohammadmovaghari
5be9131078 Merge pull request #1 from MHSanaei/main
update with main
2023-04-04 17:13:46 +03:30
82 changed files with 5132 additions and 2339 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
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ghcr.io/${{ github.repository }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64, linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,16 +1,20 @@
name: Release 3X-ui
name: Release X-ui
on:
push:
tags:
- "*"
workflow_dispatch:
jobs:
linuxamd64build:
linuxamd64build:
name: build x-ui amd64 version
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3.5.0
- uses: actions/checkout@v3.5.2
- name: Set up Go
uses: actions/setup-go@v4.0.0
with:
go-version: 'stable'
go-version: "stable"
- name: build linux amd64 version
run: |
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
@@ -24,7 +28,7 @@ jobs:
cd bin
wget https://github.com/mhsanaei/Xray-core/releases/latest/download/Xray-linux-64.zip
unzip Xray-linux-64.zip
rm -f Xray-linux-64.zip geoip.dat geosite.dat
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
@@ -42,3 +46,45 @@ jobs:
asset_name: x-ui-linux-amd64.tar.gz
prerelease: true
overwrite: true
linuxarm64build:
name: build x-ui arm64 version
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3.5.2
- name: Set up Go
uses: actions/setup-go@v4.0.0
with:
go-version: "stable"
- name: build linux arm64 version
run: |
sudo apt-get update
sudo apt install gcc-aarch64-linux-gnu
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc go build -o xui-release -v main.go
mkdir x-ui
cp xui-release x-ui/xui-release
cp x-ui.service x-ui/x-ui.service
cp x-ui.sh x-ui/x-ui.sh
cd x-ui
mv xui-release x-ui
mkdir bin
cd bin
wget https://github.com/mhsanaei/xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
unzip Xray-linux-arm64-v8a.zip
rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat iran.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
mv xray xray-linux-arm64
cd ..
cd ..
- name: package
run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui
- name: upload
uses: svenstaro/upload-release-action@2.5.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-linux-arm64.tar.gz
asset_name: x-ui-linux-arm64.tar.gz
prerelease: true
overwrite: true

3
.gitignore vendored
View File

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

69
Dockerfile Normal file
View File

@@ -0,0 +1,69 @@
# Use the official Golang image as the base image
FROM --platform=$BUILDPLATFORM golang:1.20 as builder
ARG TARGETOS TARGETARCH
# Set up the working directory
WORKDIR /app
# Copy the Go modules and download the dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy the source code
COPY . .
RUN if [ "$TARGETARCH" = "arm64" ]; then apt update && apt install gcc-aarch64-linux-gnu -y; fi
# Build the X-ui binary
RUN if [ "$TARGETARCH" = "arm64" ]; then \
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc go build -o xui-release -v main.go; \
elif [ "$TARGETARCH" = "amd64" ]; then \
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go; \
fi
# Start a new stage using the base image
FROM ubuntu:20.04
# Set up the working directory
WORKDIR /app
# Copy the X-ui binary and required files from the builder stage
COPY --from=builder /app/xui-release /app/x-ui/xui-release
COPY x-ui.service /app/x-ui/x-ui.service
COPY x-ui.sh /app/x-ui/x-ui.sh
# Set up the runtime environment
RUN apt-get update && apt-get install -y \
wget \
unzip \
tzdata \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app/x-ui/bin
# Download and set up the required files
RUN arch=$(uname -m) && \
if [ "$arch" = "aarch64" ]; then \
wget https://github.com/mhsanaei/xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip \
&& unzip Xray-linux-arm64-v8a.zip \
&& rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat iran.dat \
&& wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat \
&& wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat \
&& wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat \
&& mv xray xray-linux-arm64; \
elif [ "$arch" = "x86_64" ]; then \
wget https://github.com/mhsanaei/Xray-core/releases/latest/download/Xray-linux-64.zip \
&& unzip Xray-linux-64.zip \
&& rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat \
&& wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat \
&& wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat \
&& wget https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat \
&& mv xray xray-linux-amd64; \
fi
WORKDIR /app/x-ui
RUN chmod +x /app/x-ui/x-ui.sh
# Set the entrypoint
ENTRYPOINT ["/app/x-ui/xui-release"]

133
README.md
View File

@@ -1,33 +1,42 @@
# 3x-ui
# 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)
3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
> **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**
**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
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
## Install custom version
To install your desired version you can add the version to the end of install command. Example for ver `v1.0.9`:
To install your desired version you can add the version to the end of install command. Example for ver `v1.4.0`:
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.0.9
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.4.0
```
# SSL
```
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
**If you think this project is helpful to you, you may wish to give a** :star2:
or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
# Default settings
@@ -36,18 +45,63 @@ certbot renew --dry-run
- database path: /etc/x-ui/x-ui.db
- xray config path: /usr/local/x-ui/bin/config.json
before you set ssl on settings
- http:// ip or domain:2053/xui
Before you set ssl on settings
- http://ip:2053/xui
- http://domain:2053/xui
After you set ssl on settings
After you set ssl on settings
- https://yourdomain:2053/xui
# Enable Traffic For Users:
# Environment Variables
| Variable | Type | Default |
| -------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
Example:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
# Xray Configurations:
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
- [enable traffic](./media/enable-traffic.txt)
- [enable traffic+block all IR IP address](./media/enable-traffic+block-IR-IP.txt)
- [enable traffic+block all IR domain](./media/enable-traffic+block-IR-domain.txt)
- [traffic](./media/configs/traffic.json)
- [traffic + Block all Iran IP address](./media/configs/traffic+block-iran-ip.json)
- [traffic + Block all Iran Domains](./media/configs/traffic+block-iran-domains.json)
- [traffic + Block Ads + Use IPv4 for Google](./media/configs/traffic+block-ads+ipv4-google.json)
- [traffic + Block Ads + Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP](./media/configs/traffic+block-ads+warp.json)
# [WARP Configuration](https://github.com/fscarmen/warp) (Optional)
If you want to use routing to WARP follow steps as below:
1. If you already installed warp, you can uninstall using below command:
```sh
warp u
```
2. Install WARP on **socks proxy mode**:
```sh
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
```
3. Turn on the config you need in panel or [Copy and paste this file to Xray Configuration](./media/configs/traffic+block-ads+warp.json)
Config Features:
- Block Ads
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
- Fix Google 403 error
# Features
@@ -62,7 +116,9 @@ After you set ssl on settings
- Support https access panel (self-provided domain name + ssl certificate)
- Support one-click SSL certificate application and automatic renewal
- For more advanced configuration items, please refer to the panel
- fix api routes (user setting will create with api)
- 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
@@ -79,10 +135,11 @@ Set the robot-related parameters in the panel background, including:
Reference syntax:
- 30 * * * * * //Notify at the 30s of each point
- 0 */10 * * * * //Notify at the first second of each 10 minutes
- 30 \* \* \* \* \* //Notify at the 30s of each point
- 0 \*/10 \* \* \* \* //Notify at the first second of each 10 minutes
- @hourly // hourly notification
- @daily // Daily notification (00:00 in the morning)
- @weekly // weekly notification
- @every 8h // notify every 8 hours
# Telegram Bot Features
@@ -100,33 +157,43 @@ Reference syntax:
- Check depleted users
- Receive backup by request and in periodic reports
## API routes
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
- `/xui/API/inbounds` base for following actions:
| Method | Path | Action |
| ------------- | ------------- | ------------- |
| GET | "/list" | Get all inbounds |
| GET | "/get/:id" | Get inbound with inbound.id |
| POST | "/add" | Add inbound |
| POST | "/del/:id" | Delete Inbound |
| POST | "/update/:id" | Update Inbound |
| POST | "/clientIps/:email" | Client Ip address |
| POST | "/clearClientIps/:email" | Clear Client Ip address |
| POST | "/addClient/" | Add Client to inbound |
| POST | "/delClient/:email" | Delete Client |
| POST | "/updateClient/:index" | Update Client |
| POST | "/:id/resetClientTraffic/:email" | Reset Client's Traffic |
| POST | "/resetAllTraffics" | Reset traffics of all inbounds |
| POST | "/resetAllClientTraffics/:id" | Reset traffics of all clients in an inbound |
| Method | Path | Action |
| :----: | ---------------------------------- | ------------------------------------------- |
| `GET` | `"/list"` | Get all inbounds |
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
| `GET` | `"/getClientTraffics/:email"` | Get Client Traffics with email |
| `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 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
- [alireza0](https://github.com/alireza0/)
- [FranzKafkaYu](https://github.com/FranzKafkaYu)
# Suggestion System
- Ubuntu 20.04+
- Debian 10+
- CentOS 8+

View File

@@ -45,6 +45,22 @@ func IsDebug() bool {
return os.Getenv("XUI_DEBUG") == "true"
}
func GetDBPath() string {
return fmt.Sprintf("/etc/%s/%s.db", GetName(), GetName())
func GetBinFolderPath() string {
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
if binFolderPath == "" {
binFolderPath = "bin"
}
return binFolderPath
}
func GetDBFolderPath() string {
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
if dbFolderPath == "" {
dbFolderPath = "/etc/x-ui"
}
return dbFolderPath
}
func GetDBPath() string {
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
}

View File

@@ -1 +1 @@
1.2.2
1.4.1

View File

@@ -1,6 +1,8 @@
package database
import (
"bytes"
"io"
"io/fs"
"os"
"path"
@@ -27,8 +29,9 @@ func initUser() error {
}
if count == 0 {
user := &model.User{
Username: "admin",
Password: "admin",
Username: "admin",
Password: "admin",
LoginSecret: "",
}
return db.Create(user).Error
}
@@ -103,3 +106,13 @@ func GetDB() *gorm.DB {
func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound
}
func IsSQLiteDB(file io.Reader) (bool, error) {
signature := []byte("SQLite format 3\x00")
buf := make([]byte, len(signature))
_, err := file.Read(buf)
if err != nil {
return false, err
}
return bytes.Equal(buf, signature), nil
}

View File

@@ -18,9 +18,10 @@ const (
)
type User struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username"`
Password string `json:"password"`
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username"`
Password string `json:"password"`
LoginSecret string `json:"loginSecret"`
}
type Inbound struct {

14
go.mod
View File

@@ -13,24 +13,24 @@ require (
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.0.7
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.23.3
github.com/xtls/xray-core v1.8.0
go.uber.org/atomic v1.10.0
github.com/shirou/gopsutil/v3 v3.23.4
github.com/xtls/xray-core v1.8.1
go.uber.org/atomic v1.11.0
golang.org/x/text v0.9.0
google.golang.org/grpc v1.54.0
google.golang.org/grpc v1.55.0
gorm.io/driver/sqlite v1.5.0
gorm.io/gorm v1.25.0
)
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/bytedance/sonic v1.8.7 // indirect
github.com/bytedance/sonic v1.8.8 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // 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.13.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
@@ -39,7 +39,7 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // 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-sqlite3 v1.14.16 // indirect

58
go.sum
View File

@@ -9,8 +9,8 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/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.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q=
github.com/bytedance/sonic v1.8.8/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=
@@ -19,6 +19,7 @@ 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/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
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=
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
@@ -41,9 +42,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-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-playground/validator/v10 v10.13.0 h1:cFRQdfaSMCOSfGCCLB20MHvuoHb/s5G8L5pu2ppK5AQ=
github.com/go-playground/validator/v10 v10.13.0/go.mod h1:dwu7+CG8/CtBiJFZDz4e+5Upb6OLw04gtBYw0mcG/z4=
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=
@@ -61,7 +62,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 h1:CqYfpuYIjnlNxM3msdyPRKabhXZWbKjf3Q8BWROFBso=
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -79,13 +80,13 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
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.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
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=
@@ -97,7 +98,7 @@ github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S
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=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/miekg/dns v1.1.53 h1:ZBkuHr5dxHtB1caEOlZTLPo7D3L3TWckgUUs/RHfDxw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -106,7 +107,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/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.0 h1:Tugw2BKlNHTMfG+CheOITkYvk4LAh6MFOvikhGVnhE8=
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=
@@ -122,20 +123,19 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:Om
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A=
github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
github.com/refraction-networking/utls v1.2.3-0.20230308205431-4f1df6c200db h1:ULRv/GPW5KYDafE0FACN2no+HTCyQLUtfyOIeyp3GNc=
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/sagernet/sing v0.1.7 h1:g4vjr3q8SUlBZSx97Emz5OBfSMBxxW5Q8C2PfdoSo08=
github.com/sagernet/sing-shadowsocks v0.1.1 h1:uFK2rlVeD/b1xhDwSMbUI2goWc6fOKxp+ZeKHZq6C9Q=
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/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/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 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ=
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
@@ -164,16 +164,16 @@ 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/xtls/reality v0.0.0-20230309125256-0d0713b108c8 h1:LLtLxEe3S0Ko+ckqt4t29RLskpNdOZfgjZCC2/Byr50=
github.com/xtls/xray-core v1.8.0 h1:/OD0sDv6YIBqvE+cVfnqlKrtbMs0Fm9IP5BR5d8Eu4k=
github.com/xtls/xray-core v1.8.0/go.mod h1:i9KWgbLyxg/NT+3+g4nE74Zp3DgTCP3X04YkSfsJeDI=
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=
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=
@@ -183,10 +183,10 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
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/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
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=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
@@ -227,15 +227,15 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/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=

View File

@@ -23,23 +23,14 @@ else
fi
echo "The OS release is: $release"
arch=$(arch)
if [[ $arch == "x86_64" || $arch == "x64" || $arch == "amd64" ]]; then
arch="amd64"
elif [[ $arch == "aarch64" || $arch == "arm64" ]]; then
arch="arm64"
else
arch="amd64"
echo -e "${red} Failed to check system arch, will use default arch: ${arch}${plain}"
fi
echo "arch: ${arch}"
if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then
echo "x-ui dosen't support 32-bit(x86) system, please use 64 bit operating system(x86_64) instead, if there is something wrong, please get in touch with me!"
exit -1
fi
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 ;;
esac
}
echo "arch: $(arch3xui)"
os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
@@ -79,9 +70,10 @@ install_base() {
#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
@@ -122,18 +114,18 @@ install_x-ui() {
exit 1
fi
echo -e "Got x-ui latest version: ${last_version}, beginning the installation..."
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch3xui).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch3xui).tar.gz
if [[ $? -ne 0 ]]; then
echo -e "${red}Downloading x-ui failed, please be sure that your server can access Github ${plain}"
exit 1
fi
else
last_version=$1
url="https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
url="https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch3xui).tar.gz"
echo -e "Begining to install x-ui $1"
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url}
wget -N --no-check-certificate -O /usr/local/x-ui-linux-$(arch3xui).tar.gz ${url}
if [[ $? -ne 0 ]]; then
echo -e "${red}Download x-ui $1 failed,please check the version exists${plain}"
echo -e "${red}Download x-ui $1 failed,please check the version exists ${plain}"
exit 1
fi
fi
@@ -142,10 +134,10 @@ install_x-ui() {
rm /usr/local/x-ui/ -rf
fi
tar zxvf x-ui-linux-${arch}.tar.gz
rm x-ui-linux-${arch}.tar.gz -f
tar zxvf x-ui-linux-$(arch3xui).tar.gz
rm x-ui-linux-$(arch3xui).tar.gz -f
cd x-ui
chmod +x x-ui bin/xray-linux-${arch}
chmod +x x-ui bin/xray-linux-$(arch3xui)
cp -f x-ui.service /etc/systemd/system/
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
chmod +x /usr/local/x-ui/x-ui.sh

View File

@@ -1,8 +1,9 @@
package logger
import (
"github.com/op/go-logging"
"os"
"github.com/op/go-logging"
)
var logger *logging.Logger

56
main.go
View File

@@ -51,8 +51,8 @@ func runWebServer() {
}
sigCh := make(chan os.Signal, 1)
//信号量捕获处理
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
// Trap shutdown signals
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
for {
sig := <-sigCh
@@ -97,7 +97,7 @@ func showSetting(show bool) {
settingService := service.SettingService{}
port, err := settingService.GetPort()
if err != nil {
fmt.Println("get current port fialed,error info:", err)
fmt.Println("get current port failed,error info:", err)
}
userService := service.UserService{}
userModel, err := userService.GetFirstUser()
@@ -109,7 +109,7 @@ func showSetting(show bool) {
if (username == "") || (userpasswd == "") {
fmt.Println("current username or password is empty")
}
fmt.Println("current pannel settings as follows:")
fmt.Println("current panel settings as follows:")
fmt.Println("username:", username)
fmt.Println("userpasswd:", userpasswd)
fmt.Println("port:", port)
@@ -204,6 +204,36 @@ func updateSetting(port int, username string, password string) {
}
}
func migrateDb() {
inboundService := service.InboundService{}
err := database.InitDB(config.GetDBPath())
if err != nil {
log.Fatal(err)
}
fmt.Println("Start migrating database...")
inboundService.MigrateDB()
fmt.Println("Migration done!")
}
func removeSecret() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
userService := service.UserService{}
err = userService.RemoveUserSecret()
if err != nil {
fmt.Println(err)
}
settingService := service.SettingService{}
err = settingService.SetSecretStatus(false)
if err != nil {
fmt.Println(err)
}
}
func main() {
if len(os.Args) < 2 {
runWebServer()
@@ -217,7 +247,7 @@ func main() {
v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError)
var dbPath string
v2uiCmd.StringVar(&dbPath, "db", "/etc/v2-ui/v2-ui.db", "set v2-ui db file path")
v2uiCmd.StringVar(&dbPath, "db", fmt.Sprintf("%s/v2-ui.db", config.GetDBFolderPath()), "set v2-ui db file path")
settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
var port int
@@ -229,14 +259,15 @@ func main() {
var tgbotRuntime string
var reset bool
var show bool
var remove_secret bool
settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
settingCmd.BoolVar(&show, "show", false, "show current settings")
settingCmd.IntVar(&port, "port", 0, "set panel port")
settingCmd.StringVar(&username, "username", "", "set login username")
settingCmd.StringVar(&password, "password", "", "set login password")
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegrame bot chat id")
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegram bot token")
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegram bot cron time")
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegram bot chat id")
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
oldUsage := flag.Usage
@@ -246,6 +277,7 @@ func main() {
fmt.Println("Commands:")
fmt.Println(" run run web panel")
fmt.Println(" v2-ui migrate form v2-ui")
fmt.Println(" migrate migrate form other/old x-ui")
fmt.Println(" setting set settings")
}
@@ -263,6 +295,8 @@ func main() {
return
}
runWebServer()
case "migrate":
migrateDb()
case "v2-ui":
err := v2uiCmd.Parse(os.Args[2:])
if err != nil {
@@ -290,6 +324,12 @@ func main() {
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
}
if remove_secret {
removeSecret()
}
if enabletgbot {
updateTgbotEnableSts(enabletgbot)
}
default:
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
fmt.Println()

View File

@@ -0,0 +1,88 @@
{
"log": {
"loglevel": "warning",
"access": "./access.log",
"error": "./error.log"
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
},
{
"tag": "IPv4",
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIPv4"
}
}
],
"policy": {
"levels": {
"0": {
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true
}
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
},
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"geosite:category-ads-all",
"geosite:category-ads",
"geosite:google-ads",
"geosite:spotify-ads"
]
},
{
"type": "field",
"outboundTag": "IPv4",
"domain": ["geosite:google"]
}
]
},
"stats": {}
}

View File

@@ -0,0 +1,98 @@
{
"log": {
"loglevel": "warning",
"access": "./access.log",
"error": "./error.log"
},
"api": {
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
}
}
],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {}
},
{
"tag": "WARP",
"protocol": "socks",
"settings": {
"servers": [
{
"address": "127.0.0.1",
"port": 40000
}
]
}
}
],
"policy": {
"levels": {
"0": {
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
"statsInboundDownlink": true,
"statsInboundUplink": true
}
},
"routing": {
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"type": "field",
"inboundTag": ["api"],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": ["bittorrent"]
},
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"geosite:category-ads-all",
"geosite:category-ads",
"geosite:google-ads",
"geosite:spotify-ads"
]
},
{
"type": "field",
"outboundTag": "WARP",
"domain": [
"geosite:google",
"geosite:netflix",
"geosite:spotify",
"geosite:openai"
]
}
]
},
"stats": {}
}

View File

@@ -1,25 +1,22 @@
{
"log": {
"loglevel": "warning",
"access": "./access.log"
"access": "./access.log",
"error": "./error.log"
},
"api": {
"services": [
"HandlerService",
"LoggerService",
"StatsService"
],
"tag": "api"
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
},
"tag": "api"
}
}
],
"outbounds": [
@@ -28,16 +25,16 @@
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {},
"tag": "blocked"
"settings": {}
}
],
"policy": {
"levels": {
"0": {
"statsUserUplink": true,
"statsUserDownlink": true
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
@@ -49,36 +46,31 @@
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"inboundTag": [
"api"
],
"outboundTag": "api",
"type": "field"
"type": "field",
"inboundTag": ["api"],
"outboundTag": "api"
},
{
"ip": [
"geoip:private"
],
"type": "field",
"outboundTag": "blocked",
"type": "field"
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": [
"bittorrent"
],
"type": "field"
"protocol": ["bittorrent"]
},
{
"type": "field",
"outboundTag": "blocked",
"domain": [
"regexp:.+.ir$",
"regexp:.*\\.ir$",
"ext:iran.dat:ir",
"ext:iran.dat:other"
],
"type": "field"
"ext:iran.dat:other",
"geosite:category-ir"
]
}
]
},
"stats": {}
}
}

View File

@@ -1,25 +1,22 @@
{
"log": {
"loglevel": "warning",
"access": "./access.log"
"access": "./access.log",
"error": "./error.log"
},
"api": {
"services": [
"HandlerService",
"LoggerService",
"StatsService"
],
"tag": "api"
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
},
"tag": "api"
}
}
],
"outbounds": [
@@ -28,16 +25,16 @@
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {},
"tag": "blocked"
"settings": {}
}
],
"policy": {
"levels": {
"0": {
"statsUserUplink": true,
"statsUserDownlink": true
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
@@ -49,34 +46,31 @@
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"inboundTag": [
"api"
],
"outboundTag": "api",
"type": "field"
"type": "field",
"inboundTag": ["api"],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": [
"bittorrent"
],
"type": "field"
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "blocked",
"ip": [
"geoip:private"
],
"type": "field"
"protocol": ["bittorrent"]
},
{
"type": "field",
"outboundTag": "blocked",
"ip": [
"geoip:ir"
],
"type": "field"
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "blocked",
"ip": ["geoip:ir"]
}
]
},
"stats": {}
}
}

View File

@@ -1,25 +1,22 @@
{
"log": {
"loglevel": "warning",
"access": "./access.log"
"access": "./access.log",
"error": "./error.log"
},
"api": {
"services": [
"HandlerService",
"LoggerService",
"StatsService"
],
"tag": "api"
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
},
"tag": "api"
}
}
],
"outbounds": [
@@ -28,16 +25,16 @@
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {},
"tag": "blocked"
"settings": {}
}
],
"policy": {
"levels": {
"0": {
"statsUserUplink": true,
"statsUserDownlink": true
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
@@ -49,27 +46,21 @@
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"inboundTag": [
"api"
],
"outboundTag": "api",
"type": "field"
"type": "field",
"inboundTag": ["api"],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": [
"geoip:private"
],
"type": "field"
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": [
"bittorrent"
],
"type": "field"
"protocol": ["bittorrent"]
}
]
},
"stats": {}
}
}

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(0 150 112);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,11 @@ 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:#fff;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: linear-gradient(90deg,#009670 0,#026247 100%);color: #fff;border-radius: 0.5rem}
.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 +1032,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 +1080,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:#15223a}
.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-active,.ant-menu.ant-menu-dark .ant-menu-item-active{background-color:#38383800}
.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}
@@ -1263,11 +1264,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 +1357,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 +1390,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 +2438,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 +2464,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 +2480,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 +2539,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}
@@ -2593,8 +2595,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}
@@ -2614,7 +2616,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 +2627,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 +2944,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 +2979,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);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 +3216,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,7 +3230,7 @@ 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,.ant-input-number:hover{border-color:rgb(0, 150, 112) !important;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-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}
@@ -3268,7 +3270,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 +3280,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 +3305,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 +3532,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 +3673,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 +3969,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 +3991,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 +4052,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 +4075,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 +4494,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,5 @@
#app {
height: 100%;
height: 100vh;
}
.ant-space {
@@ -11,7 +11,7 @@
}
.ant-card {
border-radius: 30px;
border-radius: 1.5rem;
}
.ant-card-hoverable {
@@ -169,7 +169,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,7 +180,11 @@
.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);
}
.ant-card-bordered:hover {
/*box-shadow: 0 3px 12px -0.8px #0000005c;*/
}
.ant-card-dark .ant-table-thead th {
@@ -212,6 +216,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,
@@ -226,7 +231,7 @@
.ant-card-dark .ant-calendar-date:hover,
.ant-card-dark .ant-select-dropdown-menu-item-active,
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
background-color: #004488;
background-color: #11314d;
}
.ant-card-dark tbody .ant-table-expanded-row,
@@ -235,6 +240,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,28 +252,42 @@
.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-card-dark .ant-select-disabled .ant-select-selection {
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: #242c3a;
}
.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;
}
@@ -311,9 +334,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 {
@@ -340,7 +363,7 @@
}
.ant-card-dark .ant-switch-checked {
background-color: #0c61b0;
background-color: #009670;
}
.ant-card-dark .ant-btn,
@@ -356,14 +379,14 @@
.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 {
@@ -383,4 +406,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

@@ -3,10 +3,14 @@ axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.interceptors.request.use(
config => {
config.data = Qs.stringify(config.data, {
arrayFormat: 'repeat'
});
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)
);
);

View File

@@ -3,6 +3,7 @@ class User {
constructor() {
this.username = "";
this.password = "";
this.LoginSecret = "";
}
}
@@ -171,6 +172,7 @@ class AllSetting {
this.webCertFile = "";
this.webKeyFile = "";
this.webBasePath = "/";
this.sessionMaxAge = "";
this.expireDiff = "";
this.trafficDiff = "";
this.tgBotEnable = false;
@@ -180,6 +182,7 @@ class AllSetting {
this.tgBotBackup = false;
this.tgCpu = "";
this.xrayTemplateConfig = "";
this.secretEnable = false;
this.timeLocation = "Asia/Tehran";

File diff suppressed because it is too large Load Diff

View File

@@ -94,26 +94,6 @@ const shortIdSeq = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
];
const x25519Map = new Map(
[
['EH2FWe-Ij_FFAa2u9__-aiErLvVIneP601GOCdlyPWw', "goY3OtfaA4UYbiz7Hn0NysI5QJrK0VT_Chg6RLgUPQU"],
['cKI_6DoMSP1IepeWWXrG3G9nkehl94KYBhagU50g2U0', "VigpKFbSLnHLzBWobZaS1IBmw--giJ51w92y723ajnU"],
['qM2SNyK3NyHB6deWpEP3ITyCGKQFRTna_mlKP0w1QH0', "HYyIGuyNFslmcnNT7mrDdmuXwn4cm7smE_FZbYguKHQ"],
['qCWg5GMEDFd3n1nxDswlIpOHoPUXMLuMOIiLUVzubkI', "rJFC3dUjJxMnVZiUGzmf_LFsJUwFWY-CU5RQgFOHCWM"],
['4NOBxDrEsOhNI3Y3EnVIy_TN-uyBoAjQw6QM0YsOi0s', "CbcY9qc4YuMDJDyyL0OITlU824TBg1O84ClPy27e2RM"],
['eBvFb0M4HpSOwWjtXV8zliiEs_hg56zX4a2LpuuqpEI', "CjulQ2qVIky7ImIfysgQhNX7s_drGLheCGSkVHcLZhc"],
['yEpOzQV04NNcycWVeWtRNTzv5TS-ynTuKRacZCH-6U8', "O9RSr5gSdok2K_tobQnf_scyKVqnCx6C4Jrl7_rCZEQ"],
['CNt6TAUVCwqM6xIBHyni0K3Zqbn2htKQLvLb6XDgh0s', "d9cGLVBrDFS02L2OvkqyqwFZ1Ux3AHs28ehl4Rwiyl0"],
['EInKw-6Wr0rAHXlxxDuZU5mByIzcD3Z-_iWPzXlUL1k', "LlYD2nNVAvyjNvjZGZh4R8PkMIwkc6EycPTvR2LE0nQ"],
['GKIKo7rcXVyle-EUHtGIDtYnDsI6osQmOUl3DTJRAGc', "VcqHivYGGoBkcxOI6cSSjQmneltstkb2OhvO53dyhEM"],
['-FVDzv68IC17fJVlNDlhrrgX44WeBfbhwjWpCQVXGHE', "PGG2EYOvsFt2lAQTD7lqHeRxz2KxvllEDKcUrtizPBU"],
['0H3OJEYEu6XW7woqy7cKh2vzg6YHkbF_xSDTHKyrsn4', "mzevpYbS8kXengBY5p7tt56QE4tS3lwlwRemmkcQeyc"],
['8F8XywN6ci44ES6em2Z0fYYxyptB9uaXY9Hc1WSSPE4', "qCZUdWQZ2H33vWXnOkG8NpxBeq3qn5QWXlfCOWBNkkc"],
['IN0dqfkC10dj-ifRHrg2PmmOrzYs697ajGMwcLbu-1g', "2UW_EO3r7uczPGUUlpJBnMDpDmWUHE2yDzCmXS4sckE"],
['uIcmks5rAhvBe4dRaJOdeSqgxLGGMZhsGk4J4PEKL2s', "F9WJV_74IZp0Ide4hWjiJXk9FRtBUBkUr3mzU-q1lzk"],
]
);
class RandomUtil {
static randomIntRange(min, max) {
@@ -170,26 +150,6 @@ class RandomUtil {
});
}
static randowShortId() {
let str = '';
str += this.randomShortIdSeq(8)
return str;
}
static randomX25519PrivateKey() {
let num = x25519Map.size;
let index = this.randomInt(num);
let cntr = 0;
for (let key of x25519Map.keys()) {
if (cntr++ === index) {
return key;
}
}
}
static randomX25519PublicKey(key) {
return x25519Map.get(key)
}
static randomText() {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
@@ -199,6 +159,18 @@ class RandomUtil {
}
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 {

View File

@@ -19,17 +19,19 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
g.GET("/list", a.getAllInbounds)
g.GET("/get/:id", a.getSingleInbound)
g.GET("/getClientTraffics/:email", a.getClientTraffics)
g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound)
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("/delClient/:email", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient)
g.POST("/addClient", a.addInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
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)
a.inboundController = NewInboundController(g)
}
@@ -39,6 +41,9 @@ func (a *APIController) getAllInbounds(c *gin.Context) {
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)
}
@@ -74,3 +79,6 @@ func (a *APIController) resetAllTraffics(c *gin.Context) {
func (a *APIController) resetAllClientTraffics(c *gin.Context) {
a.inboundController.resetAllClientTraffics(c)
}
func (a *APIController) delDepletedClients(c *gin.Context) {
a.inboundController.delDepletedClients(c)
}

View File

@@ -1,9 +1,10 @@
package controller
import (
"github.com/gin-gonic/gin"
"net/http"
"x-ui/web/session"
"github.com/gin-gonic/gin"
)
type BaseController struct {

View File

@@ -33,12 +33,13 @@ func (a *InboundController) 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("/delClient/:email", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient)
g.POST("/addClient", a.addInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
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)
}
@@ -78,11 +79,21 @@ func (a *InboundController) getInbound(c *gin.Context) {
jsonObj(c, inbound, nil)
}
func (a *InboundController) getClientTraffics(c *gin.Context) {
email := c.Param("email")
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
if err != nil {
jsonMsg(c, "Error getting traffics", err)
return
}
jsonObj(c, clientTraffics, nil)
}
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, I18n(c, "pages.inbounds.create"), err)
return
}
user := session.GetLoginUser(c)
@@ -90,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, I18n(c, "pages.inbounds.create"), inbound, err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
@@ -112,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, I18n(c, "pages.inbounds.update"), err)
return
}
inbound := &model.Inbound{
@@ -120,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, I18n(c, "pages.inbounds.update"), err)
return
}
inbound, err = a.inboundService.UpdateInbound(inbound)
jsonMsgObj(c, I18n(c, "pages.inbounds.revise"), inbound, err)
jsonMsgObj(c, I18n(c, "pages.inbounds.update"), inbound, err)
if err == nil {
a.xrayService.SetToNeedRestart()
}
@@ -145,42 +156,41 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
err := a.inboundService.ClearClientIps(email)
if err != nil {
jsonMsg(c, "修改", err)
jsonMsg(c, "Update", err)
return
}
jsonMsg(c, "Log Cleared", nil)
}
func (a *InboundController) addInboundClient(c *gin.Context) {
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
data := &model.Inbound{}
err := c.ShouldBind(data)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return
}
err = a.inboundService.AddInboundClient(inbound)
err = a.inboundService.AddInboundClient(data)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client added", nil)
jsonMsg(c, "Client(s) added", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) delInboundClient(c *gin.Context) {
email := c.Param("email")
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
jsonMsg(c, I18n(c, "pages.inbounds.update"), err)
return
}
clientId := c.Param("clientId")
err = a.inboundService.DelInboundClient(inbound, email)
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)
@@ -190,22 +200,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, I18n(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)
@@ -217,14 +223,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, I18n(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)
@@ -236,7 +242,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)
@@ -245,14 +251,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, I18n(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, I18n(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

@@ -11,15 +11,17 @@ import (
)
type LoginForm struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
LoginSecret string `json:"loginSecret" form:"loginSecret"`
}
type IndexController struct {
BaseController
userService service.UserService
tgbot service.Tgbot
settingService service.SettingService
userService service.UserService
tgbot service.Tgbot
}
func NewIndexController(g *gin.RouterGroup) *IndexController {
@@ -32,6 +34,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index)
g.POST("/login", a.login)
g.GET("/logout", a.logout)
g.POST("/getSecretStatus", a.getSecretStatus)
}
func (a *IndexController) index(c *gin.Context) {
@@ -57,7 +60,7 @@ func (a *IndexController) login(c *gin.Context) {
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword"))
return
}
user := a.userService.CheckUser(form.Username, form.Password)
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)
@@ -69,6 +72,16 @@ func (a *IndexController) login(c *gin.Context) {
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")
}
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)
@@ -82,3 +95,11 @@ func (a *IndexController) logout(c *gin.Context) {
session.ClearSession(c)
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}
func (a *IndexController) getSecretStatus(c *gin.Context) {
status, err := a.settingService.GetSecretStatus()
if err == nil {
jsonObj(c, status, nil)
}
}

View File

@@ -41,6 +41,8 @@ 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)
}
func (a *ServerController) refreshStatus() {
@@ -98,8 +100,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 {
@@ -107,14 +109,13 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
return
}
jsonMsg(c, "Xray restarted", err)
}
func (a *ServerController) getLogs(c *gin.Context) {
count := c.Param("count")
logs, err := a.serverService.GetLogs(count)
if err != nil {
jsonMsg(c, I18n(c, "getLogs"), err)
jsonMsg(c, "getLogs", err)
return
}
jsonObj(c, logs, nil)
@@ -123,7 +124,7 @@ func (a *ServerController) getLogs(c *gin.Context) {
func (a *ServerController) getConfigJson(c *gin.Context) {
configJson, err := a.serverService.GetConfigJson()
if err != nil {
jsonMsg(c, I18n(c, "getLogs"), err)
jsonMsg(c, "get config.json", err)
return
}
jsonObj(c, configJson, nil)
@@ -132,13 +133,44 @@ func (a *ServerController) getConfigJson(c *gin.Context) {
func (a *ServerController) getDb(c *gin.Context) {
db, err := a.serverService.GetDb()
if err != nil {
jsonMsg(c, I18n(c, "getLogs"), err)
jsonMsg(c, "get Database", err)
return
}
// Set the headers for the response
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename=xui.db")
c.Header("Content-Disposition", "attachment; filename=x-ui.db")
// Write the file contents to the response
c.Writer.Write(db)
}
func (a *ServerController) importDB(c *gin.Context) {
// Get the file from the request body
file, _, err := c.Request.FormFile("db")
if err != nil {
jsonMsg(c, "Error reading db file", err)
return
}
defer file.Close()
// Always restart Xray before return
defer a.serverService.RestartXrayService()
defer func() {
a.lastGetStatusTime = time.Now()
}()
// Import it
err = a.serverService.ImportDB(file)
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, "Import DB", nil)
}
func (a *ServerController) getNewX25519Cert(c *gin.Context) {
cert, err := a.serverService.GetNewX25519Cert()
if err != nil {
jsonMsg(c, "get x25519 certificate", err)
return
}
jsonObj(c, cert, nil)
}

View File

@@ -17,6 +17,10 @@ type updateUserForm struct {
NewPassword string `json:"newPassword" form:"newPassword"`
}
type updateSecretForm struct {
LoginSecret string `json:"loginSecret" form:"loginSecret"`
}
type SettingController struct {
settingService service.SettingService
userService service.UserService
@@ -37,36 +41,48 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g.POST("/update", a.updateSetting)
g.POST("/updateUser", a.updateUser)
g.POST("/restartPanel", a.restartPanel)
g.GET("/getDefaultJsonConfig", a.getDefaultJsonConfig)
g.POST("/updateUserSecret", a.updateSecret)
g.POST("/getUserSecret", a.getUserSecret)
}
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, I18n(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, allSetting, nil)
}
func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
defaultJsonConfig, err := a.settingService.GetDefaultJsonConfig()
if err != nil {
jsonMsg(c, I18n(c, "pages.settings.toasts.getSettings"), err)
return
}
jsonObj(c, defaultJsonConfig, nil)
}
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, I18n(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, I18n(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, I18n(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, I18n(c, "pages.settings.toasts.getSettings"), err)
return
}
result := map[string]interface{}{
@@ -82,27 +98,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, I18n(c, "pages.settings.toasts.modifySettings"), err)
return
}
err = a.settingService.UpdateAllSetting(allSetting)
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
jsonMsg(c, I18n(c, "pages.settings.toasts.modifySettings"), err)
}
func (a *SettingController) updateUser(c *gin.Context) {
form := &updateUserForm{}
err := c.ShouldBind(form)
if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
jsonMsg(c, I18n(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, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(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, I18n(c, "pages.settings.toasts.modifyUser"), errors.New(I18n(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
return
}
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
@@ -111,10 +127,32 @@ 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, I18n(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, I18n(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.settings.toasts.modifySettings"), err)
}
user := session.GetLoginUser(c)
err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
if err == nil {
user.LoginSecret = form.LoginSecret
session.SetLoginUser(c, user)
}
jsonMsg(c, I18n(c, "pages.settings.toasts.modifyUser"), err)
}
func (a *SettingController) getUserSecret(c *gin.Context) {
loginUser := session.GetLoginUser(c)
user := a.userService.GetUserSecret(loginUser.Id)
if user != nil {
jsonObj(c, user, nil)
}
}

View File

@@ -29,14 +29,18 @@ 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, err := a.subService.GetSubs(subId, host)
if err != nil {
subs, header, err := a.subService.GetSubs(subId, host)
if err != nil || len(subs) == 0 {
c.String(400, "Error!")
} else {
result := ""
for _, sub := range subs {
result += sub + "\n"
}
// Add subscription-userinfo
c.Writer.Header().Set("Subscription-Userinfo", header)
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
}
}

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 != "" {
@@ -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

@@ -23,7 +23,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
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"`
@@ -42,6 +43,7 @@ type AllSetting struct {
TgCpu int `json:"tgCpu" form:"tgCpu"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
}
func (s *AllSetting) CheckValid() error {

View File

@@ -2,8 +2,9 @@ package global
import (
"context"
"github.com/robfig/cron/v3"
_ "unsafe"
"github.com/robfig/cron/v3"
)
var webServer WebServer

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

@@ -4,7 +4,8 @@
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName">
: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"

View File

@@ -57,6 +57,11 @@
<a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
</a-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)"/>
</a-input>
</a-form-item>
<a-form-item>
<a-button block @click="login" :loading="loading">{{ i18n "login" }}</a-button>
</a-form-item>
@@ -98,10 +103,12 @@
data: {
loading: false,
user: new User(),
secretEnable: false,
lang : ""
},
created(){
this.lang = getLang();
this.secretEnable = this.getSecretStatus();
},
methods: {
async login() {
@@ -111,6 +118,15 @@
if (msg.success) {
location.href = basePath + 'xui/';
}
},
async getSecretStatus() {
this.loading= true;
const msg = await HttpUtil.post('/getSecretStatus');
this.loading = false;
if (msg.success){
this.secretEnable = msg.obj;
return msg.obj;
}
}
}
});

View File

@@ -33,15 +33,58 @@
<span slot="label">{{ i18n "pages.client.clientCount" }}</span>
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item>
<a-form-item label="Subscription">
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
<a-form-item>
<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 label="Telegram ID">
<a-form-item>
<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.totalFlow" }}</span>(GB)
<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="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-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-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>
@@ -51,11 +94,13 @@
</span>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="{{ i18n "pages.client.delayedStart" }}">
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
</a-form-item>
<a-form-item label="{{ i18n "pages.client.expireDays" }}" v-if="clientsBulkModal.delayedStart">
<a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
<br>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
<a-input-number v-model="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
@@ -67,7 +112,7 @@
<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"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
@@ -83,9 +128,9 @@
confirm: null,
dbInbound: new DBInbound(),
inbound: new Inbound(),
clients: [],
quantity: 1,
totalGB: 0,
limitIp: 0,
expiryTime: '',
emailMethod: 0,
firstNum: 1,
@@ -94,8 +139,10 @@
emailPostfix: "",
subId: "",
tgId: "",
flow: "",
delayedStart: false,
ok() {
clients = [];
method=clientsBulkModal.emailMethod;
if(method>1){
start=clientsBulkModal.firstNum;
@@ -113,11 +160,18 @@
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
newClient.subId = clientsBulkModal.subId;
newClient.tgId = clientsBulkModal.tgId;
newClient.limitIp = clientsBulkModal.limitIp;
newClient._totalGB = clientsBulkModal.totalGB;
newClient._expiryTime = clientsBulkModal.expiryTime;
clientsBulkModal.clients.push(newClient);
if(clientsBulkModal.inbound.canEnableTlsFlow()){
newClient.flow = clientsBulkModal.flow;
}
if(clientsBulkModal.inbound.xtls){
newClient.flow = clientsBulkModal.flow;
}
clients.push(newClient);
}
ObjectUtil.execute(clientsBulkModal.confirm, clientsBulkModal.inbound, clientsBulkModal.dbInbound);
ObjectUtil.execute(clientsBulkModal.confirm, clients, clientsBulkModal.dbInbound.id);
},
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
this.visible = true;
@@ -128,15 +182,16 @@
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.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
this.delayedStart = false;
},
getClients(protocol, clientSettings) {

View File

@@ -12,18 +12,24 @@
confirmLoading: false,
title: '',
okText: '',
isEdit: false,
dbInbound: new DBInbound(),
inbound: new Inbound(),
clients: [],
clientStats: [],
oldClientId: "",
index: null,
clientIps: null,
isExpired: false,
delayedStart: false,
ok() {
ObjectUtil.execute(clientModal.confirm, clientModal.inbound, clientModal.dbInbound, 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=(index, dbInbound)=>{}, isEdit=false }) {
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=()=>{}, isEdit=false }) {
this.visible = true;
this.title = title;
this.okText = okText;
@@ -34,12 +40,13 @@
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 (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;
@@ -49,14 +56,23 @@
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;
}
},
@@ -139,6 +155,24 @@
}
document.getElementById("clientIPs").value = ""
},
resetClientTraffic(email,dbInboundId,iconElement) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ email);
if (msg.success) {
this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0;
}
iconElement.disabled = false;
},
})
},
},
});
</script>

View File

@@ -7,9 +7,9 @@
<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 }}xui/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-icon type="laptop"></a-icon>-->
@@ -66,7 +66,7 @@
const siderDrawer = {
visible: false,
collapsed: false,
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
isDarkTheme: localStorage.getItem("dark-mode") === 'false' ? false : true,
show() {
this.visible = true;
},

View File

@@ -9,7 +9,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>
<a-input-number :value="value" @change="value => $emit('input', value)" :min="min" style="width: 100%;"></a-input-number>
</template>
<template v-else-if="type === 'textarea'">
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
@@ -25,7 +25,7 @@
{{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>

View File

@@ -3,6 +3,10 @@
<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>
</template>
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
<a-switch v-model="client.enable"></a-switch>
</a-form-item>
<br>
<a-form-item>
<span slot="label">
<span>{{ i18n "pages.inbounds.Email" }}</span>
@@ -13,24 +17,41 @@
<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-input v-model.trim="client.email" style="width: 200px;" ></a-input>
</a-form-item>
<a-form-item label="{{ i18n "pages.inbounds.enable" }}">
<a-switch v-model="client.enable"></a-switch>
<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-input v-model.trim="client.password" style="width: 300px;" ></a-input>
</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 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-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>
<a-form-item label="Subscription" v-if="client.email">
<a-form-item v-if="client.email">
<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="client.subId"></a-input>
</a-form-item>
<a-form-item label="Telegram Username" v-if="client.email">
</a-form-item>
<a-form-item v-if="client.email">
<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 +64,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">
@@ -68,21 +89,22 @@
</a-textarea>
</a-form>
</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' : ''">
<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-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: 150px">
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow">
<a-select v-model="client.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)
<span >{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
@@ -90,21 +112,27 @@
<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) ]]
([[ sizeFormat(clientStats.up + clientStats.down) ]])
</a-tag>
</template>
<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-tooltip>
</template>
</a-form-item>
<a-form-item label="{{ i18n "pages.client.delayedStart" }}">
<br>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<a-form-item label="{{ i18n "pages.client.expireDays" }}" v-if="clientModal.delayedStart">
<a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
<br>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientModal.delayedStart">
<a-input-number v-model="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<span slot="label">
@@ -116,7 +144,7 @@
<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"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag>

View File

@@ -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>
@@ -49,7 +51,7 @@
<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"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>

View File

@@ -4,8 +4,9 @@
<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-option value="tcp,udp">TCP+UDP</a-select-option>
@@ -13,6 +14,7 @@
<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,4 +1,109 @@
{{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-icon @click="getNewEmail(client)" type="sync"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.email" style="width: 150px;"></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">
<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="client.subId"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<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="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.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' : ''">

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

View File

@@ -1,8 +1,7 @@
{{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>
@@ -10,16 +9,39 @@
<template slot="title">
<span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
</template>
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
<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-input v-model.trim="client.email" style="width: 150px;"></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>
<a-form-item label="Password">
<a-input v-model.trim="client.password" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<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="client.subId"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<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>
@@ -29,54 +51,64 @@
<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="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>
<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="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-form>
<template v-if="inbound.isTcp">
<a-form layout="inline">
<a-form-item label="Fallbacks">
@@ -109,7 +141,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,8 +1,7 @@
{{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>
@@ -13,76 +12,109 @@
<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-input v-model.trim="client.email" style="width: 150px;"></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 label="ID">
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<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="client.subId"></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: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :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 v-if="client.email">
<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="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">
<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>
<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="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>
<template v-if="inbound.isTcp">
<a-form layout="inline">
<a-form-item label="Fallbacks">
@@ -94,7 +126,7 @@
</a-row>
</a-form-item>
</a-form>
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-divider>
@@ -115,7 +147,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,8 +1,7 @@
{{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>
@@ -15,14 +14,39 @@
</span>
<a-input v-model.trim="client.email" style="width: 150px;"></a-input>
</a-form-item>
</a-form>
<a-form-item label="ID">
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
<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-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item v-if="client.email">
<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="client.subId"></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>
<a-form-item v-if="client.email">
<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>
@@ -32,52 +56,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="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-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

@@ -12,5 +12,10 @@
</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="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="none">None (Not Camouflage)</a-select-option>
<a-select-option value="srtp">SRTP (Camouflage Video Call)</a-select-option>
<a-select-option value="utp">UTP (Camouflage BT Download)</a-select-option>
<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

@@ -12,12 +12,12 @@
</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-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

@@ -6,7 +6,7 @@
<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>
<a-select-option value="http">HTTP</a-select-option>
<a-select-option value="http">H2</a-select-option>
<a-select-option value="quic">QUIC</a-select-option>
<a-select-option value="grpc">gRPC</a-select-option>
</a-select>

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

View File

@@ -10,34 +10,34 @@
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>
</span>
<a-switch v-model="inbound.reality"></a-switch>
</a-form-item>
<a-form-item v-if="inbound.canEnableXTLS()">
<a-form-item v-if="inbound.canEnableXtls()">
<span slot="label">
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>
</span>
<a-switch v-model="inbound.XTLS"></a-switch>
<a-switch v-model="inbound.xtls"></a-switch>
</a-form-item>
</a-form>
<!-- tls settings -->
<a-form v-if="inbound.tls || inbound.XTLS" layout="inline">
<a-form-item label="SNI" placeholder="Server Name Indication" v-if="inbound.tls">
<a-input v-model.trim="inbound.stream.tls.settings[0].serverName"></a-input>
<a-form v-if="inbound.tls" layout="inline">
<a-form-item 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="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<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>
@@ -52,22 +52,23 @@
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="uTLS" v-if="inbound.tls" >
<a-select v-model="inbound.stream.tls.settings[0].fingerprint" style="width: 135px">
<a-form-item label="SNI" placeholder="Server Name Indication">
<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" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
</a-form-item>
<a-form-item label="Alpn">
<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[0].allowInsecure"></a-switch>
<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">
@@ -82,7 +83,7 @@
<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>
<a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
@@ -93,33 +94,83 @@
</a-form-item>
</template>
</a-form>
<!-- xtls settings -->
<a-form v-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>
</a-checkbox-group>
</a-form-item>
<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 type="primary" icon="import" @click="setDefaultCertXtls">{{ 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>
</a-form-item>
</template>
</a-form>
<!-- reality settings -->
<a-form v-else-if="inbound.reality" layout="inline">
<a-form-item label="show">
<a-form-item label="Show">
<a-switch v-model="inbound.stream.reality.show">
</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-form-item label="xVer">
<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.fingerprint" style="width: 135px">
<a-select v-model="inbound.stream.reality.settings.fingerprint"
style="width: 135px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<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-input v-model.trim="inbound.stream.reality.dest" style="width: 360px"></a-input>
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 300px"></a-input>
</a-form-item>
<a-form-item label="serverNames">
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 360px"></a-input>
<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="privateKey">
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 360px"></a-input>
</a-form-item>
<a-form-item label="publicKey">
<a-input v-model.trim="inbound.stream.reality.publicKey" style="width: 360px"></a-input>
</a-form-item>
<a-form-item label="shortIds">
<a-form-item label="ShortIds">
<a-input v-model.trim="inbound.stream.reality.shortIds"></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>
</a-form-item>
<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-button type="primary" icon="import" @click="getNewX25519Cert">Get New Key</a-button>
</a-form-item>
</a-form>
{{end}}

View File

@@ -41,6 +41,7 @@
<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>
@@ -49,10 +50,14 @@
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">
<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>
</td>
</tr>
@@ -107,7 +112,7 @@
<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>Telegram ID</td>
<td><a :href="[[ tgBase + infoModal.clientSettings.tgId ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></td>
</tr>
</table>

View File

@@ -43,6 +43,15 @@
loading(loading) {
inModal.confirmLoading = loading;
},
getClients(protocol, clientSettings) {
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;
}
},
};
const protocols = {
@@ -62,6 +71,7 @@
inModal: inModal,
Protocols: protocols,
SSMethods: SSMethods,
delayedStart: false,
get inbound() {
return inModal.inbound;
},
@@ -70,36 +80,44 @@
},
get isEdit() {
return inModal.isEdit;
}
},
get client() {
return inModal.getClients(this.inbound.protocol, this.inbound.settings)[0];
},
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
},
set delayedExpireDays(days){
this.client.expiryTime = -86400000 * days;
},
},
methods: {
streamNetworkChange(oldValue) {
if (oldValue === 'kcp') {
this.inModal.inbound.tls = false;
streamNetworkChange() {
if (!inModal.inbound.canSetTls()) {
this.inModal.inbound.stream.security = 'none';
}
},
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());
default: return null;
if (!inModal.inbound.canEnableReality()) {
this.inModal.inbound.reality = false;
}
},
removeClient(index, clients) {
clients.splice(index, 1);
},
isExpiry(index) {
return this.inbound.isExpiry(index)
},
isClientEnable(email) {
clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
return clientStats ? clientStats['enable'] : true
},
setDefaultCertData(){
inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey;
},
setDefaultCertXtls(){
inModal.inbound.stream.xtls.certs[0].certFile = app.defaultCert;
inModal.inbound.stream.xtls.certs[0].keyFile = app.defaultKey;
},
async getNewX25519Cert(){
inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewX25519Cert');
inModal.loading(false);
if (!msg.success) {
return;
}
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
},
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';

View File

@@ -41,19 +41,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="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content">
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
</template>
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
</a-popover>
<a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content">
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
</template>
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
</a-popover>
<a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content">
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
</template>
@@ -66,11 +66,45 @@
<transition name="list" appear>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
<a-button type="primary" icon="reload" @click="resetAllTraffic">{{ i18n "pages.inbounds.resetAllTraffic" }}</a-button>
<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="siderDrawer.theme">
<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="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<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-input v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds"
:loading="spinning" :scroll="{ x: 1300 }"
@@ -78,19 +112,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-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 +131,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 +152,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">
@@ -130,29 +164,29 @@
</template>
<template slot="protocol" slot-scope="text, dbInbound">
<a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan">
<a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXTLS" color="cyan">XTLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="cyan">XTLS</a-tag>
<a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">Reality</a-tag>
</template>
</template>
<template slot="clients" slot-scope="text, dbInbound">
<template v-if="clientCount[dbInbound.id]">
<a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover>
<a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
</template>
<a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover>
<a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content">
<p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
</template>
@@ -193,7 +227,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)"
@@ -226,7 +260,7 @@
title: "ID",
align: 'center',
dataIndex: "id",
width: 30,
width: 40,
}, {
title: '{{ i18n "pages.inbounds.remark" }}',
align: 'center',
@@ -240,7 +274,7 @@
}, {
title: '{{ i18n "pages.inbounds.protocol" }}',
align: 'left',
width: 80,
width: 90,
scopedSlots: { customRender: 'protocol' },
}, {
title: '{{ i18n "clients" }}',
@@ -261,7 +295,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 +304,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({
@@ -292,25 +326,25 @@
defaultCert: '',
defaultKey: '',
clientCount: {},
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
},
methods: {
loading(spinning=true) {
this.spinning = spinning;
},
async getDBInbounds() {
this.loading();
this.refreshing = true;
const msg = await HttpUtil.post('/xui/inbound/list');
this.loading(false);
if (!msg.success) {
return;
}
this.setInbounds(msg.obj);
this.searchKey = '';
this.refreshing = false;
},
async getDefaultSettings() {
this.loading();
const msg = await HttpUtil.post('/xui/setting/defaultSettings');
this.loading(false);
if (!msg.success) {
return;
}
@@ -322,17 +356,16 @@
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);
}
}
this.searchInbounds(this.searchKey);
},
getClientCounts(dbInbound,inbound){
let clientCount = 0,active = [], deactive = [], depleted = [], expiring = [];
@@ -390,6 +423,22 @@
});
}
},
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 +471,14 @@
case "delete":
this.delInbound(dbInbound.id);
break;
case "delDepletedClients":
this.delDepletedClients(dbInbound.id)
break;
}
},
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" }}',
@@ -460,7 +512,7 @@
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 +527,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,
@@ -531,9 +583,9 @@
title: '{{ i18n "pages.client.add"}}',
okText: '{{ i18n "pages.client.submitAdd"}}',
dbInbound: dbInbound,
confirm: async (inbound, dbInbound, index) => {
confirm: async (clients, dbInboundId) => {
clientModal.loading();
await this.addClient(inbound, dbInbound);
await this.addClient(clients, dbInboundId);
clientModal.close();
},
isEdit: false
@@ -545,9 +597,9 @@
title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
okText: '{{ i18n "pages.client.bulk"}}',
dbInbound: dbInbound,
confirm: async (inbound, dbInbound) => {
confirm: async (clients, dbInboundId) => {
clientsBulkModal.loading();
await this.addClient(inbound, dbInbound);
await this.addClient(clients, dbInboundId);
clientsBulkModal.close();
},
});
@@ -561,9 +613,9 @@
okText: '{{ i18n "pages.client.submitEdit"}}',
dbInbound: dbInbound,
index: index,
confirm: async (inbound, dbInbound, index) => {
confirm: async (client, dbInboundId, clientId) => {
clientModal.loading();
await this.updateClient(inbound, dbInbound, index);
await this.updateClient(client, dbInboundId, clientId);
clientModal.close();
},
isEdit: true
@@ -573,19 +625,19 @@
firstKey = Object.keys(client)[0];
return clients.findIndex(c => c[firstKey] === client[firstKey]);
},
async addClient(inbound, dbInbound) {
async addClient(clients, dbInboundId) {
const data = {
id: dbInbound.id,
settings: inbound.settings.toString(),
id: dbInboundId,
settings: '{"clients": [' + clients.toString() +']}',
};
await this.submit('/xui/inbound/addClient/', data);
await this.submit(`/xui/inbound/addClient`, data);
},
async updateClient(inbound, dbInbound, index) {
async updateClient(client, dbInboundId, clientId) {
const data = {
id: dbInbound.id,
settings: inbound.settings.toString(),
id: dbInboundId,
settings: '{"clients": [' + client.toString() +']}',
};
await this.submit(`/xui/inbound/updateClient/${index}`, data);
await this.submit(`/xui/inbound/updateClient/${clientId}`, data);
},
resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -615,22 +667,14 @@
},
delClient(dbInboundId,client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
newDbInbound = new DBInbound(dbInbound);
inbound = newDbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client);
clients.splice(index, 1);
const data = {
id: dbInboundId,
settings: inbound.settings.toString(),
};
clientId = this.getClientId(dbInbound.protocol,client);
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delClient/' + client.email, data),
onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`),
});
},
getClients(protocol, clientSettings) {
@@ -638,9 +682,17 @@
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;
}
},
showQrcode(dbInbound, clientIndex) {
const link = dbInbound.genLink(clientIndex);
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
@@ -658,8 +710,9 @@
inbound = dbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client);
clients[index].enable = ! clients[index].enable
await this.updateClient(inbound, dbInbound, index);
clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol,clients[index]);
await this.updateClient(clients[index],dbInboundId, clientId);
this.loading(false);
},
async submit(url, data) {
@@ -670,11 +723,13 @@
},
getInboundClients(dbInbound) {
if(dbInbound.protocol == Protocols.VLESS) {
return dbInbound.toInbound().settings.vlesses
return dbInbound.toInbound().settings.vlesses;
} else if(dbInbound.protocol == Protocols.VMESS) {
return dbInbound.toInbound().settings.vmesses
return dbInbound.toInbound().settings.vmesses;
} else if(dbInbound.protocol == Protocols.TROJAN) {
return dbInbound.toInbound().settings.trojans
return dbInbound.toInbound().settings.trojans;
} else if(dbInbound.protocol == Protocols.SHADOWSOCKS) {
return dbInbound.toInbound().settings.shadowsockses;
}
},
resetClientTraffic(client,dbInboundId) {
@@ -699,14 +754,24 @@
},
resetAllClientTraffics(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
title: dbInboundId>0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: dbInboundId>0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
})
},
delDepletedClients(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId),
})
},
isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index)
},
@@ -743,6 +808,32 @@
}
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: {
searchKey: debounce(function (newVal) {
@@ -750,8 +841,15 @@
}, 500)
},
mounted() {
this.loading();
this.getDefaultSettings();
this.getDBInbounds();
if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
else {
this.getDBInbounds();
}
this.loading(false);
},
computed: {
total() {
@@ -778,7 +876,6 @@
}
},
});
</script>
{{template "inboundModal"}}

View File

@@ -111,9 +111,9 @@
<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-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">
@@ -188,6 +188,7 @@
</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 : ''"
@@ -201,6 +202,7 @@
</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 : ''"
@@ -227,10 +229,28 @@
{{ 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="siderDrawer.isDarkTheme ? darkClass : ''"
@ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
<p style="color: inherit; font-size: 16px; padding: 4px 2px;">
<a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
[[ backupModal.description ]]
</p>
<a-space direction="horizontal" align="center" style="margin-bottom: 10px;">
<a-button type="primary" @click="exportDatabase()">
[[ backupModal.exportText ]]
</a-button>
<a-button type="primary" @click="importDatabase()">
[[ backupModal.importText ]]
</a-button>
</a-space>
</a-modal>
</a-layout>
{{template "js" .}}
{{template "textModal"}}
@@ -339,6 +359,29 @@
},
};
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',
@@ -347,6 +390,7 @@
status: new Status(),
versionModal,
logModal,
backupModal,
spinning: false,
loadingTip: '{{ i18n "loading"}}',
},
@@ -382,13 +426,12 @@
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 +440,6 @@
return;
}
},
//here add restart xray function
async restartXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService');
@@ -413,20 +455,60 @@
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("/xui/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,381 +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">
<temlate>
<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>
</temlate>
</a-col>
</a-row>
</a-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="2" tab='{{ i18n "pages.setting.userSetting"}}'>
<a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'">
<a-form-item label='{{ i18n "pages.setting.oldUsername"}}'>
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.setting.currentPassword"}}'>
<a-input type="password" v-model="user.oldPassword"
style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.setting.newUsername"}}'>
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.setting.newPassword"}}'>
<a-input type="password" v-model="user.newPassword"
style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item>
<!-- <a-button type="primary" @click="updateUser">Revise</a-button>-->
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigTorrent"}}' desc='{{ i18n "pages.setting.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.setting.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
<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>
<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: {},
lang : getLang()
},
methods: {
loading(spinning = true) {
this.spinning = spinning;
},
async getAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/all");
this.loading(false);
if (msg.success) {
this.oldAllSetting = new AllSetting(msg.obj);
this.allSetting = new AllSetting(msg.obj);
this.saveBtnDisable = true;
}
},
async updateAllSetting() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/update", this.allSetting);
this.loading(false);
if (msg.success) {
await this.getAllSetting();
}
},
async updateUser() {
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/updateUser", this.user);
this.loading(false);
if (msg.success) {
this.user = {};
}
},
async restartPanel() {
await new Promise(resolve => {
this.$confirm({
title: '{{ i18n "pages.setting.restartPanel" }}',
content: '{{ i18n "pages.setting.restartPanelDesc" }}',
okText: '{{ i18n "sure" }}',
cancelText: '{{ i18n "cancel" }}',
onOk: () => resolve(),
});
});
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/restartPanel");
this.loading(false);
if (msg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
location.reload();
}
}
},
async mounted() {
await this.getAllSetting();
while (true) {
await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
}
},
computed: {
templateSettings: {
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null ; },
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
},
inboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.inbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
outboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.outbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.rules = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
torrentSettings: {
get: function () {
torrentFilter = false
if(this.templateSettings != null){
this.templateSettings.routing.rules.forEach(routingRule => {
if(routingRule.hasOwnProperty("protocol")){
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
torrentFilter = true
}
}
});
}
return torrentFilter
},
set: function (newValue) {
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
if (newValue){
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"protocol\": [\"bittorrent\"],\"type\": \"field\"}"))
}
else {
newTemplateSettings.routing.rules = [];
this.templateSettings.routing.rules.forEach(routingRule => {
if (routingRule.hasOwnProperty('protocol')){
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
return;
}
}
newTemplateSettings.routing.rules.push(routingRule);
});
}
this.templateSettings = newTemplateSettings
},
},
privateIpSettings: {
get: function () {
localIpFilter = false
if(this.templateSettings != null){
this.templateSettings.routing.rules.forEach(routingRule => {
if(routingRule.hasOwnProperty("ip")){
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
localIpFilter = true
}
}
});
}
return localIpFilter
},
set: function (newValue) {
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
if (newValue){
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"ip\": [\"geoip:private\"],\"type\": \"field\"}"))
}
else {
newTemplateSettings.routing.rules = [];
this.templateSettings.routing.rules.forEach(routingRule => {
if (routingRule.hasOwnProperty('ip')){
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
return;
}
}
newTemplateSettings.routing.rules.push(routingRule);
});
}
this.templateSettings = newTemplateSettings
},
},
IRIpSettings: {
get: function () {
localIpFilter = false
if(this.templateSettings != null){
this.templateSettings.routing.rules.forEach(routingRule => {
if(routingRule.hasOwnProperty("ip")){
if (routingRule.ip[0] === "geoip:ir" && routingRule.outboundTag == "blocked"){
localIpFilter = true
}
}
});
}
return localIpFilter
},
set: function (newValue) {
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
if (newValue){
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"ip\": [\"geoip:ir\"],\"type\": \"field\"}"))
}
else {
newTemplateSettings.routing.rules = [];
this.templateSettings.routing.rules.forEach(routingRule => {
if (routingRule.hasOwnProperty('ip')){
if (routingRule.ip[0] === "geoip:ir" && routingRule.outboundTag == "blocked"){
return;
}
}
newTemplateSettings.routing.rules.push(routingRule);
});
}
this.templateSettings = newTemplateSettings
},
},
IRdomainSettings: {
get: function () {
localdomainFilter = false
if(this.templateSettings != null){
this.templateSettings.routing.rules.forEach(routingRule => {
if(routingRule.hasOwnProperty("domain")){
if ((routingRule.domain[0] === "regexp:.+.ir$" || routingRule.domain[0] === "ext:iran.dat:ir" || routingRule.domain[0] === "ext:iran.dat:other") && routingRule.outboundTag == "blocked") {
localdomainFilter = true
}
}
});
}
return localdomainFilter
},
set: function (newValue) {
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
if (newValue){
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"domain\": [\"regexp:.+.ir$\", \"ext:iran.dat:ir\", \"ext:iran.dat:other\"],\"type\": \"field\"}"))
}
else {
newTemplateSettings.routing.rules = [];
this.templateSettings.routing.rules.forEach(routingRule => {
if (routingRule.hasOwnProperty('domain')){
if ((routingRule.domain[0] === "regexp:.+.ir$" || routingRule.domain[0] === "ext:iran.dat:ir" || routingRule.domain[0] === "ext:iran.dat:other") && routingRule.outboundTag == "blocked"){
return;
}
}
newTemplateSettings.routing.rules.push(routingRule);
});
}
this.templateSettings = newTemplateSettings
},
},
}
});
</script>
</body>
</html>

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

@@ -0,0 +1,776 @@
<!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.settings.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
</a-space>
<a-tabs default-active-key="1" :class="siderDrawer.isDarkTheme ? darkClass : ''" >
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings"}}'>
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.panelUrlPath"}}' desc='{{ i18n "pages.settings.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.sessionMaxAge" }}' desc='{{ i18n "pages.settings.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.expireTimeDiff" }}' desc='{{ i18n "pages.settings.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.trafficDiff" }}' desc='{{ i18n "pages.settings.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.timeZone"}}' desc='{{ i18n "pages.settings.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title="Language" />
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
ref="selectLang"
v-model="lang"
@change="setLang(lang)"
:dropdown-class-name="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.settings.securitySettings"}}' style="padding: 20px;">
<a-tabs default-active-key="sec-1" :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-tab-pane key="sec-1" tab='{{ i18n "pages.settings.security.admin"}}'>
<a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'">
<a-form-item label='{{ i18n "pages.settings.oldUsername"}}'>
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.settings.currentPassword"}}'>
<a-input type="password" v-model="user.oldPassword" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.settings.newUsername"}}'>
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.settings.newPassword"}}'>
<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-tab-pane>
<a-tab-pane key="sec-2" tab='{{ i18n "pages.settings.security.secret"}}'>
<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.settings.security.loginSecurity" }}' description='{{ i18n "pages.settings.security.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.settings.security.secretToken" }}' description='{{ i18n "pages.settings.security.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-tabs>
</a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.settings.xrayConfiguration"}}'>
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<a-divider style="padding: 20px;">{{ i18n "pages.settings.actions"}}</a-divider>
<a-space direction="horizontal" style="padding: 0px 20px">
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
</a-space>
<a-divider style="padding: 20px;">{{ i18n "pages.settings.templates.title"}} </a-divider>
<a-tabs default-active-key="tpl-1" :class="siderDrawer.isDarkTheme ? darkClass : ''" style="padding: 20px 20px;">
<a-tab-pane key="tpl-1" tab='{{ i18n "pages.settings.templates.basicTemplate"}}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.generalConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigTorrent"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigAds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigPorn"}}' desc='{{ i18n "pages.settings.templates.xrayConfigPornDesc"}}' v-model="PornSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.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.settings.templates.countryConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.settings.templates.ipv4ConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.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.settings.templates.warpConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleWARPDesc"}}' v-model="GoogleWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigOpenAIWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOutboundsDesc"}}' v-model="outboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-tab-pane>
</a-tabs>
</a-list>
</a-tab-pane>
<a-tab-pane key="4" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.telegramNotifyTime"}}' desc='{{ i18n "pages.settings.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.tgNotifyBackup" }}' desc='{{ i18n "pages.settings.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.tgNotifyCpu" }}' desc='{{ i18n "pages.settings.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
</a-list>
</a-tab-pane>
</a-tabs>
</a-space>
</a-spin>
</a-layout-content>
</a-layout>
</a-layout>
{{template "js" .}}
{{template "component/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 = {};
window.location.replace("/logout")
}
},
async restartPanel() {
await new Promise(resolve => {
this.$confirm({
title: '{{ i18n "pages.settings.restartPanel" }}',
content: '{{ i18n "pages.settings.restartPanelDesc" }}',
okText: '{{ i18n "sure" }}',
cancelText: '{{ i18n "cancel" }}',
onOk: () => resolve(),
});
});
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/restartPanel");
this.loading(false);
if (msg.success) {
this.loading(true);
await PromiseUtil.sleep(5000);
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;
window.location.replace("/logout")
}
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>

View File

@@ -1,28 +1,29 @@
package job
import (
"x-ui/logger"
"x-ui/web/service"
"encoding/json"
"os"
"regexp"
"x-ui/database"
"x-ui/database/model"
"os"
ss "strings"
"regexp"
"encoding/json"
// "strconv"
"x-ui/logger"
"x-ui/web/service"
"x-ui/xray"
"net"
"sort"
"strings"
"time"
"net"
"github.com/go-cmd/cmd"
"sort"
"github.com/go-cmd/cmd"
)
type CheckClientIpJob struct {
xrayService service.XrayService
inboundService service.InboundService
xrayService service.XrayService
}
var job *CheckClientIpJob
var disAllowedIps []string
var disAllowedIps []string
func NewCheckClientIpJob() *CheckClientIpJob {
job = new(CheckClientIpJob)
@@ -34,94 +35,89 @@ func (j *CheckClientIpJob) Run() {
processLogFile()
// disAllowedIps = []string{"192.168.1.183","192.168.1.197"}
blockedIps := []byte(ss.Join(disAllowedIps,","))
err := os.WriteFile("./bin/blockedIPs", blockedIps, 0755)
blockedIps := []byte(strings.Join(disAllowedIps, ","))
err := os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755)
checkError(err)
}
func processLogFile() {
accessLogPath := GetAccessLogPath()
if(accessLogPath == "") {
if accessLogPath == "" {
logger.Warning("xray log not init in config.json")
return
}
data, err := os.ReadFile(accessLogPath)
data, err := os.ReadFile(accessLogPath)
InboundClientIps := make(map[string][]string)
checkError(err)
checkError(err)
// clean log
if err := os.Truncate(GetAccessLogPath(), 0); err != nil {
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) {
if len(matchesIp) > 0 {
ip := string(matchesIp)
if( ip == "127.0.0.1" || ip == "1.1.1.1") {
if ip == "127.0.0.1" || ip == "1.1.1.1" {
continue
}
matchesEmail := emailRegx.FindString(line)
if(matchesEmail == "") {
if matchesEmail == "" {
continue
}
matchesEmail = ss.Split(matchesEmail, "email: ")[1]
if(InboundClientIps[matchesEmail] != nil) {
if(contains(InboundClientIps[matchesEmail],ip)){
matchesEmail = strings.Split(matchesEmail, "email: ")[1]
if InboundClientIps[matchesEmail] != nil {
if contains(InboundClientIps[matchesEmail], ip) {
continue
}
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail],ip)
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
}else{
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail],ip)
}
} else {
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
}
}
}
disAllowedIps = []string{}
for clientEmail, ips := range InboundClientIps {
inboundClientIps,err := GetInboundClientIps(clientEmail)
sort.Sort(sort.StringSlice(ips))
if(err != nil){
addInboundClientIps(clientEmail,ips)
}else{
updateInboundClientIps(inboundClientIps,clientEmail,ips)
inboundClientIps, err := GetInboundClientIps(clientEmail)
sort.Strings(ips)
if err != nil {
addInboundClientIps(clientEmail, ips)
} else {
updateInboundClientIps(inboundClientIps, clientEmail, ips)
}
}
// check if inbound connection is more than limited ip and drop connection
LimitDevice := func() { LimitDevice() }
stop := schedule(LimitDevice, 1000 *time.Millisecond)
stop := schedule(LimitDevice, 1000*time.Millisecond)
time.Sleep(10 * time.Second)
stop <- true
}
func GetAccessLogPath() string {
config, err := os.ReadFile("bin/config.json")
checkError(err)
config, err := os.ReadFile(xray.GetConfigPath())
checkError(err)
jsonConfig := map[string]interface{}{}
err = json.Unmarshal([]byte(config), &jsonConfig)
err = json.Unmarshal([]byte(config), &jsonConfig)
checkError(err)
if(jsonConfig["log"] != nil) {
if jsonConfig["log"] != nil {
jsonLog := jsonConfig["log"].(map[string]interface{})
if(jsonLog["access"] != nil) {
if jsonLog["access"] != nil {
accessLogPath := jsonLog["access"].(string)
@@ -132,7 +128,7 @@ func GetAccessLogPath() string {
}
func checkError(e error) {
if e != nil {
if e != nil {
logger.Warning("client ip job err:", e)
}
}
@@ -182,20 +178,20 @@ func addInboundClientIps(clientEmail string, ips []string) error {
}
return nil
}
func updateInboundClientIps(inboundClientIps *model.InboundClientIps,clientEmail string,ips []string) error {
func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmail string, ips []string) error {
jsonIps, err := json.Marshal(ips)
jsonIps, err := json.Marshal(ips)
checkError(err)
inboundClientIps.ClientEmail = clientEmail
inboundClientIps.Ips = string(jsonIps)
// check inbound limitation
inbound, err := GetInboundByEmail(clientEmail)
checkError(err)
if inbound.Settings == "" {
logger.Debug("wrong data ",inbound)
logger.Debug("wrong data ", inbound)
return nil
}
@@ -203,19 +199,21 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps,clientEmail
json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"]
var disAllowedIps []string // initialize the slice
for _, client := range clients {
if client.Email == clientEmail {
limitIp := client.LimitIP
if(limitIp < len(ips) && limitIp != 0 && inbound.Enable) {
disAllowedIps = append(disAllowedIps,ips[limitIp:]...)
if limitIp < len(ips) && limitIp != 0 && inbound.Enable {
disAllowedIps = append(disAllowedIps, ips[limitIp:]...)
}
}
}
logger.Debug("disAllowedIps ",disAllowedIps)
sort.Sort(sort.StringSlice(disAllowedIps))
logger.Debug("disAllowedIps ", disAllowedIps)
sort.Strings(disAllowedIps)
db := database.GetDB()
err = db.Save(inboundClientIps).Error
@@ -224,13 +222,14 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps,clientEmail
}
return nil
}
func DisableInbound(id int) error{
func DisableInbound(id int) error {
db := database.GetDB()
result := db.Model(model.Inbound{}).
Where("id = ? and enable = ?", id, true).
Update("enable", false)
err := result.Error
logger.Warning("disable inbound with id:",id)
logger.Warning("disable inbound with id:", id)
if err == nil {
job.xrayService.SetToNeedRestart()
@@ -242,7 +241,7 @@ func DisableInbound(id int) error{
func GetInboundByEmail(clientEmail string) (*model.Inbound, error) {
db := database.GetDB()
var inbounds *model.Inbound
err := db.Model(model.Inbound{}).Where("settings LIKE ?", "%" + clientEmail + "%").Find(&inbounds).Error
err := db.Model(model.Inbound{}).Where("settings LIKE ?", "%"+clientEmail+"%").Find(&inbounds).Error
if err != nil {
return nil, err
}
@@ -250,45 +249,44 @@ func GetInboundByEmail(clientEmail string) (*model.Inbound, error) {
}
func LimitDevice() {
var destIp, destPort, srcIp, srcPort string
localIp,err := LocalIP()
checkError(err)
var destIp, destPort, srcIp, srcPort string
c := cmd.NewCmd("bash","-c","ss --tcp | grep -E '" + IPsToRegex(localIp) + "'| awk '{if($1==\"ESTAB\") print $4,$5;}'","| sort | uniq -c | sort -nr | head")
localIp, err := LocalIP()
checkError(err)
<-c.Start()
if len(c.Status().Stdout) > 0 {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
portRegx, _ := regexp.Compile(`(?:(:))([0-9]..[^.][0-9]+)`)
c := cmd.NewCmd("bash", "-c", "ss --tcp | grep -E '"+IPsToRegex(localIp)+"'| awk '{if($1==\"ESTAB\") print $4,$5;}'", "| sort | uniq -c | sort -nr | head")
for _, row := range c.Status().Stdout {
<-c.Start()
if len(c.Status().Stdout) > 0 {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
portRegx, _ := regexp.Compile(`(?:(:))([0-9]..[^.][0-9]+)`)
data := strings.Split(row," ")
for _, row := range c.Status().Stdout {
if len(data) < 2 {
continue // Skip this row if it doesn't have at least two elements
}
data := strings.Split(row, " ")
destIp = string(ipRegx.FindString(data[0]))
destPort = portRegx.FindString(data[0])
destPort = strings.Replace(destPort,":","",-1)
if len(data) < 2 {
continue // Skip this row if it doesn't have at least two elements
}
srcIp = string(ipRegx.FindString(data[1]))
srcPort = portRegx.FindString(data[1])
srcPort = strings.Replace(srcPort,":","",-1)
destIp = string(ipRegx.FindString(data[0]))
destPort = portRegx.FindString(data[0])
destPort = strings.Replace(destPort, ":", "", -1)
if contains(disAllowedIps,srcIp){
dropCmd := cmd.NewCmd("bash","-c","ss -K dport = " + srcPort)
dropCmd.Start()
srcIp = string(ipRegx.FindString(data[1]))
srcPort = portRegx.FindString(data[1])
srcPort = strings.Replace(srcPort, ":", "", -1)
logger.Debug("request droped : ",srcIp,srcPort,"to",destIp,destPort)
}
}
}
if contains(disAllowedIps, srcIp) {
dropCmd := cmd.NewCmd("bash", "-c", "ss -K dport = "+srcPort)
dropCmd.Start()
logger.Debug("request droped : ", srcIp, srcPort, "to", destIp, destPort)
}
}
}
}
func LocalIP() ([]string, error) {
// get machine ips
@@ -312,24 +310,23 @@ func LocalIP() ([]string, error) {
ip = v.IP
}
ips = append(ips,ip.String())
ips = append(ips, ip.String())
}
}
logger.Debug("System IPs : ",ips)
logger.Debug("System IPs : ", ips)
return ips, nil
}
func IPsToRegex(ips []string) (string){
func IPsToRegex(ips []string) string {
regx := ""
for _, ip := range ips {
regx += "(" + strings.Replace(ip, ".", "\\.", -1) + ")"
}
regx = "(" + strings.Replace(regx, ")(", ")|(.", -1) + ")"
regx = "(" + strings.Replace(regx, ")(", ")|(.", -1) + ")"
return regx
}

View File

@@ -1,25 +1,22 @@
{
"log": {
"loglevel": "warning",
"access": "./access.log"
"access": "./access.log",
"error": "./error.log"
},
"api": {
"services": [
"HandlerService",
"LoggerService",
"StatsService"
],
"tag": "api"
"tag": "api",
"services": ["HandlerService", "LoggerService", "StatsService"]
},
"inbounds": [
{
"tag": "api",
"listen": "127.0.0.1",
"port": 62789,
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1"
},
"tag": "api"
}
}
],
"outbounds": [
@@ -28,16 +25,16 @@
"settings": {}
},
{
"tag": "blocked",
"protocol": "blackhole",
"settings": {},
"tag": "blocked"
"settings": {}
}
],
"policy": {
"levels": {
"0": {
"statsUserUplink": true,
"statsUserDownlink": true
"statsUserDownlink": true,
"statsUserUplink": true
}
},
"system": {
@@ -49,27 +46,21 @@
"domainStrategy": "IPIfNonMatch",
"rules": [
{
"inboundTag": [
"api"
],
"outboundTag": "api",
"type": "field"
"type": "field",
"inboundTag": ["api"],
"outboundTag": "api"
},
{
"type": "field",
"outboundTag": "blocked",
"ip": [
"geoip:private"
],
"type": "field"
"ip": ["geoip:private"]
},
{
"type": "field",
"outboundTag": "blocked",
"protocol": [
"bittorrent"
],
"type": "field"
"protocol": ["bittorrent"]
}
]
},
"stats": {}
}
}

File diff suppressed because it is too large Load Diff

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"
@@ -73,7 +76,8 @@ type Release struct {
}
type ServerService struct {
xrayService XrayService
xrayService XrayService
inboundService InboundService
}
func (s *ServerService) GetStatus(lastStatus *Status) *Status {
@@ -323,6 +327,10 @@ func (s *ServerService) UpdateXray(version string) error {
if err != nil {
return err
}
err = copyZipFile("iran.dat", xray.GetIranPath())
if err != nil {
return err
}
return nil
@@ -390,3 +398,129 @@ 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")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
privateKeyLine := strings.Split(lines[0], ":")
publicKeyLine := strings.Split(lines[1], ":")
privateKey := strings.TrimSpace(privateKeyLine[1])
publicKey := strings.TrimSpace(publicKeyLine[1])
keyPair := map[string]interface{}{
"privateKey": privateKey,
"publicKey": publicKey,
}
return keyPair, nil
}

View File

@@ -2,6 +2,7 @@ package service
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"reflect"
@@ -28,6 +29,7 @@ var defaultValueMap = map[string]string{
"webKeyFile": "",
"secret": random.Seq(32),
"webBasePath": "/",
"sessionMaxAge": "0",
"expireDiff": "0",
"trafficDiff": "0",
"timeLocation": "Asia/Tehran",
@@ -37,11 +39,21 @@ var defaultValueMap = map[string]string{
"tgRunTime": "@daily",
"tgBotBackup": "false",
"tgCpu": "0",
"secretEnable": "false",
}
type SettingService struct {
}
func (s *SettingService) GetDefaultJsonConfig() (interface{}, error) {
var jsonData interface{}
err := json.Unmarshal([]byte(xrayTemplateConfig), &jsonData)
if err != nil {
return nil, err
}
return jsonData, nil
}
func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
db := database.GetDB()
settings := make([]*model.Setting, 0)
@@ -119,7 +131,13 @@ func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
func (s *SettingService) ResetSettings() error {
db := database.GetDB()
return db.Where("1 = 1").Delete(model.Setting{}).Error
err := db.Where("1 = 1").Delete(model.Setting{}).Error
if err != nil {
return err
}
return db.Model(model.User{}).
Where("1 = 1").
Update("login_secret", "").Error
}
func (s *SettingService) getSetting(key string) (*model.Setting, error) {
@@ -234,18 +252,10 @@ 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) GetPort() (int, error) {
return s.getInt("webPort")
}
@@ -266,16 +276,20 @@ 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) {
return s.getBool("secretEnable")
}
func (s *SettingService) SetSecretStatus(value bool) error {
return s.setBool("secretEnable", value)
}
func (s *SettingService) GetSecret() ([]byte, error) {

View File

@@ -8,6 +8,7 @@ import (
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/xray"
"github.com/goccy/go-json"
"gorm.io/gorm"
@@ -18,12 +19,15 @@ type SubService struct {
inboundService InboundService
}
func (s *SubService) GetSubs(subId string, host string) ([]string, error) {
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
s.address = host
var result []string
var header string
var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId)
if err != nil {
return nil, err
return nil, "", err
}
for _, inbound := range inbounds {
clients, err := s.inboundService.getClients(inbound)
@@ -37,22 +41,54 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, error) {
if client.SubID == subId {
link := s.getLink(inbound, client.Email)
result = append(result, link)
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
}
}
}
return result, nil
for index, clientTraffic := range clientTraffics {
if index == 0 {
traffic.Up = clientTraffic.Up
traffic.Down = clientTraffic.Down
traffic.Total = clientTraffic.Total
if clientTraffic.ExpiryTime > 0 {
traffic.ExpiryTime = clientTraffic.ExpiryTime
}
} else {
traffic.Up += clientTraffic.Up
traffic.Down += clientTraffic.Down
if traffic.Total == 0 || clientTraffic.Total == 0 {
traffic.Total = 0
} else {
traffic.Total += clientTraffic.Total
}
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
traffic.ExpiryTime = 0
}
}
}
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
return result, header, nil
}
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
return inbounds, nil
}
func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email string) xray.ClientTraffic {
for _, traffic := range traffics {
if traffic.Email == email {
return traffic
}
}
return xray.ClientTraffic{}
}
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
switch inbound.Protocol {
case "vmess":
@@ -61,85 +97,97 @@ 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)
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)
}
}
serverName, _ := tlsSetting["serverName"].(string)
if serverName != "" {
address = serverName
obj["add"] = serverName
}
}
@@ -151,24 +199,9 @@ 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,
}
jsonStr, _ := json.MarshalIndent(obj, "", " ")
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
}
@@ -230,6 +263,9 @@ 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)
@@ -271,21 +307,29 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
if security == "reality" {
params["security"] = "reality"
realitySettings, _ := stream["realitySettings"].(map[string]interface{})
if realitySettings != nil {
if sniValue, ok := searchKey(realitySettings, "serverNames"); ok {
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
realitySettings, _ := searchKey(realitySetting, "settings")
if realitySetting != nil {
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
sNames, _ := sniValue.([]interface{})
params["sni"], _ = sNames[0].(string)
}
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
params["pbk"], _ = pbkValue.(string)
}
if sidValue, ok := searchKey(realitySettings, "shortIds"); ok {
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
shortIds, _ := sidValue.([]interface{})
params["sid"], _ = shortIds[0].(string)
}
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
params["fp"] = fp
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
}
}
}
@@ -296,7 +340,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
if security == "xtls" {
params["security"] = "xtls"
xtlsSetting, _ := stream["XTLSSettings"].(map[string]interface{})
xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
alpns, _ := xtlsSetting["alpn"].([]interface{})
var alpn []string
for _, a := range alpns {
@@ -306,19 +350,19 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
params["alpn"] = strings.Join(alpn, ",")
}
XTLSSettings, _ := searchKey(xtlsSetting, "settings")
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
if xtlsSetting != nil {
if sniValue, ok := searchKey(XTLSSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
if fpValue, ok := searchKey(XTLSSettings, "fingerprint"); ok {
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(XTLSSettings, "allowInsecure"); ok {
if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["allowInsecure"] = "1"
}
}
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@@ -342,7 +386,8 @@ 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)
url.Fragment = remark
return url.String()
}
@@ -403,6 +448,9 @@ 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)
@@ -440,9 +488,10 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if security == "reality" {
params["security"] = "reality"
realitySettings, _ := stream["realitySettings"].(map[string]interface{})
if realitySettings != nil {
if sniValue, ok := searchKey(realitySettings, "serverNames"); ok {
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
realitySettings, _ := searchKey(realitySetting, "settings")
if realitySetting != nil {
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
sNames, _ := sniValue.([]interface{})
params["sni"], _ = sNames[0].(string)
}
@@ -454,7 +503,14 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["sid"], _ = shortIds[0].(string)
}
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
params["fp"] = fp
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
}
}
}
@@ -465,7 +521,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if security == "xtls" {
params["security"] = "xtls"
xtlsSetting, _ := stream["XTLSSettings"].(map[string]interface{})
xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
alpns, _ := xtlsSetting["alpn"].([]interface{})
var alpn []string
for _, a := range alpns {
@@ -475,19 +531,19 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["alpn"] = strings.Join(alpn, ",")
}
XTLSSettings, _ := searchKey(xtlsSetting, "settings")
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
if xtlsSetting != nil {
if sniValue, ok := searchKey(XTLSSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
if fpValue, ok := searchKey(XTLSSettings, "fingerprint"); ok {
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(XTLSSettings, "allowInsecure"); ok {
if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["allowInsecure"] = "1"
}
}
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
}
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
@@ -512,10 +568,33 @@ 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)
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)
return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, clients[clientIndex].Email)
}
func searchKey(data interface{}, key string) (interface{}, bool) {
switch val := data.(type) {
case map[string]interface{}:
@@ -544,7 +623,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

@@ -31,6 +31,7 @@ type Tgbot struct {
inboundService InboundService
settingService SettingService
serverService ServerService
xrayService XrayService
lastStatus *Status
}
@@ -148,6 +149,170 @@ func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin b
}
func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bool) {
if isAdmin {
dataArray := strings.Split(callbackQuery.Data, " ")
if len(dataArray) >= 2 && len(dataArray[1]) > 0 {
email := dataArray[1]
switch dataArray[0] {
case "client_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Client Refreshed successfully.", email))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
case "client_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("❌ %s : Operation canceled.", email))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
case "ips_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IPs Refreshed successfully.", email))
t.searchClientIps(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
case "ips_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("❌ %s : Operation canceled.", email))
t.searchClientIps(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
case "reset_traffic":
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Cancel Reset", "client_cancel "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("✅ Confirm Reset Traffic?", "reset_traffic_c "+email),
),
)
t.editMessageCallbackTgBot(callbackQuery.From.ID, callbackQuery.Message.MessageID, inlineKeyboard)
case "reset_traffic_c":
err := t.inboundService.ResetClientTrafficByEmail(email)
if err == nil {
t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Traffic reset successfully.", email))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
}
case "reset_exp":
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Cancel Reset", "client_cancel "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("♾ Unlimited", "reset_exp_c "+email+" 0"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("1 Month", "reset_exp_c "+email+" 30"),
tgbotapi.NewInlineKeyboardButtonData("2 Months", "reset_exp_c "+email+" 60"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("3 Months", "reset_exp_c "+email+" 90"),
tgbotapi.NewInlineKeyboardButtonData("6 Months", "reset_exp_c "+email+" 180"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("9 Months", "reset_exp_c "+email+" 270"),
tgbotapi.NewInlineKeyboardButtonData("12 Months", "reset_exp_c "+email+" 360"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("10 Days", "reset_exp_c "+email+" 10"),
tgbotapi.NewInlineKeyboardButtonData("20 Days", "reset_exp_c "+email+" 20"),
),
)
t.editMessageCallbackTgBot(callbackQuery.From.ID, callbackQuery.Message.MessageID, inlineKeyboard)
case "reset_exp_c":
if len(dataArray) == 3 {
days, err := strconv.Atoi(dataArray[2])
if err == nil {
var date int64 = 0
if days > 0 {
date = int64(-(days * 24 * 60 * 60000))
}
err := t.inboundService.ResetClientExpiryTimeByEmail(email, date)
if err == nil {
t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Expire days reset successfully.", email))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
return
}
}
}
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
case "ip_limit":
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Cancel IP Limit", "client_cancel "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("♾ Unlimited", "ip_limit_c "+email+" 0"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("1", "ip_limit_c "+email+" 1"),
tgbotapi.NewInlineKeyboardButtonData("2", "ip_limit_c "+email+" 2"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("3", "ip_limit_c "+email+" 3"),
tgbotapi.NewInlineKeyboardButtonData("4", "ip_limit_c "+email+" 4"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("5", "ip_limit_c "+email+" 5"),
tgbotapi.NewInlineKeyboardButtonData("6", "ip_limit_c "+email+" 6"),
tgbotapi.NewInlineKeyboardButtonData("7", "ip_limit_c "+email+" 7"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("8", "ip_limit_c "+email+" 8"),
tgbotapi.NewInlineKeyboardButtonData("9", "ip_limit_c "+email+" 9"),
tgbotapi.NewInlineKeyboardButtonData("10", "ip_limit_c "+email+" 10"),
),
)
t.editMessageCallbackTgBot(callbackQuery.From.ID, callbackQuery.Message.MessageID, inlineKeyboard)
case "ip_limit_c":
if len(dataArray) == 3 {
count, err := strconv.Atoi(dataArray[2])
if err == nil {
err := t.inboundService.ResetClientIpLimitByEmail(email, count)
if err == nil {
t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IP limit %d saved successfully.", email, count))
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
return
}
}
}
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
case "clear_ips":
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Cancel", "ips_cancel "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("✅ Confirm Clear IPs?", "clear_ips_c "+email),
),
)
t.editMessageCallbackTgBot(callbackQuery.From.ID, callbackQuery.Message.MessageID, inlineKeyboard)
case "clear_ips_c":
err := t.inboundService.ClearClientIps(email)
if err == nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : IPs cleared successfully.", email))
t.searchClientIps(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
}
case "ip_log":
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Get IP Log.", email))
t.searchClientIps(callbackQuery.From.ID, email)
case "toggle_enable":
enabled, err := t.inboundService.ToggleClientEnableByEmail(email)
if err == nil {
t.xrayService.SetToNeedRestart()
if enabled {
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Enabled successfully.", email))
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, fmt.Sprintf("✅ %s : Disabled successfully.", email))
}
t.searchClient(callbackQuery.From.ID, email, callbackQuery.Message.MessageID)
} else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, "❗ Error in Operation.")
}
}
return
}
}
// Respond to the callback query, telling Telegram to show the user
// a message with the data received.
callback := tgbotapi.NewCallback(callbackQuery.ID, callbackQuery.Data)
@@ -165,9 +330,9 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
case "get_backup":
t.sendBackup(callbackQuery.From.ID)
case "client_traffic":
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName)
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName, strconv.FormatInt(callbackQuery.From.ID, 10))
case "client_commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Passowrd]</code>\r\n \r\nUse UID for vmess/vless and Password for Trojan.")
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Password]</code>\r\n \r\nUse UID for vmess/vless and Password for Trojan.")
case "commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "Search for a client email:\r\n<code>/usage email</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [remark]</code>")
}
@@ -215,7 +380,7 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
}
}
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string, inlineKeyboard ...tgbotapi.InlineKeyboardMarkup) {
var allMessages []string
limit := 2000
// paging message if it is big
@@ -236,6 +401,9 @@ func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
for _, message := range allMessages {
info := tgbotapi.NewMessage(tgid, message)
info.ParseMode = "HTML"
if len(inlineKeyboard) > 0 {
info.ReplyMarkup = inlineKeyboard[0]
}
_, err := bot.Send(info)
if err != nil {
logger.Warning("Error sending telegram message :", err)
@@ -362,13 +530,8 @@ func (t *Tgbot) getInboundUsages() string {
return info
}
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
if len(tgUserName) == 0 {
msg := "Your configuration is not found!\nYou should configure your telegram username and ask Admin to add it to your configuration."
t.SendMsgToTgbot(chatId, msg)
return
}
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string) {
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
@@ -376,7 +539,21 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
return
}
if len(traffics) == 0 {
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>"
if len(tgUserName) == 0 {
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram user id in your configuration(s).\n\nYour user id: <b>" + tgUserID + "</b>"
t.SendMsgToTgbot(chatId, msg)
return
}
traffics, err = t.inboundService.GetClientTrafficTgBot(tgUserName)
}
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if len(traffics) == 0 {
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username or user id in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>\n\nYour user id: <b>" + tgUserID + "</b>"
t.SendMsgToTgbot(chatId, msg)
return
}
@@ -403,38 +580,79 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
t.SendAnswer(chatId, "Please choose:", false)
}
func (t *Tgbot) searchClient(chatId int64, email string) {
traffics, err := t.inboundService.GetClientTrafficByEmail(email)
func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
ips, err := t.inboundService.GetInboundClientIps(email)
if err != nil || len(ips) == 0 {
ips = "No IP Record"
}
output := fmt.Sprintf("📧 Email: %s\r\n🔢 IPs: \r\n%s\r\n", email, ips)
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔄 Refresh", "ips_refresh "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ Clear IPs", "clear_ips "+email),
),
)
if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
} else {
t.SendMsgToTgbot(chatId, output, inlineKeyboard)
}
}
func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if len(traffics) == 0 {
if traffic == nil {
msg := "No result!"
t.SendMsgToTgbot(chatId, msg)
return
}
for _, traffic := range traffics {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
var inlineKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔄 Refresh", "client_refresh "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("📈 Reset Traffic", "reset_traffic "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("📅 Reset Expire Days", "reset_exp "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔢 IP Log", "ip_log "+email),
tgbotapi.NewInlineKeyboardButtonData("🔢 IP Limit", "ip_limit "+email),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔘 Enable / Disable", "toggle_enable "+email),
),
)
if len(messageID) > 0 {
t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
} else {
t.SendMsgToTgbot(chatId, output, inlineKeyboard)
}
}
@@ -610,3 +828,28 @@ func (t *Tgbot) sendBackup(chatId int64) {
logger.Warning("Error in uploading config.json: ", err)
}
}
func (t *Tgbot) sendCallbackAnswerTgBot(id string, message string) {
callback := tgbotapi.NewCallback(id, message)
if _, err := bot.Request(callback); err != nil {
logger.Warning(err)
}
}
func (t *Tgbot) editMessageCallbackTgBot(chatId int64, messageID int, inlineKeyboard tgbotapi.InlineKeyboardMarkup) {
edit := tgbotapi.NewEditMessageReplyMarkup(chatId, messageID, inlineKeyboard)
if _, err := bot.Request(edit); err != nil {
logger.Warning(err)
}
}
func (t *Tgbot) editMessageTgBot(chatId int64, messageID int, text string, inlineKeyboard ...tgbotapi.InlineKeyboardMarkup) {
edit := tgbotapi.NewEditMessageText(chatId, messageID, text)
edit.ParseMode = "HTML"
if len(inlineKeyboard) > 0 {
edit.ReplyMarkup = &inlineKeyboard[0]
}
if _, err := bot.Request(edit); err != nil {
logger.Warning(err)
}
}

View File

@@ -25,12 +25,12 @@ func (s *UserService) GetFirstUser() (*model.User, error) {
return user, nil
}
func (s *UserService) CheckUser(username string, password string) *model.User {
func (s *UserService) CheckUser(username string, password string, secret string) *model.User {
db := database.GetDB()
user := &model.User{}
err := db.Model(model.User{}).
Where("username = ? and password = ?", username, password).
Where("username = ? and password = ? and login_secret = ?", username, password, secret).
First(user).
Error
if err == gorm.ErrRecordNotFound {
@@ -50,6 +50,35 @@ func (s *UserService) UpdateUser(id int, username string, password string) error
Error
}
func (s *UserService) UpdateUserSecret(id int, secret string) error {
db := database.GetDB()
return db.Model(model.User{}).
Where("id = ?", id).
Update("login_secret", secret).
Error
}
func (s *UserService) RemoveUserSecret() error {
db := database.GetDB()
return db.Model(model.User{}).
Where("1 = 1").
Update("login_secret", "").
Error
}
func (s *UserService) GetUserSecret(id int) *model.User {
db := database.GetDB()
user := &model.User{}
err := db.Model(model.User{}).
Where("id = ?", id).
First(user).
Error
if err == gorm.ErrRecordNotFound {
return nil
}
return user
}
func (s *UserService) UpdateFirstUser(username string, password string) error {
if username == "" {
return errors.New("username can not be empty")

View File

@@ -118,12 +118,15 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "alterId" {
delete(c, key)
}
if c["flow"] == "xtls-rprx-vision-udp443" {
c["flow"] = "xtls-rprx-vision"
}
}
final_clients = append(final_clients, interface{}(c))
}
settings["clients"] = final_clients
modifiedSettings, err := json.Marshal(settings)
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}

View File

@@ -2,9 +2,10 @@ package session
import (
"encoding/gob"
"x-ui/database/model"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"x-ui/database/model"
)
const (
@@ -21,6 +22,15 @@ func SetLoginUser(c *gin.Context, user *model.User) error {
return s.Save()
}
func SetMaxAge(c *gin.Context, maxAge int) error {
s := sessions.Default(c)
s.Options(sessions.Options{
Path: "/",
MaxAge: maxAge,
})
return s.Save()
}
func GetLoginUser(c *gin.Context) *model.User {
s := sessions.Default(c)
obj := s.Get(loginUser)

View File

@@ -22,11 +22,11 @@
"unlimited" = "Unlimited"
"none" = "None"
"qrCode" = "QR Code"
"info" = "More information"
"info" = "More Information"
"edit" = "Edit"
"delete" = "Delete"
"reset" = "Reset"
"copySuccess" = "Copy successfully"
"copySuccess" = "Copied successfully"
"sure" = "Sure"
"encryption" = "Encryption"
"transmission" = "Transmission"
@@ -40,31 +40,32 @@
"depletingSoon" = "Depleting soon"
"domainName" = "Domain name"
"additional" = "Alter"
"monitor" = "Listen IP"
"certificate" = "Certificat"
"monitor" = "Listening IP"
"certificate" = "Certificate"
"fail" = "Fail"
"success" = " Success"
"success" = "Success"
"getVersion" = "Get version"
"install" = "Install"
"clients" = "Clients"
"usage" = "Usage"
"secretToken" = "Secret Token"
[menu]
"dashboard" = "System Status"
"inbounds" = "Inbounds"
"setting" = "Panel Setting"
"logout" = "LogOut"
"settings" = "Panel Settings"
"logout" = "Logout"
"link" = "Other"
[pages.login]
"title" = "Login"
"loginAgain" = "The login time limit has expired, please log in again"
"loginAgain" = "The login time limit has expired. Please log in again."
[pages.login.toasts]
"invalidFormData" = "Input Data Format Is Invalid"
"emptyUsername" = "Please Enter Username"
"emptyPassword" = "Please Enter Password"
"wrongUsernameOrPassword" = "Invalid username or password"
"invalidFormData" = "Input data format is invalid."
"emptyUsername" = "Please enter username."
"emptyPassword" = "Please enter password."
"wrongUsernameOrPassword" = "Invalid username or password."
"successLogin" = "Login"
[pages.index]
@@ -75,27 +76,34 @@
"stopXray" = "Stop"
"restartXray" = "Restart"
"xraySwitch" = "Switch Version"
"xraySwitchClick" = "Click on the version you want to switch"
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
"xraySwitchClick" = "Choose the version you want to switch to."
"xraySwitchClickDesk" = "Choose wisely, as older versions may not be compatible with current configurations."
"operationHours" = "Operation Hours"
"operationHoursDesc" = "The running time of the system since it was started"
"operationHoursDesc" = "System uptime: time since startup."
"systemLoad" = "System Load"
"connectionCount" = "Connection Count"
"connectionCountDesc" = "The total number of connections for all network cards"
"upSpeed" = "Total upload speed for all network cards"
"downSpeed" = "Total download speed for all network cards"
"totalSent" = "Total upload traffic of all network cards since system startup"
"totalReceive" = "Total download traffic of all network cards since system startup"
"xraySwitchVersionDialog" = "Switch xray version"
"xraySwitchVersionDialogDesc" = "Whether to switch the xray version to"
"dontRefreshh" = "Installation is in progress, please do not refresh this page"
"connectionCount" = "Number of Connections"
"connectionCountDesc" = "Total connections across all network cards."
"upSpeed" = "Total upload speed for all network cards."
"downSpeed" = "Total download speed for all network cards."
"totalSent" = "Total upload traffic of all network cards since system startup."
"totalReceive" = "Total download data across all network cards since system startup."
"xraySwitchVersionDialog" = "Switch Xray Version"
"xraySwitchVersionDialogDesc" = "Are you sure you want to switch the Xray version to"
"dontRefresh" = "Installation is in progress, please do not refresh this page."
"logs" = "Logs"
"config" = "Config"
"backup" = "Backup & Restore"
"backupTitle" = "Backup & Restore Database"
"backupDescription" = "Remember to backup before importing a new database."
"exportDatabase" = "Download Database"
"importDatabase" = "Upload Database"
[pages.inbounds]
"title" = "Inbounds"
"totalDownUp" = "Total uploads/downloads"
"totalUsage" = "Total usage"
"inboundCount" = "Number of inbound"
"operate" = "Actions"
"totalDownUp" = "Total Uploads/Downloads"
"totalUsage" = "Total Usage"
"inboundCount" = "Number of Inbounds"
"operate" = "Menu"
"enable" = "Enable"
"remark" = "Remark"
"protocol" = "Protocol"
@@ -103,70 +111,79 @@
"traffic" = "Traffic"
"details" = "Details"
"transportConfig" = "Transport"
"expireDate" = "Expire date"
"resetTraffic" = "Reset traffic"
"expireDate" = "Expire Date"
"resetTraffic" = "Reset Traffic"
"addInbound" = "Add Inbound"
"addTo" = "Add To"
"revise" = "Revise"
"modifyInbound" = "Modify InBound"
"generalActions" = "General Actions"
"create" = "Create"
"update" = "Update"
"modifyInbound" = "Modify Inbound"
"deleteInbound" = "Delete Inbound"
"deleteInboundContent" = "Are you sure you want to delete inbound?"
"resetTrafficContent" = "Are you sure you want to reset traffic?"
"deleteInboundContent" = "Confirm deletion of inbound?"
"resetTrafficContent" = "Confirm traffic reset?"
"copyLink" = "Copy Link"
"address" = "Address"
"network" = "Network"
"destinationPort" = "Destination port"
"targetAddress" = "Target address"
"disableInsecureEncryption" = "Disable insecure encryption"
"destinationPort" = "Destination Port"
"targetAddress" = "Target Address"
"disableInsecureEncryption" = "Disable Insecure Encryption"
"monitorDesc" = "Leave blank by default"
"meansNoLimit" = "Means no limit"
"totalFlow" = "Total flow"
"leaveBlankToNeverExpire" = "Leave blank to never expire"
"noRecommendKeepDefault" = "There are no special requirements to keep the default"
"certificatePath" = "Certificate file path"
"certificateContent" = "Certificate file content"
"publicKeyPath" = "Public key path"
"publicKeyContent" = "Public key content"
"keyPath" = "Private Key path"
"keyContent" = "Private Key content"
"meansNoLimit" = "Means No Limit"
"totalFlow" = "Total Flow"
"leaveBlankToNeverExpire" = "Leave Blank to Never Expire"
"noRecommendKeepDefault" = "No special requirements to maintain default settings"
"certificatePath" = "Certificate File Path"
"certificateContent" = "Certificate File Content"
"publicKeyPath" = "Public Key Path"
"publicKeyContent" = "Public Key Content"
"keyPath" = "Private Key Path"
"keyContent" = "Private Key Content"
"clickOnQRcode" = "Click on QR Code to Copy"
"client" = "Client"
"export" = "Export links"
"Clone" = "Clone"
"cloneInbound" = "Create"
"cloneInboundContent" = "All items of this inbound except Port, Listening IP, Clients will be applied to the clone"
"cloneInboundOk" = "Creating a clone from"
"export" = "Export Links"
"clone" = "Clone"
"cloneInbound" = "Clone"
"cloneInboundContent" = "All settings of this inbound, except for Port, Listening IP, and Clients, will be applied to the clone."
"cloneInboundOk" = "Clone"
"resetAllTraffic" = "Reset All Inbounds Traffic"
"resetAllTrafficTitle" = "Reset all inbounds traffic"
"resetAllTrafficContent" = "Are you sure to reset all inbounds traffic ?"
"resetAllTrafficContent" = "Are you sure you want to reset all inbounds traffic?"
"resetAllTrafficOkText" = "Confirm"
"resetAllTrafficCancelText" = "Cancel"
"IPLimit" = "IP Limit"
"IPLimitDesc" = "disable inbound if more than entered count (0 for disable limit ip)"
"resetAllClientTraffics" = "Reset Clients Traffic"
"IPLimitDesc" = "Disable inbound if the count exceeds the entered value (enter 0 to disable IP limit)."
"resetInboundClientTraffics" = "Reset Clients Traffic"
"resetInboundClientTrafficTitle" = "Reset all client traffic"
"resetInboundClientTrafficContent" = "Are you sure you want to reset all traffic for this inbound's clients?"
"resetAllClientTraffics" = "Reset All Clients Traffic"
"resetAllClientTrafficTitle" = "Reset all clients traffic"
"resetAllClientTrafficContent" = "Are you sure to reset all traffics of this inbound's clients ?"
"resetAllClientTrafficContent" = "Are you sure you want to reset all traffics for all clients?"
"delDepletedClients" = "Delete Depleted Clients"
"delDepletedClientsTitle" = "Delete depleted clients"
"delDepletedClientsContent" = "Are you sure you want to delete all depleted clients?"
"Email" = "Email"
"EmailDesc" = "The Email Must Be Completely Unique"
"EmailDesc" = "Please provide a unique email address."
"IPLimitlog" = "IP Log"
"IPLimitlogDesc" = "IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)"
"IPLimitlogDesc" = "IPs history log (before enabling inbound after it has been disabled by IP limit, you should clear the log)."
"IPLimitlogclear" = "Clear The Log"
"setDefaultCert" = "Set cert from panel"
"XTLSdec" = "Xray core needs to be 1.7.5 and below"
"Realitydec" = "Xray core needs to be 1.8.0 and above"
"xtlsDesc" = "Xray core needs to be 1.7.5"
"realityDesc" = "Xray core needs to be 1.8.0 or higher."
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot )"
"subscriptionDesc" = "you can find your sub link on Details, also ou can use the same name for several configurations"
[pages.client]
"add" = "Add client"
"edit" = "Edit client"
"submitAdd" = "Add client"
"add" = "Add Client"
"edit" = "Edit Client"
"submitAdd" = "Add Client"
"submitEdit" = "Save changes"
"clientCount" = "Number of clients"
"bulk" = "Add bulk"
"clientCount" = "Number of Clients"
"bulk" = "Add Bulk"
"method" = "Method"
"first" = "First"
"last" = "Last"
"prefix" = "Prefix"
"postfix" = "postfix"
"postfix" = "Postfix"
"delayedStart" = "Start after first use"
"expireDays" = "Expire days"
"days" = "day(s)"
@@ -191,70 +208,119 @@
[pages.inbounds.stream.quic]
"encryption" = "Encryption"
[pages.setting]
"title" = "Setting"
[pages.settings]
"title" = "Settings"
"save" = "Save"
"restartPanel" = "Restart Panel"
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information"
"panelConfig" = "Panel Configuration"
"userSetting" = "User Setting"
"restartPanel" = "Restart Panel "
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log information on the server."
"actions" = "Actions"
"resetDefaultConfig" = "Reset to Default Configuration"
"panelSettings" = "Panel Settings"
"securitySettings" = "Security Settings"
"xrayConfiguration" = "Xray Configuration"
"TGReminder" = "TG Reminder Related Settings"
"otherSetting" = "Other Setting"
"panelListeningIP" = "Panel listening IP"
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs, restart the panel to take effect"
"TGBotSettings" = "Telegram Bot Settings"
"panelListeningIP" = "Panel Listening IP"
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs. Restart the panel to apply changes."
"panelPort" = "Panel Port"
"panelPortDesc" = "Restart the panel to take effect"
"publicKeyPath" = "Panel certificate public key file path"
"publicKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
"privateKeyPath" = "Panel certificate private key file path"
"privateKeyPathDesc" = "Fill in an absolute path starting with '/', restart the panel to take effect"
"panelUrlPath" = "panel url root path"
"panelUrlPathDesc" = "Must start with '/' and end with '/', restart the panel to take effect"
"panelPortDesc" = "Restart the panel to apply changes."
"publicKeyPath" = "Panel Certificate Public Key File Path"
"publicKeyPathDesc" = "Fill in an absolute path starting with '/'. Restart the panel to apply changes."
"privateKeyPath" = "Panel Certificate Private Key File Path"
"privateKeyPathDesc" = "Fill in an absolute path starting with '/'. Restart the panel to apply changes."
"panelUrlPath" = "Panel URL Root Path"
"panelUrlPathDesc" = "Must start with '/' and end with '/'. Restart the panel to apply changes."
"oldUsername" = "Current Username"
"currentPassword" = "Current Password"
"newUsername" = "New Username"
"newPassword" = "New Password"
"advancedTemplate" = "Advanced template parts"
"completeTemplate" = "Complete template of Xray configuration"
"xrayConfigTemplate" = "Xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect."
"xrayConfigTorrent" = "Ban bittorrent usage"
"xrayConfigTorrentDesc" = "Change the configuration temlate to avoid using bittorrent by users, restart the panel to take effect"
"xrayConfigPrivateIp" = "Ban private IP ranges to connect"
"xrayConfigPrivateIpDesc" = "Change the configuration temlate to avoid connecting with private IP ranges, restart the panel to take effect"
"xrayConfigIRIp" = "Ban Iran IP ranges to connect"
"xrayConfigIRIpDesc" = "Change the configuration temlate to avoid connecting with Iran IP ranges, restart the panel to take effect"
"xrayConfigIRdomain" = "Ban IR domains to connect"
"xrayConfigIRdomainDesc" = "Change the configuration temlate to avoid connecting with IR domains, restart the panel to take effect"
"xrayConfigInbounds" = "Configuration of Inbounds"
"xrayConfigInboundsDesc" = "Change the configuration temlate to accept special clients, restart the panel to take effect"
"xrayConfigOutbounds" = "Configuration of Outbounds"
"xrayConfigOutboundsDesc" = "Change the configuration temlate to define outgoing ways for this server, restart the panel to take effect"
"xrayConfigRoutings" = "Configuration of Routing rules"
"xrayConfigRoutingsDesc" = "Change the configuration temlate to define Routing rules for this server, restart the panel to take effect"
"telegramBotEnable" = "Enable telegram bot"
"telegramBotEnableDesc" = "Restart the panel to take effect"
"telegramBotEnable" = "Enable Telegram bot"
"telegramBotEnableDesc" = "Restart the panel to take effect."
"telegramToken" = "Telegram Token"
"telegramTokenDesc" = "Restart the panel to take effect"
"telegramChatId" = "Telegram Admin ChatIds"
"telegramChatIdDesc" = "Multi chatIDs separated by comma. Restart the panel to take effect"
"telegramTokenDesc" = "Restart the panel to take effect."
"telegramChatId" = "Telegram Admin Chat IDs"
"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot to get your Chat IDs. Restart the panel to apply changes."
"telegramNotifyTime" = "Telegram bot notification time"
"telegramNotifyTimeDesc" = "Using Crontab timing format. Restart the panel to take effect"
"tgNotifyBackup" = "Database backup"
"tgNotifyBackupDesc" = "Sending database backup file with report notification. Restart the panel to take effect"
"expireTimeDiff" = "Exhaustion time threshold"
"expireTimeDiffDesc" = "Detect exhaustion before expiration (unit:day)"
"trafficDiff" = "Exhaustion traffic threshold"
"trafficDiffDesc" = "Detect exhaustion before finishing traffic (unit:GB)"
"telegramNotifyTimeDesc" = "Use Crontab timing format. Restart the panel to apply changes."
"tgNotifyBackup" = "Database Backup"
"tgNotifyBackupDesc" = "Include database backup file with report notification. Restart the panel to apply changes."
"sessionMaxAge" = "Session maximum age"
"sessionMaxAgeDesc" = "The duration of a login session (unit: minute)"
"expireTimeDiff" = "Expiration threshold for notification"
"expireTimeDiffDesc" = "Get notified about account expiration before the threshold (unit: day)"
"trafficDiff" = "Traffic threshold for notification"
"trafficDiffDesc" = "Get notified about traffic exhaustion before reaching the threshold (unit: GB)"
"tgNotifyCpu" = "CPU percentage alert threshold"
"tgNotifyCpuDesc" = "This telegram bot will send you a notification if CPU usage is more than this percentage (unit:%)"
"timeZonee" = "Time Zone"
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"
"tgNotifyCpuDesc" = "Receive notification if CPU usage exceeds this threshold (unit: %)"
"timeZone" = "Time zone"
"timeZoneDesc" = "Scheduled tasks run according to the time in this time zone. Restart the panel to apply changes."
[pages.setting.toasts]
"modifySetting" = "Modify setting"
"getSetting" = "Get setting"
"modifyUser" = "Modify user"
"originalUserPassIncorrect" = "The original user name or original password is incorrect"
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"
[pages.settings.templates]
"title" = "Templates"
"basicTemplate" = "Basic Template"
"advancedTemplate" = "Advanced Template"
"completeTemplate" = "Complete Template"
"generalConfigs" = "General Configs"
"generalConfigsDesc" = "These options will prevent users from connecting to specific protocols and websites."
"countryConfigs" = "Country Configs"
"countryConfigsDesc" = "These options will prevent users from connecting to specific country domains."
"ipv4Configs" = "IPv4 Configs"
"ipv4ConfigsDesc" = "These options will route to target domains only via IPv4."
"warpConfigs" = "WARP Configs"
"warpConfigsDesc" = "Caution: Before using these options, install WARP in socks5 proxy mode on your server by following the steps on the panel's GitHub. WARP will route traffic to websites through Cloudflare servers."
"xrayConfigTemplate" = "Xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final Xray configuration file based on this template. Restart the panel to apply changes."
"xrayConfigTorrent" = "Ban BitTorrent Usage"
"xrayConfigTorrentDesc" = "Change the configuration template to avoid using BitTorrent by users. Restart the panel to apply changes."
"xrayConfigPrivateIp" = "Ban Private IP Ranges to Connect"
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting with private IP ranges. Restart the panel to apply changes."
"xrayConfigAds" = "Block Ads"
"xrayConfigAdsDesc" = "Change the configuration template to block ads. Restart the panel to apply changes."
"xrayConfigPorn" = "Block Porn Websites"
"xrayConfigPornDesc" = "Change the configuration template to avoid connecting to porn websites. Restart the panel to apply changes."
"xrayConfigIRIp" = "Disable connection to Iran IP ranges"
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting with Iran IP ranges. Restart the panel to apply changes."
"xrayConfigIRDomain" = "Disable connection to Iran domains"
"xrayConfigIRDomainDesc" = "Change the configuration template to avoid connecting with Iran domains. Restart the panel to apply changes."
"xrayConfigChinaIp" = "Disable connection to China IP ranges"
"xrayConfigChinaIpDesc" = "Change the configuration template to avoid connecting with China IP ranges. Restart the panel to apply changes."
"xrayConfigChinaDomain" = "Disable connection to China domains"
"xrayConfigChinaDomainDesc" = "Change the configuration template to avoid connecting with China domains. Restart the panel to apply changes."
"xrayConfigRussiaIp" = "Disable connection to Russia IP ranges"
"xrayConfigRussiaIpDesc" = "Change the configuration template to avoid connecting with Russia IP ranges. Restart the panel to apply changes."
"xrayConfigRussiaDomain" = "Disable connection to Russia domains"
"xrayConfigRussiaDomainDesc" = "Change the configuration template to avoid connecting with Russia domains. Restart the panel to apply changes."
"xrayConfigGoogleIPv4" = "Use IPv4 for Google"
"xrayConfigGoogleIPv4Desc" = "Add routing for Google to connect with IPv4. Restart the panel to apply changes."
"xrayConfigNetflixIPv4" = "Use IPv4 for Netflix"
"xrayConfigNetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4. Restart the panel to apply changes."
"xrayConfigGoogleWARP" = "Route Google through WARP."
"xrayConfigGoogleWARPDesc" = "Add routing for Google via WARP. Restart the panel to apply changes."
"xrayConfigOpenAIWARP" = "Route OpenAI (ChatGPT) through WARP."
"xrayConfigOpenAIWARPDesc" = "Add routing for OpenAI (ChatGPT) via WARP. Restart the panel to apply changes."
"xrayConfigNetflixWARP" = "Route Netflix through WARP."
"xrayConfigNetflixWARPDesc" = "Add routing for Netflix via WARP. Restart the panel to apply changes."
"xrayConfigSpotifyWARP" = "Route Spotify through WARP."
"xrayConfigSpotifyWARPDesc" = "Add routing for Spotify via WARP. Restart the panel to apply changes."
"xrayConfigIRWARP" = "Route Iran domains through WARP."
"xrayConfigIRWARPDesc" = "Add routing for Iran domains via WARP. Restart the panel to apply changes."
"xrayConfigInbounds" = "Configuration of Inbounds"
"xrayConfigInboundsDesc" = "Change the configuration template to accept specific clients. Restart the panel to apply changes."
"xrayConfigOutbounds" = "Configuration of Outbounds"
"xrayConfigOutboundsDesc" = "Change the configuration template to define outgoing ways for this server. Restart the panel to apply changes."
"xrayConfigRoutings" = "Configuration of routing rules."
"xrayConfigRoutingsDesc" = "Change the configuration template to define routing rules for this server. Restart the panel to apply changes."
[pages.settings.security]
"admin" = "Admin"
"secret" = "Secret Token"
"loginSecurity" = "Login security"
"loginSecurityDesc" = "Enable additional user login security step"
"secretToken" = "Secret Token"
"secretTokenDesc" = "Please copy and securely store this token in a safe place. This token is required for login and cannot be recovered from the x-ui command tool."
[pages.settings.toasts]
"modifySettings" = "Modify Settings "
"getSettings" = "Get Settings "
"modifyUser" = "Modify User "
"originalUserPassIncorrect" = "Incorrect original username or password"
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"

View File

@@ -48,11 +48,12 @@
"install" = "نصب"
"clients" = "کاربران"
"usage" = "استفاده"
"secretToken" = "توکن امنیتی"
[menu]
"dashboard" = "وضعیت سیستم"
"inbounds" = "سرویس ها"
"setting" = "تنظیمات پنل"
"inbounds" = "سرویس ها"
"settings" = "تنظیمات پنل"
"logout" = "خروج"
"link" = "دیگر"
@@ -88,14 +89,21 @@
"totalReceive" = "جمع کل ترافیک دانلود مصرفی"
"xraySwitchVersionDialog" = "تغییر ورژن"
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید "
"dontRefresh" = "در حال نصب ، لطفا رفرش نکنید "
"logs" = "گزارش ها"
"config" = "تنظیمات"
"backup" = "پشتیبان گیری و بازیابی"
"backupTitle" = "پشتیبان گیری و بازیابی دیتابیس"
"backupDescription" = "به یاد داشته باشید که قبل از وارد کردن یک دیتابیس جدید، نسخه پشتیبان تهیه کنید."
"exportDatabase" = "دانلود دیتابیس"
"importDatabase" = "آپلود دیتابیس"
[pages.inbounds]
"title" = "کاربران"
"totalDownUp" = "جمع آپلود/دانلود"
"totalUsage" = "جمع کل"
"inboundCount" = "تعداد سرویس ها"
"operate" = "عملیات"
"operate" = "فهرست"
"enable" = "فعال"
"remark" = "نام"
"protocol" = "پروتکل"
@@ -106,8 +114,9 @@
"expireDate" = "تاریخ انقضا"
"resetTraffic" = "ریست ترافیک"
"addInbound" = "اضافه کردن سرویس"
"addTo" = "اضافه کردن"
"revise" = "ویرایش"
"generalActions" = "عملیات کلی"
"create" = "اضافه کردن"
"update" = "ویرایش"
"modifyInbound" = "ویرایش سرویس"
"deleteInbound" = "حذف سرویس"
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
@@ -132,16 +141,22 @@
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
"client" = "کاربر"
"export" = "استخراج لینکها"
"Clone" = "شبیه سازی"
"clone" = "شبیه سازی"
"cloneInbound" = "ایجاد"
"cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد"
"cloneInboundOk" = "ساختن شبیه ساز"
"resetAllTraffic" = "ریست ترافیک کل سرویس ها"
"resetAllTrafficTitle" = "ریست ترافیک کل سرویس ها"
"resetAllTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک سرویس ها را ریست کنید؟"
"resetInboundClientTraffics" = "ریست ترافیک کاربران"
"resetInboundClientTrafficTitle" = "ریست ترافیک کل کاربران"
"resetInboundClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
"resetAllClientTraffics" = "ریست ترافیک کاربران"
"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران"
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران را ریست کنید؟"
"delDepletedClients" = "حذف کاربران منقضی"
"delDepletedClientsTitle" = "حذف کاربران منقضی"
"delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟"
"IPLimit" = "محدودیت ای پی"
"IPLimitDesc" = "غیرفعال کردن ورودی در صورت بیش از تعداد وارد شده (0 برای غیرفعال کردن محدودیت ای پی )"
"Email" = "ایمیل"
@@ -150,8 +165,10 @@
"IPLimitlogDesc" = "گزارش سابقه ای پی (قبل از فعال کردن ورودی پس از غیرفعال شدن توسط محدودیت ای پی، باید گزارش را پاک کنید)"
"IPLimitlogclear" = "پاک کردن گزارش ها"
"setDefaultCert" = "استفاده از گواهی پنل"
"XTLSdec" = "هسته Xray باید 1.7.5 و کمتر باشد"
"Realitydec" = "هسته Xray باید 1.8.0 و بالاتر باشد"
"xtlsDesc" = "هسته Xray باید 1.7.5 باشد"
"realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)"
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
[pages.client]
"add" = "کاربر جدید"
@@ -189,16 +206,17 @@
[pages.inbounds.stream.quic]
"encryption" = "رمزنگاری"
[pages.setting]
[pages.settings]
"title" = "تنظیمات"
"save" = "ذخیره"
"restartPanel" = "ریستارت پنل"
"restartPanelDesc" = "آیا مطمئن هستید که می خواهید پنل را دوباره راه اندازی کنید؟ برای راه اندازی مجدد روی OK کلیک کنید. اگر بعد از 3 ثانیه نمی توانید به پنل دسترسی پیدا کنید، لطفاً برای مشاهده اطلاعات گزارش پانل به سرور برگردید"
"panelConfig" = "تنظیمات پنل"
"userSetting" = "تنظیمات مدیر"
"actions" = "عملیات ها"
"resetDefaultConfig" = "برگشت به تنظیمات پیشفرض"
"panelSettings" = "تنظیمات پنل"
"securitySettings" = "تنظیمات امنیتی"
"xrayConfiguration" = "تنظیمات Xray"
"TGReminder" = "تنظیمات ربات تلگرام"
"otherSetting" = "دیگر تنظیمات"
"TGBotSettings" = "تنظیمات ربات تلگرام"
"panelListeningIP" = "محدودیت آی پی پنل"
"panelListeningIPDesc" = "برای استفاده از تمام IP ها به طور پیش فرض خالی بگذارید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"panelPort" = "پورت پنل"
@@ -213,46 +231,94 @@
"currentPassword" = "رمز عبور فعلی"
"newUsername" = "نام کاربری جدید"
"newPassword" = "رمز عبور جدید"
"advancedTemplate" = "بخش های پیشرفته الگو"
"completeTemplate" = "الگوی کامل تنظیمات ایکس ری"
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigPrivateIp" = "جلوگیری از اتصال آی پی های نامعتبر"
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آی پی های نامعتبر و بسته های سرگردان تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigIRIp" = "جلوگیری از اتصال آی پی های ایران"
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آی پی های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigIRdomain" = "جلوگیری از اتصال دامنه های ایران"
"xrayConfigIRdomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigInbounds" = "تنظیمات ورودی"
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigOutbounds" = "تنظیمات خروجی"
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramBotEnable" = "فعالسازی ربات تلگرام"
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"sessionMaxAge" = "بیشینه زمان جلسه وب"
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
"expireTimeDiff" = "آستانه زمان باقی مانده"
"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
"trafficDiff" = "آستانه ترافیک باقی مانده"
"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
"timeZonee" = "منظقه زمانی"
"timeZone" = "منظقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
[pages.setting.toasts]
"modifySetting" = "ویرایش تنظیمات"
"getSetting" = "دریافت تنظیمات"
[pages.settings.templates]
"title" = "الگوها"
"basicTemplate" = "بخش الگو پایه"
"advancedTemplate" = "بخش الگو پیشرفته"
"completeTemplate" = "بخش الگو کامل"
"generalConfigs" = "تنظیمات عمومی"
"generalConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند."
"countryConfigs" = "تنظیمات برای کشورها"
"countryConfigsDesc" = "این گزینه از اتصال کاربران به دامنه های کشوری خاص جلوگیری می کند."
"ipv4Configs" = "تنظیمات برای IPv4"
"ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن 4 به دامنه های هدف هدایت می شود."
"warpConfigs" = "تنظیمات برای WARP"
"warpConfigsDesc" = "هشدار: قبل از استفاده از این گزینه، WARP را در حالت پراکسی socks5 با دنبال کردن مراحل در GitHub پنل روی سرور خود نصب کنید. WARP ترافیک را از طریق سرورهای Cloudflare به وب سایت ها هدایت می کند."
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigPrivateIp" = "جلوگیری از اتصال آیپی های خصوصی یا محلی"
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های خصوصی یا محلی و بسته های سرگردان تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigAds" = "مسدود کردن تبلیغات"
"xrayConfigAdsDesc" = "الگوی تنظیمات را برای مسدود کردن تبلیغات تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigPorn" = "جلوگیری از اتصال به سایت های پورن"
"xrayConfigPornDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال به سایت های پورن تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigIRIp" = "جلوگیری از اتصال آیپی های ایران"
"xrayConfigIRIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigIRDomain" = "جلوگیری از اتصال دامنه های ایران"
"xrayConfigIRDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های ایران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigChinaIp" = "جلوگیری از اتصال آیپی های چین"
"xrayConfigChinaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های چین تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigChinaDomain" = "جلوگیری از اتصال دامنه های چین"
"xrayConfigChinaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های چین تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigRussiaIp" = "جلوگیری از اتصال آیپی های روسیه"
"xrayConfigRussiaIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آیپی های روسیه تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigRussiaDomain" = "جلوگیری از اتصال دامنه های روسیه"
"xrayConfigRussiaDomainDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال دامنه های روسیه تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigGoogleIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به گوگل"
"xrayConfigGoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigNetflixIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به نتفلیکس"
"xrayConfigNetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigGoogleWARP" = "مسیردهی گوگل به WARP"
"xrayConfigGoogleWARPDesc" = "مسیردهی جدید برای اتصال به گوگل به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigOpenAIWARP" = "مسیردهی OpenAI (ChatGPT) به WARP"
"xrayConfigOpenAIWARPDesc" = "مسیردهی جدید برای اتصال به OpenAI (ChatGPT) به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigNetflixWARP" = "مسیردهی نتفلیکس به WARP"
"xrayConfigNetflixWARPDesc" = "مسیردهی جدید برای اتصال به نتفلیکس به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigSpotifyWARP" = "مسیردهی اسپاتیفای به WARP"
"xrayConfigSpotifyWARPDesc" = "مسیردهی جدید برای اتصال به اسپاتیفای به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigIRWARP" = "مسیردهی دامنه های ایران به WARP"
"xrayConfigIRWARPDesc" = "مسیردهی جدید برای اتصال به دامنه های ایران به WARP اضافه میکند. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigInbounds" = "تنظیمات ورودی"
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigOutbounds" = "تنظیمات خروجی"
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
[pages.settings.security]
"admin" = "مدیر"
"secret" = "توکن امنیتی"
"loginSecurity" = "لاگین ایمن"
"loginSecurityDesc" = "افزودن یک مرحله دیگر به فرآیند لاگین"
"secretToken" = "توکن امنیتی"
"secretTokenDesc" = "این کد امنیتی را نزد خود در این جای امن نگه داری، بدون این کد امکان ورود به پنل را نخواهید داشت. امکان بازیابی آن وجود ندارد!"
[pages.settings.toasts]
"modifySettings" = "ویرایش تنظیمات"
"getSettings" = "دریافت تنظیمات"
"modifyUser" = "ویرایش کاربر"
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد ."
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد ."
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد ."

View File

@@ -48,11 +48,12 @@
"install" = "安装"
"clients" = "客户端"
"usage" = "用法"
"secretToken" = "秘密令牌"
[menu]
"dashboard" = "系统状态"
"inbounds" = "入站列表"
"setting" = "面板设置"
"settings" = "面板设置"
"logout" = "退出登录"
"link" = "其他"
@@ -88,14 +89,21 @@
"totalReceive" = "系统启动以来所有网卡的总下载流量"
"xraySwitchVersionDialog" = "切换 xray 版本"
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
"dontRefreshh" = "安装中,请不要刷新此页面"
"dontRefresh" = "安装中,请不要刷新此页面"
"logs" = "日志"
"config" = "配置"
"backup" = "备份还原"
"backupTitle" = "备份和恢复数据库"
"backupDescription" = "请记住在导入新数据库之前进行备份。"
"exportDatabase" = "下载数据库"
"importDatabase" = "上传数据库"
[pages.inbounds]
"title" = "入站列表"
"totalDownUp" = "总上传 / 下载"
"totalUsage" = "总用量"
"inboundCount" = "入站数量"
"operate" = "操作"
"operate" = "菜单"
"enable" = "启用"
"remark" = "备注"
"protocol" = "协议"
@@ -106,8 +114,9 @@
"expireDate" = "到期时间"
"resetTraffic" = "重置流量"
"addInbound" = "添加入"
"addTo" = "添加"
"revise" = "修改"
"generalActions" = "通用操作"
"create" = "添加"
"update" = "修改"
"modifyInbound" = "修改入站"
"deleteInbound" = "删除入站"
"deleteInboundContent" = "确定要删除入站吗?"
@@ -132,16 +141,22 @@
"clickOnQRcode" = "点击二维码复制"
"client" = "客户"
"export" = "导出链接"
"Clone" = "克隆"
"clone" = "克隆"
"cloneInbound" = "创造"
"cloneInboundContent" = "此入站的所有项目除 Port、Listening IP、Clients 将应用于克隆"
"cloneInboundOk" = "从创建克隆"
"resetAllTraffic" = "重置所有入站流量"
"resetAllTrafficTitle" = "重置所有入站流量"
"resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
"resetAllClientTraffics" = "重置客户端流量"
"resetInboundClientTraffics" = "重置客户端流量"
"resetInboundClientTrafficTitle" = "重置所有客户端流量"
"resetInboundClientTrafficContent" = "您确定要重置此入站客户端的所有流量吗?"
"resetAllClientTraffics" = "重置所有客户端流量"
"resetAllClientTrafficTitle" = "重置所有客户端流量"
"resetAllClientTrafficContent" = "确定要重置此入站客户端的所有流量吗?"
"resetAllClientTrafficContent" = "确定要重置所有客户端的所有流量吗?"
"delDepletedClients" = "删除耗尽的客户端"
"delDepletedClientsTitle" = "删除耗尽的客户"
"delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?"
"IPLimit" = "IP限制"
"IPLimitDesc" = "如果超过输入的计数则禁用入站0 表示禁用限制 ip"
"Email" = "电子邮件"
@@ -150,8 +165,10 @@
"IPLimitlogDesc" = "IP 历史日志 通过IP限制禁用inbound之前需要清空日志"
"IPLimitlogclear" = "清除日志"
"setDefaultCert" = "从面板设置证书"
"XTLSdec" = "Xray核心需要1.7.5及以下版本"
"Realitydec" = "Xray核心需要1.8.0及以上版本"
"xtlsDesc" = "Xray核心需要1.7.5"
"realityDesc" = "Xray核心需要1.8.0及以上版本"
"telegramDesc" = "使用不带@的电报 ID 或聊天 ID您可以在此处获取 @userinfobot"
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
[pages.client]
"add" = "添加客户端"
@@ -189,16 +206,17 @@
[pages.inbounds.stream.quic]
"encryption" = "加密"
[pages.setting]
[pages.settings]
"title" = "设置"
"save" = "保存配置"
"restartPanel" = "重启面板"
"restartPanelDesc" = "确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息"
"panelConfig" = "面板配置"
"userSetting" = "用户设置"
"actions" = "动作"
"resetDefaultConfig" = "重置为默认配置"
"panelSettings" = "面板配置"
"securitySettings" = "安全设定"
"xrayConfiguration" = "xray 相关设置"
"TGReminder" = "TG提醒相关设置"
"otherSetting" = "其他设置"
"TGBotSettings" = "TG提醒相关设置"
"panelListeningIP" = "面板监听 IP"
"panelListeningIPDesc" = "默认留空监听所有 IP重启面板生效"
"panelPort" = "面板监听端口"
@@ -213,46 +231,94 @@
"currentPassword" = "原密码"
"newUsername" = "新用户名"
"newPassword" = "新密码"
"advancedTemplate" = "高级模板部件"
"completeTemplate" = "Xray 配置的完整模板"
"xrayConfigTemplate" = "xray 配置模板"
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件重新启动面板生成效率"
"xrayConfigTorrent" = "禁止使用 bittorrent"
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent重启面板生效"
"xrayConfigPrivateIp" = "禁止私人 ip 范围连接"
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围,重启面板生效"
"xrayConfigIRIp" = "禁止伊朗 IP 范围连接"
"xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP范围重启面板生效"
"xrayConfigIRdomain" = "禁止伊朗域连接"
"xrayConfigIRdomainDesc" = "修改配置模板避免连接伊朗域名,重启面板生效"
"xrayConfigInbounds" = "入站配置"
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端,重启面板生效"
"xrayConfigOutbounds" = "出站配置"
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效"
"xrayConfigRoutings" = "路由规则配置"
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效"
"telegramBotEnable" = "启用电报机器人"
"telegramBotEnableDesc" = "重启面板生效"
"telegramToken" = "电报机器人TOKEN"
"telegramTokenDesc" = "重启面板生效"
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
"telegramChatIdDesc" = "重启面板生效"
"telegramChatIdDesc" = "多个聊天 ID 以逗号分隔。使用@userinfobot 获取您的聊天 ID。重新启动面板以应用更改。"
"telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
"sessionMaxAge" = "会话最大年龄"
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
"expireTimeDiff" = "耗尽时间阈值"
"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
"trafficDiff" = "耗尽流量阈值"
"trafficDiffDesc" = "完成流量前检测耗尽单位GB"
"tgNotifyCpu" = "CPU 百分比警报阈值"
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
"timeZonee" = "时区"
"timeZone" = "时区"
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
[pages.setting.toasts]
"modifySetting" = "修改设置"
"getSetting" = "获取设置"
[pages.settings.templates]
"title" = "模板"
"basicTemplate" = "基本模板"
"advancedTemplate" = "高级模板部件"
"completeTemplate" = "Xray 配置的完整模板"
"generalConfigs" = "一般配置"
"generalConfigsDesc" = "此选项将阻止用户连接到特定协议和网站。"
"countryConfigs" = "国家配置"
"countryConfigsDesc" = "此选项将阻止用户连接到特定国家/地区的域。"
"ipv4Configs" = "IPv4 配置"
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域。"
"warpConfigs" = "WARP 配置"
"warpConfigsDesc" = "警告:在使用此选项之前,请按照面板 GitHub 上的步骤在您的服务器上以 socks5 代理模式安装 WARP。 WARP 将通过 Cloudflare 服务器将流量路由到网站。"
"xrayConfigTemplate" = "xray 配置模板"
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件重新启动面板生成效率"
"xrayConfigTorrent" = "禁止使用 bittorrent"
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent重启面板生效"
"xrayConfigPrivateIp" = "禁止私人 IP 范围连接"
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围,重启面板生效"
"xrayConfigAds" = "屏蔽广告"
"xrayConfigAdsDesc" = "修改配置模板屏蔽广告,重启面板生效"
"xrayConfigPorn" = "禁止色情网站连接"
"xrayConfigPornDesc" = "更改配置模板避免连接色情网站,重启面板生效"
"xrayConfigIRIp" = "禁止伊朗 IP 范围连接"
"xrayConfigIRIpDesc" = "修改配置模板避免连接伊朗IP段重启面板生效"
"xrayConfigIRDomain" = "禁止伊朗域连接"
"xrayConfigIRDomainDesc" = "更改配置模板避免连接伊朗域名,重启面板生效"
"xrayConfigChinaIp" = "禁止中国 IP 范围连接"
"xrayConfigChinaIpDesc" = "修改配置模板避免连接中国IP段重启面板生效"
"xrayConfigChinaDomain" = "禁止中国域名连接"
"xrayConfigChinaDomainDesc" = "更改配置模板避免连接中国域,重启面板生效"
"xrayConfigRussiaIp" = "禁止俄罗斯 IP 范围连接"
"xrayConfigRussiaIpDesc" = "修改配置模板避免连接俄罗斯IP范围重启面板生效"
"xrayConfigRussiaDomain" = "禁止俄罗斯域连接"
"xrayConfigRussiaDomainDesc" = "更改配置模板避免连接俄罗斯域,重启面板生效"
"xrayConfigGoogleIPv4" = "为谷歌使用 IPv4"
"xrayConfigGoogleIPv4Desc" = "添加谷歌连接IPv4的路由重启面板生效"
"xrayConfigNetflixIPv4" = "为 Netflix 使用 IPv4"
"xrayConfigNetflixIPv4Desc" = "添加Netflix连接IPv4的路由重启面板生效"
"xrayConfigGoogleWARP" = "将谷歌路由到 WARP"
"xrayConfigGoogleWARPDesc" = "为谷歌添加路由到WARP重启面板生效"
"xrayConfigOpenAIWARP" = "将 OpenAI (ChatGPT) 路由到 WARP"
"xrayConfigOpenAIWARPDesc" = "将OpenAIChatGPT路由添加到WARP重启面板生效"
"xrayConfigNetflixWARP" = "将 Netflix 路由到 WARP"
"xrayConfigNetflixWARPDesc" = "为Netflix添加路由到WARP重启面板生效"
"xrayConfigSpotifyWARP" = "将 Spotify 路由到 WARP"
"xrayConfigSpotifyWARPDesc" = "为Spotify添加路由到WARP重启面板生效"
"xrayConfigIRWARP" = "将伊朗域名路由到 WARP"
"xrayConfigIRWARPDesc" = "将伊朗域的路由添加到 WARP。 重启面板生效"
"xrayConfigInbounds" = "入站配置"
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端,重启面板生效"
"xrayConfigOutbounds" = "出站配置"
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效"
"xrayConfigRoutings" = "路由规则配置"
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效"
[pages.settings.security]
"admin" = "行政"
"secret" = "秘密令牌"
"loginSecurity" = "登录安全"
"loginSecurityDesc" = "在用户登录页面中切换附加步骤"
"secretToken" = "秘密令牌"
"secretTokenDesc" = "复制此秘密令牌并将其保存在安全的地方;没有这个你将无法登录。这也无法从 x-ui 命令工具中恢复"
[pages.settings.toasts]
"modifySettings" = "修改设置"
"getSettings" = "获取设置"
"modifyUser" = "修改用户"
"originalUserPassIncorrect" = "原用户名或原密码错误"
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空"
"userPassMustBeNotEmpty" = "新用户名和新密码不能为空"

View File

@@ -33,9 +33,6 @@ import (
//go:embed assets/*
var assetsFS embed.FS
//go:embed assets/favicon.ico
var favicon []byte
//go:embed html/*
var htmlFS embed.FS
@@ -161,11 +158,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default()
// Add favicon
engine.GET("/favicon.ico", func(c *gin.Context) {
c.Data(200, "image/x-icon", favicon)
})
secret, err := s.settingService.GetSecret()
if err != nil {
return nil, err

369
x-ui.sh
View File

@@ -17,6 +17,7 @@ function LOGE() {
function LOGI() {
echo -e "${green}[INF] $* ${plain}"
}
# check root
[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1
@@ -34,7 +35,6 @@ fi
echo "The OS release is: $release"
os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
@@ -44,31 +44,28 @@ if [[ "${release}" == "centos" ]]; then
fi
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1
fi
elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then
echo -e "${red}please use Fedora 36 or higher version${plain}\n" && exit 1
echo -e "${red}please use Fedora 36 or higher version! ${plain}\n" && exit 1
fi
elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 10 ]]; then
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
fi
fi
confirm() {
if [[ $# > 1 ]]; then
echo && read -p "$1 [Default$2]: " temp
if [[ x"${temp}" == x"" ]]; then
echo && read -p "$1 [Default $2]: " temp
if [[ "${temp}" == "" ]]; then
temp=$2
fi
else
read -p "$1 [y/n]: " temp
fi
if [[ x"${temp}" == x"y" || x"${temp}" == x"Y" ]]; then
if [[ "${temp}" == "y" || "${temp}" == "Y" ]]; then
return 0
else
return 1
@@ -133,7 +130,7 @@ uninstall() {
rm /usr/local/x-ui/ -rf
echo ""
echo -e "Uninstalled SuccessfullyIf you want to remove this scriptthen after exiting the script run ${green}rm /usr/bin/x-ui -f${plain} to delete it."
echo -e "Uninstalled Successfully, If you want to remove this script, then after exiting the script run ${green}rm /usr/bin/x-ui -f${plain} to delete it."
echo ""
if [[ $# == 0 ]]; then
@@ -142,20 +139,28 @@ uninstall() {
}
reset_user() {
confirm "Reset your username and password to admin?" "n"
confirm "Are you sure to reset the username and password of the panel?" "n"
if [[ $? != 0 ]]; then
if [[ $# == 0 ]]; then
show_menu
fi
return 0
fi
/usr/local/x-ui/x-ui setting -username admin -password admin
echo -e "Username and password have been reset to ${green}admin${plain}Please restart the panel now."
read -rp "Please set the login username [default is a random username]: " config_account
[[ -z $config_account ]] && config_account=$(date +%s%N | md5sum | cut -c 1-8)
read -rp "Please set the login password [default is a random password]: " config_password
[[ -z $config_password ]] && config_password=$(date +%s%N | md5sum | cut -c 1-8)
/usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password} >/dev/null 2>&1
/usr/local/x-ui/x-ui setting -remove_secret >/dev/null 2>&1
echo -e "Panel login username has been reset to: ${green} ${config_account} ${plain}"
echo -e "Panel login password has been reset to: ${green} ${config_password} ${plain}"
echo -e "${yellow} Panel login secret token disabled ${plain}"
echo -e "${green} Please use the new login username and password to access the X-UI panel. Also remember them! ${plain}"
confirm_restart
}
reset_config() {
confirm "Are you sure you want to reset all panel settingsAccount data will not be lostUsername and password will not change" "n"
confirm "Are you sure you want to reset all panel settings, Account data will not be lost, Username and password will not change" "n"
if [[ $? != 0 ]]; then
if [[ $# == 0 ]]; then
show_menu
@@ -163,14 +168,14 @@ reset_config() {
return 0
fi
/usr/local/x-ui/x-ui setting -reset
echo -e "All panel settings have been reset to defaultPlease restart the panel nowand use the default ${green}2053${plain} Port to Access the web Panel"
echo -e "All panel settings have been reset to default, Please restart the panel now, and use the default ${green}2053${plain} Port to Access the web Panel"
confirm_restart
}
check_config() {
info=$(/usr/local/x-ui/x-ui setting -show true)
if [[ $? != 0 ]]; then
LOGE "get current settings error,please check logs"
LOGE "get current settings error, please check logs"
show_menu
fi
LOGI "${info}"
@@ -183,7 +188,7 @@ set_port() {
before_show_menu
else
/usr/local/x-ui/x-ui setting -port ${port}
echo -e "The port is setPlease restart the panel nowand use the new port ${green}${port}${plain} to access web panel"
echo -e "The port is set, Please restart the panel now, and use the new port ${green}${port}${plain} to access web panel"
confirm_restart
fi
}
@@ -192,7 +197,7 @@ start() {
check_status
if [[ $? == 0 ]]; then
echo ""
LOGI "Panel is runningNo need to start againIf you need to restart, please select restart"
LOGI "Panel is running, No need to start again, If you need to restart, please select restart"
else
systemctl start x-ui
sleep 2
@@ -200,7 +205,7 @@ start() {
if [[ $? == 0 ]]; then
LOGI "x-ui Started Successfully"
else
LOGE "panel Failed to startProbably because it takes longer than two seconds to startPlease check the log information later"
LOGE "panel Failed to start, Probably because it takes longer than two seconds to start, Please check the log information later"
fi
fi
@@ -213,7 +218,7 @@ stop() {
check_status
if [[ $? == 1 ]]; then
echo ""
LOGI "Panel stoppedNo need to stop again!"
LOGI "Panel stopped, No need to stop again!"
else
systemctl stop x-ui
sleep 2
@@ -221,7 +226,7 @@ stop() {
if [[ $? == 1 ]]; then
LOGI "x-ui and xray stopped successfully"
else
LOGE "Panel stop failedProbably because the stop time exceeds two secondsPlease check the log information later"
LOGE "Panel stop failed, Probably because the stop time exceeds two seconds, Please check the log information later"
fi
fi
@@ -237,7 +242,7 @@ restart() {
if [[ $? == 0 ]]; then
LOGI "x-ui and xray Restarted successfully"
else
LOGE "Panel restart failedProbably because it takes longer than two seconds to startPlease check the log information later"
LOGE "Panel restart failed, Probably because it takes longer than two seconds to start, Please check the log information later"
fi
if [[ $# == 0 ]]; then
before_show_menu
@@ -285,51 +290,49 @@ show_log() {
}
enable_bbr() {
if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then
echo -e "${green}BBR is already enabled!${plain}"
exit 0
fi
if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then
echo -e "${green}BBR is already enabled!${plain}"
exit 0
fi
# Check the OS and install necessary packages
if [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "ubuntu" ]]; then
sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "debian" ]]; then
sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "fedora" ]]; then
sudo dnf -y update && sudo dnf -y install ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "centos" ]]; then
sudo yum -y update && sudo yum -y install ca-certificates
else
echo "Unsupported operating system. Please check the script and install the necessary packages manually."
exit 1
fi
# Check the OS and install necessary packages
if [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "ubuntu" ]]; then
sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "debian" ]]; then
sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "fedora" ]]; then
sudo dnf -y update && sudo dnf -y install ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "centos" ]]; then
sudo yum -y update && sudo yum -y install ca-certificates
else
echo "Unsupported operating system. Please check the script and install the necessary packages manually."
exit 1
fi
# Enable BBR
echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.conf
# Enable BBR
echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.conf
# Apply changes
sudo sysctl -p
# Verify that BBR is enabled
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then
echo -e "${green}BBR has been enabled successfully.${plain}"
else
echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}"
fi
# Apply changes
sudo sysctl -p
# Verify that BBR is enabled
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then
echo -e "${green}BBR has been enabled successfully.${plain}"
else
echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}"
fi
}
update_shell() {
wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/MHSanaei/3x-ui/raw/main/x-ui.sh
if [[ $? != 0 ]]; then
echo ""
LOGE "Failed to download scriptPlease check whether the machine can connect Github"
LOGE "Failed to download script, Please check whether the machine can connect Github"
before_show_menu
else
chmod +x /usr/bin/x-ui
LOGI "Upgrade script succeededPlease rerun the script" && exit 0
LOGI "Upgrade script succeeded, Please rerun the script" && exit 0
fi
}
@@ -339,7 +342,7 @@ check_status() {
return 2
fi
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1)
if [[ x"${temp}" == x"running" ]]; then
if [[ "${temp}" == "running" ]]; then
return 0
else
return 1
@@ -348,7 +351,7 @@ check_status() {
check_enabled() {
temp=$(systemctl is-enabled x-ui)
if [[ x"${temp}" == x"enabled" ]]; then
if [[ "${temp}" == "enabled" ]]; then
return 0
else
return 1
@@ -359,7 +362,7 @@ check_uninstall() {
check_status
if [[ $? != 2 ]]; then
echo ""
LOGE "Panel installedPlease do not reinstall"
LOGE "Panel installed, Please do not reinstall"
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -428,96 +431,77 @@ show_xray_status() {
fi
}
#this will be an entrance for ssl cert issue
#here we can provide two different methods to issue cert
#first.standalone mode second.DNS API mode
ssl_cert_issue() {
local method=""
echo -E ""
LOGD "********Usage********"
LOGI "this shell script will use acme to help issue certs."
LOGI "here we provide two methods for issuing certs:"
LOGI "method 1:acme standalone mode,need to keep port:80 open"
LOGI "method 2:acme DNS API mode,need provide Cloudflare Global API Key"
LOGI "recommend method 2 first,if it fails,you can try method 1."
LOGI "certs will be installed in /root/cert directory"
read -p "please choose which method do you want,type 1 or 2": method
LOGI "you choosed method:${method}"
if [ "${method}" == "1" ]; then
ssl_cert_issue_standalone
elif [ "${method}" == "2" ]; then
ssl_cert_issue_by_cloudflare
else
LOGE "invalid input,please check it..."
exit 1
fi
}
open_ports() {
if ! command -v ufw &> /dev/null
then
echo "ufw firewall is not installed. Installing now..."
sudo apt-get update
sudo apt-get install -y ufw
else
echo "ufw firewall is already installed"
fi
# Check if the firewall is inactive
if sudo ufw status | grep -q "Status: active"; then
echo "firewall is already active"
else
# Open the necessary ports
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw allow 2053/tcp
# Enable the firewall
sudo ufw --force enable
fi
# Prompt the user to enter a list of ports
read -p "Enter the ports you want to open (e.g. 80,443,2053 or range 400-500): " ports
# Check if the input is valid
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2; exit 1
fi
# Open the specified ports using ufw
IFS=',' read -ra PORT_LIST <<< "$ports"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
# Split the range into start and end ports
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# Loop through the range and open each port
for ((i=start_port; i<=end_port; i++)); do
sudo ufw allow $i
done
if ! command -v ufw &> /dev/null
then
echo "ufw firewall is not installed. Installing now..."
sudo apt-get update
sudo apt-get install -y ufw
else
sudo ufw allow "$port"
echo "ufw firewall is already installed"
fi
done
# Confirm that the ports are open
sudo ufw status | grep $ports
# Check if the firewall is inactive
if sudo ufw status | grep -q "Status: active"; then
echo "firewall is already active"
else
# Open the necessary ports
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw allow 2053/tcp
# Enable the firewall
sudo ufw --force enable
fi
# Prompt the user to enter a list of ports
read -p "Enter the ports you want to open (e.g. 80,443,2053 or range 400-500): " ports
# Check if the input is valid
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2; exit 1
fi
# Open the specified ports using ufw
IFS=',' read -ra PORT_LIST <<< "$ports"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
# Split the range into start and end ports
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# Loop through the range and open each port
for ((i=start_port; i<=end_port; i++)); do
sudo ufw allow $i
done
else
sudo ufw allow "$port"
fi
done
# Confirm that the ports are open
sudo ufw status | grep $ports
}
update_geo() {
local defaultBinFolder="/usr/local/x-ui/bin"
read -p "Please enter x-ui bin folder path. Leave blank for default. (Default: '${defaultBinFolder}')" binFolder
binFolder=${binFolder:-${defaultBinFolder}}
if [[ ! -d ${binFolder} ]]; then
LOGE "Folder ${binFolder} not exists!"
LOGI "making bin folder: ${binFolder}..."
mkdir -p ${binFolder}
fi
update_geo(){
systemctl stop x-ui
cd /usr/local/x-ui/bin
cd ${binFolder}
rm -f geoip.dat geosite.dat iran.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
wget -N https://github.com/bootmortis/iran-hosted-domains/releases/latest/download/iran.dat
systemctl start x-ui
echo -e "${green}Geosite and Geoip have been updated successfully!${plain}"
before_show_menu
echo -e "${green}Geosite.dat + Geoip.dat + Iran.dat have been updated successfully in bin folder '${binfolder}'!${plain}"
before_show_menu
}
install_acme() {
@@ -534,7 +518,7 @@ install_acme() {
}
#method for standalone mode
ssl_cert_issue_standalone() {
ssl_cert_issue() {
#check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
echo "acme.sh could not be found. we will install it"
@@ -545,7 +529,7 @@ ssl_cert_issue_standalone() {
fi
fi
#install socat second
if [[ x"${release}" == x"centos" ]]; then
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]] ; then
yum install socat -y
else
apt install socat -y
@@ -559,7 +543,7 @@ ssl_cert_issue_standalone() {
#get the domain here,and we need verify it
local domain=""
read -p "please input your domain:" domain
read -p "Please enter your domain name:" domain
LOGD "your domain is:${domain},check it..."
#here we need to judge whether there exists cert already
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
@@ -626,98 +610,11 @@ ssl_cert_issue_standalone() {
}
#method for DNS API mode
ssl_cert_issue_by_cloudflare() {
echo -E ""
LOGD "******Preconditions******"
LOGI "1.need Cloudflare account associated email"
LOGI "2.need Cloudflare Global API Key"
LOGI "3.your domain use Cloudflare as resolver"
confirm "I have confirmed all these info above[y/n]" "y"
if [ $? -eq 0 ]; then
install_acme
if [ $? -ne 0 ]; then
LOGE "install acme failed,please check logs"
exit 1
fi
CF_Domain=""
CF_GlobalKey=""
CF_AccountEmail=""
LOGD "please input your domain:"
read -p "Input your domain here:" CF_Domain
LOGD "your domain is:${CF_Domain},check it..."
#here we need to judge whether there exists cert already
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
if [ ${currentCert} == ${CF_Domain} ]; then
local certInfo=$(~/.acme.sh/acme.sh --list)
LOGE "system already have certs here,can not issue again,current certs details:"
LOGI "$certInfo"
exit 1
else
LOGI "your domain is ready for issuing cert now..."
fi
#create a directory for install cert
certPath="/root/cert/${CF_Domain}"
if [ ! -d "$certPath" ]; then
mkdir -p "$certPath"
else
rm -rf "$certPath"
mkdir -p "$certPath"
fi
LOGD "please inout your cloudflare global API key:"
read -p "Input your key here:" CF_GlobalKey
LOGD "your cloudflare global API key is:${CF_GlobalKey}"
LOGD "please input your cloudflare account email:"
read -p "Input your email here:" CF_AccountEmail
LOGD "your cloudflare account email:${CF_AccountEmail}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
if [ $? -ne 0 ]; then
LOGE "change the default CA to Lets'Encrypt failed,exit"
exit 1
fi
export CF_Key="${CF_GlobalKey}"
export CF_Email=${CF_AccountEmail}
~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log
if [ $? -ne 0 ]; then
LOGE "issue cert failed,exit"
rm -rf ~/.acme.sh/${CF_Domain}
exit 1
else
LOGI "Certificate issued Successfully, Installing..."
fi
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \
--key-file /root/cert/${CF_Domain}/privkey.pem \
--fullchain-file /root/cert/${CF_Domain}/fullchain.pem
if [ $? -ne 0 ]; then
LOGE "install cert failed,exit"
rm -rf ~/.acme.sh/${CF_Domain}
exit 1
else
LOGI "Certificate installed Successfully,Turning on automatic updates..."
fi
~/.acme.sh/acme.sh --upgrade --auto-upgrade
if [ $? -ne 0 ]; then
LOGE "auto renew failed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
exit 1
else
LOGI "auto renew succeed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
fi
else
show_menu
fi
}
google_recaptcha() {
curl -O https://raw.githubusercontent.com/jinwyp/one_click_script/master/install_kernel.sh && chmod +x ./install_kernel.sh && ./install_kernel.sh
echo ""
before_show_menu
warp_fixchatgpt() {
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
echo ""
before_show_menu
}
run_speedtest() {
@@ -726,19 +623,19 @@ run_speedtest() {
# If not installed, install it
if command -v dnf &> /dev/null; then
sudo dnf install -y curl
curl -s https://install.speedtest.net/app/cli/install.rpm.sh | sudo bash
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash
sudo dnf install -y speedtest
elif command -v yum &> /dev/null; then
sudo yum install -y curl
curl -s https://install.speedtest.net/app/cli/install.rpm.sh | sudo bash
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.rpm.sh | sudo bash
sudo yum install -y speedtest
elif command -v apt-get &> /dev/null; then
sudo apt-get update && sudo apt-get install -y curl
curl -s https://install.speedtest.net/app/cli/install.deb.sh | sudo bash
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash
sudo apt-get install -y speedtest
elif command -v apt &> /dev/null; then
sudo apt update && sudo apt install -y curl
curl -s https://install.speedtest.net/app/cli/install.deb.sh | sudo bash
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | sudo bash
sudo apt install -y speedtest
else
echo "Error: Package manager not found. You may need to install Speedtest manually."
@@ -778,7 +675,7 @@ show_menu() {
${green}2.${plain} Update x-ui
${green}3.${plain} Uninstall x-ui
————————————————
${green}4.${plain} Reset Username And Password
${green}4.${plain} Reset Username & Password & Secret Token
${green}5.${plain} Reset Panel Settings
${green}6.${plain} Change Panel Port
${green}7.${plain} View Current Panel Settings
@@ -790,13 +687,13 @@ show_menu() {
${green}12.${plain} Check x-ui Logs
————————————————
${green}13.${plain} Enable x-ui On System Startup
${green}14.${plain} Disabel x-ui On System Startup
${green}14.${plain} Disable x-ui On System Startup
————————————————
${green}15.${plain} Enable BBR
${green}16.${plain} Apply for an SSL Certificate
${green}17.${plain} Update Geo Files
${green}18.${plain} Active Firewall and open ports
${green}19.${plain} Fixing Google reCAPTCHA
${green}19.${plain} Install WARP
${green}20.${plain} Speedtest by Ookla
"
show_status
@@ -861,7 +758,7 @@ show_menu() {
open_ports
;;
19)
google_recaptcha
warp_fixchatgpt
;;
20)
run_speedtest

View File

@@ -14,6 +14,7 @@ import (
"runtime"
"strings"
"time"
"x-ui/config"
"x-ui/util/common"
"github.com/Workiva/go-datastructures/queue"
@@ -29,19 +30,27 @@ func GetBinaryName() string {
}
func GetBinaryPath() string {
return "bin/" + GetBinaryName()
return config.GetBinFolderPath() + "/" + GetBinaryName()
}
func GetConfigPath() string {
return "bin/config.json"
return config.GetBinFolderPath() + "/config.json"
}
func GetGeositePath() string {
return "bin/geosite.dat"
return config.GetBinFolderPath() + "/geosite.dat"
}
func GetGeoipPath() string {
return "bin/geoip.dat"
return config.GetBinFolderPath() + "/geoip.dat"
}
func GetIranPath() string {
return config.GetBinFolderPath() + "/iran.dat"
}
func GetBlockedIPsPath() string {
return config.GetBinFolderPath() + "/blockedIPs"
}
func stopProcess(p *Process) {
@@ -162,7 +171,7 @@ func (p *process) Start() (err error) {
return common.NewErrorf("Failed to write configuration file: %v", err)
}
cmd := exec.Command(GetBinaryPath(), "-c", configPath, "-restrictedIPsPath", "./bin/blockedIPs")
cmd := exec.Command(GetBinaryPath(), "-c", configPath, "-restrictedIPsPath", GetBlockedIPsPath())
p.cmd = cmd
stdReader, err := cmd.StdoutPipe()