1027 Commits

Author SHA1 Message Date
v2board
43134f9fe8 Update exchange rate API URL in Helper class 2023-12-19 01:27:12 +08:00
v2board
4f04eab073 Refactor ConfigController to use ConfigService 2023-12-19 01:26:41 +08:00
v2board
69c85983d1 update: add custom subscribe path 2023-12-08 15:27:33 +08:00
v2board
fa6670597a update: fix app 2023-10-27 03:11:47 +08:00
v2board
e980a6bbe0 update: i18n 2023-10-21 04:22:07 +08:00
v2board
80b96e730a update: remove guest plan api 2023-10-15 21:52:17 +08:00
v2board
3c372bd268 update: hytseria2 2023-10-01 22:44:31 +08:00
v2board
4434b13361 update: hysteria2 2023-09-28 13:34:58 +08:00
v2board
3f24ba9917 update: plan surplus feature 2023-08-20 03:21:33 +08:00
v2board
31c5cf1c2b update: vless 2023-08-02 01:52:25 +08:00
v2board
07de70d8ab update: set cancel order interval to 2 hours 2023-07-25 23:38:30 +08:00
v2board
e6b6d1022e update: rollback 2023-07-25 19:23:40 +08:00
v2board
c1097ad48f update: vless 2023-07-17 15:50:33 +08:00
v2board
4c865d0262 update: add vless 2023-07-17 15:40:08 +08:00
v2board
1a30aa30ad update: send email verify 2023-07-02 01:44:34 +08:00
v2board
6f8e395681 update: send email verify 2023-06-24 23:18:35 +08:00
v2board
757e605921 update: prevention of blasting 2023-06-24 23:00:02 +08:00
v2board
61f1d8a623 update: reconsitution 2023-06-14 11:38:21 +08:00
v2board
98bee6fa87 update: add v2 api 2023-06-13 18:49:40 +08:00
v2board
5ee58f32ca update: route 2023-06-13 18:43:48 +08:00
v2board
4c97d7e429 update: api version 2023-06-12 02:32:49 +08:00
v2board
32eaf301fe update: stat command 2023-06-05 14:33:58 +08:00
v2board
948177f22e update: stat service 2023-06-05 14:10:14 +08:00
v2board
b39299be23 update: stat command 2023-06-05 13:56:45 +08:00
v2board
228355a520 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2023-06-05 03:43:44 +08:00
v2board
68db4f51b5 update: add system log filter 2023-06-05 03:43:34 +08:00
tokumeikoi
f33eb0b4bb Merge pull request #844 from cubemaze/patch-2
update: fix flag name matching confusion
2023-06-04 01:45:20 +08:00
v2board
2285a7c92f update: add request log to middleware 2023-06-01 23:31:56 +08:00
cubemaze
3884cf96ed update: fix flag name matching confusion
using ss2022, flag clashmeta will be caught by the clash match, resulting in the desired clashmeta configuration not being output
2023-05-29 17:10:26 +08:00
v2board
1b8ec77bcc update: rollback 2023-05-24 11:35:27 +08:00
v2board
fa50194055 update: sql 2023-05-24 01:18:34 +08:00
v2board
b16ef8a7f3 update: add route 2023-05-24 01:10:40 +08:00
v2board
de75063e0b Merge branch 'dev' of https://github.com/v2board/v2board into dev 2023-05-24 01:08:28 +08:00
v2board
3752bed0d6 update: log api 2023-05-24 01:08:13 +08:00
tokumeikoi
74c025e719 Merge pull request #832 from betaxab/loon-p1
Client: Loon: add TCP-HTTP support, set cipher to auto
2023-05-22 12:20:40 +08:00
v2board
4dc6d29076 update: subscribe info 2023-05-22 00:34:24 +08:00
v2board
45fd04137b Merge branch 'dev' of https://github.com/v2board/v2board into dev 2023-05-20 20:51:41 +08:00
v2board
b20504a431 update: more api 2023-05-20 20:50:53 +08:00
betaxab
3f986992e5 Client: Loon: add TCP-HTTP support, set cipher to auto 2023-05-19 09:38:02 +08:00
tokumeikoi
d6f35d0250 Merge pull request #739 from betaxab/add-loon-support
Client: add iOS Loon Support, Close #729
2023-05-17 23:02:06 +08:00
tokumeikoi
6c327f9a63 Merge pull request #752 from Cp0204/features-subscription-optimization
update: Support TCP-HTTP sub rules
2023-05-17 23:01:21 +08:00
tokumeikoi
e5f5e1b693 Merge pull request #813 from betaxab/stripecheckout-p1
Payments: StripeCheckout add invoice and contact info support
2023-05-17 22:58:24 +08:00
v2board
2f04505562 update: clash enhanced mode force fake-ip 2023-05-17 11:30:18 +08:00
v2board
4ba6edc328 update: fix statistics 2023-05-17 11:28:59 +08:00
v2board
c42097e92f update: fix hysteria submit 2023-05-11 00:40:09 +08:00
v2board
9db5d3d483 update: add statis api 2023-05-11 00:24:03 +08:00
betaxab
7964bee769 Payments: StripeCheckout add invoice and contact info support 2023-05-09 03:46:50 +08:00
v2board
76f4a1764b update: statistics 2023-05-03 21:34:29 +08:00
v2board
d9bd54cbc5 update: add route 2023-05-03 20:51:54 +08:00
v2board
3b1159187f update: user statistical 2023-05-03 18:24:30 +08:00
v2board
c6fbb89452 update: statistics service 2023-05-03 15:44:32 +08:00
v2board
ae0fd63929 update: statistics service 2023-05-03 15:43:20 +08:00
v2board
eee5152f52 update: statistics service 2023-05-03 15:37:19 +08:00
v2board
8b3ea1f8ea update: fix register email code type 2023-04-30 13:05:05 +08:00
v2board
6bf60eb4ec update: default config 2023-04-28 19:13:39 +08:00
v2board
1e6210290b update: fix switch payment handling fee calc 2023-04-23 14:40:21 +08:00
v2board
d8aace8647 update: statistical service 2023-04-17 21:02:57 +08:00
v2board
24b4c174c1 update: statistical service 2023-04-15 19:43:19 +08:00
v2board
7be6553396 update: statistical service 2023-04-15 19:34:54 +08:00
v2board
6dd509d73e update: exception message 2023-04-14 17:41:22 +08:00
v2board
bc5fb4956c update: exception message 2023-04-14 17:40:39 +08:00
v2board
de478db2c7 update: default theme 2023-04-10 13:17:10 +08:00
v2board
61ae86be6f update: default theme 2023-04-10 13:10:09 +08:00
Cp0204
df6d567962 Merge branch 'v2board:master' into features-subscription-optimization 2023-04-10 10:06:00 +08:00
v2board
a7275e005b update: forget verify 2023-04-08 21:52:48 +08:00
v2board
3be96ff99c update: frontend 2023-04-06 16:31:35 +08:00
v2board
f874da19f0 update: performance optimization 2023-04-06 16:27:20 +08:00
v2board
2f153acd51 update: job timeout 2023-04-04 17:58:25 +08:00
Cp0204
f9e79018d8 update: Support TCP-HTTP sub rules 2023-03-27 21:53:52 +08:00
v2board
d4ac203805 update: hysteria 2023-03-26 22:45:01 +08:00
v2board
82aa63b2e8 update: coupon generate default enable 2023-03-22 01:45:07 +08:00
betaxab
f3699ce421 Client: add iOS Loon Support, Close #729 2023-03-21 13:44:19 +08:00
v2board
aa474f02b9 update: plan capacity limit sql 2023-03-18 00:20:18 +08:00
v2board
921b3fce4e update: add sub unmatch rule 2023-03-09 20:36:35 +08:00
v2board
fae83ab0e5 update: add hysteria 2023-03-08 14:18:27 +08:00
v2board
969e8ec1e1 update: add hysteria 2023-03-08 14:15:45 +08:00
v2board
d6cc451a45 update: remove todo 2023-03-08 02:53:35 +08:00
v2board
6626c734c3 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2023-03-08 02:43:09 +08:00
v2board
529a72dda5 update: clash remove proxies empty group 2023-03-08 02:42:54 +08:00
v2board
4c06c0cd51 update: protocol match rule 2023-03-08 02:31:46 +08:00
v2board
9f2c83a21e update: add hysteria 2023-03-08 02:28:00 +08:00
v2board
feb673cab3 update: clash remove proxies empty group 2023-03-08 02:26:33 +08:00
v2board
e745c2a5be update: clash remove proxies empty group 2023-03-08 02:26:10 +08:00
tokumeikoi
23b6364cc0 Merge pull request #712 from v2board/dev
1.7.3
2023-03-08 01:51:28 +08:00
v2board
9a28d27082 update: sql 2023-03-07 23:11:56 +08:00
v2board
5114611f4b update: sql 2023-03-07 22:38:29 +08:00
v2board
c89faf172a update: clash x compatibility 2023-03-03 14:07:16 +08:00
v2board
01a6723e3e update: clash x compatibility 2023-03-02 23:00:58 +08:00
v2board
57943b85b0 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2023-03-01 01:23:20 +08:00
v2board
2afa05a312 update: clash enhanced-mode 2023-03-01 01:23:11 +08:00
v2board
986acf67d0 update: order service 2023-02-25 20:35:18 +08:00
v2board
7c7f7288d4 update: v2ray char to vmess 2023-02-16 18:53:13 +08:00
v2board
c40314ed96 update: v2ray char to vmess 2023-02-16 18:52:42 +08:00
v2board
891c84eaae update: generate order no 2023-02-15 14:54:56 +08:00
v2board
0cbb44cdea update: v2ray char to vmess 2023-02-15 14:53:13 +08:00
v2board
f062e57a81 update: fix issue #650 2023-02-11 15:56:15 +08:00
v2board
5e582292a8 update: fix issue #658 2023-02-11 15:48:24 +08:00
v2board
1ffe78541d update: fix issue #663 2023-02-11 15:17:43 +08:00
v2board
2429ff6d58 update: banned user remove session 2023-02-03 23:39:31 +08:00
v2board
10861856b5 update: add order filter column 2023-01-30 20:37:09 +08:00
v2board
1957dab114 update: server sort 2023-01-30 20:12:01 +08:00
v2board
7f25fb674f update: fix calcResetDayByYearExpiredAt 2023-01-29 19:04:20 +08:00
v2board
2c9f45a193 update: standard 2023-01-28 22:42:05 +08:00
v2board
7c4f206819 update: standard 2023-01-28 22:32:46 +08:00
v2board
4034fd4d97 update: fix ss server key 2023-01-25 23:04:55 +08:00
v2board
9f574a6208 update: fix order service 2023-01-21 13:37:54 +08:00
v2board
ad619b6a3a update: fix order service 2023-01-21 12:41:32 +08:00
v2board
db563062e5 update: fix server route edit 2023-01-20 23:45:06 +08:00
v2board
72a1359cb2 update: remove old route 2023-01-20 23:40:18 +08:00
v2board
2bc3c9c7aa update: fix protocols 2023-01-20 23:34:19 +08:00
v2board
4bb3e46308 update: fix #640 2023-01-19 15:43:46 +08:00
v2board
398ab4d005 update: order discount 2023-01-19 12:44:07 +08:00
v2board
973d90572f update: cancel order alert 2023-01-05 21:46:00 +08:00
v2board
0c935c5e3e update: fix node etag 2023-01-04 23:02:28 +08:00
v2board
63566fbd2c update: fix node status 2023-01-04 22:52:34 +08:00
tokumeikoi
6b235e592d Merge pull request #604 from v2board/dev
1.7.2
2022-12-24 18:36:15 +08:00
v2board
908696a54d update: add clash meta 2022-12-23 22:39:07 +08:00
v2board
731a2b247a update: fix tryout speedlimit 2022-12-22 21:57:20 +08:00
v2board
e5fcec6a2a update: config 2022-12-22 21:21:42 +08:00
v2board
89d5a7fb42 update: alipay2f2 custom product name 2022-12-20 01:58:02 +08:00
v2board
0992dde314 update: alipay2f2 custom product name 2022-12-20 01:57:49 +08:00
v2board
8aef19ac4c update: alipay2f2 custom product name 2022-12-20 01:57:35 +08:00
v2board
2fed7652fa update: config & custom password attack rule 2022-12-20 01:45:21 +08:00
v2board
c8f3684312 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2022-12-19 13:50:52 +08:00
v2board
c946a247ae update: install command 2022-12-19 13:50:37 +08:00
v2board
044e8f6c30 update: session manage 2022-12-18 16:06:51 +08:00
v2board
2e251872b7 update: add get active session api 2022-12-18 15:03:23 +08:00
v2board
f621619cc4 update: update command 2022-12-18 14:31:30 +08:00
v2board
ed2a3b034e update: remove ga & fix route 2022-12-18 14:15:41 +08:00
v2board
f6c6b5fb1c update: remove admin google analytics 2022-12-18 00:28:01 +08:00
tokumeikoi
0d3aef4fd5 Merge branch 'master' into dev 2022-12-17 23:05:26 +08:00
tokumeikoi
020f0680e5 update: readme.me 2022-12-17 23:05:01 +08:00
tokumeikoi
b6baf6485d Merge branch 'dev' of https://github.com/v2board/v2board into dev 2022-12-17 23:00:32 +08:00
tokumeikoi
b88bbbc4ba update: theme config 2022-12-17 23:00:23 +08:00
tokumeikoi
3bfc08f4b4 Merge pull request #546 from coldice945/feature-surgeConfigPanel 2022-12-17 22:05:31 +08:00
tokumeikoi
0490e38239 update: theme config 2022-12-17 21:42:27 +08:00
tokumeikoi
99311e12a5 update: route data struct & remove google analytics 2022-12-17 21:37:08 +08:00
coldice945
358d036e33 Surge 面板增加增加剩余流量显示 2022-12-17 18:59:45 +08:00
v2board
5fc49dc840 Merge branch 'master' into dev 2022-12-16 23:08:59 +08:00
tokumeikoi
08653fb2cd update: issue template 2022-12-16 23:08:36 +08:00
v2board
f8bf23fae3 Merge branch 'master' into dev 2022-12-16 23:05:01 +08:00
coldice945
858e68399a Surge,Surfboard 配置面板流量显示优化去掉内部没用的引号 2022-12-16 20:08:33 +08:00
tokumeikoi
44e8588d3d update: rollback update command 2022-12-16 16:15:39 +08:00
tokumeikoi
9c47d4d09a update: v2rayn 2022-12-16 15:59:37 +08:00
tokumeikoi
48056cf03f update: fix route save issue 2022-12-16 13:56:07 +08:00
tokumeikoi
f56a943c35 update: issue templates 2022-12-16 02:56:28 +08:00
tokumeikoi
f91a1df749 update: secure path regex 2022-12-16 00:37:33 +08:00
tokumeikoi
8eab09440b update: reset user confirm 2022-12-15 23:57:27 +08:00
tokumeikoi
3ba1e87222 update: uniproxy 2022-12-15 23:30:47 +08:00
tokumeikoi
cc43ae5d38 update: add more auth meta 2022-12-15 23:16:06 +08:00
tokumeikoi
57ef51f5d1 update: secure_path minimum length limit 2022-12-15 22:32:51 +08:00
tokumeikoi
4880bd97fa Merge branch 'dev' 2022-12-15 17:35:49 +08:00
tokumeikoi
1a3618499f update: reset user 2022-12-15 17:32:46 +08:00
tokumeikoi
286ba79a67 update: fix speed limit no outpus 2022-12-15 17:08:05 +08:00
tokumeikoi
2f50a0e90f update: auth service 2022-12-15 16:05:31 +08:00
tokumeikoi
3d9416bf26 update: multiple session 2022-12-15 15:53:25 +08:00
tokumeikoi
d646a3b27f update: new version 2022-12-15 13:48:31 +08:00
tokumeikoi
df9c8977c4 Merge branch 'dev' 2022-12-15 11:39:11 +08:00
tokumeikoi
28677f45be update: fix config save 2022-12-15 11:39:00 +08:00
tokumeikoi
933ccf3e4f Merge branch 'dev' 2022-12-15 11:30:18 +08:00
tokumeikoi
3f7ecb23df update: default secure path 2022-12-15 11:29:54 +08:00
tokumeikoi
c2f43a5258 update: default secure path 2022-12-15 11:28:29 +08:00
tokumeikoi
0dfbadf715 Merge branch 'dev' 2022-12-15 11:04:40 +08:00
tokumeikoi
0a8fe5267f update: set config value trim 2022-12-15 11:04:19 +08:00
tokumeikoi
ac47a879fa update: reset server log period 2022-12-15 11:01:31 +08:00
tokumeikoi
b9f3838e3b update: default secure path 2022-12-15 10:59:27 +08:00
tokumeikoi
b6f0508858 update: secure path default value 2022-12-15 10:45:16 +08:00
tokumeikoi
c3a47fddb5 update: show secure path 2022-12-15 10:34:32 +08:00
tokumeikoi
3e91a7b57a 1.7.0
1.7.0
2022-12-15 03:31:37 +08:00
tokumeikoi
957fe95449 update: version 2022-12-15 03:30:25 +08:00
tokumeikoi
0768392b24 update: install default secure path 2022-12-15 01:58:01 +08:00
tokumeikoi
e57c09438a update: user filter 2022-12-15 01:19:58 +08:00
tokumeikoi
d0d3c6629b update: user filter 2022-12-15 01:16:51 +08:00
tokumeikoi
63a2ffe165 update: config 2022-12-15 01:03:05 +08:00
tokumeikoi
4d8bb0d8e9 update: more secure path 2022-12-15 00:59:32 +08:00
tokumeikoi
a77523c3b5 update: password check limit 2022-12-14 23:02:12 +08:00
tokumeikoi
837701f20a update: password check limit 2022-12-14 23:01:18 +08:00
tokumeikoi
125a882a7e update: password check limit 2022-12-14 23:00:35 +08:00
tokumeikoi
c36a54dae2 update: password check limit 2022-12-14 22:58:42 +08:00
tokumeikoi
4398f05b91 update: install random password 2022-12-14 22:12:28 +08:00
tokumeikoi
5976bcc65a update: weak password risk 2022-12-13 12:29:23 +08:00
tokumeikoi
70bde7b742 update: fix register limit language 2022-12-05 19:45:02 +08:00
tokumeikoi
87e61e1b9a update: fix typo 2022-12-02 13:52:20 +08:00
tokumeikoi
e82a145d5e update: update sql 2022-11-30 17:07:07 +08:00
tokumeikoi
f864d7249e update: fix ui 2022-11-30 14:16:03 +08:00
tokumeikoi
f781f22cde update: uniproxy 2022-11-29 14:33:08 +08:00
tokumeikoi
40e6400b9b update: route manage 2022-11-29 14:31:31 +08:00
tokumeikoi
153721be55 update: system config 2022-11-27 23:50:07 +08:00
tokumeikoi
69dd10f205 update: add route sql 2022-11-27 15:15:22 +08:00
tokumeikoi
849b98e876 update: new feature route manage 2022-11-27 15:13:08 +08:00
tokumeikoi
16693b94bf update: new feature route manage 2022-11-27 15:11:10 +08:00
tokumeikoi
f9e2afe9d1 update: order fetch speed limit 2022-11-26 22:19:19 +08:00
tokumeikoi
e86ac44b2a update: fix reset package not exist 2022-11-26 18:58:24 +08:00
tokumeikoi
2930f1957c update: server etag 2022-11-25 03:53:18 +08:00
tokumeikoi
56a6025ef9 update: app controller 2022-11-24 17:43:02 +08:00
tokumeikoi
d62307b112 update: stat controller 2022-11-24 02:11:44 +08:00
tokumeikoi
2999648435 update: database 2022-11-24 02:10:14 +08:00
tokumeikoi
7810db0b47 update: server controller 2022-11-24 02:09:15 +08:00
tokumeikoi
bb900d59b0 update: fix client 2022-11-20 15:24:03 +08:00
tokumeikoi
d1194ef310 update: client config 2022-11-18 20:45:00 +08:00
tokumeikoi
c5d714d64d update: add speedlimit 2022-11-18 15:36:15 +08:00
tokumeikoi
fc85fd0606 update: uniproxy 2022-11-18 04:00:14 +08:00
tokumeikoi
5c4e863560 update: new cipher 2022-11-18 03:25:02 +08:00
tokumeikoi
964376fa3c update: new cipher 2022-11-18 02:39:28 +08:00
tokumeikoi
7872516037 update: uniproxy 2022-11-17 02:19:11 +08:00
coldice945
3e0abe93ab feature: add a subscription information panel to Surge and Surfboard for easy viewing 2022-11-13 23:54:47 +08:00
tokumeikoi
a82b78d770 update: fix typo 2022-11-13 03:43:56 +08:00
tokumeikoi
3f7100f351 update: fix mgate 2022-11-02 18:47:22 +08:00
tokumeikoi
6d3927cf2a update: fix typo 2022-11-01 02:09:45 +08:00
tokumeikoi
99077b68f9 update: support fa-IR 2022-11-01 02:07:19 +08:00
tokumeikoi
3f8382aab2 update: version 2022-10-29 14:31:36 +08:00
tokumeikoi
37f1f64442 update: commission stat 2022-10-29 14:23:51 +08:00
tokumeikoi
44b2d56db9 update: fix order capacity 2022-10-26 21:06:01 +08:00
tokumeikoi
1a79a7e7f6 update: admin/stat/getOverride 2022-10-21 15:46:48 +08:00
tokumeikoi
30f0166ed1 update: commission stats 2022-10-21 15:44:15 +08:00
tokumeikoi
dc72c6dced update: compatible 2022-10-19 13:50:52 +08:00
tokumeikoi
d34c909bb0 update: compatible 2022-10-19 02:09:48 +08:00
tokumeikoi
1a0b09edd2 Merge pull request #542 from v2board/dev
1.6.1
2022-10-17 14:16:34 +08:00
tokumeikoi
ef8483a50f update: version 2022-10-16 19:40:26 +08:00
tokumeikoi
0f8641f2a3 update: readme 2022-10-16 19:40:04 +08:00
tokumeikoi
9accd71732 update: frontend 2022-10-16 19:18:04 +08:00
tokumeikoi
ce120bad63 update: remove old api 2022-10-16 15:26:13 +08:00
tokumeikoi
6503664fcc update: fix stat 2022-10-16 15:06:02 +08:00
tokumeikoi
3d1d4ac9d0 update: frontend 2022-10-16 03:17:23 +08:00
tokumeikoi
38e25e9039 update: fix stat 2022-10-15 17:43:12 +08:00
tokumeikoi
7907f455ce update: fix config char type 2022-10-15 13:32:14 +08:00
tokumeikoi
bc9cf36b2b Merge pull request #535 from coldice945/fix-surfboard-filename
fix exporting surfboard config file without file name
2022-10-12 02:02:27 +08:00
tokumeikoi
1ecf7120fe Merge pull request #537 from mmmdbybyd/dev
修复Stash无法获取订阅
2022-10-12 02:01:53 +08:00
tokumeikoi
b65fa65e75 Merge pull request #539 from betaxab/add-stripe-checkout-support
Stripe: add Checkout support & directly show the payments id
2022-10-12 02:00:45 +08:00
tokumeikoi
4a9e1a14af update: more feature 2022-10-12 01:35:07 +08:00
betaxab
4136f365a6 Stripe: add Checkout support & directly show the payments id 2022-10-09 13:51:44 +08:00
tokumeikoi
56ea13ea3b update: frontend 2022-10-02 20:52:32 +08:00
tokumeikoi
1d304d608b update: add cfw new feature 2022-10-01 03:20:05 +08:00
tokumeikoi
b6ce8314cd update: fix close ticket rules 2022-10-01 02:32:54 +08:00
tokumeikoi
59b0cb4ed9 update: fix undefined offset 2022-10-01 02:28:49 +08:00
tokumeikoi
8e23e74e53 update: add payment sort 2022-09-24 02:37:02 +08:00
mmmdbybyd
82a20ff72c Add the missing $this->isRegexe() 2022-09-22 15:11:08 +08:00
tokumeikoi
b6085fd34d update: ticket ui 2022-08-28 02:52:27 +08:00
tokumeikoi
e1a523b363 update: add knowledge search 2022-08-26 17:14:23 +08:00
coldice945
4d2e358784 update: fix exporting surfboard config file without file name 2022-08-22 10:47:46 +08:00
tokumeikoi
eebdf79b68 update: add view user traffic log 2022-08-22 02:13:41 +08:00
tokumeikoi
08ab004dd1 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2022-08-21 17:18:35 +08:00
tokumeikoi
10bf65a5f9 update: stat user view api 2022-08-21 17:18:24 +08:00
tokumeikoi
550e628972 update: horizon 2022-08-21 16:24:07 +08:00
tokumeikoi
28c5844777 Merge pull request #534 from coldice945/fix-surge-filename
fix exporting surge config file without file name
2022-08-19 02:17:31 +08:00
coldice945
c6317abba5 update: fix exporting surge config file without file name 2022-08-15 16:19:10 +08:00
tokumeikoi
36cb5f0bb5 update: ui 2022-08-12 01:52:11 +08:00
tokumeikoi
446d16c7da update: add commission history pagination 2022-08-10 01:31:20 +08:00
tokumeikoi
724abdd49f update: reset package ui 2022-08-10 01:02:28 +08:00
tokumeikoi
234faeebba Merge branch 'dev' of https://github.com/v2board/v2board into dev 2022-08-07 02:33:10 +08:00
tokumeikoi
ef366d8d8b update: reset day 2022-08-07 02:32:57 +08:00
tokumeikoi
1fd86aab63 update: have capacity 2022-08-03 14:52:42 +08:00
tokumeikoi
a456ecaa81 update: have capacity 2022-08-03 14:51:00 +08:00
tokumeikoi
bd1ec0fe89 update: fix capacity 2022-08-03 01:16:56 +08:00
tokumeikoi
1c644e8c5f update: package limit 2022-08-02 16:37:42 +08:00
tokumeikoi
b0687b9dfd Merge branch 'dev' of https://github.com/v2board/v2board into dev 2022-08-01 12:04:03 +08:00
tokumeikoi
9250d7b19c update: fix coupon 2022-07-31 19:12:31 +08:00
tokumeikoi
604cb807f1 update: fix typo 2022-07-29 22:02:00 +08:00
tokumeikoi
ef96fca97b update: remove horizon assets 2022-07-29 17:30:53 +08:00
tokumeikoi
1d62f87efc update: queue 2022-07-29 16:20:01 +08:00
tokumeikoi
3fdd6ac30c update: reset package 2022-07-29 13:46:29 +08:00
tokumeikoi
763efef9df update: telegram controller 2022-07-28 15:40:59 +08:00
tokumeikoi
1f88f74155 update: frontend 2022-07-28 15:14:50 +08:00
tokumeikoi
5ccf508040 update: new auth 2022-07-28 15:13:17 +08:00
tokumeikoi
3362287195 update: frontend 2022-07-28 15:11:14 +08:00
tokumeikoi
e18590c2f9 update: telegram group join request approve 2022-07-28 15:07:10 +08:00
tokumeikoi
5f573f5306 update: new auth 2022-07-28 15:05:48 +08:00
tokumeikoi
df8ea58456 update: add queue api 2022-07-20 03:09:06 +08:00
tokumeikoi
adf465696a update: new auth 2022-07-19 03:11:36 +08:00
tokumeikoi
49d9c453d8 update: fix stat server 2022-07-17 01:50:57 +08:00
tokumeikoi
8702a3489b update: fix coupon & server record rate issue 2022-07-17 00:59:35 +08:00
tokumeikoi
dc27410c12 update: new auth 2022-07-11 14:48:35 +08:00
tokumeikoi
2073727a0a update: check user 2022-07-08 15:50:24 +08:00
tokumeikoi
346d0222f5 update: fix 2022-07-08 12:07:36 +08:00
tokumeikoi
838fc7bdba update: rewrite buy limit 2022-07-08 02:36:33 +08:00
tokumeikoi
2823f1bd47 update: reset log 2022-07-07 04:04:47 +08:00
tokumeikoi
e6e7cbf48d update: ui 2022-07-07 03:34:02 +08:00
tokumeikoi
7713489945 update: inventory manage 2022-07-03 03:11:57 +08:00
tokumeikoi
90b5364039 update: inventory manage 2022-07-02 03:46:18 +08:00
tokumeikoi
ed8d4a3917 update: ui 2022-07-01 02:12:02 +08:00
tokumeikoi
e086586e8e update: add inventory limit 2022-06-30 03:22:17 +08:00
tokumeikoi
aa65440556 update: add inventory limit 2022-06-30 03:20:13 +08:00
tokumeikoi
8a8c6dd116 update: add inventory limit 2022-06-30 03:18:30 +08:00
tokumeikoi
cc6b07d7b8 update: add inventory limit 2022-06-29 03:57:12 +08:00
tokumeikoi
bdb10bed32 update: add login with mail link api 2022-06-27 01:47:18 +08:00
tokumeikoi
2b4e8f4b88 update: fix config utf8 2022-06-25 00:51:36 +08:00
tokumeikoi
d18904b631 update: frontend 2022-06-20 03:35:06 +08:00
tokumeikoi
fb48e1a721 update: remove v2ray viewconfig api 2022-06-12 23:20:24 +08:00
tokumeikoi
0e75b83507 Merge pull request #529 from v2board/dev
patch-1
2022-06-12 21:49:28 +08:00
tokumeikoi
a0b14029cd update: fix reset package 2022-06-12 21:48:31 +08:00
tokumeikoi
cb631920a1 Merge pull request #528 from v2board/dev
1.6.0
2022-06-12 20:11:06 +08:00
tokumeikoi
0f7d787622 update: version 2022-06-12 20:10:43 +08:00
tokumeikoi
7a64038133 Merge pull request #527 from v2board/dev
1.6.0
2022-06-12 19:56:18 +08:00
tokumeikoi
12767350ef update: version 2022-06-12 19:54:48 +08:00
tokumeikoi
c992e0bde6 update: version 2022-06-12 19:51:43 +08:00
tokumeikoi
ecef0315a0 Merge branch 'dev' 2022-06-12 19:47:56 +08:00
tokumeikoi
4863e7577a Merge pull request #501 from betaxab/tgbot-add-at-support
TelegramController: add at support for Telegram Bot command
2022-06-12 19:39:43 +08:00
tokumeikoi
dec00ebe54 update: order save 2022-06-11 15:28:55 +08:00
tokumeikoi
5bb5cbe751 update: fix mail log char type 2022-06-11 01:13:21 +08:00
tokumeikoi
cdadbf6509 update: fix mail log char type 2022-06-11 01:12:23 +08:00
tokumeikoi
3fb600a3b0 update: fix stat server 2022-06-09 15:42:26 +08:00
tokumeikoi
3840df1203 update: add traffic reset method 2022-06-08 02:19:19 +08:00
tokumeikoi
b24041cc23 update: custom show more info to server subscribe 2022-06-07 22:11:13 +08:00
tokumeikoi
da2f942a28 update: config save 2022-06-01 12:37:31 +08:00
tokumeikoi
4a9e0ba94c update: reset by next year 2022-05-30 15:42:25 +08:00
tokumeikoi
cdd55eae4c Merge branch 'dev' of https://github.com/v2board/v2board into dev 2022-05-30 14:28:25 +08:00
tokumeikoi
fe1ab11bbd update: reset by next year 2022-05-30 14:28:15 +08:00
tokumeikoi
1d1ac37d4d Merge pull request #524 from coldice945/fix-dowload-app-name 2022-05-30 03:55:10 +08:00
tokumeikoi
25dc7294f2 Merge pull request #523 from betaxab/update-subs-config 2022-05-30 03:52:34 +08:00
tokumeikoi
4ad44c5f45 update: guzzle 2022-05-29 22:20:17 +08:00
tokumeikoi
7dc626650f update: ui 2022-05-29 22:04:41 +08:00
tokumeikoi
6b978c421a update: config save 2022-05-29 21:21:24 +08:00
tokumeikoi
304e67a632 update: frontend 2022-05-29 15:26:18 +08:00
tokumeikoi
9d3ba5dd62 update: frontend 2022-05-29 02:48:16 +08:00
tokumeikoi
1dba8e3f0d update: frontend 2022-05-29 02:39:19 +08:00
tokumeikoi
2426d88339 update: support plan utf8mb4 2022-05-29 02:14:35 +08:00
tokumeikoi
5e7f782583 update: config save 2022-05-28 21:24:32 +08:00
Haniel Cui
f81f6e0716 Delete missing codes that need to be removed 2022-05-23 10:13:56 +08:00
coldice945
25cae43430 修复如果网站名称为中文的时候,CFW and Stash 不显示文件名或显示文件名乱码的问题 2022-05-19 23:01:11 +08:00
tokumeikoi
84f8089604 update: fix theme init 2022-05-19 02:34:25 +08:00
tokumeikoi
6d6ab5543a update: theme config 2022-05-16 17:31:37 +08:00
tokumeikoi
1ddc05652d update: custom logo 2022-05-15 01:46:35 +08:00
tokumeikoi
7d92714fa9 update: error message 2022-05-15 01:12:25 +08:00
tokumeikoi
649c03c214 update: error message 2022-05-15 01:10:27 +08:00
tokumeikoi
fae48e2c81 update: fix client protocol 2022-05-12 03:38:28 +08:00
tokumeikoi
bd0834bd3f update: shadowsocks obfs pre support 2022-05-12 02:39:59 +08:00
tokumeikoi
c09ab693bb update: fix typo 2022-05-11 01:01:55 +08:00
tokumeikoi
709929a5a3 update: fix typo 2022-05-10 14:16:04 +08:00
tokumeikoi
ca9847cc45 update: shell 2022-05-10 13:52:52 +08:00
tokumeikoi
512c48adeb update: fix config save 2022-05-10 01:07:12 +08:00
tokumeikoi
9d9c977ff1 update: force https 2022-05-09 23:46:33 +08:00
tokumeikoi
ed749f85ae update: get subscribe url 2022-05-09 23:26:59 +08:00
tokumeikoi
d74ab728fe update: shell 2022-05-09 01:45:33 +08:00
tokumeikoi
e72d28e2b3 update: theme 2022-05-09 01:05:20 +08:00
tokumeikoi
2f977c937f update: frontend version 2022-05-09 00:34:16 +08:00
tokumeikoi
531a3a5dc4 update: theme 2022-05-09 00:33:17 +08:00
tokumeikoi
74265a5b59 update: theme 2022-05-09 00:30:44 +08:00
tokumeikoi
db06001254 update: theme 2022-05-09 00:26:38 +08:00
tokumeikoi
8311722fda update: checkout round 2022-05-06 16:54:25 +08:00
tokumeikoi
5bd811e217 update: sql 2022-05-06 13:53:31 +08:00
tokumeikoi
20e365e771 update: custom theme 2022-05-03 14:28:00 +08:00
tokumeikoi
a0ebcb948b update: custom theme 2022-05-03 03:16:16 +08:00
tokumeikoi
32bb9fccb5 update: new server backend 2022-05-02 15:20:02 +08:00
tokumeikoi
a4a70525df update: xproxy controller 2022-05-02 13:37:11 +08:00
tokumeikoi
3f32fadc85 update: server 2022-05-02 13:35:02 +08:00
tokumeikoi
fae1e1f945 update: remove useless field 2022-05-02 03:50:36 +08:00
tokumeikoi
12caada8dd update: surplus by onetime 2022-04-30 02:38:22 +08:00
tokumeikoi
7ec4b06536 update: surplus by onetime 2022-04-29 16:56:22 +08:00
tokumeikoi
b5a614d901 update: ticket 2022-04-28 00:58:37 +08:00
tokumeikoi
f40480c918 update: frontend 2022-04-26 10:28:12 +08:00
tokumeikoi
c8e2d54d2b update: support laravel 8 2022-04-26 01:07:03 +08:00
tokumeikoi
ecb085343e update: support laravel 8 2022-04-26 01:06:36 +08:00
tokumeikoi
e0404a6b49 update: frontend 2022-04-25 03:18:12 +08:00
Alpha Kane
58731faf23 rules: update rules to fix issues on some websites
clash: Change to more working DOH servers
clash, surge, surfboard:
Fix wrong direct connection on some websites
Fix lineme cannot receive photos
2022-04-25 00:56:18 +08:00
tokumeikoi
472a692b3c update: logic optimization 2022-04-19 04:10:02 +08:00
tokumeikoi
b49ffcbfb4 update: frontend 2022-04-17 14:40:21 +08:00
tokumeikoi
22ddb0086a update: frontend 2022-04-17 14:23:00 +08:00
tokumeikoi
aa9bbf8009 update: frontend 2022-04-17 14:13:50 +08:00
tokumeikoi
82584cb18d update: payment 2022-04-17 02:17:20 +08:00
tokumeikoi
a1a95ea9c8 update: fix new ui some bug 2022-04-17 00:42:04 +08:00
tokumeikoi
4ef5a4ca81 update: ui 2022-04-16 23:13:32 +08:00
tokumeikoi
f046a396d6 update: ui 2022-04-16 22:58:41 +08:00
tokumeikoi
d20dce7f69 update: add notice tags 2022-04-15 02:45:52 +08:00
tokumeikoi
0bcaf2889a Merge pull request #521 from betaxab/fix-surge-subs
Protocols: Surge: fix surge & surfboard MANAGED-CONFIG URL
2022-04-15 01:07:47 +08:00
tokumeikoi
5466e4dbba Merge pull request #519 from betaxab/fix-coinpayments-ipn
Payments: fix CoinPayments IPN Notification
2022-04-15 01:07:37 +08:00
tokumeikoi
7b2fa79cdf Merge pull request #516 from betaxab/rename-sagernet
Protocols: rename AnXray to SagerNet
2022-04-15 01:03:57 +08:00
tokumeikoi
e2597b4ac3 update: fix generate coupon 2022-04-15 01:02:06 +08:00
tokumeikoi
7faa56a4fd update: remove cache token 2022-04-15 00:58:11 +08:00
Alpha Kane
66815f4b91 Protocols: Surge: fix surge & surfboard MANAGED-CONFIG URL 2022-04-13 19:40:12 +08:00
tokumeikoi
84ce0cc0c9 update: frontend 2022-04-12 23:32:53 +08:00
tokumeikoi
f439040375 update: ticket save lock 2022-04-11 11:30:53 +08:00
tokumeikoi
27271e3ffb update: rollback token cache 2022-04-11 10:19:36 +08:00
tokumeikoi
e82f28b670 update: add client token in cache 2022-04-10 18:08:26 +08:00
tokumeikoi
447ff0f554 update: set default url to app url 2022-04-10 14:43:25 +08:00
tokumeikoi
9dfe44bf82 update: add qr subscribe 2022-04-09 16:55:44 +08:00
tokumeikoi
6b1f3a73c4 update: sql 2022-04-09 16:08:36 +08:00
tokumeikoi
8fdd755107 update: frontend 2022-04-09 16:05:53 +08:00
tokumeikoi
077c8ba0e8 update: frontend 2022-04-06 15:06:45 +08:00
tokumeikoi
92d236f5c0 update: mgate 2022-04-03 19:55:58 +08:00
Alpha Kane
24e896d301 Payments: fix CoinPayments IPN Notification 2022-04-01 19:25:45 +08:00
tokumeikoi
5b293f4cb0 update: add register ip limit 2022-03-29 21:25:29 +08:00
tokumeikoi
e3ffdb7bce update: add register ip limit 2022-03-29 21:11:37 +08:00
tokumeikoi
7c473d6325 update: register ip limit 2022-03-29 20:57:19 +08:00
tokumeikoi
d4183d2c7f update: user get traffic log 2022-03-29 20:45:54 +08:00
tokumeikoi
9d45b71731 update: register limit by ip 2022-03-29 20:39:22 +08:00
tokumeikoi
c6cc307147 update: optimization stat user 2022-03-29 15:32:40 +08:00
tokumeikoi
b1fc316e3d update: optimization stat user 2022-03-29 15:30:07 +08:00
tokumeikoi
983b752f20 update: optimization stat user 2022-03-29 15:00:05 +08:00
tokumeikoi
af2f6a31da update: optimization stat user 2022-03-29 14:57:09 +08:00
tokumeikoi
20b60d553c update: drop alert 2022-03-25 13:45:06 +08:00
Alpha Kane
f97d9fcc81 Protocols: rename AnXray to SagerNet 2022-03-25 02:33:32 +08:00
tokumeikoi
4d657823f6 update: frontend 2022-03-20 14:43:20 +08:00
tokumeikoi
791ba463c3 update: add payment show 2022-03-18 02:49:51 +08:00
tokumeikoi
6a2125794b update: payment save 2022-03-18 02:41:30 +08:00
tokumeikoi
8f3281c60e update: add payment handling fee 2022-03-17 17:33:39 +08:00
tokumeikoi
88187f5a6c update: add payment handling fee 2022-03-17 14:53:26 +08:00
tokumeikoi
a5ea79493c update: add payment handling fee 2022-03-17 14:50:02 +08:00
tokumeikoi
1111c6f13d update: support cf etag 2022-03-15 22:40:30 +08:00
tokumeikoi
fc2b4bd422 update: add etag 2022-03-15 21:45:15 +08:00
tokumeikoi
d76c2b3bca update: fix coupon multi generate 2022-03-13 03:41:46 +08:00
tokumeikoi
82730acdac update: fix coupon multi generate 2022-03-13 01:16:13 +08:00
tokumeikoi
d184225b2b udpate: invite details 2022-03-11 23:08:57 +08:00
tokumeikoi
bdf65247e0 update: fix commission statistics 2022-03-11 13:34:10 +08:00
tokumeikoi
1e9c16543d update: fix sql 2022-03-11 01:12:49 +08:00
tokumeikoi
d42c271942 update: fix knowledge access data foreach 2022-03-10 16:33:23 +08:00
tokumeikoi
d5504354bc update: fix knowledge access data foreach 2022-03-10 16:16:14 +08:00
tokumeikoi
2eb428fc3c update: remove hitokoto & add reset admin password 2022-03-10 11:31:44 +08:00
tokumeikoi
8832fde4fa update: fix get server last rank 2022-03-10 11:04:09 +08:00
tokumeikoi
7f848ccb13 update: fix admin currency show 2022-03-09 22:20:34 +08:00
tokumeikoi
dd0b60071e update: frontend 2022-03-09 21:18:33 +08:00
tokumeikoi
fe80d5e89b update: commission log 2022-03-09 17:14:36 +08:00
tokumeikoi
93eaf0ae36 Merge pull request #510 from v2board/dev
1.5.5 patch 1
2022-03-09 02:41:27 +08:00
tokumeikoi
9f6683592c update: fix alipay f2f sdk 2022-03-09 02:40:29 +08:00
tokumeikoi
e46217e085 Merge pull request #509 from v2board/dev
1.5.5
2022-03-09 00:22:02 +08:00
tokumeikoi
5d051994be Merge branch 'dev' of https://github.com/v2board/v2board into dev 2022-03-09 00:21:34 +08:00
tokumeikoi
c399c7a6dd update: fix word 2022-03-09 00:21:12 +08:00
tokumeikoi
e40d9231e7 Merge pull request #508 from v2board/dev
1.5.5
2022-03-08 22:25:19 +08:00
tokumeikoi
daece8dac3 Merge pull request #504 from ryosukeeeeee/dev
add coinbase payment
2022-03-08 22:24:41 +08:00
tokumeikoi
6ac0538513 Merge pull request #505 from ryosukeeeeee/btcpay
add btcpay
2022-03-08 22:24:32 +08:00
tokumeikoi
f1598d1c74 update: version 2022-03-08 22:19:37 +08:00
tokumeikoi
01b7c3b2b0 update: fix word 2022-03-08 22:00:45 +08:00
tokumeikoi
0f95918df3 update: frontend 2022-03-08 13:35:52 +08:00
tokumeikoi
8283dd7fc1 update: tidalab server sdk 2022-03-06 23:07:55 +08:00
tokumeikoi
63fe749cf4 update: frontend 2022-03-06 22:06:57 +08:00
ryosuke
828a4ffe39 add btcpay 2022-03-06 15:29:08 +08:00
tokumeikoi
85e6dd7210 update: stat server job 2022-03-06 13:37:21 +08:00
tokumeikoi
5c5500bb2d update: frontend 2022-03-06 13:22:37 +08:00
ryosuke
d3e81a1b00 add coinbase 2022-03-05 23:52:10 +08:00
tokumeikoi
c60cd0a34a update: reset traffic 2022-03-05 13:37:49 +08:00
tokumeikoi
e9c79c1c08 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2022-03-05 12:40:26 +08:00
tokumeikoi
9ff76dedd7 update: order service 2022-03-05 12:40:11 +08:00
tokumeikoi
535cf0df12 Merge pull request #499 from betaxab/add-coinpayments
Payments: add CoinPayments support
2022-03-05 01:20:36 +08:00
tokumeikoi
5aef3f20d5 Merge pull request #502 from betaxab/update-lang-php
lang: update language
2022-03-05 01:17:28 +08:00
tokumeikoi
4c9a38f722 update: frontend 2022-03-05 01:08:19 +08:00
tokumeikoi
5b5293bfab update: frontend 2022-03-05 00:42:30 +08:00
tokumeikoi
98b12205f7 update: server/getServerLog to stat/getTrafficLog 2022-03-05 00:27:43 +08:00
tokumeikoi
d0cab99ae4 update: order service 2022-03-04 22:48:44 +08:00
tokumeikoi
b85eb72d1a update: fix coupon error 2022-03-01 18:45:14 +08:00
tokumeikoi
5345823b63 update: css 2022-03-01 12:54:29 +08:00
tokumeikoi
3795557bc5 update: fix onetime refund issue 2022-02-26 00:47:53 +08:00
tokumeikoi
632205fb6c update: fix onetime refund issue 2022-02-26 00:42:14 +08:00
tokumeikoi
ebfba1b178 update: sql 2022-02-25 17:10:43 +08:00
Alpha Kane
8645002d54 Payments: add CoinPayments support 2022-02-25 02:36:00 +08:00
tokumeikoi
1813fdca5d update: alipay f2f sdk 2022-02-24 23:26:51 +08:00
tokumeikoi
33b59d126e update: alipay f2f sdk 2022-02-24 23:09:04 +08:00
tokumeikoi
313ac9d27f update: alipay f2f sdk 2022-02-24 21:11:32 +08:00
tokumeikoi
5fde09344c update: payment service update 2022-02-24 21:02:10 +08:00
tokumeikoi
e9cd4a1b27 update: fix knowledge css lost 2022-02-24 14:59:58 +08:00
tokumeikoi
40d757dda3 update: payment service 2022-02-24 14:47:00 +08:00
tokumeikoi
980f1e3093 update: clash protocol 2022-02-23 22:01:46 +08:00
tokumeikoi
92caf3fc20 update: admin text 2022-02-23 18:14:01 +08:00
tokumeikoi
4eb3e23ddc update: payment add custom notify domain name 2022-02-23 16:07:18 +08:00
Alpha Kane
1c569a2d45 lang: update language 2022-02-22 02:42:13 +08:00
tokumeikoi
2ceb910812 update: admin set habit 2022-02-22 02:12:06 +08:00
tokumeikoi
0309befb47 update: fix admin network settings box 2022-02-22 01:55:00 +08:00
tokumeikoi
dcb45ab6ed update: admin 2022-02-20 01:41:52 +08:00
tokumeikoi
c270f3ab5a update: stat user 2022-02-20 01:06:02 +08:00
tokumeikoi
23f98d7abc update: server service 2022-02-19 13:45:57 +08:00
tokumeikoi
60b6a6177d update: server service 2022-02-19 13:44:03 +08:00
Alpha Kane
0697d1cd7a TelegramController: add at support for Telegram Bot command 2022-02-17 21:04:24 +08:00
tokumeikoi
766d1193c7 update: add random port 2022-02-17 03:17:05 +08:00
tokumeikoi
a1ada9183c update: add random port 2022-02-17 02:44:49 +08:00
tokumeikoi
c8c96365ea update: mgate 2022-02-16 22:42:19 +08:00
tokumeikoi
57a746d52b update: user sort 2022-02-16 03:14:05 +08:00
tokumeikoi
8d2d1b25a3 update: ui 2022-02-14 01:26:13 +08:00
tokumeikoi
659cc987fa update: clash config 2022-02-13 20:48:49 +08:00
tokumeikoi
8cd29641c6 update: ui 2022-02-13 03:18:24 +08:00
tokumeikoi
0efd0d5e99 update: ui 2022-02-13 01:09:55 +08:00
tokumeikoi
391ad04447 update: ui 2022-02-13 01:07:30 +08:00
tokumeikoi
04e9109d90 update: new login ui 2022-02-13 01:00:14 +08:00
tokumeikoi
e990468d18 update: compatible api 2022-02-12 01:33:08 +08:00
tokumeikoi
6508b289e0 update: script 2022-02-11 02:02:18 +08:00
tokumeikoi
ad50e77422 update: ui font size 2022-02-11 00:16:46 +08:00
tokumeikoi
7fa3d1e58f update: invite 2022-02-10 15:15:41 +08:00
tokumeikoi
659fa85b1d update: mgate sdk custom notify domain 2022-02-09 12:18:42 +08:00
tokumeikoi
21a9074b3f update: fix reset traffic 2022-02-09 02:58:49 +08:00
tokumeikoi
13327d004d update: fix order/detail 2022-02-09 02:39:19 +08:00
tokumeikoi
adc9e9e241 update: rename order/detail api 2022-02-07 15:47:06 +08:00
tokumeikoi
4305e3a246 update: optimize commission calculation 2022-02-07 15:36:53 +08:00
tokumeikoi
1d5a493ef1 update: optimize commission calculation 2022-02-07 15:35:40 +08:00
tokumeikoi
9dc44c0e1d update: optimize commission calculation 2022-02-07 15:05:56 +08:00
tokumeikoi
8a18bdf9c3 update: optimize commission calculation 2022-02-07 15:01:11 +08:00
tokumeikoi
03846022ef update: fix word 2022-02-07 14:54:13 +08:00
tokumeikoi
8a7297d7cd update: add kr and tw language 2022-02-07 14:37:04 +08:00
tokumeikoi
7435e9f9cc update: telegram commands 2022-01-27 15:20:03 +08:00
tokumeikoi
78a87125c8 update: reply ticket for telegram 2022-01-27 15:18:41 +08:00
tokumeikoi
e363666b89 update: fix telegram message format 2022-01-27 14:56:44 +08:00
tokumeikoi
ca9d7df3b5 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2022-01-27 01:46:37 +08:00
tokumeikoi
5bf1dd3426 update: telegram bot modularization 2022-01-27 01:46:26 +08:00
tokumeikoi
2d348cb078 Merge pull request #495 from betaxab/surgeaead
Protocols: Surge: add VMess AEAD support
2022-01-23 01:24:10 +08:00
tokumeikoi
1790de63f6 update: add coupon and notice switch 2022-01-22 02:30:05 +08:00
tokumeikoi
94a7ab412c update: fix word 2022-01-22 02:07:18 +08:00
tokumeikoi
e89c84ad0e update: command 2022-01-22 02:06:53 +08:00
tokumeikoi
6f849664cc update: admin replyed reopen ticket 2022-01-22 01:52:02 +08:00
tokumeikoi
36f87bd61f update: rewrite alipay f2f sdk 2022-01-22 01:46:34 +08:00
tokumeikoi
bd73c3f03a update: fix word 2022-01-22 00:08:01 +08:00
tokumeikoi
be1f030deb update: admin 2022-01-18 13:34:18 +08:00
tokumeikoi
a5acb86c66 update: fix dark mode 2022-01-16 15:32:10 +08:00
tokumeikoi
1f25edaaa4 update: stash regex 2022-01-11 23:39:17 +08:00
tokumeikoi
90f9c181a0 update: clash regex 2022-01-11 20:33:21 +08:00
Alpha Kane
e584a95767 Protocols: Surge: add VMess AEAD support 2022-01-10 21:45:17 +08:00
tokumeikoi
2b698b63e0 update: stash compatible 2022-01-10 01:53:14 +08:00
tokumeikoi
ec946918c9 update: clash compatible 2022-01-10 01:52:23 +08:00
tokumeikoi
3e78ac1625 Merge pull request #494 from betaxab/newv2rayconfig 2022-01-07 10:55:37 +08:00
Alpha Kane
da90ea106b ServerService: update v2ray configuration 2022-01-07 03:35:24 +08:00
tokumeikoi
9b21cca314 update: v2ray config 2022-01-07 02:20:34 +08:00
tokumeikoi
3ea8781146 update: server log 2022-01-06 15:42:20 +08:00
tokumeikoi
1bcffc1dd6 update: mgate sdk 2022-01-06 03:22:14 +08:00
tokumeikoi
da51d267e2 update: rebuild traffic log page 2022-01-05 23:41:06 +08:00
tokumeikoi
1716f2f6ca update: change server log api to getServerLogs 2022-01-05 23:18:38 +08:00
tokumeikoi
b4e657f463 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2022-01-05 15:08:33 +08:00
tokumeikoi
870e82e155 update: fix reset day show 2022-01-05 15:07:48 +08:00
tokumeikoi
77e9e3adeb Merge pull request #493 from DesperadoJ/dev
update: latest Clash vmess-ws configuration style
2022-01-05 03:17:20 +08:00
tokumeikoi
c3a74e6610 update: ui optimization 2022-01-05 01:11:24 +08:00
tokumeikoi
9fde0b35eb update: sort ui 2022-01-04 23:10:35 +08:00
DesperadoJ
85c52f6499 Merge branch 'v2board:dev' into dev 2022-01-04 19:14:38 +08:00
tokumeikoi
50768735a8 update: version 2022-01-04 13:41:35 +08:00
tokumeikoi
ab5fce51a1 update: remove v2ray alterid 2022-01-04 13:40:35 +08:00
tokumeikoi
54ea079d4d Merge branch 'dev' of https://github.com/v2board/v2board into dev 2022-01-04 03:00:47 +08:00
tokumeikoi
bded1a4ee5 update: default theme custom file 2022-01-04 03:00:37 +08:00
DesperadoJ
546e53ed2e update: latest Clash vmess-ws configuration style 2022-01-03 14:44:13 +08:00
tokumeikoi
339ab3925f Merge pull request #492 from v2board/dev
1.5.4
2022-01-01 00:02:25 +08:00
tokumeikoi
927d575255 update: fix admin dark mode 2022-01-01 00:01:06 +08:00
tokumeikoi
796bcc4ab3 Merge pull request #491 from v2board/dev
update: fix admin dark mode
2021-12-31 23:56:12 +08:00
tokumeikoi
552a80f5f9 update: fix admin dark mode 2021-12-31 23:55:23 +08:00
tokumeikoi
d78a8a2a9f Merge pull request #490 from v2board/dev
1.5.4
2021-12-31 23:30:59 +08:00
tokumeikoi
3fad649377 update: cache version 2021-12-31 23:30:34 +08:00
tokumeikoi
389db6fb97 Merge pull request #489 from v2board/dev
1.5.4
2021-12-31 23:10:22 +08:00
tokumeikoi
080af12f39 Merge pull request #487 from Mr-Sheep/master
fix issue with surge subscribe
2021-12-31 23:09:57 +08:00
tokumeikoi
ecbdd12a6d Merge pull request #488 from betaxab/updaterules
rules: update clash & surge rules
2021-12-31 23:08:34 +08:00
tokumeikoi
3ade2bf89c update: code optimization 2021-12-31 01:49:27 +08:00
tokumeikoi
45a5022d82 update: logic optimization 2021-12-31 01:48:21 +08:00
tokumeikoi
e1a8d6ca35 update: logic optimization 2021-12-30 00:20:30 +08:00
tokumeikoi
5df2504f73 update: fix 2021-12-28 15:12:25 +08:00
Alpha Kane
708a83017b rules: update clash & surge rules
1. linkedin will be proxy
2. no longer use jsdelivr, avoid ruleset resources failing to load
3. update app.clash.yaml
2021-12-28 13:29:04 +08:00
tokumeikoi
edb2e7956c update: sql 2021-12-28 12:04:30 +08:00
tokumeikoi
a557aa6d32 update: add coupon limit period 2021-12-28 01:59:59 +08:00
tokumeikoi
6ed9cc559e update: more 2021-12-28 01:49:33 +08:00
tokumeikoi
6718a61890 update: support stash 2021-12-27 00:13:27 +08:00
tokumeikoi
333611da48 update: support stash 2021-12-27 00:05:28 +08:00
tokumeikoi
8ee7839d00 update: admin comission manage 2021-12-26 20:13:53 +08:00
tokumeikoi
e29261a87f update: get reset day 2021-12-26 19:35:24 +08:00
tokumeikoi
ba75d6e993 update: add send mass mail queue 2021-12-26 19:21:29 +08:00
tokumeikoi
ce8b5dde28 update: fix telegram markdown 2021-12-23 14:25:05 +08:00
tokumeikoi
55357a1b0e update: fix markdown parse 2021-12-17 13:35:54 +08:00
tokumeikoi
0f777f1a77 update: add telegram discuss link & ui 2021-12-17 01:53:03 +08:00
tokumeikoi
8fa73c2d4b update: remove favicon 2021-12-15 22:13:06 +08:00
tokumeikoi
80ae5f17b7 update: invite copy link 2021-12-15 21:35:01 +08:00
tokumeikoi
de4f01f653 update: invite copy link 2021-12-15 21:23:06 +08:00
tokumeikoi
bbfabdb72f update: add clash regex filter 2021-12-13 14:26:05 +08:00
tokumeikoi
b392fa3345 update: payment icon 2021-12-13 14:24:53 +08:00
Snorlax
ecfb9ce8b0 Fix Surge Subscribing issue 2021-12-11 21:24:27 +08:00
tokumeikoi
a69eb4058b update: ui & payment icons 2021-12-07 16:34:44 +08:00
tokumeikoi
8d56377c8a update: fix user commission show 2021-12-02 14:03:39 +08:00
tokumeikoi
30aec3d8e9 update: add test send mail 2021-11-30 16:23:46 +08:00
tokumeikoi
05769ea591 update: fix some bug 2021-11-24 13:33:42 +08:00
tokumeikoi
fb8bcdcbe0 update: fix user filter 2021-11-24 13:15:28 +08:00
tokumeikoi
39c47a06eb update: fix darkreader 2021-11-23 23:05:55 +08:00
tokumeikoi
4c71eb5633 update: fix dark mode qrcode 2021-11-23 01:42:00 +08:00
tokumeikoi
b8df411ffe update: fix darkrader 2021-11-22 03:20:52 +08:00
tokumeikoi
66728f13ea update: ui components language 2021-11-15 02:35:13 +08:00
tokumeikoi
7b1a8ee5ff update: add custom currency 2021-11-14 02:43:41 +08:00
tokumeikoi
9d96d68f12 update: reset traffic 2021-11-11 01:25:45 +08:00
tokumeikoi
d0947d1aaa update: fix coupon use 2021-11-01 02:31:27 +08:00
tokumeikoi
6f67096fe3 update: order callback no filter & save notice 2021-10-31 21:08:14 +08:00
tokumeikoi
44b369d0e7 update: fix subscribe loading 2021-10-28 14:59:48 +08:00
tokumeikoi
04fcd6758d update: fix v2ray config edit 2021-10-27 12:59:25 +08:00
tokumeikoi
99f3004d6b update: remove get subscribe id 2021-10-25 01:44:51 +08:00
tokumeikoi
56c726b173 update: fix payment update 2021-10-22 00:00:49 +08:00
tokumeikoi
37a6f3861c update: add payment save request validate 2021-10-18 17:10:40 +08:00
tokumeikoi
90211c1018 update: fix user filter 2021-10-16 20:47:27 +08:00
tokumeikoi
cc3e3b3fd1 update: fix background 2021-10-15 15:14:25 +08:00
tokumeikoi
10d3feb57c update: check order 2021-10-14 15:45:42 +08:00
tokumeikoi
eb98d706ae update: shell 2021-10-13 21:23:18 +08:00
tokumeikoi
4996c317c9 update: install 2021-10-13 21:04:23 +08:00
tokumeikoi
f380bd3c0e update: install 2021-10-13 21:04:02 +08:00
tokumeikoi
8e7f0bbc44 update: shell 2021-10-13 21:00:21 +08:00
tokumeikoi
53011cb95c Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-10-11 21:46:45 +08:00
tokumeikoi
037e22aa2d update: getInfo show uuid 2021-10-11 21:39:55 +08:00
tokumeikoi
ffd5eb269d Merge pull request #485 from betaxab/update-surfboard 2021-10-11 00:28:09 +09:00
tokumeikoi
68def9b4de update: fix filter 2021-10-10 01:07:58 +08:00
tokumeikoi
a4d617608f update: fix filter 2021-10-09 23:21:39 +08:00
tokumeikoi
364e259188 update: fix v2ray config drawer 2021-10-08 20:29:55 +08:00
tokumeikoi
b18c810f7f update: fix v2ray config drawer 2021-10-08 20:00:34 +08:00
Alpha Kane
239eb2075c Protocols: Surfboard: The SS can now be directly supported 2021-10-07 23:01:39 +08:00
tokumeikoi
b40272a8fa update: fix user generate 2021-10-06 00:20:28 +08:00
tokumeikoi
a6d5a433b0 update: composer 2021-10-05 21:03:27 +08:00
tokumeikoi
fe0f0afa53 update: dark mode 2021-10-04 23:10:11 +08:00
tokumeikoi
9ff524fd15 update: fix context menu 2021-10-04 21:45:17 +08:00
tokumeikoi
f6d9e6e7f6 update: fix v2ray network settings check 2021-10-04 14:37:32 +08:00
tokumeikoi
d911069e18 update: fix table context menu 2021-10-04 12:07:55 +08:00
tokumeikoi
32c539d2c0 Merge pull request #484 from v2board/dev
1.5.3
2021-10-03 22:19:10 +09:00
tokumeikoi
52fa1ce6af update: language 2021-10-01 20:18:40 +09:00
tokumeikoi
82d2d91582 update: user 2021-10-01 19:30:49 +09:00
tokumeikoi
8085c2ba6a update: schedule 2021-10-01 17:37:49 +09:00
tokumeikoi
b60fde5762 update: fix charts 2021-09-27 14:58:58 +09:00
tokumeikoi
3b51f12ab1 update: rollback app rule 2021-09-26 22:09:19 +09:00
tokumeikoi
ab4e66a5b6 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-09-26 22:07:34 +09:00
tokumeikoi
1856e0e87e update: fix support safari 15 theme color 2021-09-26 22:07:24 +09:00
tokumeikoi
e20ae29fb1 Merge pull request #483 from betaxab/fix-clash-rule-for-gp
rules: clash: Solve Google Play app download issue on China phones
2021-09-26 20:57:06 +09:00
tokumeikoi
cfd528739a update: fix en-us 2021-09-26 20:44:47 +09:00
tokumeikoi
dd23609658 update: change charts component 2021-09-25 00:33:11 +09:00
tokumeikoi
c7f3cf9e67 update: support safari theme 2021-09-24 01:50:58 +09:00
tokumeikoi
ddf2f45d6c update: user filter 2021-09-22 21:13:20 +09:00
tokumeikoi
ba3de90733 update: fix commission 2021-09-22 18:04:16 +09:00
tokumeikoi
43f4ecce93 update: fix 2021-09-21 22:30:00 +09:00
tokumeikoi
30b3587771 update: fix 2021-09-21 19:11:14 +09:00
tokumeikoi
6ab9a4d54d update: email notice default off 2021-09-21 19:07:53 +09:00
tokumeikoi
7a4bd468a2 update: add plan reset method 2021-09-21 18:51:53 +09:00
Alpha Kane
c97d37a070 rules: Solve Google Play app download issue on China phones
Update: app.clash.yaml up-to-date
2021-09-20 10:52:37 +08:00
tokumeikoi
25b3b11efd update: add commission distribution 2021-09-18 21:04:35 +09:00
tokumeikoi
edfc4043e8 update: add commission distribution 2021-09-18 20:53:34 +09:00
tokumeikoi
ab9abf5b93 update: crisp more data 2021-09-14 20:31:56 +09:00
tokumeikoi
6dd199631b update: crisp more data 2021-09-14 20:07:57 +09:00
tokumeikoi
4b863d681e update: crisp more data 2021-09-14 19:47:57 +09:00
tokumeikoi
5d6010045d update: support md5 with sha256 2021-09-14 13:12:44 +09:00
tokumeikoi
0374a03892 update: support md5 with sha256 2021-09-14 13:10:29 +09:00
tokumeikoi
ec00fc4496 update: support md5 with sha256 2021-09-14 02:38:24 +09:00
tokumeikoi
a365357770 update: fix stat 2021-09-07 12:49:35 +09:00
tokumeikoi
14579d1eea update: update alert 2021-09-07 03:49:04 +09:00
tokumeikoi
9f6d1ada93 update: v2board install 2021-09-07 03:18:45 +09:00
tokumeikoi
895976b830 update: horizon config 2021-09-07 01:47:29 +09:00
tokumeikoi
4b011225db Merge pull request #482 from betaxab/subs-domain-force-direct 2021-09-07 01:33:37 +09:00
tokumeikoi
243aed3f55 Merge pull request #480 from betaxab/horizon-metrics 2021-09-07 01:33:24 +09:00
tokumeikoi
d25d7ba69b Merge pull request #479 from betaxab/update 2021-09-07 01:33:16 +09:00
Beta Soft
c0c84efb7c Protocols: force the current subscription domain to be a direct rule 2021-09-06 18:26:03 +08:00
tokumeikoi
91c7dfc0ed update: add no reset method 2021-09-06 02:31:45 +09:00
tokumeikoi
6830e6af38 update: push tag 2021-09-06 01:24:41 +09:00
tokumeikoi
cb75579772 update: fix push tag 2021-09-06 01:19:43 +09:00
tokumeikoi
e980c2d8f3 update: set horizon with mem 2021-09-05 18:05:25 +09:00
Beta Soft
b6195494d3 Kernel: add horizon snapshot schedule 2021-09-05 10:32:33 +08:00
tokumeikoi
ccf3497241 update: register api 2021-09-04 21:41:57 +09:00
tokumeikoi
c80d93fa25 update: order queue 2021-09-04 16:31:25 +09:00
tokumeikoi
c2577e37c4 update: server log 2021-09-03 00:31:12 +09:00
tokumeikoi
c183462ef3 update: server log 2021-09-02 21:09:04 +09:00
tokumeikoi
2a5e9ef079 update: rollback 2021-09-02 20:28:24 +09:00
tokumeikoi
decbae1413 update: traffic fetch job 2021-09-02 19:58:04 +09:00
tokumeikoi
0c14652ff7 update: server log fetch 2021-09-02 18:42:36 +09:00
tokumeikoi
91418caf04 update: log record 2021-09-01 03:32:15 +09:00
tokumeikoi
607aa82f88 update: horizon config 2021-09-01 03:08:31 +09:00
tokumeikoi
adc2d02c49 update: horizon config 2021-09-01 03:08:07 +09:00
tokumeikoi
fabb49baea update: horizon auth 2021-09-01 02:11:19 +09:00
tokumeikoi
35e11a6816 update: add horizon 2021-09-01 01:56:16 +09:00
tokumeikoi
1d87a1b99a update: traffic fetch queue 2021-09-01 00:55:07 +09:00
tokumeikoi
fbced4d09b update: fix order assign modal 2021-08-31 20:59:56 +09:00
tokumeikoi
52914e354e update: order assign notice 2021-08-31 20:54:08 +09:00
tokumeikoi
d392d29b50 update: reset text 2021-08-30 19:11:16 +09:00
tokumeikoi
347a3bb4b0 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-08-30 01:18:17 +09:00
tokumeikoi
7ac1f69a71 update: fix css 2021-08-30 01:18:06 +09:00
Beta Soft
6317c4a4f3 Protocols: Clash: update 2021-08-29 21:44:51 +08:00
tokumeikoi
3da4de02d0 Merge pull request #478 from betaxab/fix-clash-appname
Protocols: Clash: fix Clash app_name not fould
2021-08-29 22:38:38 +09:00
Beta Soft
85686df2e6 Protocols: Clash: fix Clash app_name not found 2021-08-29 21:32:46 +08:00
tokumeikoi
adb5d041e6 fix: remind expire 2021-08-29 12:56:55 +09:00
tokumeikoi
7ba0e9a4e0 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-08-29 12:55:08 +09:00
tokumeikoi
99a64f41e1 Merge pull request #477 from betaxab/abandon-clash-provider
Protocols: Clash: Rollback the previous config
2021-08-29 12:54:58 +09:00
tokumeikoi
62053bc30f Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-08-29 12:54:32 +09:00
tokumeikoi
e5e7a06514 update: fix coupon 2021-08-29 12:54:15 +09:00
Beta Soft
905e2dacd1 Protocols: Clash: Rollback the previous configuration
because the provider cannot test the delay manually
2021-08-29 11:20:55 +08:00
tokumeikoi
8189a442bc Merge pull request #475 from WayChan/update_clash_subscribe
update Clash Subscribe
2021-08-29 02:52:51 +09:00
tokumeikoi
b38f7979d6 update: coupon 2021-08-28 16:34:22 +09:00
tokumeikoi
5a3b897c57 update: add coupon per user limit 2021-08-28 16:32:55 +09:00
tokumeikoi
2d5fb03937 update: remind mail i18n 2021-08-28 15:39:10 +09:00
tokumeikoi
982c47d0b4 update: remind mail i18n 2021-08-28 15:32:38 +09:00
tokumeikoi
4cebeed2d7 update: server log 2021-08-28 15:22:51 +09:00
tokumeikoi
c95d374cc0 update: order manual 2021-08-28 15:11:59 +09:00
tokumeikoi
23462e2753 update: add more tips 2021-08-27 00:54:35 +09:00
tokumeikoi
b26a3e0e70 update: fix order update 2021-08-25 18:06:08 +09:00
tokumeikoi
3600c9a166 update: rollback traffic fetch 2021-08-24 16:53:53 +09:00
tokumeikoi
8a58a1ad88 update 2021-08-24 16:50:45 +09:00
tokumeikoi
968d55e2e3 update: readme 2021-08-22 15:46:49 +09:00
root
98ac5cb680 Fix Clash Json decode error 2021-08-22 00:09:37 +08:00
tokumeikoi
0288d2df4b update: order success handle 2021-08-21 20:52:00 +09:00
root
cb8e34fa2e change profile-update-interval time 2021-08-21 17:59:58 +08:00
tokumeikoi
b0c818c661 update 2021-08-21 16:20:16 +09:00
tokumeikoi
303d4a1c66 update: short payment generate key 2021-08-20 23:42:07 +09:00
tokumeikoi
e6416712f0 Merge pull request #471 from betaxab/add-trojan-surfboard
Protocols: Surfboard: add trojan protocol support
2021-08-20 22:32:08 +09:00
tokumeikoi
5c6236366c Merge pull request #472 from betaxab/change-anxray-flag
Protocols: AnXray: change AnXray flag to axxray
2021-08-20 22:31:49 +09:00
tokumeikoi
9b0a487c69 update: add knowledge jump func 2021-08-20 22:30:18 +09:00
root
27a6cc98d1 update Clash Subscribe 2021-08-20 00:54:00 +08:00
Beta Soft
e05f6116b6 Protocols: AnXray: change AnXray flag to axxray 2021-08-19 15:47:18 +08:00
Beta Soft
a9db11877f Protocols: Surfboard: add trojan protocol support 2021-08-19 08:10:36 +08:00
tokumeikoi
abe1ebccae update: new generate order number method 2021-08-19 00:58:51 +09:00
tokumeikoi
c957a4ca83 update: fix ticket message length support utf8mb4 2021-08-18 21:23:45 +09:00
tokumeikoi
c9cd307cc4 update: auto open knowledge by id 2021-08-18 18:54:40 +09:00
tokumeikoi
31cdcef3aa update: remove apple id config 2021-08-12 17:28:15 +09:00
tokumeikoi
9f75d4cbde update: default theme add green color 2021-08-12 17:17:36 +09:00
tokumeikoi
de5f80b5a3 update: script 2021-08-10 13:37:37 +09:00
tokumeikoi
474df5e18f update: payment config format 2021-08-08 16:56:50 +09:00
tokumeikoi
dfa75f49bd update: fix tg 2021-08-07 19:00:33 +09:00
tokumeikoi
f2c7d092ac update: fix model 2021-08-06 23:41:41 +09:00
tokumeikoi
2c389ebe8c update: v2board update 2021-08-06 23:20:10 +09:00
tokumeikoi
25e19446e6 update: user tags 2021-08-06 17:33:16 +09:00
tokumeikoi
a8761e9d4d update: opt editor 2021-08-06 14:02:47 +09:00
tokumeikoi
5f4e9c0301 update: cache version 2021-08-06 13:51:55 +09:00
tokumeikoi
b6e9260464 update: fix editor 2021-08-06 13:51:30 +09:00
tokumeikoi
6aa96fe856 update: fix editor 2021-08-06 13:01:17 +09:00
tokumeikoi
ab02935fd7 update: fix v2board udpate 2021-08-06 02:40:31 +09:00
tokumeikoi
b275ad469b update: cache version 2021-08-06 01:53:51 +09:00
tokumeikoi
e6a7c2c11c update: opt code 2021-08-06 01:43:01 +09:00
tokumeikoi
0f0f726269 update: fix code support php8 2021-08-05 15:22:15 +09:00
tokumeikoi
00cd3e26be update: fix admin editor 2021-08-05 14:48:24 +09:00
tokumeikoi
d95974019a update: fix 2021-08-02 03:00:01 +09:00
tokumeikoi
7a80950ab5 update: laravel 7 2021-08-01 23:56:11 +09:00
tokumeikoi
73a6d3236a update: fix reset traffic maybe failure issue 2021-08-01 16:48:00 +09:00
tokumeikoi
59dd34674e update: rollback ui 2021-07-31 16:24:12 +09:00
tokumeikoi
5dda531c2b update: ui 2021-07-31 15:15:05 +09:00
tokumeikoi
7234ccf4c1 update: ui 2021-07-31 14:17:48 +09:00
tokumeikoi
448b5382b9 Merge pull request #468 from betaxab/fix-grpc-protocol 2021-07-31 13:17:06 +09:00
Beta Soft
bd2b056fbf protocols: fix gRPC protocol serviceName field
fix anXray serverName filed
2021-07-31 12:14:40 +08:00
tokumeikoi
ad8e2b8e80 update: api 2021-07-31 03:36:49 +09:00
tokumeikoi
8a20a70513 update: fix ui 2021-07-31 02:34:58 +09:00
tokumeikoi
06adaa2124 update: fix order statistics 2021-07-31 02:21:49 +09:00
tokumeikoi
ebf98d42a8 update: new feature 2021-07-31 02:05:39 +09:00
tokumeikoi
1adb1bcfa0 update: fix default app_url with subscribe_url 2021-07-30 23:53:37 +09:00
tokumeikoi
bb8da6e2c5 update: fix payment verify app_url 2021-07-30 16:00:20 +09:00
tokumeikoi
c426aabbcf update: theme 2021-07-29 21:29:53 +09:00
tokumeikoi
18bb1bd962 update: theme 2021-07-29 21:24:37 +09:00
tokumeikoi
07e9377417 update: theme 2021-07-29 21:22:07 +09:00
tokumeikoi
c0d3150461 update: add payment app_url alert 2021-07-29 21:12:11 +09:00
tokumeikoi
1a9b8b09bb Merge pull request #467 from v2board/dev
1.5.2
2021-07-29 20:59:52 +09:00
tokumeikoi
cb621a93ae update: version 2021-07-29 20:56:28 +09:00
tokumeikoi
fb449b490e Merge pull request #466 from v2board/dev
1.5.2
2021-07-29 20:15:19 +09:00
tokumeikoi
e35ec76f81 update: add subscribe anxray 2021-07-29 20:13:01 +09:00
tokumeikoi
5a60380765 update: add theme config 2021-07-29 13:38:13 +09:00
tokumeikoi
f409d89c4a update: add config 2021-07-29 02:41:31 +09:00
tokumeikoi
de045c79f5 update: custom theme 2021-07-29 02:36:51 +09:00
tokumeikoi
6a336d4253 update: web route 2021-07-29 02:29:15 +09:00
tokumeikoi
0ce3948c12 update: theme directory 2021-07-29 02:21:09 +09:00
tokumeikoi
7338c4a294 update: user 2021-07-27 19:16:49 +09:00
tokumeikoi
9417623fcf fix: email suffix select 2021-07-27 18:32:29 +09:00
tokumeikoi
4f29264de9 update: fix email suffix 2021-07-26 13:50:04 +09:00
tokumeikoi
34d8f0d5f0 update: statistics check time 2021-07-24 01:31:23 +09:00
tokumeikoi
b585038916 update: user 2021-07-23 20:43:09 +09:00
tokumeikoi
5abb642277 update: user 2021-07-22 22:36:48 +09:00
tokumeikoi
01cf486137 update: comm config 2021-07-22 22:32:42 +09:00
tokumeikoi
cb811bda5a update: user 2021-07-22 21:12:09 +09:00
tokumeikoi
2306e38d0c update: remove aliyun repo 2021-07-22 01:22:14 +09:00
tokumeikoi
2798c1df06 update: user language 2021-07-20 21:16:46 +09:00
tokumeikoi
a727745b43 update: order ui 2021-07-20 18:22:26 +09:00
tokumeikoi
af901291a5 update: quick login url 2021-07-19 21:17:34 +09:00
tokumeikoi
42d755c7d9 update: get comm config api 2021-07-19 18:44:40 +09:00
tokumeikoi
56a4ce5fe4 fix: surplus 2021-07-18 22:27:23 +09:00
tokumeikoi
67ab0d1f22 update: user 2021-07-16 18:06:11 +09:00
tokumeikoi
230470c037 fix: knowledge sort 2021-07-16 17:46:36 +09:00
tokumeikoi
77aec7d553 update: commission type and opt knowledge sort timestamp 2021-07-14 20:08:30 +09:00
tokumeikoi
88948eb8ee fix: change plan refund surplus amount 2021-07-11 01:13:09 +09:00
tokumeikoi
4d38ce3968 Merge pull request #463 from betaxab/fix-vmss-tls 2021-07-09 20:24:25 +09:00
tokumeikoi
a88121626b fix: knowledge subscribe url 2021-07-09 00:01:13 +09:00
tokumeikoi
90409b1107 update: add default subscribe, 1.5.3 remove this 2021-07-07 19:54:31 +09:00
tokumeikoi
04615688f7 update: subscribe url random 2021-07-06 18:04:59 +09:00
Beta Soft
f48de9f07d Protocols: fix vmess tls sni field 2021-07-05 21:01:28 +08:00
tokumeikoi
4722379e79 update: add new client protocol 2021-07-04 13:21:10 +09:00
tokumeikoi
9aed2554ee Merge pull request #462 from betaxab/routerprotocol
Protocols: add Passwall & SSRPlus subscription support
2021-07-04 13:08:07 +09:00
Beta Soft
7eb3d9fed7 Protocols: add Passwall & SSRPlus subscription support
Usage:
add &flag=passwall at the end of subscription link for OpenWRT Passwall Luci Plugin
add &flag=ssrplus at the end of subscription link for OpenWRT ShadowsocksR Plus+ Luci Plugin
2021-07-03 23:02:47 +08:00
tokumeikoi
567acdd03b Merge pull request #461 from betaxab/v2rayprotocol
Protocols: add V2ray Client Protocol
2021-07-03 23:56:00 +09:00
Beta Soft
a0d18d93d3 Protocols: add V2ray Client Protocol
Usage:
add &flag=v2rayng at the end of subscription link for V2rayNG Client
add &flag=v2rayn at the end of subscription link for V2rayN Client
2021-07-03 22:54:13 +08:00
tokumeikoi
1c419283c0 Merge pull request #460 from betaxab/p3
Protocols: fix QuantumultX subscription
2021-07-03 14:26:42 +09:00
Beta Soft
d25f6bff65 Protocols: fix QuantumultX subscription 2021-07-03 12:53:22 +08:00
tokumeikoi
0b97dd0995 fix: app subscribe 2021-07-03 03:31:28 +09:00
tokumeikoi
6f90c6b878 update: code 2021-07-02 23:35:10 +09:00
tokumeikoi
5b8591fde9 update: code 2021-07-02 23:34:42 +09:00
tokumeikoi
dfeec044ea fix: statsistics 2021-07-02 22:43:22 +09:00
tokumeikoi
70b47ec4b0 update: remove origin subscribe method 2021-07-02 22:24:39 +09:00
tokumeikoi
cd85fba9c7 restore: origin subscribe method 2021-07-02 22:14:07 +09:00
tokumeikoi
e01e951f7f update: code 2021-07-02 22:07:54 +09:00
tokumeikoi
0284e47155 fix: shadowrocket grpc 2021-07-02 22:05:12 +09:00
tokumeikoi
a4e1ba4016 fix: shadowrocket grpc 2021-07-02 21:57:00 +09:00
tokumeikoi
f95deb3f16 fix: shadowrocket grpc 2021-07-02 21:52:13 +09:00
tokumeikoi
dfef6d2d94 update: remove origin subscribe method 2021-07-02 21:25:26 +09:00
tokumeikoi
efc8419eb5 update: grpc 2021-07-02 20:46:16 +09:00
tokumeikoi
aecfa85efd update 2021-07-01 23:25:59 +09:00
tokumeikoi
9db5de09f2 update: order process event 2021-07-01 22:04:22 +09:00
tokumeikoi
b174403a2a remove: soft delete 2021-07-01 20:06:09 +09:00
tokumeikoi
0f488540f4 update: middleware 2021-07-01 18:16:27 +09:00
tokumeikoi
d00ad94c46 update: sql 2021-07-01 18:14:49 +09:00
tokumeikoi
5fa7c534ff update: middleware 2021-07-01 18:11:23 +09:00
tokumeikoi
386c1339f5 fix: plan update 2021-07-01 18:02:13 +09:00
tokumeikoi
6509091e4f update: check order php env memory limit 2021-07-01 12:41:34 +09:00
tokumeikoi
078dfbf339 fix: safe delete sql 2021-07-01 03:01:26 +09:00
tokumeikoi
de6ff1dca9 update: add user softdelete api 2021-07-01 00:35:22 +09:00
tokumeikoi
044d1e9b7f update: message box 2021-06-30 00:48:28 +09:00
tokumeikoi
9c711b4ea6 update: middleware 2021-06-25 22:35:14 +09:00
tokumeikoi
760d248e4e update: ui 2021-06-23 19:36:40 +09:00
tokumeikoi
1a2e8e2966 fix: ui event 2021-06-23 19:24:30 +09:00
tokumeikoi
b00b58d73d update: ui 2021-06-23 19:15:19 +09:00
tokumeikoi
43027660be update: ui 2021-06-23 19:11:56 +09:00
tokumeikoi
fe69a5976b update: ui 2021-06-23 19:06:48 +09:00
tokumeikoi
ecebba1d51 fix: language change 2021-06-23 18:02:35 +09:00
tokumeikoi
efc43dc45e update: ui 2021-06-23 16:32:25 +09:00
tokumeikoi
c5d88bfbfc update: ui 2021-06-23 16:25:55 +09:00
tokumeikoi
6928fd3fef update readme 2021-06-23 02:31:50 +09:00
tokumeikoi
e4f178e167 add: wechatpay native 2021-06-14 22:29:31 +09:00
tokumeikoi
343c93ad8e add: wechatpay native 2021-06-14 22:27:29 +09:00
tokumeikoi
f80af8efdc update: dev version 2021-06-12 16:49:34 +09:00
tokumeikoi
e5b998ee8d fix: user update 2021-06-12 16:46:58 +09:00
tokumeikoi
d510064732 update: support grpc for clash & shadowrocket 2021-06-12 15:42:44 +09:00
tokumeikoi
c5e2ec1d12 update: gitignore 2021-06-12 01:57:49 +09:00
tokumeikoi
ee2ca23487 update: language more 2021-06-12 01:56:39 +09:00
tokumeikoi
a5532490ba update: telegram ticket replay notify 2021-06-10 22:24:54 +09:00
tokumeikoi
2f175323a3 update: css 2021-06-08 20:30:36 +09:00
tokumeikoi
f896370e64 update: sort leave alert 2021-06-08 20:07:10 +09:00
tokumeikoi
0313c35dbe update: user edit 2021-06-04 00:56:28 +09:00
tokumeikoi
232cb18a25 update: frontend 2021-06-02 21:10:16 +09:00
tokumeikoi
04a05a6ba4 Merge pull request #445 from betaxab/p1
ClientController: clash: add subscription-userinfo header
2021-05-26 23:26:35 +09:00
tokumeikoi
e2b4df592d update: frontend 2021-05-26 23:15:18 +09:00
tokumeikoi
668663f978 update: user frontend 2021-05-26 22:11:03 +09:00
Beta Soft
2e7544c71c ClientController: clash: add subscription-userinfo header 2021-05-26 17:07:38 +08:00
tokumeikoi
1fb3f62cff update: user frontend 2021-05-26 17:35:57 +09:00
tokumeikoi
02a1728bff update: payment service 2021-05-26 01:38:57 +09:00
tokumeikoi
4ea71d85be update: user frontend 2021-05-24 22:49:57 +09:00
tokumeikoi
afe8bb3171 update: check order 2021-05-24 21:10:54 +09:00
tokumeikoi
ef17be2046 fix: ticket filter 2021-05-22 14:18:27 +09:00
tokumeikoi
53dea06a6c update: user 2021-05-21 02:40:31 +09:00
tokumeikoi
229a3022ac Merge pull request #439 from v2board/dev
1.5.1
2021-05-18 21:00:54 +09:00
tokumeikoi
554dc4c12b update: version 2021-05-18 21:00:01 +09:00
tokumeikoi
6175e59e6f remove: old route 2021-05-17 02:30:39 +09:00
tokumeikoi
9df6e52438 update: composer2 2021-05-17 02:30:00 +09:00
tokumeikoi
63acb4c581 fix: remark input 2021-05-16 23:16:33 +09:00
tokumeikoi
f54c82dcf9 remove: old code 2021-05-16 03:11:24 +09:00
tokumeikoi
b92b38a635 fix: stripe notify 2021-05-15 02:59:46 +09:00
tokumeikoi
ddab443d75 fix: payment input 2021-05-14 22:53:07 +09:00
tokumeikoi
8f806e6de7 update: chart smooth 2021-05-13 18:18:06 +09:00
tokumeikoi
f1c62e2732 knowledge: add access area 2021-05-13 18:08:09 +09:00
tokumeikoi
3275b96a0a fix: user tos target 2021-05-09 20:26:00 +09:00
tokumeikoi
240555104a Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-05-09 00:46:31 +09:00
tokumeikoi
30ff71bd11 payment: support epay 2021-05-09 00:46:21 +09:00
tokumeikoi
b17bbad745 Merge pull request #417 from ColetteContreras/patch-2 2021-05-07 14:16:51 +09:00
tokumeikoi
a301b8461e update: user 2021-05-07 12:07:20 +09:00
tokumeikoi
3e354bf5af update: admin 2021-05-07 01:27:54 +09:00
tokumeikoi
b9796f462c update: payment 2021-05-07 01:18:38 +09:00
tokumeikoi
dc6317db97 update 2021-05-07 01:17:55 +09:00
tokumeikoi
b3b0988730 update: tos config 2021-05-07 01:06:21 +09:00
tokumeikoi
4070761cd0 update: add tos 2021-05-07 00:56:54 +09:00
tokumeikoi
2c408a2f56 update 2021-05-07 00:15:30 +09:00
tokumeikoi
2cfaeb2fb9 add: tos url 2021-05-07 00:12:58 +09:00
tokumeikoi
ef1c0b6091 add: payment drop 2021-05-07 00:10:05 +09:00
tokumeikoi
8e1a313709 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-05-07 00:08:00 +09:00
tokumeikoi
d5cf1bc735 Merge pull request #407 from betaxab/p1 2021-05-03 13:32:19 +09:00
tokumeikoi
e1c63bd556 Merge pull request #412 from betaxab/p3 2021-05-03 13:31:07 +09:00
tokumeikoi
324e218767 update: frontent 2021-05-02 22:38:22 +09:00
tokumeikoi
04b5d16457 add: alipay f2f 2021-05-01 00:27:02 +09:00
tokumeikoi
2431ffaba7 update: payment 2021-04-29 01:39:45 +09:00
tokumeikoi
6b31c39e6e update: payment 2021-04-29 01:33:54 +09:00
tokumeikoi
2769a6adf4 update: stripe wepay 2021-04-29 01:27:24 +09:00
tokumeikoi
aa78761115 fix: payment 2021-04-29 01:25:11 +09:00
tokumeikoi
3e60cbf968 add: stripe wepay 2021-04-29 01:22:40 +09:00
tokumeikoi
84583bd384 update: payment 2021-04-28 18:48:18 +09:00
tokumeikoi
0333d62e6f fix: payment 2021-04-28 18:29:28 +09:00
tokumeikoi
3bfa2180e6 update 2021-04-28 18:00:22 +09:00
tokumeikoi
22ee741200 rewrite: payment 2021-04-28 17:56:08 +09:00
tokumeikoi
58b27cdd50 fix: hidden order fetch params 2021-04-15 22:41:29 +09:00
Colette Contreras
743311f2f7 Update PoseidonController.php
Fix server status
2021-04-06 07:03:45 +08:00
tokumeikoi
9c04f75b85 update: exchange api 2021-04-02 01:45:11 +09:00
tokumeikoi
c7d7dfda28 fix: more 2021-03-28 22:58:35 +09:00
tokumeikoi
35362689c4 update: config 2021-03-26 02:20:53 +09:00
tokumeikoi
b740998760 feature: customer service system 2021-03-25 18:26:22 +09:00
tokumeikoi
488eafdd67 feature: order filter 2021-03-25 17:59:28 +09:00
tokumeikoi
ff30d3dcb2 feature: server push status 2021-03-24 20:57:06 +09:00
tokumeikoi
cbe5882e01 feature: server push status 2021-03-24 16:04:09 +09:00
tokumeikoi
4e2e3cd2a0 rm: remove server name check 2021-03-22 02:33:33 +09:00
tokumeikoi
326fb5d918 fix: server name repeat 2021-03-20 02:24:55 +09:00
Beta Soft
a49faf3e1f V2boardStatistics: comply with psr-4 autoloading standard 2021-03-19 18:28:58 +08:00
tokumeikoi
e4b76a705f fix: stat 2021-03-13 01:03:53 +09:00
tokumeikoi
a03125ee6a fix: stat 2021-03-12 01:24:50 +09:00
tokumeikoi
58a63ae819 add: server name check 2021-03-10 19:52:51 +09:00
Beta Soft
ab8ebd593f URLSchemes: let's support standard v2ray vmess URL Schemes 2021-03-08 19:12:25 +08:00
tokumeikoi
11b6c8448a fix: user filter 2021-03-07 02:44:39 +09:00
tokumeikoi
9ce9af4917 fix: statistics 2021-03-06 01:22:37 +09:00
tokumeikoi
550f0fee37 update: admin user update 2021-03-02 21:12:32 +09:00
tokumeikoi
ab9c6c85bb fix: server rank 2021-02-26 23:54:38 +09:00
tokumeikoi
0e6f6358d8 opt v2board statistics 2021-02-23 14:49:07 +09:00
tokumeikoi
b0ddf7d45f opt 2021-02-19 01:15:37 +09:00
tokumeikoi
f8a851d464 Merge pull request #401 from v2board/dev
1.5.0
2021-02-17 17:20:51 +09:00
tokumeikoi
da8bff5609 update: frontend 2021-02-17 17:15:41 +09:00
tokumeikoi
d3150cadac update: version 2021-02-17 17:11:45 +09:00
tokumeikoi
cdddbae19a fix: something 2021-02-17 17:11:17 +09:00
tokumeikoi
7c40a146a9 update: app config 2021-02-16 01:53:37 +09:00
tokumeikoi
f81426b2c4 Merge pull request #399 from v2board/dev
1.4.3
2021-02-13 17:34:45 +09:00
tokumeikoi
e581d08f95 feature: more language switch 2021-02-06 00:03:58 +09:00
tokumeikoi
2ba924e578 update: withdraw close 2021-02-03 19:27:21 +09:00
tokumeikoi
77f6b3f289 feature: close withdraw 2021-02-03 19:17:32 +09:00
tokumeikoi
2979003b6f fix: get reset day 2021-02-03 19:03:28 +09:00
tokumeikoi
2be497b04d Merge pull request #388 from betaxab/p1 2021-02-03 13:08:17 +09:00
tokumeikoi
f55fc86547 fix: order chart 2021-02-01 19:04:03 +09:00
tokumeikoi
bc9b3b6e76 fix: server charts 2021-01-26 22:22:47 +09:00
tokumeikoi
fcafb65ce9 fix: config save 2021-01-26 22:20:27 +09:00
tokumeikoi
34be1b9579 fix: stat charts 2021-01-26 19:33:11 +09:00
tokumeikoi
627ff98882 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-01-21 23:29:34 +09:00
tokumeikoi
007b10d925 update: user remarks 2021-01-21 23:29:15 +09:00
tokumeikoi
f855ecc758 remove: test line 2021-01-21 15:52:12 +09:00
Beta Soft
af8b2b61d4 language: update English translation
- minor Chinese entry adjustments
2021-01-21 00:50:56 +08:00
tokumeikoi
f7a7c21c16 update: backend language and order result page 2021-01-20 19:51:55 +09:00
tokumeikoi
edee396c9b fix: coupon generate 2021-01-15 00:35:36 +09:00
tokumeikoi
54d1226c8b fix: coupon generate 2021-01-15 00:33:30 +09:00
tokumeikoi
1ce019cfa5 update: commission 2021-01-13 12:24:22 +09:00
tokumeikoi
fe736df68d update: crisp and tawk info 2021-01-13 00:47:41 +09:00
tokumeikoi
72bf45e4d5 update: user 2021-01-12 00:49:59 +09:00
tokumeikoi
c5639a9bef update: other 2021-01-10 03:25:31 +09:00
tokumeikoi
3f1c505922 update: user 2021-01-08 14:21:13 +09:00
tokumeikoi
ec0fbabd07 update 2021-01-08 02:00:18 +09:00
tokumeikoi
eca105b7d1 update: frontend 2021-01-08 01:58:44 +09:00
tokumeikoi
a55647ab7a update: config 2021-01-08 01:33:11 +09:00
tokumeikoi
633b9ad912 Merge pull request #377 from wloot/patch-6
[security] Fix user info leak in getSubscribe()
2021-01-07 17:16:07 +09:00
tokumeikoi
be146bc98b Merge pull request #373 from betaxab/p1
subs: following shadowsocks SIP008 new changes
2021-01-07 17:11:11 +09:00
tokumeikoi
79cbe6ce39 Merge pull request #364 from wloot/patch-2
QuantumultX: keep expected behavior for host
2021-01-07 17:10:25 +09:00
tokumeikoi
f8185b51b7 update: more feature 2021-01-02 22:35:41 +09:00
tokumeikoi
b4d74a016a fix: traffic show 2021-01-02 12:07:28 +09:00
Julian Liu
dd51daf9d8 [security] Fix user info leak in getSubscribe()
getSubscribe() leaks all user info even password hash, fix it.
2020-12-31 08:23:46 +08:00
tokumeikoi
4958c94bf2 update: user frontend 2020-12-28 17:23:56 +08:00
tokumeikoi
5766ec1951 update: statistics 2020-12-25 00:53:46 +08:00
tokumeikoi
faeaa02a84 compress 2020-12-24 23:49:52 +08:00
tokumeikoi
66c547d0ac update: more feature 2020-12-24 23:41:32 +08:00
Beta Soft
d1cfc9815b subs: following shadowsocks SIP008 new changes
- do not use infix dereference operator to following the whole
2020-12-24 13:02:57 +08:00
tokumeikoi
b8b7033132 update: sql 2020-12-22 15:21:18 +08:00
tokumeikoi
9a465d9bb4 update: statistics 2020-12-21 17:11:48 +08:00
tokumeikoi
bcea2e7a54 update: statistics 2020-12-20 21:06:23 +08:00
tokumeikoi
7e5e696c70 update: statistics 2020-12-20 16:57:33 +08:00
tokumeikoi
1d70382aee update: db 2020-12-20 02:49:37 +08:00
tokumeikoi
8af862633f fix: frontend user edit 2020-12-19 21:33:11 +08:00
tokumeikoi
c0e60978ce fix: frontend user edit 2020-12-19 19:16:37 +08:00
Julian Liu
f036b46bf0 QuantumultX: keep expected behavior for host
According to the documentation, quantumultx exposes one param for both sni and ws host. and sni priority is usually higher than ws host in other similar apps, we keep the behavior here.
ref: 8b6d7d84fc/sample.conf (L91)
2020-12-16 23:49:34 +08:00
tokumeikoi
a0d1b10614 fix: quantumultx tls-host key error 2020-12-16 23:15:20 +08:00
tokumeikoi
5fbefc7a98 Merge branch 'dev' 2020-12-14 11:03:57 +08:00
tokumeikoi
5a5491591b update: version 2020-12-14 11:03:32 +08:00
tokumeikoi
77e5eeea1b Merge branch 'dev' 2020-12-14 11:00:46 +08:00
tokumeikoi
da86b8cba9 update 2020-12-14 11:00:11 +08:00
tokumeikoi
91dfa5069c Merge pull request #361 from v2board/dev
1.4.2
2020-12-13 23:58:49 +08:00
tokumeikoi
2393ccbec7 update: server manage 2020-12-13 23:53:20 +08:00
tokumeikoi
962cfa9bea Merge pull request #359 from v2board/dev
1.4.2
2020-12-13 23:19:33 +08:00
tokumeikoi
8df4b6869a update: frontend 2020-12-13 23:15:47 +08:00
tokumeikoi
2159ab568a update: admin copy subscribe url 2020-12-13 23:09:32 +08:00
tokumeikoi
ef077cd095 update: frontend 2020-12-13 14:49:36 +08:00
tokumeikoi
17be643ce2 update: server manage 2020-12-13 14:16:07 +08:00
tokumeikoi
3b969c2c52 update: server manage 2020-12-13 12:17:48 +08:00
tokumeikoi
67d257c7b2 update: server manage 2020-12-13 12:16:05 +08:00
tokumeikoi
bc709a9c94 update: user filter 2020-12-11 22:32:10 +08:00
tokumeikoi
06f1ea0b81 update: user filter 2020-12-11 21:56:30 +08:00
tokumeikoi
0b425c1a01 update: admin 2020-12-11 20:45:54 +08:00
tokumeikoi
d072dc3e32 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-12-11 20:40:43 +08:00
tokumeikoi
db3e2abbbf update: user filter 2020-12-11 20:40:27 +08:00
tokumeikoi
c72e6cf4ea Merge pull request #356 from v2board/dev
1.4.1
2020-12-11 16:09:12 +08:00
tokumeikoi
b58df71db4 Merge pull request #355 from wloot/patch-1
Patch 1
2020-12-11 16:08:37 +08:00
Julian Liu
91eb83388c Fix allowInsecure 2020-12-11 01:11:26 +08:00
Julian Liu
16cf1f135a Fix allowInsecure 2020-12-11 01:11:04 +08:00
Julian Liu
f3ef9e457e Fix allowInsecure 2020-12-11 01:09:49 +08:00
tokumeikoi
be1d22b423 Merge pull request #352 from betaxab/p1
rules: update default clash & surfboard & surge rules
2020-12-10 23:50:37 +08:00
tokumeikoi
ba2c92866c fix: mail knowledge link 2020-12-10 23:40:34 +08:00
tokumeikoi
91b016c231 update: log 2020-12-09 11:46:37 +08:00
tokumeikoi
d1b2e315ce update: log 2020-12-09 11:44:51 +08:00
tokumeikoi
8f0e3ce27d update: frontend 2020-12-08 18:59:07 +08:00
tokumeikoi
a47bac5ebc fix: ipad ua 2020-12-07 18:31:47 +08:00
tokumeikoi
d84af96535 update: app rules 2020-12-07 18:05:22 +08:00
Beta Soft
29e7be855c rules: update default clash & surfboard & surge rules
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-12-05 21:25:11 +08:00
tokumeikoi
1ca9899437 Merge pull request #345 from wloot/buildvmess
clean up buildvmess()
2020-12-05 17:16:41 +08:00
tokumeikoi
254fdf6049 update: reset day 2020-12-04 21:17:25 +08:00
tokumeikoi
896d9eb030 update: frontend 2020-12-03 21:29:13 +08:00
tokumeikoi
501986bf53 update: frontend 2020-12-03 14:16:47 +08:00
tokumeikoi
c83c7478b7 update: frontend 2020-11-30 14:07:04 +08:00
tokumeikoi
e0e9187655 update: frontend 2020-11-30 12:11:06 +08:00
tokumeikoi
8a894a9a32 update: reset day 2020-11-30 12:04:00 +08:00
tokumeikoi
f9e55a3905 update: remove coupon log 2020-11-28 23:04:56 +08:00
tokumeikoi
f81ecbea5d Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-11-28 23:04:45 +08:00
tokumeikoi
487bece1bb update: ui 2020-11-28 23:04:11 +08:00
tokumeikoi
5f0e62f43c Merge pull request #344 from betaxab/p1
subscription: add shadowsocks SIP008 subscription support
2020-11-27 01:48:50 +08:00
tokumeikoi
a8b6ebdc60 Merge pull request #333 from betaxab/p2
rules: update default clash & surfboard & surge rules
2020-11-27 01:48:00 +08:00
tokumeikoi
9af98f72fd update: check commission 2020-11-26 15:07:47 +08:00
tokumeikoi
a54f64b698 update: check commission 2020-11-26 14:44:00 +08:00
Beta Soft
3e97b26593 rules: update default clash & surfboard & surge rules
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-11-25 17:38:39 +08:00
Tokumeikoi
cb2dd63e98 update: ui 2020-11-24 14:58:04 +08:00
Tokumeikoi
512c06f15d update: ui 2020-11-24 00:57:27 +08:00
Tokumeikoi
d645e9b98a update: app version 2020-11-22 16:25:56 +08:00
Tokumeikoi
49c00c7a63 update: app version 2020-11-22 02:01:44 +08:00
Julian Liu
7fe8a5b239 clean up buildvmess() 2020-11-20 00:53:53 +08:00
Julian Liu
589eec9392 clean up buildvmess() 2020-11-20 00:30:49 +08:00
Tokumeikoi
cada8ae9e8 update: app version 2020-11-19 21:30:27 +08:00
Tokumeikoi
3ec90c4c52 update: app version 2020-11-19 21:27:53 +08:00
Tokumeikoi
c1c6dccbfa update: app version 2020-11-19 21:24:14 +08:00
Tokumeikoi
baf719fcff Merge remote-tracking branch 'origin/dev' into dev 2020-11-19 21:21:39 +08:00
Tokumeikoi
7a9527d48a update: app version 2020-11-19 21:21:21 +08:00
Beta Soft
7f12a79d07 subscription: add shadowsocks SIP008 subscription support
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-11-19 13:05:49 +08:00
tokumeikoi
a0dcf51c28 Merge pull request #343 from ColetteContreras/fixPoseidon
Fix Poseidon, add ETag
2020-11-19 12:24:19 +08:00
tokumeikoi
6e4693534f Merge pull request #332 from betaxab/p1
OrderAssign: fixes can not assign two year & three year plan
2020-11-19 12:19:08 +08:00
Your Name
d2bce02d4e Add ETag for caching 2020-11-18 05:10:32 -05:00
Your Name
2b5ef08fe0 Fix alterid 2020-11-18 04:56:27 -05:00
Tokumeikoi
732edcad57 update: fix opcache return null 2020-11-18 11:12:38 +08:00
Tokumeikoi
e030ed8d80 update: custom v2ray alter id 2020-11-17 23:02:12 +08:00
Tokumeikoi
717043c96a update: custom v2ray alter id 2020-11-17 22:18:00 +08:00
Tokumeikoi
04250fb7ac update: custom v2ray alter id 2020-11-17 22:12:51 +08:00
Tokumeikoi
d1e831e961 update: custom v2ray alter id 2020-11-17 21:24:42 +08:00
Tokumeikoi
fb9465b5a6 update: custom v2ray alter id 2020-11-17 21:23:16 +08:00
Tokumeikoi
ad98651f93 update: admin ui 2020-11-17 17:30:28 +08:00
Tokumeikoi
9035afaa78 update: commission process 2020-11-17 17:01:02 +08:00
Tokumeikoi
92e3e4e01f update: fix status 2020-11-17 16:40:13 +08:00
Tokumeikoi
be19a93efb update: fix commission first 2020-11-17 16:34:10 +08:00
Tokumeikoi
3aabeff5de update: fix guest 2020-11-16 22:22:23 +08:00
Tokumeikoi
5889d527ad update: server 2020-11-16 11:58:34 +08:00
Tokumeikoi
2063e3c51d update: quantumult 2020-11-15 23:42:58 +08:00
Tokumeikoi
592b751e2c update: server 2020-11-15 17:10:32 +08:00
Tokumeikoi
b4212e7af4 update: server 2020-11-15 15:25:32 +08:00
Tokumeikoi
c7aa63759e update: frontend 2020-11-15 15:20:51 +08:00
Tokumeikoi
b3f075ae42 update: server 2020-11-15 15:14:47 +08:00
Tokumeikoi
6fd7b48d6f update: server 2020-11-15 15:14:19 +08:00
Tokumeikoi
f1fd300a35 update: version 2020-11-15 12:45:35 +08:00
Tokumeikoi
204982e4c5 update: frontend 2020-11-15 12:44:23 +08:00
Tokumeikoi
5df67aef16 update: app 2020-11-14 23:04:11 +08:00
Tokumeikoi
ad8dee359f fix: v2ray advance config 2020-11-14 23:02:49 +08:00
Tokumeikoi
6ccb3fa7be update: frontend 2020-11-14 21:33:53 +08:00
Tokumeikoi
3bf11a136a update: server sort 2020-11-14 17:41:26 +08:00
Tokumeikoi
2fc7c8c07c clear: files 2020-11-14 17:27:54 +08:00
Tokumeikoi
5b79ea569c update: server sort 2020-11-14 17:26:17 +08:00
Tokumeikoi
9fa53efc0c update: sql 2020-11-13 02:17:19 +08:00
Tokumeikoi
fb257c999f update: comm 2020-11-11 17:15:07 +08:00
Tokumeikoi
cefb2b3a27 update: app 2020-11-10 21:20:18 +08:00
Tokumeikoi
7263b6d0ed update: user filter 2020-11-10 21:08:57 +08:00
Tokumeikoi
e6d5aadbd2 update: server submit 2020-11-09 20:44:03 +08:00
Beta Soft
342415d1db OrderAssign: fixes can not assign two year & three year plan
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-11-09 09:26:07 +08:00
Tokumeikoi
1788250dc8 update: server submit 2020-11-09 00:15:20 +08:00
Tokumeikoi
9917e21a61 update: server submit 2020-11-09 00:12:19 +08:00
Tokumeikoi
59a8429456 update: user manage 2020-11-07 18:01:23 +08:00
Tokumeikoi
9345bf7979 update: order 2020-11-07 17:23:47 +08:00
Tokumeikoi
7042dce0a1 update: traffic submit 2020-11-07 16:38:57 +08:00
Tokumeikoi
675353bbe1 fix: bug 2020-11-07 16:05:26 +08:00
Tokumeikoi
93c90ddaf0 fix: bug 2020-11-07 16:04:42 +08:00
Tokumeikoi
1d92d6b2f9 fix: more bug 2020-11-07 15:44:57 +08:00
Tokumeikoi
34b8b666f4 Merge branch 'master' into dev 2020-11-04 02:12:03 +08:00
Tokumeikoi
6d9ab7dfe2 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-11-02 22:22:48 +08:00
Tokumeikoi
ebce59a0d1 fix: stat 2020-11-02 22:22:18 +08:00
331 changed files with 14939 additions and 5525 deletions

View File

@@ -0,0 +1,39 @@
---
name: Bug report | 问题反馈
about: Tell us what problems you have encountered
title: "[BUG]"
labels: ''
assignees: ''
---
🙇🙇🙇注意XrayR等非V2Board问题请前往项目方提问
🙇🙇🙇Note: XrayR and other non-V2Board issues please go to the project side to ask questions
The V2Board version number you are using
当前使用的V2Board版本号
--------
Briefly describe the problem you are experiencing
简单描述你遇到的问题
--------
Screenshot of the reported error(Please do desensitization)
报告错误的截图(请做脱敏处理)
--------
Screenshot of the reported error(Please do desensitization)
报告错误的截图(请做脱敏处理)
--------
The latest log files in the storage/logs directory report from #1 (Please do desensitization)
storage/logs 目录下最新的日志文件从 #1 开始报告(请做脱敏处理)
--------

View File

@@ -0,0 +1,11 @@
---
name: Feature request | 功能请求
about: Tell us what you need
title: "[Feature request]"
labels: ''
assignees: ''
---
Please describe in detail the problems or needs you have encountered.
请详细描述你遇到的问题或需求。

1
.gitignore vendored
View File

@@ -18,3 +18,4 @@ composer.phar
composer.lock
yarn.lock
docker-compose.yml
.DS_Store

View File

@@ -1,16 +0,0 @@
php:
preset: laravel
enabled:
- alpha_ordered_imports
disabled:
- length_ordered_imports
- unused_use
finder:
not-name:
- index.php
- server.php
js:
finder:
not-name:
- webpack.mix.js
css: true

View File

@@ -2,9 +2,11 @@
namespace App\Console\Commands;
use App\Models\CommissionLog;
use Illuminate\Console\Command;
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class CheckCommission extends Command
{
@@ -47,6 +49,7 @@ class CheckCommission extends Command
{
if ((int)config('v2board.commission_auto_check_enable', 1)) {
Order::where('commission_status', 0)
->where('invite_user_id', '!=', NULL)
->where('status', 3)
->where('updated_at', '<=', strtotime('-3 day', time()))
->update([
@@ -57,20 +60,68 @@ class CheckCommission extends Command
public function autoPayCommission()
{
$order = Order::where('commission_status', 1)
->where('status', 3)
$orders = Order::where('commission_status', 1)
->where('invite_user_id', '!=', NULL)
->get();
foreach ($order as $item) {
if ($item->invite_user_id) {
$inviter = User::find($item->invite_user_id);
if (!$inviter) continue;
$inviter->commission_balance = $inviter->commission_balance + $item->commission_balance;
if ($inviter->save()) {
$item->commission_status = 2;
$item->save();
}
foreach ($orders as $order) {
DB::beginTransaction();
if (!$this->payHandle($order->invite_user_id, $order)) {
DB::rollBack();
continue;
}
$order->commission_status = 2;
if (!$order->save()) {
DB::rollBack();
continue;
}
DB::commit();
}
}
public function payHandle($inviteUserId, Order $order)
{
$level = 3;
if ((int)config('v2board.commission_distribution_enable', 0)) {
$commissionShareLevels = [
0 => (int)config('v2board.commission_distribution_l1'),
1 => (int)config('v2board.commission_distribution_l2'),
2 => (int)config('v2board.commission_distribution_l3')
];
} else {
$commissionShareLevels = [
0 => 100
];
}
for ($l = 0; $l < $level; $l++) {
$inviter = User::find($inviteUserId);
if (!$inviter) continue;
if (!isset($commissionShareLevels[$l])) continue;
$commissionBalance = $order->commission_balance * ($commissionShareLevels[$l] / 100);
if (!$commissionBalance) continue;
if ((int)config('v2board.withdraw_close_enable', 0)) {
$inviter->balance = $inviter->balance + $commissionBalance;
} else {
$inviter->commission_balance = $inviter->commission_balance + $commissionBalance;
}
if (!$inviter->save()) {
DB::rollBack();
return false;
}
if (!CommissionLog::create([
'invite_user_id' => $inviteUserId,
'user_id' => $order->user_id,
'trade_no' => $order->trade_no,
'order_amount' => $order->total_amount,
'get_amount' => $commissionBalance
])) {
DB::rollBack();
return false;
}
$inviteUserId = $inviter->invite_user_id;
// update order actual commission balance
$order->actual_commission_balance = $order->actual_commission_balance + $commissionBalance;
}
return true;
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Jobs\OrderHandleJob;
use App\Services\OrderService;
use Illuminate\Console\Command;
use App\Models\Order;
@@ -42,21 +43,12 @@ class CheckOrder extends Command
*/
public function handle()
{
$orders = Order::get();
foreach ($orders as $item) {
$orderService = new OrderService($item);
switch ($item->status) {
// cancel
case 0:
if (strtotime($item->created_at) <= (time() - 1800)) {
$orderService->cancel();
}
break;
case 1:
$orderService->open();
break;
}
ini_set('memory_limit', -1);
$orders = Order::whereIn('status', [0, 1])
->orderBy('created_at', 'ASC')
->get();
foreach ($orders as $order) {
OrderHandleJob::dispatch($order->trade_no);
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Console\Commands;
use App\Services\ServerService;
use App\Services\TelegramService;
use App\Utils\CacheKey;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
class CheckServer extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:server';
/**
* The console command description.
*
* @var string
*/
protected $description = '节点检查任务';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->checkOffline();
}
private function checkOffline()
{
$serverService = new ServerService();
$servers = $serverService->getAllServers();
foreach ($servers as $server) {
if ($server['parent_id']) continue;
if ($server['last_check_at'] && (time() - $server['last_check_at']) > 1800) {
$telegramService = new TelegramService();
$message = sprintf(
"节点掉线通知\r\n----\r\n节点名称:%s\r\n节点地址:%s\r\n",
$server['name'],
$server['host']
);
$telegramService->sendMessageWithAdmin($message);
Cache::forget(CacheKey::get(sprintf("SERVER_%s_LAST_CHECK_AT", strtoupper($server['type'])), $server->id));
}
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use App\Models\Ticket;
use Illuminate\Console\Command;
class CheckTicket extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'check:ticket';
/**
* The console command description.
*
* @var string
*/
protected $description = '工单检查任务';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
ini_set('memory_limit', -1);
$tickets = Ticket::where('status', 0)
->where('updated_at', '<=', time() - 24 * 3600)
->where('reply_status', 0)
->get();
foreach ($tickets as $ticket) {
if ($ticket->user_id === $ticket->last_reply_user_id) continue;
$ticket->status = 1;
$ticket->save();
}
}
}

View File

@@ -2,25 +2,25 @@
namespace App\Console\Commands;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use App\Models\ServerLog;
class ResetServerLog extends Command
class ClearUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:serverLog';
protected $signature = 'clear:user';
/**
* The console command description.
*
* @var string
*/
protected $description = '节点服务器日志重置';
protected $description = '清理用户';
/**
* Create a new command instance.
@@ -39,6 +39,13 @@ class ResetServerLog extends Command
*/
public function handle()
{
ServerLog::truncate();
$builder = User::where('plan_id', NULL)
->where('transfer_enable', 0)
->where('expired_at', 0)
->where('last_login_at', NULL);
$count = $builder->count();
if ($builder->delete()) {
$this->info("已删除${count}位没有任何数据的用户");
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use App\Models\Log;
use App\Models\Plan;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Utils\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetLog extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:log';
/**
* The console command description.
*
* @var string
*/
protected $description = '清空日志';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
StatUser::where('record_at', '<', strtotime('-2 month', time()))->delete();
StatServer::where('record_at', '<', strtotime('-2 month', time()))->delete();
Log::where('created_at', '<', strtotime('-1 month', time()))->delete();
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands;
use App\Models\Plan;
use App\Utils\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetPassword extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:password {email}';
/**
* The console command description.
*
* @var string
*/
protected $description = '重置用户密码';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$user = User::where('email', $this->argument('email'))->first();
if (!$user) abort(500, '邮箱不存在');
$password = Helper::guid(false);
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->password_algo = null;
if (!$user->save()) abort(500, '重置失败');
$this->info("!!!重置成功!!!");
$this->info("新密码为:{$password},请尽快修改密码。");
}
}

View File

@@ -2,8 +2,10 @@
namespace App\Console\Commands;
use App\Models\Plan;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetTraffic extends Command
{
@@ -41,22 +43,96 @@ class ResetTraffic extends Command
*/
public function handle()
{
$resetTrafficMethod = config('v2board.reset_traffic_method', 0);
switch ((int)$resetTrafficMethod) {
// 1 a month
case 0:
$this->resetByMonthFirstDay();
break;
// expire day
case 1:
$this->resetByExpireDay();
break;
ini_set('memory_limit', -1);
$resetMethods = Plan::select(
DB::raw("GROUP_CONCAT(`id`) as plan_ids"),
DB::raw("reset_traffic_method as method")
)
->groupBy('reset_traffic_method')
->get()
->toArray();
foreach ($resetMethods as $resetMethod) {
$planIds = explode(',', $resetMethod['plan_ids']);
switch (true) {
case ($resetMethod['method'] === NULL): {
$resetTrafficMethod = config('v2board.reset_traffic_method', 0);
$builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
switch ((int)$resetTrafficMethod) {
// month first day
case 0:
$this->resetByMonthFirstDay($builder);
break;
// expire day
case 1:
$this->resetByExpireDay($builder);
break;
// no action
case 2:
break;
// year first day
case 3:
$this->resetByYearFirstDay($builder);
// year expire day
case 4:
$this->resetByExpireYear($builder);
}
break;
}
case ($resetMethod['method'] === 0): {
$builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
$this->resetByMonthFirstDay($builder);
break;
}
case ($resetMethod['method'] === 1): {
$builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
$this->resetByExpireDay($builder);
break;
}
case ($resetMethod['method'] === 2): {
break;
}
case ($resetMethod['method'] === 3): {
$builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
$this->resetByYearFirstDay($builder);
break;
}
case ($resetMethod['method'] === 4): {
$builder = with(clone($this->builder))->whereIn('plan_id', $planIds);
$this->resetByExpireYear($builder);
break;
}
}
}
}
private function resetByMonthFirstDay():void
private function resetByExpireYear($builder):void
{
$users = [];
foreach ($builder->get() as $item) {
$expireDay = date('m-d', $item->expired_at);
$today = date('m-d');
if ($expireDay === $today) {
array_push($users, $item->id);
}
}
User::whereIn('id', $users)->update([
'u' => 0,
'd' => 0
]);
}
private function resetByYearFirstDay($builder):void
{
if ((string)date('md') === '0101') {
$builder->update([
'u' => 0,
'd' => 0
]);
}
}
private function resetByMonthFirstDay($builder):void
{
$builder = $this->builder;
if ((string)date('d') === '01') {
$builder->update([
'u' => 0,
@@ -65,9 +141,8 @@ class ResetTraffic extends Command
}
}
private function resetByExpireDay():void
private function resetByExpireDay($builder):void
{
$builder = $this->builder;
$lastDay = date('d', strtotime('last day of +0 months'));
$users = [];
foreach ($builder->get() as $item) {

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use App\Models\Plan;
use App\Utils\Helper;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class ResetUser extends Command
{
protected $builder;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reset:user';
/**
* The console command description.
*
* @var string
*/
protected $description = '重置所有用户信息';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (!$this->confirm("确定要重置所有用户安全信息吗?")) {
return;
}
ini_set('memory_limit', -1);
$users = User::all();
foreach ($users as $user)
{
$user->token = Helper::guid();
$user->uuid = Helper::guid(true);
$user->save();
$this->info("已重置用户{$user->email}的安全信息");
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Services\MailService;
use Illuminate\Console\Command;
use App\Models\User;
use App\Models\MailLog;
@@ -41,23 +42,10 @@ class SendRemindMail extends Command
public function handle()
{
$users = User::all();
$mailService = new MailService();
foreach ($users as $user) {
if ($user->remind_expire) $this->remindExpire($user);
}
}
private function remindExpire($user)
{
if ($user->expired_at !== NULL && ($user->expired_at - 86400) < time() && $user->expired_at > time()) {
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => '在' . config('v2board.app_name', 'V2board') . '的服务即将到期',
'template_name' => 'remindExpire',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url')
]
]);
if ($user->remind_expire) $mailService->remindExpire($user);
if ($user->remind_traffic) $mailService->remindTraffic($user);
}
}
}

View File

@@ -1,68 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Utils\CacheKey;
use Illuminate\Console\Command;
use App\Models\ServerLog;
use App\Models\ServerStat;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class V2boardCache extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'v2board:cache';
/**
* The console command description.
*
* @var string
*/
protected $description = '缓存任务';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
}
private function cacheServerStat()
{
$serverLogs = ServerLog::select(
'server_id',
DB::raw("sum(u) as u"),
DB::raw("sum(d) as d"),
DB::raw("count(*) as online")
)
->where('updated_at', '>=', time() - 3600)
->groupBy('server_id')
->get();
foreach ($serverLogs as $serverLog) {
$data = [
'server_id' => $serverLog->server_id,
'u' => $serverLog->u,
'd' => $serverLog->d,
'online' => $serverLog->online
];
// ServerStat::create($data);
}
}
}

View File

@@ -47,13 +47,14 @@ class V2boardInstall extends Command
$this->info(" \ \ / / __) | _ \ / _ \ / _` | '__/ _` | ");
$this->info(" \ V / / __/| |_) | (_) | (_| | | | (_| | ");
$this->info(" \_/ |_____|____/ \___/ \__,_|_| \__,_| ");
if (\File::exists(base_path() . '/.lock')) {
abort(500, 'V2board 已安装,如需重新安装请删除目录下.lock文件');
if (\File::exists(base_path() . '/.env')) {
$securePath = config('v2board.secure_path', config('v2board.frontend_admin_path', hash('crc32b', config('app.key'))));
$this->info("访问 http(s)://你的站点/{$securePath} 进入管理面板,你可以在用户中心修改你的密码。");
abort(500, '如需重新安装请删除目录下.env文件');
}
if (!\File::exists(base_path() . '/.env')) {
if (!copy(base_path() . '/.env.example', base_path() . '/.env')) {
abort(500, '复制环境文件失败,请检查目录权限');
}
if (!copy(base_path() . '/.env.example', base_path() . '/.env')) {
abort(500, '复制环境文件失败,请检查目录权限');
}
$this->saveToEnv([
'APP_KEY' => 'base64:' . base64_encode(Encrypter::generateKey('AES-256-CBC')),
@@ -90,17 +91,17 @@ class V2boardInstall extends Command
while (!$email) {
$email = $this->ask('请输入管理员邮箱?');
}
$password = '';
while (!$password) {
$password = $this->ask('请输入管理员密码?');
}
$password = Helper::guid(false);
if (!$this->registerAdmin($email, $password)) {
abort(500, '管理员账号注册失败,请重试');
}
$this->info('一切就绪');
$this->info('访问 http(s)://你的站点/admin 进入管理面板');
\File::put(base_path() . '/.lock', time());
$this->info("管理员邮箱:{$email}");
$this->info("管理员密码:{$password}");
$defaultSecurePath = hash('crc32b', config('app.key'));
$this->info("访问 http(s)://你的站点/{$defaultSecurePath} 进入管理面板,你可以在用户中心修改你的密码。");
} catch (\Exception $e) {
$this->error($e->getMessage());
}

View File

@@ -0,0 +1,140 @@
<?php
namespace App\Console\Commands;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Services\StatisticalService;
use Illuminate\Console\Command;
use App\Models\Stat;
use Illuminate\Support\Facades\DB;
class V2boardStatistics extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'v2board:statistics';
/**
* The console command description.
*
* @var string
*/
protected $description = '统计任务';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$startAt = microtime(true);
ini_set('memory_limit', -1);
$this->statUser();
$this->statServer();
$this->stat();
info('统计任务执行完毕。耗时:' . (microtime(true) - $startAt) / 1000);
}
private function statServer()
{
try {
DB::beginTransaction();
$createdAt = time();
$recordAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$statService->setServerStats();
$stats = $statService->getStatServer();
foreach ($stats as $stat) {
if (!StatServer::insert([
'server_id' => $stat['server_id'],
'server_type' => $stat['server_type'],
'u' => $stat['u'],
'd' => $stat['d'],
'created_at' => $createdAt,
'updated_at' => $createdAt,
'record_type' => 'd',
'record_at' => $recordAt
])) {
throw new \Exception('stat server fail');
}
}
DB::commit();
$statService->clearStatServer();
} catch (\Exception $e) {
DB::rollback();
\Log::error($e->getMessage(), ['exception' => $e]);
}
}
private function statUser()
{
try {
DB::beginTransaction();
$createdAt = time();
$recordAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$statService = new StatisticalService();
$statService->setStartAt($recordAt);
$statService->setUserStats();
$stats = $statService->getStatUser();
foreach ($stats as $stat) {
if (!StatUser::insert([
'user_id' => $stat['user_id'],
'u' => $stat['u'],
'd' => $stat['d'],
'server_rate' => $stat['server_rate'],
'created_at' => $createdAt,
'updated_at' => $createdAt,
'record_type' => 'd',
'record_at' => $recordAt
])) {
throw new \Exception('stat user fail');
}
}
DB::commit();
$statService->clearStatUser();
} catch (\Exception $e) {
DB::rollback();
\Log::error($e->getMessage(), ['exception' => $e]);
}
}
private function stat()
{
try {
$endAt = strtotime(date('Y-m-d'));
$startAt = strtotime('-1 day', $endAt);
$statisticalService = new StatisticalService();
$statisticalService->setStartAt($startAt);
$statisticalService->setEndAt($endAt);
$data = $statisticalService->generateStatData();
$data['record_at'] = $startAt;
$data['record_type'] = 'd';
$statistic = Stat::where('record_at', $startAt)
->where('record_type', 'd')
->first();
if ($statistic) {
$statistic->update($data);
return;
}
Stat::create($data);
} catch (\Exception $e) {
\Log::error($e->getMessage(), ['exception' => $e]);
}
}
}

View File

@@ -51,11 +51,13 @@ class V2boardUpdate extends Command
}
$this->info('正在导入数据库请稍等...');
foreach ($sql as $item) {
if (!$item) continue;
try {
DB::select(DB::raw($item));
} catch (\Exception $e) {
}
}
$this->info('更新完毕');
\Artisan::call('horizon:terminate');
$this->info('更新完毕,队列服务已重启,你无需进行任何操作。');
}
}

10
app/Console/Kernel.php Executable file → Normal file
View File

@@ -2,8 +2,10 @@
namespace App\Console;
use App\Utils\CacheKey;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\Cache;
class Kernel extends ConsoleKernel
{
@@ -24,16 +26,20 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
Cache::put(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null), time());
// v2board
$schedule->command('v2board:cache')->hourly();
$schedule->command('v2board:statistics')->dailyAt('0:10');
// check
$schedule->command('check:order')->everyMinute();
$schedule->command('check:commission')->everyMinute();
$schedule->command('check:ticket')->everyMinute();
// reset
$schedule->command('reset:traffic')->daily();
$schedule->command('reset:serverLog')->quarterly();
$schedule->command('reset:log')->daily();
// send
$schedule->command('send:remindMail')->dailyAt('11:30');
// horizon metrics
$schedule->command('horizon:snapshot')->everyFiveMinutes();
}
/**

View File

@@ -2,8 +2,11 @@
namespace App\Exceptions;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Support\Arr;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
use Facade\Ignition\Exceptions\ViewException;
class Handler extends ExceptionHandler
{
@@ -29,10 +32,12 @@ class Handler extends ExceptionHandler
/**
* Report or log an exception.
*
* @param \Exception $exception
* @param \Throwable $exception
* @return void
*
* @throws \Throwable
*/
public function report(Exception $exception)
public function report(Throwable $exception)
{
parent::report($exception);
}
@@ -40,16 +45,33 @@ class Handler extends ExceptionHandler
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return \Illuminate\Http\Response
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Throwable
*/
public function render($request, Exception $exception)
public function render($request, Throwable $exception)
{
if($exception instanceof \Illuminate\Http\Exceptions\ThrottleRequestsException) {
abort(429, '请求频繁,请稍后再试');
if ($exception instanceof ViewException) {
abort(500, "主题渲染失败。如更新主题,参数可能发生变化请重新配置主题后再试。");
}
return parent::render($request, $exception);
}
protected function convertExceptionToArray(Throwable $e)
{
return config('app.debug') ? [
'message' => $e->getMessage(),
'exception' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => collect($e->getTrace())->map(function ($trace) {
return Arr::except($trace, ['args']);
})->all(),
] : [
'message' => $this->isHttpException($e) ? $e->getMessage() : __("Uh-oh, we've had some problems, we're working on it."),
];
}
}

View File

@@ -1,164 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\ConfigSave;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Utils\Dict;
use App\Http\Controllers\Controller;
class ConfigController extends Controller
{
public function getEmailTemplate()
{
$path = resource_path('views/mail/');
$files = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
return response([
'data' => $files
]);
}
public function setTelegramWebhook(Request $request)
{
$telegramService = new TelegramService($request->input('telegram_bot_token'));
$telegramService->getMe();
$telegramService->setWebhook(
url(
'/api/v1/guest/telegram/webhook?access_token=' . md5(config('v2board.telegram_bot_token', $request->input('telegram_bot_token')))
)
);
return response([
'data' => true
]);
}
public function fetch()
{
// TODO: default should be in Dict
return response([
'data' => [
'invite' => [
'invite_force' => (int)config('v2board.invite_force', 0),
'invite_commission' => config('v2board.invite_commission', 10),
'invite_gen_limit' => config('v2board.invite_gen_limit', 5),
'invite_never_expire' => config('v2board.invite_never_expire', 0),
'commission_first_time_enable' => config('v2board.commission_first_time_enable', 1),
'commission_auto_check_enable' => config('v2board.commission_auto_check_enable', 1),
'commission_withdraw_limit' => config('v2board.commission_withdraw_limit', 100)
],
'site' => [
'safe_mode_enable' => (int)config('v2board.safe_mode_enable', 0),
'stop_register' => (int)config('v2board.stop_register', 0),
'email_verify' => (int)config('v2board.email_verify', 0),
'app_name' => config('v2board.app_name', 'V2Board'),
'app_description' => config('v2board.app_description', 'V2Board is best!'),
'app_url' => config('v2board.app_url'),
'subscribe_url' => config('v2board.subscribe_url'),
'try_out_plan_id' => (int)config('v2board.try_out_plan_id', 0),
'try_out_hour' => (int)config('v2board.try_out_hour', 1),
'email_whitelist_enable' => (int)config('v2board.email_whitelist_enable', 0),
'email_whitelist_suffix' => config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
'email_gmail_limit_enable' => config('v2board.email_gmail_limit_enable', 0),
'recaptcha_enable' => (int)config('v2board.recaptcha_enable', 0),
'recaptcha_key' => config('v2board.recaptcha_key'),
'recaptcha_site_key' => config('v2board.recaptcha_site_key')
],
'subscribe' => [
'plan_change_enable' => (int)config('v2board.plan_change_enable', 1),
'reset_traffic_method' => (int)config('v2board.reset_traffic_method', 0),
'renew_reset_traffic_enable' => (int)config('v2board.renew_reset_traffic_enable', 0),
'surplus_enable' => (int)config('v2board.surplus_enable', 1)
],
'pay' => [
// alipay
'alipay_enable' => (int)config('v2board.alipay_enable'),
'alipay_appid' => config('v2board.alipay_appid'),
'alipay_pubkey' => config('v2board.alipay_pubkey'),
'alipay_privkey' => config('v2board.alipay_privkey'),
// stripe
'stripe_alipay_enable' => (int)config('v2board.stripe_alipay_enable', 0),
'stripe_wepay_enable' => (int)config('v2board.stripe_wepay_enable', 0),
'stripe_card_enable' => (int)config('v2board.stripe_card_enable', 0),
'stripe_sk_live' => config('v2board.stripe_sk_live'),
'stripe_pk_live' => config('v2board.stripe_pk_live'),
'stripe_webhook_key' => config('v2board.stripe_webhook_key'),
'stripe_currency' => config('v2board.stripe_currency', 'hkd'),
// bitpayx
'bitpayx_name' => config('v2board.bitpayx_name', '在线支付'),
'bitpayx_enable' => (int)config('v2board.bitpayx_enable', 0),
'bitpayx_appsecret' => config('v2board.bitpayx_appsecret'),
// mGate
'mgate_name' => config('v2board.mgate_name', '在线支付'),
'mgate_enable' => (int)config('v2board.mgate_enable', 0),
'mgate_url' => config('v2board.mgate_url'),
'mgate_app_id' => config('v2board.mgate_app_id'),
'mgate_app_secret' => config('v2board.mgate_app_secret'),
// Epay
'epay_name' => config('v2board.epay_name', '在线支付'),
'epay_enable' => (int)config('v2board.epay_enable', 0),
'epay_url' => config('v2board.epay_url'),
'epay_pid' => config('v2board.epay_pid'),
'epay_key' => config('v2board.epay_key'),
],
'frontend' => [
'frontend_theme_sidebar' => config('v2board.frontend_theme_sidebar', 'light'),
'frontend_theme_header' => config('v2board.frontend_theme_header', 'dark'),
'frontend_theme_color' => config('v2board.frontend_theme_color', 'default'),
'frontend_background_url' => config('v2board.frontend_background_url'),
'frontend_admin_path' => config('v2board.frontend_admin_path', 'admin')
],
'server' => [
'server_token' => config('v2board.server_token'),
'server_license' => config('v2board.server_license'),
'server_log_enable' => config('v2board.server_log_enable', 0),
'server_v2ray_domain' => config('v2board.server_v2ray_domain'),
'server_v2ray_protocol' => config('v2board.server_v2ray_protocol'),
],
'tutorial' => [
'apple_id' => config('v2board.apple_id')
],
'email' => [
'email_template' => config('v2board.email_template', 'default'),
'email_host' => config('v2board.email_host'),
'email_port' => config('v2board.email_port'),
'email_username' => config('v2board.email_username'),
'email_password' => config('v2board.email_password'),
'email_encryption' => config('v2board.email_encryption'),
'email_from_address' => config('v2board.email_from_address')
],
'telegram' => [
'telegram_bot_enable' => config('v2board.telegram_bot_enable', 0),
'telegram_bot_token' => config('v2board.telegram_bot_token')
]
]
]);
}
public function save(ConfigSave $request)
{
$data = $request->input();
$array = \Config::get('v2board');
foreach ($data as $k => $v) {
if (!in_array($k, array_keys($request->validated()))) {
abort(500, '参数' . $k . '不在规则内,禁止修改');
}
$array[$k] = $v;
}
$data = var_export($array, 1);
if (!\File::put(base_path() . '/config/v2board.php', "<?php\n return $data ;")) {
abort(500, '修改失败');
}
if (function_exists('opcache_reset')) {
if (!opcache_reset()) {
abort(500, '缓存清除失败请卸载或检查opcache配置状态');
}
}
\Artisan::call('config:cache');
return response([
'data' => true
]);
}
}

View File

@@ -1,168 +0,0 @@
<?php
namespace App\Http\Controllers\Admin\Server;
use App\Http\Requests\Admin\ServerV2raySave;
use App\Http\Requests\Admin\ServerV2raySort;
use App\Http\Requests\Admin\ServerV2rayUpdate;
use App\Services\ServerService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Server;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class V2rayController extends Controller
{
public function fetch(Request $request)
{
$server = Server::orderBy('sort', 'ASC')->get();
for ($i = 0; $i < count($server); $i++) {
if (!empty($server[$i]['tags'])) {
$server[$i]['tags'] = json_decode($server[$i]['tags']);
}
$server[$i]['group_id'] = json_decode($server[$i]['group_id']);
$server[$i]['online'] = Cache::get(CacheKey::get('SERVER_V2RAY_ONLINE_USER', $server[$i]['parent_id'] ? $server[$i]['parent_id'] : $server[$i]['id']));
if ($server[$i]['parent_id']) {
$server[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_V2RAY_LAST_CHECK_AT', $server[$i]['parent_id']));
} else {
$server[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_V2RAY_LAST_CHECK_AT', $server[$i]['id']));
}
}
return response([
'data' => $server
]);
}
public function save(ServerV2raySave $request)
{
$params = $request->validated();
$params['group_id'] = json_encode($params['group_id']);
if (isset($params['tags'])) {
$params['tags'] = json_encode($params['tags']);
}
if (isset($params['dnsSettings'])) {
if (!is_object(json_decode($params['dnsSettings']))) {
abort(500, 'DNS规则配置格式不正确');
}
}
if (isset($params['ruleSettings'])) {
if (!is_object(json_decode($params['ruleSettings']))) {
abort(500, '审计规则配置格式不正确');
}
}
if (isset($params['networkSettings'])) {
if (!is_object(json_decode($params['networkSettings']))) {
abort(500, '传输协议配置格式不正确');
}
}
if (isset($params['tlsSettings'])) {
if (!is_object(json_decode($params['tlsSettings']))) {
abort(500, 'TLS配置格式不正确');
}
}
if ($request->input('id')) {
$server = Server::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!Server::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = Server::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(ServerV2rayUpdate $request)
{
$params = $request->only([
'show',
]);
$server = Server::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function copy(Request $request)
{
$server = Server::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!Server::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
public function viewConfig(Request $request)
{
$serverService = new ServerService();
$config = $serverService->getVmessConfig($request->input('node_id'), 23333);
return response([
'data' => $config
]);
}
public function sort(ServerV2raySort $request)
{
DB::beginTransaction();
foreach ($request->input('server_ids') as $k => $v) {
if (!Server::find($v)->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
}
DB::commit();
return response([
'data' => true
]);
}
}

View File

@@ -1,46 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\ServerGroup;
use App\Models\Server;
use App\Models\Plan;
use App\Models\User;
use App\Models\Ticket;
use App\Models\Order;
use Illuminate\Support\Facades\Cache;
class StatController extends Controller
{
public function getOverride(Request $request)
{
return response([
'data' => [
'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->whereIn('status', [3, 4])
->sum('total_amount'),
'month_register_total' => User::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->count(),
'ticket_pendding_total' => Ticket::where('status', 0)
->count(),
'commission_pendding_total' => Order::where('commission_status', 0)
->where('invite_user_id', '!=', NULL)
->where('status', 3)
->where('commission_balance', '>', 0)
->count(),
'day_income' => Order::where('created_at', '>=', strtotime(date('Y-m-d')))
->where('created_at', '<', time())
->where('status', 3)
->sum('total_amount'),
'last_month_income' => Order::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1'))))
->where('created_at', '<', strtotime(date('Y-m-1')))
->where('status', 3)
->sum('total_amount')
]
]);
}
}

View File

@@ -1,121 +0,0 @@
<?php
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\Clash;
use Illuminate\Http\Request;
use App\Models\Server;
use Symfony\Component\Yaml\Yaml;
class AppController extends Controller
{
CONST CLIENT_CONFIG = '{"policy":{"levels":{"0":{"uplinkOnly":0}}},"dns":{"servers":["114.114.114.114","8.8.8.8"]},"outboundDetour":[{"protocol":"freedom","tag":"direct","settings":{}}],"inbound":{"listen":"0.0.0.0","port":31211,"protocol":"socks","settings":{"auth":"noauth","udp":true,"ip":"127.0.0.1"}},"inboundDetour":[{"listen":"0.0.0.0","allocate":{"strategy":"always","refresh":5,"concurrency":3},"port":31210,"protocol":"http","tag":"httpDetour","domainOverride":["http","tls"],"streamSettings":{},"settings":{"timeout":0}}],"routing":{"strategy":"rules","settings":{"domainStrategy":"IPIfNonMatch","rules":[{"type":"field","ip":["geoip:cn"],"outboundTag":"direct"},{"type":"field","ip":["0.0.0.0/8","10.0.0.0/8","100.64.0.0/10","127.0.0.0/8","169.254.0.0/16","172.16.0.0/12","192.0.0.0/24","192.0.2.0/24","192.168.0.0/16","198.18.0.0/15","198.51.100.0/24","203.0.113.0/24","::1/128","fc00::/7","fe80::/10"],"outboundTag":"direct"}]}},"outbound":{"tag":"proxy","sendThrough":"0.0.0.0","mux":{"enabled":false,"concurrency":8},"protocol":"vmess","settings":{"vnext":[{"address":"server","port":443,"users":[{"id":"uuid","alterId":2,"security":"auto","level":0}],"remark":"remark"}]},"streamSettings":{"network":"tcp","tcpSettings":{"header":{"type":"none"}},"security":"none","tlsSettings":{"allowInsecure":true,"allowInsecureCiphers":true},"kcpSettings":{"header":{"type":"none"},"mtu":1350,"congestion":false,"tti":20,"uplinkCapacity":5,"writeBufferSize":1,"readBufferSize":1,"downlinkCapacity":20},"wsSettings":{"path":"","headers":{"Host":"server.cc"}}}}}';
CONST SOCKS_PORT = 10010;
CONST HTTP_PORT = 10011;
public function getConfig(Request $request)
{
$servers = [];
$user = $request->user;
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAllServers($user);
}
$config = Yaml::parseFile(base_path() . '/resources/rules/app.clash.yaml');
$proxy = [];
$proxies = [];
foreach ($servers['shadowsocks'] as $item) {
array_push($proxy, Clash::buildShadowsocks($user->uuid, $item));
array_push($proxies, $item->name);
}
foreach ($servers['vmess'] as $item) {
array_push($proxy, Clash::buildVmess($user->uuid, $item));
array_push($proxies, $item->name);
}
foreach ($servers['trojan'] as $item) {
array_push($proxy, Clash::buildTrojan($user->uuid, $item));
array_push($proxies, $item->name);
}
$config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
foreach ($config['proxy-groups'] as $k => $v) {
$config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
}
die(Yaml::dump($config));
}
public function getVersion()
{
return response([
'data' => [
'version' => '4.0.0',
'download_url' => ''
]
]);
}
public function config(Request $request)
{
if (empty($request->input('server_id'))) {
abort(500, '参数错误');
}
$user = $request->user;
if ($user->expired_at < time() && $user->expired_at !== NULL) {
abort(500, '订阅计划已过期');
}
$server = Server::where('show', 1)
->where('id', $request->input('server_id'))
->first();
if (!$server) {
abort(500, '服务器不存在');
}
$json = json_decode(self::CLIENT_CONFIG);
//socks
$json->inbound->port = (int)self::SOCKS_PORT;
//http
$json->inboundDetour[0]->port = (int)self::HTTP_PORT;
//other
$json->outbound->settings->vnext[0]->address = (string)$server->host;
$json->outbound->settings->vnext[0]->port = (int)$server->port;
$json->outbound->settings->vnext[0]->users[0]->id = (string)$user->uuid;
$json->outbound->settings->vnext[0]->users[0]->alterId = (int)$user->v2ray_alter_id;
$json->outbound->settings->vnext[0]->remark = (string)$server->name;
$json->outbound->streamSettings->network = $server->network;
if ($server->networkSettings) {
switch ($server->network) {
case 'tcp':
$json->outbound->streamSettings->tcpSettings = json_decode($server->networkSettings);
break;
case 'kcp':
$json->outbound->streamSettings->kcpSettings = json_decode($server->networkSettings);
break;
case 'ws':
$json->outbound->streamSettings->wsSettings = json_decode($server->networkSettings);
break;
case 'http':
$json->outbound->streamSettings->httpSettings = json_decode($server->networkSettings);
break;
case 'domainsocket':
$json->outbound->streamSettings->dsSettings = json_decode($server->networkSettings);
break;
case 'quic':
$json->outbound->streamSettings->quicSettings = json_decode($server->networkSettings);
break;
}
}
if ($request->input('is_global')) {
$json->routing->settings->rules[0]->outboundTag = 'proxy';
}
if ($server->tls) {
$json->outbound->streamSettings->security = "tls";
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
}

View File

@@ -1,245 +0,0 @@
<?php
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Utils\Clash;
use App\Utils\QuantumultX;
use App\Utils\Shadowrocket;
use App\Utils\Surge;
use App\Utils\Surfboard;
use App\Utils\URLSchemes;
use Illuminate\Http\Request;
use App\Models\Server;
use App\Utils\Helper;
use Symfony\Component\Yaml\Yaml;
use App\Services\UserService;
class ClientController extends Controller
{
public function subscribe(Request $request)
{
$flag = $request->input('flag')
?? (isset($_SERVER['HTTP_USER_AGENT'])
? $_SERVER['HTTP_USER_AGENT']
: '');
$flag = strtolower($flag);
$user = $request->user;
// account not expired and is not banned.
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAllServers($user);
if ($flag) {
if (strpos($flag, 'quantumult%20x') !== false) {
die($this->quantumultX($user, $servers['shadowsocks'], $servers['vmess'], $servers['trojan']));
}
if (strpos($flag, 'quantumult') !== false) {
die($this->quantumult($user, $servers['vmess']));
}
if (strpos($flag, 'clash') !== false) {
die($this->clash($user, $servers['shadowsocks'], $servers['vmess'], $servers['trojan']));
}
if (strpos($flag, 'surfboard') !== false) {
die($this->surfboard($user, $servers['shadowsocks'], $servers['vmess']));
}
if (strpos($flag, 'surge') !== false) {
die($this->surge($user, $servers['shadowsocks'], $servers['vmess'], $servers['trojan']));
}
if (strpos($flag, 'shadowrocket') !== false) {
die($this->shadowrocket($user, $servers['shadowsocks'], $servers['vmess'], $servers['trojan']));
}
}
die($this->origin($user, $servers['shadowsocks'], $servers['vmess'], $servers['trojan']));
}
}
// TODO: Ready to stop support
private function quantumult($user, $vmess = [])
{
$uri = '';
header('subscription-userinfo: upload=' . $user->u . '; download=' . $user->d . ';total=' . $user->transfer_enable);
foreach ($vmess as $item) {
$str = '';
$str .= $item->name . '= vmess, ' . $item->host . ', ' . $item->port . ', chacha20-ietf-poly1305, "' . $user->uuid . '", over-tls=' . ($item->tls ? "true" : "false") . ', certificate=0, group=' . config('v2board.app_name', 'V2Board');
if ($item->network === 'ws') {
$str .= ', obfs=ws';
if ($item->networkSettings) {
$wsSettings = json_decode($item->networkSettings);
if (isset($wsSettings->path)) $str .= ', obfs-path="' . $wsSettings->path . '"';
if (isset($wsSettings->headers->Host)) $str .= ', obfs-header="Host:' . $wsSettings->headers->Host . '"';
}
}
$uri .= "vmess://" . base64_encode($str) . "\r\n";
}
return base64_encode($uri);
}
private function shadowrocket($user, $shadowsocks = [], $vmess = [], $trojan = [])
{
$uri = '';
//display remaining traffic and expire date
$upload = round($user->u / (1024*1024*1024), 2);
$download = round($user->d / (1024*1024*1024), 2);
$totalTraffic = round($user->transfer_enable / (1024*1024*1024), 2);
$expiredDate = date('Y-m-d', $user->expired_at);
$uri .= "STATUS=🚀↑:{$upload}GB,↓:{$download}GB,TOT:{$totalTraffic}GB💡Expires:{$expiredDate}\r\n";
foreach ($shadowsocks as $item) {
$uri .= Shadowrocket::buildShadowsocks($user->uuid, $item);
}
foreach ($vmess as $item) {
$uri .= Shadowrocket::buildVmess($user->uuid, $item);
}
foreach ($trojan as $item) {
$uri .= Shadowrocket::buildTrojan($user->uuid, $item);
}
return base64_encode($uri);
}
private function quantumultX($user, $shadowsocks = [], $vmess = [], $trojan = [])
{
$uri = '';
header("subscription-userinfo: upload={$user->u}; download={$user->d}; total={$user->transfer_enable}; expire={$user->expired_at}");
foreach ($shadowsocks as $item) {
$uri .= QuantumultX::buildShadowsocks($user->uuid, $item);
}
foreach ($vmess as $item) {
$uri .= QuantumultX::buildVmess($user->uuid, $item);
}
foreach ($trojan as $item) {
$uri .= QuantumultX::buildTrojan($user->uuid, $item);
}
return base64_encode($uri);
}
private function origin($user, $shadowsocks = [], $vmess = [], $trojan = [])
{
$uri = '';
foreach ($shadowsocks as $item) {
$uri .= URLSchemes::buildShadowsocks($item, $user);
}
foreach ($vmess as $item) {
$uri .= URLSchemes::buildVmess($item, $user);
}
foreach ($trojan as $item) {
$uri .= URLSchemes::buildTrojan($item, $user);
}
return base64_encode($uri);
}
private function surge($user, $shadowsocks = [], $vmess = [], $trojan = [])
{
$proxies = '';
$proxyGroup = '';
foreach ($shadowsocks as $item) {
// [Proxy]
$proxies .= Surge::buildShadowsocks($user->uuid, $item);
// [Proxy Group]
$proxyGroup .= $item->name . ', ';
}
foreach ($vmess as $item) {
// [Proxy]
$proxies .= Surge::buildVmess($user->uuid, $item);
// [Proxy Group]
$proxyGroup .= $item->name . ', ';
}
foreach ($trojan as $item) {
// [Proxy]
$proxies .= Surge::buildTrojan($user->uuid, $item);
// [Proxy Group]
$proxyGroup .= $item->name . ', ';
}
$defaultConfig = base_path() . '/resources/rules/default.surge.conf';
$customConfig = base_path() . '/resources/rules/custom.surge.conf';
if (\File::exists($customConfig)) {
$config = file_get_contents("$customConfig");
} else {
$config = file_get_contents("$defaultConfig");
}
// Subscription link
$subsURL = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$config = str_replace('$subs_link', $subsURL, $config);
$config = str_replace('$proxies', $proxies, $config);
$config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
return $config;
}
private function surfboard($user, $shadowsocks = [], $vmess = [])
{
$proxies = '';
$proxyGroup = '';
foreach ($shadowsocks as $item) {
// [Proxy]
$proxies .= Surfboard::buildShadowsocks($user->uuid, $item);
// [Proxy Group]
$proxyGroup .= $item->name . ', ';
}
foreach ($vmess as $item) {
// [Proxy]
$proxies .= Surfboard::buildVmess($user->uuid, $item);
// [Proxy Group]
$proxyGroup .= $item->name . ', ';
}
$defaultConfig = base_path() . '/resources/rules/default.surfboard.conf';
$customConfig = base_path() . '/resources/rules/custom.surfboard.conf';
if (\File::exists($customConfig)) {
$config = file_get_contents("$customConfig");
} else {
$config = file_get_contents("$defaultConfig");
}
// Subscription link
$subsURL = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$config = str_replace('$subs_link', $subsURL, $config);
$config = str_replace('$proxies', $proxies, $config);
$config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
return $config;
}
private function clash($user, $shadowsocks = [], $vmess = [], $trojan = [])
{
$defaultConfig = base_path() . '/resources/rules/default.clash.yaml';
$customConfig = base_path() . '/resources/rules/custom.clash.yaml';
if (\File::exists($customConfig)) {
$config = Yaml::parseFile($customConfig);
} else {
$config = Yaml::parseFile($defaultConfig);
}
$proxy = [];
$proxies = [];
foreach ($shadowsocks as $item) {
array_push($proxy, Clash::buildShadowsocks($user->uuid, $item));
array_push($proxies, $item->name);
}
foreach ($vmess as $item) {
array_push($proxy, Clash::buildVmess($user->uuid, $item));
array_push($proxies, $item->name);
}
foreach ($trojan as $item) {
array_push($proxy, Clash::buildTrojan($user->uuid, $item));
array_push($proxies, $item->name);
}
$config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
foreach ($config['proxy-groups'] as $k => $v) {
if (!is_array($config['proxy-groups'][$k]['proxies'])) continue;
$config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
}
$yaml = Yaml::dump($config);
$yaml = str_replace('$app_name', config('v2board.app_name', 'V2Board'), $yaml);
return $yaml;
}
}

View File

@@ -1,177 +0,0 @@
<?php
namespace App\Http\Controllers\Guest;
use App\Services\OrderService;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Order;
use Library\Epay;
use Omnipay\Omnipay;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Library\BitpayX;
use Library\MGate;
class OrderController extends Controller
{
public function alipayNotify(Request $request)
{
// Log::info('alipayNotifyData: ' . json_encode($_POST));
$gateway = Omnipay::create('Alipay_AopF2F');
$gateway->setSignType('RSA2'); //RSA/RSA2
$gateway->setAppId(config('v2board.alipay_appid'));
$gateway->setPrivateKey(config('v2board.alipay_privkey')); // 可以是路径,也可以是密钥内容
$gateway->setAlipayPublicKey(config('v2board.alipay_pubkey')); // 可以是路径,也可以是密钥内容
$request = $gateway->completePurchase();
$request->setParams($_POST); //Optional
try {
/** @var \Omnipay\Alipay\Responses\AopCompletePurchaseResponse $response */
$response = $request->send();
if ($response->isPaid()) {
/**
* Payment is successful
*/
if (!$this->handle($_POST['out_trade_no'], $_POST['trade_no'])) {
abort(500, 'fail');
}
die('success'); //The response should be 'success' only
} else {
/**
* Payment is not successful
*/
die('fail');
}
} catch (Exception $e) {
/**
* Payment is not successful
*/
die('fail');
}
}
public function stripeNotify(Request $request)
{
// Log::info('stripeNotifyData: ' . json_encode($request->input()));
\Stripe\Stripe::setApiKey(config('v2board.stripe_sk_live'));
try {
$event = \Stripe\Webhook::constructEvent(
file_get_contents('php://input'),
$_SERVER['HTTP_STRIPE_SIGNATURE'],
config('v2board.stripe_webhook_key')
);
} catch (\Stripe\Error\SignatureVerification $e) {
abort(400);
}
switch ($event->type) {
case 'source.chargeable':
$object = $event->data->object;
\Stripe\Charge::create([
'amount' => $object->amount,
'currency' => $object->currency,
'source' => $object->id,
'metadata' => json_decode($object->metadata, true)
]);
die('success');
break;
case 'charge.succeeded':
$object = $event->data->object;
if ($object->status === 'succeeded') {
$metaData = isset($object->metadata->out_trade_no) ? $object->metadata : $object->source->metadata;
$tradeNo = $metaData->out_trade_no;
if (!$tradeNo) {
abort(500, 'trade no is not found in metadata');
}
if (!$this->handle($tradeNo, $object->balance_transaction)) {
abort(500, 'fail');
}
die('success');
}
break;
default:
abort(500, 'event is not support');
}
}
public function bitpayXNotify(Request $request)
{
$inputString = file_get_contents('php://input', 'r');
// Log::info('bitpayXNotifyData: ' . $inputString);
$inputStripped = str_replace(array("\r", "\n", "\t", "\v"), '', $inputString);
$inputJSON = json_decode($inputStripped, true); //convert JSON into array
$bitpayX = new BitpayX(config('v2board.bitpayx_appsecret'));
$params = [
'status' => $inputJSON['status'],
'order_id' => $inputJSON['order_id'],
'merchant_order_id' => $inputJSON['merchant_order_id'],
'price_amount' => $inputJSON['price_amount'],
'price_currency' => $inputJSON['price_currency'],
'pay_amount' => $inputJSON['pay_amount'],
'pay_currency' => $inputJSON['pay_currency'],
'created_at_t' => $inputJSON['created_at_t']
];
$strToSign = $bitpayX->prepareSignId($inputJSON['merchant_order_id']);
if (!$bitpayX->verify($strToSign, $inputJSON['token'])) {
abort(500, 'sign error');
}
if ($params['status'] !== 'PAID') {
abort(500, 'order is not paid');
}
if (!$this->handle($params['merchant_order_id'], $params['order_id'])) {
abort(500, 'order process fail');
}
die(json_encode([
'status' => 200
]));
}
public function mgateNotify(Request $request)
{
$mgate = new MGate(config('v2board.mgate_url'), config('v2board.mgate_app_id'), config('v2board.mgate_app_secret'));
if (!$mgate->verify($request->input())) {
abort(500, 'fail');
}
if (!$this->handle($request->input('out_trade_no'), $request->input('trade_no'))) {
abort(500, 'fail');
}
die('success');
}
public function epayNotify(Request $request)
{
$epay = new Epay(config('v2board.epay_url'), config('v2board.epay_pid'), config('v2board.epay_key'));
if (!$epay->verify($request->input())) {
abort(500, 'fail');
}
if (!$this->handle($request->input('out_trade_no'), $request->input('trade_no'))) {
abort(500, 'fail');
}
die('success');
}
private function handle($tradeNo, $callbackNo)
{
$order = Order::where('trade_no', $tradeNo)->first();
if ($order->status === 1) return true;
if (!$order) {
abort(500, 'order is not found');
}
$orderService = new OrderService($order);
if (!$orderService->success($callbackNo)) {
return false;
}
$telegramService = new TelegramService();
$message = sprintf(
"💰成功收款%s元\n———————————————\n订单号:%s",
$order->total_amount / 100,
$order->trade_no
);
$telegramService->sendMessageWithAdmin($message);
return true;
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace App\Http\Controllers\Guest;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Plan;
class PlanController extends Controller
{
public function fetch(Request $request)
{
$plan = Plan::where('show', 1)->get();
return response([
'data' => $plan
]);
}
}

View File

@@ -1,199 +0,0 @@
<?php
namespace App\Http\Controllers\Guest;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Utils\Helper;
use App\Services\TicketService;
class TelegramController extends Controller
{
protected $msg;
public function __construct(Request $request)
{
if ($request->input('access_token') !== md5(config('v2board.telegram_bot_token'))) {
abort(500, 'authentication failed');
}
}
public function webhook(Request $request)
{
$this->msg = $this->getMessage($request->input());
if (!$this->msg) return;
try {
switch($this->msg->message_type) {
case 'send':
$this->fromSend();
break;
case 'reply':
$this->fromReply();
break;
}
} catch (\Exception $e) {
$telegramService = new TelegramService();
$telegramService->sendMessage($this->msg->chat_id, $e->getMessage());
}
}
private function fromSend()
{
switch($this->msg->command) {
case '/bind': $this->bind();
break;
case '/traffic': $this->traffic();
break;
case '/getlatesturl': $this->getLatestUrl();
break;
case '/unbind': $this->unbind();
break;
default: $this->help();
}
}
private function fromReply()
{
// ticket
if (preg_match("/[#](.*)/", $this->msg->reply_text, $match)) {
$this->replayTicket($match[1]);
}
}
private function getMessage(array $data)
{
if (!isset($data['message'])) return false;
$obj = new \StdClass();
$obj->is_private = $data['message']['chat']['type'] === 'private' ? true : false;
if (!isset($data['message']['text'])) return false;
$text = explode(' ', $data['message']['text']);
$obj->command = $text[0];
$obj->args = array_slice($text, 1);
$obj->chat_id = $data['message']['chat']['id'];
$obj->message_id = $data['message']['message_id'];
$obj->message_type = !isset($data['message']['reply_to_message']['text']) ? 'send' : 'reply';
$obj->text = $data['message']['text'];
if ($obj->message_type === 'reply') {
$obj->reply_text = $data['message']['reply_to_message']['text'];
}
return $obj;
}
private function bind()
{
$msg = $this->msg;
if (!$msg->is_private) return;
if (!isset($msg->args[0])) {
abort(500, '参数有误,请携带订阅地址发送');
}
$subscribeUrl = $msg->args[0];
$subscribeUrl = parse_url($subscribeUrl);
parse_str($subscribeUrl['query'], $query);
$token = $query['token'];
if (!$token) {
abort(500, '订阅地址无效');
}
$user = User::where('token', $token)->first();
if (!$user) {
abort(500, '用户不存在');
}
if ($user->telegram_id) {
abort(500, '该账号已经绑定了Telegram账号');
}
$user->telegram_id = $msg->chat_id;
if (!$user->save()) {
abort(500, '设置失败');
}
$telegramService = new TelegramService();
$telegramService->sendMessage($msg->chat_id, '绑定成功');
}
private function unbind()
{
$msg = $this->msg;
if (!$msg->is_private) return;
$user = User::where('telegram_id', $msg->chat_id)->first();
$telegramService = new TelegramService();
if (!$user) {
$this->help();
$telegramService->sendMessage($msg->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
return;
}
$user->telegram_id = NULL;
if (!$user->save()) {
abort(500, '解绑失败');
}
$telegramService->sendMessage($msg->chat_id, '解绑成功', 'markdown');
}
private function help()
{
$msg = $this->msg;
if (!$msg->is_private) return;
$telegramService = new TelegramService();
$commands = [
'/bind 订阅地址 - 绑定你的' . config('v2board.app_name', 'V2Board') . '账号',
'/traffic - 查询流量信息',
'/getlatesturl - 获取最新的' . config('v2board.app_name', 'V2Board') . '网址',
'/unbind - 解除绑定'
];
$text = implode(PHP_EOL, $commands);
$telegramService->sendMessage($msg->chat_id, "你可以使用以下命令进行操作:\n\n$text", 'markdown');
}
private function traffic()
{
$msg = $this->msg;
if (!$msg->is_private) return;
$user = User::where('telegram_id', $msg->chat_id)->first();
$telegramService = new TelegramService();
if (!$user) {
$this->help();
$telegramService->sendMessage($msg->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
return;
}
$transferEnable = Helper::trafficConvert($user->transfer_enable);
$up = Helper::trafficConvert($user->u);
$down = Helper::trafficConvert($user->d);
$remaining = Helper::trafficConvert($user->transfer_enable - ($user->u + $user->d));
$text = "🚥流量查询\n———————————————\n计划流量:`{$transferEnable}`\n已用上行:`{$up}`\n已用下行:`{$down}`\n剩余流量:`{$remaining}`";
$telegramService->sendMessage($msg->chat_id, $text, 'markdown');
}
private function getLatestUrl()
{
$msg = $this->msg;
$user = User::where('telegram_id', $msg->chat_id)->first();
$telegramService = new TelegramService();
$text = sprintf(
"%s的最新网址是%s",
config('v2board.app_name', 'V2Board'),
config('v2board.app_url')
);
$telegramService->sendMessage($msg->chat_id, $text, 'markdown');
}
private function replayTicket($ticketId)
{
$msg = $this->msg;
if (!$msg->is_private) return;
$user = User::where('telegram_id', $msg->chat_id)->first();
if (!$user) {
abort(500, '用户不存在');
}
$ticketService = new TicketService();
if ($user->is_admin || $user->is_staff) {
$ticketService->replyByAdmin(
$ticketId,
$msg->text,
$user->id
);
}
$telegramService = new TelegramService();
$telegramService->sendMessage($msg->chat_id, "#`{$ticketId}` 的工单已回复成功", 'markdown');
}
}

View File

@@ -1,123 +0,0 @@
<?php
namespace App\Http\Controllers\Server;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Server;
use App\Models\ServerLog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
/*
* V2ray Aurora
* Github: https://github.com/tokumeikoi/aurora
*/
class DeepbworkController extends Controller
{
public function __construct(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
abort(500, 'token is null');
}
if ($token !== config('v2board.server_token')) {
abort(500, 'token is error');
}
}
// 后端获取用户
public function user(Request $request)
{
$nodeId = $request->input('node_id');
$server = Server::find($nodeId);
if (!$server) {
abort(500, 'fail');
}
Cache::put(CacheKey::get('SERVER_V2RAY_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers(json_decode($server->group_id));
$result = [];
foreach ($users as $user) {
$user->v2ray_user = [
"uuid" => $user->uuid,
"email" => sprintf("%s@v2board.user", $user->uuid),
"alter_id" => $user->v2ray_alter_id,
"level" => $user->v2ray_level,
];
unset($user['uuid']);
unset($user['v2ray_alter_id']);
unset($user['v2ray_level']);
array_push($result, $user);
}
return response([
'msg' => 'ok',
'data' => $result,
]);
}
// 后端提交数据
public function submit(Request $request)
{
// Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
$server = Server::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
}
$data = file_get_contents('php://input');
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_V2RAY_ONLINE_USER', $server->id), count($data), 3600);
$serverService = new ServerService();
$userService = new UserService();
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
if (!$userService->trafficFetch($u, $d, $item['user_id'])) {
return response([
'ret' => 0,
'msg' => 'user fetch fail'
]);
}
$serverService->log(
$item['user_id'],
$request->input('node_id'),
$item['u'],
$item['d'],
$server->rate,
'vmess'
);
}
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
// 后端获取配置
public function config(Request $request)
{
$nodeId = $request->input('node_id');
$localPort = $request->input('local_port');
if (empty($nodeId) || empty($localPort)) {
abort(500, '参数错误');
}
$serverService = new ServerService();
try {
$json = $serverService->getVmessConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
}

View File

@@ -1,154 +0,0 @@
<?php
namespace App\Http\Controllers\Server;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Plan;
use App\Models\Server;
use App\Models\ServerLog;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
/*
* V2ray Poseidon
* Github: https://github.com/ColetteContreras/trojan-poseidon
*/
class PoseidonController extends Controller
{
public $poseidonVersion;
public function __construct(Request $request)
{
$this->poseidonVersion = $request->input('poseidon_version');
}
// 后端获取用户
public function user(Request $request)
{
if ($r = $this->verifyToken($request)) { return $r; }
$nodeId = $request->input('node_id');
$server = Server::find($nodeId);
if (!$server) {
return $this->error("server could not be found", 404);
}
Cache::put(CacheKey::get('SERVER_V2RAY_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers(json_decode($server->group_id));
$result = [];
foreach ($users as $user) {
$user->v2ray_user = [
"uuid" => $user->uuid,
"email" => sprintf("%s@v2board.user", $user->uuid),
"alter_id" => $user->v2ray_alter_id,
"level" => $user->v2ray_level,
];
unset($user['uuid']);
unset($user['v2ray_alter_id']);
unset($user['v2ray_level']);
array_push($result, $user);
}
return $this->success($result);
}
// 后端提交数据
public function submit(Request $request)
{
if ($r = $this->verifyToken($request)) { return $r; }
$server = Server::find($request->input('node_id'));
if (!$server) {
return $this->error("server could not be found", 404);
}
$data = file_get_contents('php://input');
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_V2RAY_ONLINE_USER', $server->id), count($data), 3600);
$serverService = new ServerService();
$userService = new UserService();
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
if (!$userService->trafficFetch($u, $d, $item['user_id'])) {
return $this->error("user fetch fail", 500);
}
$serverService->log(
$item['user_id'],
$request->input('node_id'),
$item['u'],
$item['d'],
$server->rate,
'vmess'
);
}
return $this->success('');
}
// 后端获取配置
public function config(Request $request)
{
if ($r = $this->verifyToken($request)) { return $r; }
$nodeId = $request->input('node_id');
$localPort = $request->input('local_port');
if (empty($nodeId) || empty($localPort)) {
return $this->error('invalid parameters', 400);
}
$serverService = new ServerService();
try {
$json = $serverService->getVmessConfig($nodeId, $localPort);
$json->poseidon = [
'license_key' => (string)config('v2board.server_license'),
];
if ($this->poseidonVersion >= 'v1.5.0') {
// don't need it after v1.5.0
unset($json->inboundDetour);
unset($json->stats);
unset($json->api);
array_shift($json->routing->rules);
}
foreach($json->policy->levels as &$level) {
$level->handshake = 2;
$level->uplinkOnly = 2;
$level->downlinkOnly = 2;
$level->connIdle = 60;
}
return $this->success($json);
} catch (\Exception $e) {
return $this->error($e->getMessage(), 500);
}
}
protected function verifyToken(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
return $this->error("token must be set");
}
if ($token !== config('v2board.server_token')) {
return $this->error("invalid token");
}
}
protected function error($msg, int $status = 400) {
return response([
'msg' => $msg,
], $status);
}
protected function success($data) {
return response([
'msg' => 'ok',
'data' => $data,
]);
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace App\Http\Controllers\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class CommController extends Controller
{
public function config()
{
return response([
'data' => [
'isTelegram' => (int)config('v2board.telegram_bot_enable', 0),
'stripePk' => config('v2board.stripe_pk_live')
]
]);
}
}

View File

@@ -1,40 +0,0 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Coupon;
class CouponController extends Controller
{
public function check(Request $request)
{
if (empty($request->input('code'))) {
abort(500, '优惠券码不能为空');
}
$coupon = Coupon::where('code', $request->input('code'))->first();
if (!$coupon) {
abort(500, '优惠券无效');
}
if ($coupon->limit_use <= 0 && $coupon->limit_use !== NULL) {
abort(500, '优惠券已无可用次数');
}
if (time() < $coupon->started_at) {
abort(500, '优惠券还未到可用时间');
}
if (time() > $coupon->ended_at) {
abort(500, '优惠券已过期');
}
if ($coupon->limit_plan_ids) {
$limitPlanIds = json_decode($coupon->limit_plan_ids);
info($limitPlanIds);
if (!in_array($request->input('plan_id'), $limitPlanIds)) {
abort(500, '这个计划无法使用该优惠码');
}
}
return response([
'data' => $coupon
]);
}
}

View File

@@ -1,79 +0,0 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Order;
use App\Models\InviteCode;
use App\Utils\Helper;
class InviteController extends Controller
{
public function save(Request $request)
{
if (InviteCode::where('user_id', $request->session()->get('id'))->where('status', 0)->count() >= config('v2board.invite_gen_limit', 5)) {
abort(500, '已达到创建数量上限');
}
$inviteCode = new InviteCode();
$inviteCode->user_id = $request->session()->get('id');
$inviteCode->code = Helper::randomChar(8);
return response([
'data' => $inviteCode->save()
]);
}
public function details(Request $request)
{
return response([
'data' => Order::where('invite_user_id', $request->session()->get('id'))
->where('commission_balance', '>', 0)
->where('status', 3)
->select([
'id',
'commission_status',
'commission_balance',
'created_at',
'updated_at'
])
->get()
]);
}
public function fetch(Request $request)
{
$codes = InviteCode::where('user_id', $request->session()->get('id'))
->where('status', 0)
->get();
$commission_rate = config('v2board.invite_commission', 10);
$user = User::find($request->session()->get('id'));
if ($user->commission_rate) {
$commission_rate = $user->commission_rate;
}
$stat = [
//已注册用户数
(int)User::where('invite_user_id', $request->session()->get('id'))->count(),
//有效的佣金
(int)Order::where('status', 3)
->where('commission_status', 2)
->where('invite_user_id', $request->session()->get('id'))
->sum('commission_balance'),
//确认中的佣金
(int)Order::where('status', 3)
->where('commission_status', 0)
->where('invite_user_id', $request->session()->get('id'))
->sum('commission_balance'),
//佣金比例
(int)$commission_rate,
//可用佣金
(int)$user->commission_balance
];
return response([
'data' => [
'codes' => $codes,
'stat' => $stat
]
]);
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\UserService;
use Illuminate\Http\Request;
use App\Models\Knowledge;
class KnowledgeController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$knowledge = Knowledge::where('id', $request->input('id'))
->where('show', 1)
->first()
->toArray();
if (!$knowledge) abort(500, '知识不存在');
$user = User::find($request->session()->get('id'));
$userService = new UserService();
$appleId = $userService->isAvailable($user) ? config('v2board.apple_id') : '没有有效订阅无法使用本站提供的AppleID';
$appleIdPassword = $userService->isAvailable($user) ? config('v2board.apple_id_password') : '没有有效订阅无法使用本站提供的AppleID';
$subscribeUrl = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$knowledge['body'] = str_replace('{{siteName}}', config('v2board.app_name', 'V2Board'), $knowledge['body']);
$knowledge['body'] = str_replace('{{appleId}}', $appleId, $knowledge['body']);
$knowledge['body'] = str_replace('{{appleIdPassword}}', $appleIdPassword, $knowledge['body']);
$knowledge['body'] = str_replace('{{subscribeUrl}}', $subscribeUrl, $knowledge['body']);
$knowledge['body'] = str_replace('{{urlEncodeSubscribeUrl}}', urlencode($subscribeUrl), $knowledge['body']);
$knowledge['body'] = str_replace(
'{{safeBase64SubscribeUrl}}',
str_replace(
array('+', '/', '='),
array('-', '_', ''),
base64_encode($subscribeUrl)
),
$knowledge['body']
);
return response([
'data' => $knowledge
]);
}
$knowledges = Knowledge::select(['id', 'category', 'title', 'updated_at'])
->where('language', $request->input('language'))
->where('show', 1)
->orderBy('sort', 'ASC')
->get()
->groupBy('category');
return response([
'data' => $knowledges
]);
}
}

View File

@@ -1,494 +0,0 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\OrderSave;
use App\Services\CouponService;
use App\Services\OrderService;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Utils\Helper;
use Omnipay\Omnipay;
use Stripe\Stripe;
use Stripe\Source;
use Library\BitpayX;
use Library\MGate;
use Library\Epay;
class OrderController extends Controller
{
public function fetch(Request $request)
{
$model = Order::where('user_id', $request->session()->get('id'))
->orderBy('created_at', 'DESC');
if ($request->input('status') !== null) {
$model->where('status', $request->input('status'));
}
$order = $model->get();
$plan = Plan::get();
for ($i = 0; $i < count($order); $i++) {
for ($x = 0; $x < count($plan); $x++) {
if ($order[$i]['plan_id'] === $plan[$x]['id']) {
$order[$i]['plan'] = $plan[$x];
}
}
}
return response([
'data' => $order
]);
}
public function details(Request $request)
{
$order = Order::where('user_id', $request->session()->get('id'))
->where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
$order['plan'] = Plan::find($order->plan_id);
$order['try_out_plan_id'] = (int)config('v2board.try_out_plan_id');
if (!$order['plan']) {
abort(500, '订阅不存在');
}
return response([
'data' => $order
]);
}
public function save(OrderSave $request)
{
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($request->session()->get('id'))) {
abort(500, '您有未付款或开通中的订单,请稍后或取消再试');
}
$plan = Plan::find($request->input('plan_id'));
$user = User::find($request->session()->get('id'));
if (!$plan) {
abort(500, '该订阅不存在');
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
if ($request->input('cycle') !== 'reset_price') {
abort(500, '该订阅已售罄');
}
}
if (!$plan->renew && $user->plan_id == $plan->id && $request->input('cycle') !== 'reset_price') {
abort(500, '该订阅无法续费,请更换其他订阅');
}
if ($plan[$request->input('cycle')] === NULL) {
abort(500, '该订阅周期无法进行购买,请选择其他周期');
}
if ($request->input('cycle') === 'reset_price') {
if ($user->expired_at <= time() || !$user->plan_id) {
abort(500, '订阅已过期或无有效订阅,无法购买重置包');
}
}
DB::beginTransaction();
$order = new Order();
$orderService = new OrderService($order);
$order->user_id = $request->session()->get('id');
$order->plan_id = $plan->id;
$order->cycle = $request->input('cycle');
$order->trade_no = Helper::guid();
$order->total_amount = $plan[$request->input('cycle')];
if ($request->input('coupon_code')) {
$couponService = new CouponService($request->input('coupon_code'));
if (!$couponService->use($order)) {
DB::rollBack();
abort(500, '优惠券使用失败');
}
$order->coupon_id = $couponService->getId();
}
$orderService->setVipDiscount($user);
$orderService->setOrderType($user);
$orderService->setInvite($user);
if ($user->balance && $order->total_amount > 0) {
$remainingBalance = $user->balance - $order->total_amount;
$userService = new UserService();
if ($remainingBalance > 0) {
if (!$userService->addBalance($order->user_id, - $order->total_amount)) {
DB::rollBack();
abort(500, '余额不足');
}
$order->balance_amount = $order->total_amount;
$order->total_amount = 0;
} else {
if (!$userService->addBalance($order->user_id, - $user->balance)) {
DB::rollBack();
abort(500, '余额不足');
}
$order->balance_amount = $user->balance;
$order->total_amount = $order->total_amount - $user->balance;
}
}
if (!$order->save()) {
DB::rollback();
abort(500, '订单创建失败');
}
DB::commit();
return response([
'data' => $order->trade_no
]);
}
public function checkout(Request $request)
{
$tradeNo = $request->input('trade_no');
$method = $request->input('method');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->session()->get('id'))
->where('status', 0)
->first();
if (!$order) {
abort(500, '订单不存在或已支付');
}
// free process
if ($order->total_amount <= 0) {
$order->total_amount = 0;
$order->status = 1;
$order->save();
return response([
'type' => -1,
'data' => true
]);
}
switch ($method) {
// return type => 0: QRCode / 1: URL / 2: No action
case 0:
// alipayF2F
if (!(int)config('v2board.alipay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 0,
'data' => $this->alipayF2F($tradeNo, $order->total_amount)
]);
case 2:
// stripeAlipay
if (!(int)config('v2board.stripe_alipay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->stripeAlipay($order)
]);
case 3:
// stripeWepay
if (!(int)config('v2board.stripe_wepay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 0,
'data' => $this->stripeWepay($order)
]);
case 4:
// bitpayX
if (!(int)config('v2board.bitpayx_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->bitpayX($order)
]);
case 5:
if (!(int)config('v2board.mgate_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->mgate($order)
]);
case 6:
if (!(int)config('v2board.stripe_card_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 2,
'data' => $this->stripeCard($order, $request->input('token'))
]);
case 7:
if (!(int)config('v2board.epay_enable')) {
abort(500, '支付方式不可用');
}
return response([
'type' => 1,
'data' => $this->epay($order)
]);
default:
abort(500, '支付方式不存在');
}
}
public function check(Request $request)
{
$tradeNo = $request->input('trade_no');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->session()->get('id'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
return response([
'data' => $order->status
]);
}
public function getPaymentMethod()
{
$data = [];
if ((int)config('v2board.alipay_enable')) {
$alipayF2F = new \StdClass();
$alipayF2F->name = '支付宝';
$alipayF2F->method = 0;
$alipayF2F->icon = 'alipay';
array_push($data, $alipayF2F);
}
if ((int)config('v2board.stripe_alipay_enable')) {
$stripeAlipay = new \StdClass();
$stripeAlipay->name = '支付宝';
$stripeAlipay->method = 2;
$stripeAlipay->icon = 'alipay';
array_push($data, $stripeAlipay);
}
if ((int)config('v2board.stripe_wepay_enable')) {
$stripeWepay = new \StdClass();
$stripeWepay->name = '微信';
$stripeWepay->method = 3;
$stripeWepay->icon = 'wechat';
array_push($data, $stripeWepay);
}
if ((int)config('v2board.bitpayx_enable')) {
$bitpayX = new \StdClass();
$bitpayX->name = config('v2board.bitpayx_name', '在线支付');
$bitpayX->method = 4;
$bitpayX->icon = 'wallet';
array_push($data, $bitpayX);
}
if ((int)config('v2board.mgate_enable')) {
$obj = new \StdClass();
$obj->name = config('v2board.mgate_name', '在线支付');
$obj->method = 5;
$obj->icon = 'wallet';
array_push($data, $obj);
}
if ((int)config('v2board.stripe_card_enable')) {
$obj = new \StdClass();
$obj->name = '信用卡';
$obj->method = 6;
$obj->icon = 'card';
array_push($data, $obj);
}
if ((int)config('v2board.epay_enable')) {
$obj = new \StdClass();
$obj->name = config('v2board.epay_name', '在线支付');
$obj->method = 7;
$obj->icon = 'wallet';
array_push($data, $obj);
}
return response([
'data' => $data
]);
}
public function cancel(Request $request)
{
if (empty($request->input('trade_no'))) {
abort(500, '参数有误');
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('user_id', $request->session()->get('id'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
if ($order->status !== 0) {
abort(500, '只可以取消待支付订单');
}
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, '取消失败');
}
return response([
'data' => true
]);
}
private function alipayF2F($tradeNo, $totalAmount)
{
$gateway = Omnipay::create('Alipay_AopF2F');
$gateway->setSignType('RSA2'); //RSA/RSA2
$gateway->setAppId(config('v2board.alipay_appid'));
$gateway->setPrivateKey(config('v2board.alipay_privkey')); // 可以是路径,也可以是密钥内容
$gateway->setAlipayPublicKey(config('v2board.alipay_pubkey')); // 可以是路径,也可以是密钥内容
$gateway->setNotifyUrl(url('/api/v1/guest/order/alipayNotify'));
$request = $gateway->purchase();
$request->setBizContent([
'subject' => config('v2board.app_name', 'V2Board') . ' - 订阅',
'out_trade_no' => $tradeNo,
'total_amount' => $totalAmount / 100
]);
/** @var \Omnipay\Alipay\Responses\AopTradePreCreateResponse $response */
$response = $request->send();
$result = $response->getAlipayResponse();
if ($result['code'] !== '10000') {
abort(500, $result['sub_msg']);
}
// 获取收款二维码内容
return $response->getQrCode();
}
private function stripeAlipay($order)
{
$currency = config('v2board.stripe_currency', 'hkd');
$exchange = Helper::exchange('CNY', strtoupper($currency));
if (!$exchange) {
abort(500, '货币转换超时,请稍后再试');
}
Stripe::setApiKey(config('v2board.stripe_sk_live'));
$source = Source::create([
'amount' => floor($order->total_amount * $exchange),
'currency' => $currency,
'type' => 'alipay',
'statement_descriptor' => $order->trade_no,
'metadata' => [
'user_id' => $order->user_id,
'out_trade_no' => $order->trade_no,
'identifier' => ''
],
'redirect' => [
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
]
]);
if (!$source['redirect']['url']) {
abort(500, '支付网关请求失败');
}
return $source['redirect']['url'];
}
private function stripeWepay($order)
{
$currency = config('v2board.stripe_currency', 'hkd');
$exchange = Helper::exchange('CNY', strtoupper($currency));
if (!$exchange) {
abort(500, '货币转换超时,请稍后再试');
}
Stripe::setApiKey(config('v2board.stripe_sk_live'));
$source = Source::create([
'amount' => floor($order->total_amount * $exchange),
'currency' => $currency,
'type' => 'wechat',
'metadata' => [
'user_id' => $order->user_id,
'out_trade_no' => $order->trade_no,
'identifier' => ''
],
'redirect' => [
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
]
]);
if (!$source['wechat']['qr_code_url']) {
abort(500, '支付网关请求失败');
}
return $source['wechat']['qr_code_url'];
}
private function stripeCard($order, string $token)
{
$currency = config('v2board.stripe_currency', 'hkd');
$exchange = Helper::exchange('CNY', strtoupper($currency));
if (!$exchange) {
abort(500, '货币转换超时,请稍后再试');
}
Stripe::setApiKey(config('v2board.stripe_sk_live'));
try {
$charge = \Stripe\Charge::create([
'amount' => floor($order->total_amount * $exchange),
'currency' => $currency,
'source' => $token,
'metadata' => [
'user_id' => $order->user_id,
'out_trade_no' => $order->trade_no,
'identifier' => ''
]
]);
} catch (\Exception $e) {
abort(500, '遇到了点问题,请刷新页面稍后再试');
}
info($charge);
if (!$charge->paid) {
abort(500, '扣款失败,请检查信用卡信息');
}
return $charge->paid;
}
private function bitpayX($order)
{
$bitpayX = new BitpayX(config('v2board.bitpayx_appsecret'));
$params = [
'merchant_order_id' => $order->trade_no,
'price_amount' => $order->total_amount / 100,
'price_currency' => 'CNY',
'title' => '支付单号:' . $order->trade_no,
'description' => '充值:' . $order->total_amount / 100 . ' 元',
'callback_url' => url('/api/v1/guest/order/bitpayXNotify'),
'success_url' => config('v2board.app_url', env('APP_URL')) . '/#/order',
'cancel_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
];
$strToSign = $bitpayX->prepareSignId($params['merchant_order_id']);
$params['token'] = $bitpayX->sign($strToSign);
$result = $bitpayX->mprequest($params);
// Log::info('bitpayXSubmit: ' . json_encode($result));
return isset($result['payment_url']) ? $result['payment_url'] : false;
}
private function mgate($order)
{
$mgate = new MGate(config('v2board.mgate_url'), config('v2board.mgate_app_id'), config('v2board.mgate_app_secret'));
$result = $mgate->pay([
'app_id' => config('v2board.mgate_app_id'),
'out_trade_no' => $order->trade_no,
'total_amount' => $order->total_amount,
'notify_url' => url('/api/v1/guest/order/mgateNotify'),
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order'
]);
return $result;
}
private function epay($order)
{
$epay = new Epay(config('v2board.epay_url'), config('v2board.epay_pid'), config('v2board.epay_key'));
return $epay->pay([
'money' => $order->total_amount / 100,
'name' => $order->trade_no,
'notify_url' => url('/api/v1/guest/order/epayNotify'),
'return_url' => config('v2board.app_url', env('APP_URL')) . '/#/order',
'out_trade_no' => $order->trade_no
]);
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Plan;
class PlanController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$plan = Plan::where('id', $request->input('id'))
->first();
if (!$plan) {
abort(500, '该订阅不存在');
}
return response([
'data' => $plan
]);
}
$plan = Plan::where('show', 1)
->orderBy('sort', 'ASC')
->get();
return response([
'data' => $plan
]);
}
}

View File

@@ -1,59 +0,0 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use App\Models\Server;
use App\Models\ServerLog;
use App\Models\User;
use App\Utils\Helper;
class ServerController extends Controller
{
public function fetch(Request $request)
{
$user = User::find($request->session()->get('id'));
$servers = [];
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAllServers($user);
$servers = array_merge($servers['shadowsocks'], $servers['vmess'], $servers['trojan']);
}
return response([
'data' => $servers
]);
}
public function logFetch(Request $request)
{
$type = $request->input('type') ? $request->input('type') : 0;
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$serverLogModel = ServerLog::where('user_id', $request->session()->get('id'))
->orderBy('created_at', 'DESC');
switch ($type) {
case 0:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-d')));
break;
case 1:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-d')) - 604800);
break;
case 2:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-1')));
}
$total = $serverLogModel->count();
$res = $serverLogModel->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total
]);
}
}

View File

@@ -1,183 +0,0 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\UserUpdate;
use App\Http\Requests\User\UserChangePassword;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Plan;
use App\Models\Server;
use App\Models\Ticket;
use App\Utils\Helper;
use App\Models\Order;
use App\Models\ServerLog;
class UserController extends Controller
{
public function logout(Request $request)
{
$request->session()->flush();
return response([
'data' => true
]);
}
public function changePassword(UserChangePassword $request)
{
$user = User::find($request->session()->get('id'));
if (!Helper::multiPasswordVerify(
$user->password_algo,
$request->input('old_password'),
$user->password)
) {
abort(500, '旧密码有误');
}
$user->password = password_hash($request->input('new_password'), PASSWORD_DEFAULT);
$user->password_algo = NULL;
if (!$user->save()) {
abort(500, '保存失败');
}
$request->session()->flush();
return response([
'data' => true
]);
}
public function info(Request $request)
{
$user = User::where('id', $request->session()->get('id'))
->select([
'email',
'transfer_enable',
'last_login_at',
'created_at',
'banned',
'remind_expire',
'remind_traffic',
'expired_at',
'balance',
'commission_balance',
'plan_id',
'discount',
'commission_rate',
'telegram_id'
])
->first();
$user['avatar_url'] = 'https://cdn.v2ex.com/gravatar/' . md5($user->email) . '?s=64&d=identicon';
return response([
'data' => $user
]);
}
public function getStat(Request $request)
{
$stat = [
Order::where('status', 0)
->where('user_id', $request->session()->get('id'))
->count(),
Ticket::where('status', 0)
->where('user_id', $request->session()->get('id'))
->count(),
User::where('invite_user_id', $request->session()->get('id'))
->count()
];
return response([
'data' => $stat
]);
}
public function getSubscribe(Request $request)
{
$user = User::find($request->session()->get('id'));
if ($user->plan_id) {
$user['plan'] = Plan::find($user->plan_id);
if (!$user['plan']) {
abort(500, '订阅计划不存在');
}
}
$user['subscribe_url'] = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$user['reset_day'] = $this->getResetDay($user);
return response([
'data' => $user
]);
}
public function resetSecurity(Request $request)
{
$user = User::find($request->session()->get('id'));
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
if (!$user->save()) {
abort(500, '重置失败');
}
return response([
'data' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user->token
]);
}
public function update(UserUpdate $request)
{
$updateData = $request->only([
'remind_expire',
'remind_traffic'
]);
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, '该用户不存在');
}
try {
$user->update($updateData);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function transfer(Request $request)
{
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, '该用户不存在');
}
if ($request->input('transfer_amount') <= 0) {
abort(500, '参数错误');
}
if ($request->input('transfer_amount') > $user->commission_balance) {
abort(500, '推广佣金余额不足');
}
$user->commission_balance = $user->commission_balance - $request->input('transfer_amount');
$user->balance = $user->balance + $request->input('transfer_amount');
if (!$user->save()) {
abort(500, '划转失败');
}
return response([
'data' => true
]);
}
private function getResetDay(User $user)
{
if ($user->expired_at <= time() || $user->expired_at === NULL) return null;
$day = date('d', $user->expired_at);
$today = date('d');
$lastDay = date('d', strtotime('last day of +0 months'));
if ((int)config('v2board.reset_traffic_method') === 0) {
return $lastDay - $today;
}
if ((int)config('v2board.reset_traffic_method') === 1) {
if ((int)$day >= (int)$today) {
return $day - $today;
} else {
return $lastDay - $today + $day;
}
}
return null;
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ConfigSave;
use App\Jobs\SendEmailJob;
use App\Services\ConfigService;
use App\Services\TelegramService;
use App\Utils\Dict;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Mail;
class ConfigController extends Controller
{
public function getEmailTemplate()
{
$path = resource_path('views/mail/');
$files = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
return response([
'data' => $files
]);
}
public function getThemeTemplate()
{
$path = public_path('theme/');
$files = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
return response([
'data' => $files
]);
}
public function testSendMail(Request $request)
{
$obj = new SendEmailJob([
'email' => $request->user['email'],
'subject' => 'This is v2board test email',
'template_name' => 'notify',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'content' => 'This is v2board test email',
'url' => config('v2board.app_url')
]
]);
return response([
'data' => true,
'log' => $obj->handle()
]);
}
public function setTelegramWebhook(Request $request)
{
$hookUrl = url('/api/v1/guest/telegram/webhook?access_token=' . md5(config('v2board.telegram_bot_token', $request->input('telegram_bot_token'))));
$telegramService = new TelegramService($request->input('telegram_bot_token'));
$telegramService->getMe();
$telegramService->setWebhook($hookUrl);
return response([
'data' => true
]);
}
public function fetch(Request $request)
{
$key = $request->input('key');
$data = (new ConfigService)->getDefaultConfig();
if ($key && isset($data[$key])) {
return response([
'data' => [
$key => $data[$key]
]
]);
};
// TODO: default should be in Dict
return response([
'data' => $data
]);
}
public function save(ConfigSave $request)
{
$data = $request->validated();
$config = config('v2board');
foreach (ConfigSave::RULES as $k => $v) {
if (!in_array($k, array_keys(ConfigSave::RULES))) {
unset($config[$k]);
continue;
}
if (array_key_exists($k, $data)) {
$config[$k] = $data[$k];
}
}
$data = var_export($config, 1);
if (!File::put(base_path() . '/config/v2board.php', "<?php\n return $data ;")) {
abort(500, '修改失败');
}
if (function_exists('opcache_reset')) {
if (opcache_reset() === false) {
abort(500, '缓存清除失败请卸载或检查opcache配置状态');
}
}
Artisan::call('config:cache');
return response([
'data' => true
]);
}
}

View File

@@ -1,15 +1,13 @@
<?php
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\V1\Admin;
use App\Http\Requests\Admin\CouponSave;
use App\Http\Requests\Admin\CouponGenerate;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\CouponGenerate;
use App\Http\Requests\Admin\CouponSave;
use App\Models\Coupon;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class CouponController extends Controller
@@ -19,40 +17,29 @@ class CouponController extends Controller
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$sort = $request->input('sort') ? $request->input('sort') : 'id';
$builder = Coupon::orderBy($sort, $sortType);
$total = $builder->count();
$coupons = $builder->forPage($current, $pageSize)
->get();
foreach ($coupons as $k => $v) {
if ($coupons[$k]['limit_plan_ids']) $coupons[$k]['limit_plan_ids'] = json_decode($coupons[$k]['limit_plan_ids']);
}
return response([
'data' => $coupons,
'total' => $total
]);
}
public function save(CouponSave $request)
public function show(Request $request)
{
$params = $request->validated();
if (isset($params['limit_plan_ids'])) {
$params['limit_plan_ids'] = json_encode($params['limit_plan_ids']);
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
if (!$request->input('id')) {
if (!isset($params['code'])) {
$params['code'] = Helper::randomChar(8);
}
if (!Coupon::create($params)) {
abort(500, '创建失败');
}
} else {
try {
Coupon::find($request->input('id'))->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
$coupon = Coupon::find($request->input('id'));
if (!$coupon) {
abort(500, '优惠券不存在');
}
$coupon->show = $coupon->show ? 0 : 1;
if (!$coupon->save()) {
abort(500, '保存失败');
}
return response([
@@ -68,9 +55,6 @@ class CouponController extends Controller
}
$params = $request->validated();
if (isset($params['limit_plan_ids'])) {
$params['limit_plan_ids'] = json_encode($params['limit_plan_ids']);
}
if (!$request->input('id')) {
if (!isset($params['code'])) {
$params['code'] = Helper::randomChar(8);
@@ -94,16 +78,25 @@ class CouponController extends Controller
private function multiGenerate(CouponGenerate $request)
{
$coupons = [];
$coupon = $request->validated();
$coupon['created_at'] = $coupon['updated_at'] = time();
$coupon['show'] = 1;
unset($coupon['generate_count']);
for ($i = 0;$i < $request->input('generate_count');$i++) {
$coupon = $request->validated();
$coupon['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']);
$coupon['code'] = Helper::randomChar(8);
$coupon['created_at'] = $coupon['updated_at'] = time();
unset($coupon['generate_count']);
array_push($coupons, $coupon);
}
DB::beginTransaction();
if (!Coupon::insert($coupons)) {
if (!Coupon::insert(array_map(function ($item) use ($coupon) {
// format data
if (isset($item['limit_plan_ids']) && is_array($item['limit_plan_ids'])) {
$item['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']);
}
if (isset($item['limit_period']) && is_array($item['limit_period'])) {
$item['limit_period'] = json_encode($coupon['limit_period']);
}
return $item;
}, $coupons))) {
DB::rollBack();
abort(500, '生成失败');
}
@@ -116,7 +109,8 @@ class CouponController extends Controller
$endTime = date('Y-m-d H:i:s', $coupon['ended_at']);
$limitUse = $coupon['limit_use'] ?? '不限制';
$createTime = date('Y-m-d H:i:s', $coupon['created_at']);
$data .= "{$coupon['name']},{$type},{$value},{$startTime},{$endTime},{$limitUse},{$coupon['limit_plan_ids']},{$coupon['code']},{$createTime}\r\n";
$limitPlanIds = isset($coupon['limit_plan_ids']) ? implode("/", $coupon['limit_plan_ids']) : '不限制';
$data .= "{$coupon['name']},{$type},{$value},{$startTime},{$endTime},{$limitUse},{$limitPlanIds},{$coupon['code']},{$createTime}\r\n";
}
echo $data;
}

View File

@@ -1,12 +1,12 @@
<?php
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\KnowledgeSave;
use App\Http\Requests\Admin\KnowledgeSort;
use App\Models\Knowledge;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\DB;
class KnowledgeController extends Controller
@@ -77,11 +77,15 @@ class KnowledgeController extends Controller
public function sort(KnowledgeSort $request)
{
DB::beginTransaction();
foreach ($request->input('knowledge_ids') as $k => $v) {
if (!Knowledge::find($v)->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
try {
foreach ($request->input('knowledge_ids') as $k => $v) {
$knowledge = Knowledge::find($v);
$knowledge->timestamps = false;
$knowledge->update(['sort' => $k + 1]);
}
} catch (\Exception $e) {
DB::rollBack();
abort(500, '保存失败');
}
DB::commit();
return response([

View File

@@ -1,11 +1,11 @@
<?php
namespace App\Http\Controllers\Staff;
namespace App\Http\Controllers\V1\Admin;
use App\Http\Requests\Admin\NoticeSave;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\NoticeSave;
use App\Models\Notice;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class NoticeController extends Controller
@@ -22,7 +22,8 @@ class NoticeController extends Controller
$data = $request->only([
'title',
'content',
'img_url'
'img_url',
'tags'
]);
if (!$request->input('id')) {
if (!Notice::create($data)) {
@@ -40,6 +41,27 @@ class NoticeController extends Controller
]);
}
public function show(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$notice = Notice::find($request->input('id'));
if (!$notice) {
abort(500, '公告不存在');
}
$notice->show = $notice->show ? 0 : 1;
if (!$notice->save()) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {

View File

@@ -1,39 +1,66 @@
<?php
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\OrderAssign;
use App\Http\Requests\Admin\OrderFetch;
use App\Http\Requests\Admin\OrderUpdate;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Services\OrderService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Models\User;
use App\Models\Plan;
use Illuminate\Support\Facades\DB;
class OrderController extends Controller
{
public function fetch(Request $request)
private function filter(Request $request, &$builder)
{
if ($request->input('filter')) {
foreach ($request->input('filter') as $filter) {
if ($filter['key'] === 'email') {
$user = User::where('email', "%{$filter['value']}%")->first();
if (!$user) continue;
$builder->where('user_id', $user->id);
continue;
}
if ($filter['condition'] === '模糊') {
$filter['condition'] = 'like';
$filter['value'] = "%{$filter['value']}%";
}
$builder->where($filter['key'], $filter['condition'], $filter['value']);
}
}
}
public function detail(Request $request)
{
$order = Order::find($request->input('id'));
if (!$order) abort(500, '订单不存在');
$order['commission_log'] = CommissionLog::where('trade_no', $order->trade_no)->get();
if ($order->surplus_order_ids) {
$order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
}
return response([
'data' => $order
]);
}
public function fetch(OrderFetch $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$orderModel = Order::orderBy('created_at', 'DESC');
if ($request->input('trade_no')) {
$orderModel->where('trade_no', $request->input('trade_no'));
}
if ($request->input('is_commission')) {
$orderModel->where('invite_user_id', '!=', NULL);
$orderModel->where('status', 3);
$orderModel->whereNotIn('status', [0, 2]);
$orderModel->where('commission_balance', '>', 0);
}
if ($request->input('id')) {
$orderModel->where('id', $request->input('id'));
}
if ($request->input('user_id')) {
$orderModel->where('user_id', $request->input('user_id'));
}
$this->filter($request, $orderModel);
$total = $orderModel->count();
$res = $orderModel->forPage($current, $pageSize)
->get();
@@ -51,10 +78,45 @@ class OrderController extends Controller
]);
}
public function paid(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
if ($order->status !== 0) abort(500, '只能对待支付的订单进行操作');
$orderService = new OrderService($order);
if (!$orderService->paid('manual_operation')) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
public function cancel(Request $request)
{
$order = Order::where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
}
if ($order->status !== 0) abort(500, '只能对待支付的订单进行操作');
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
public function update(OrderUpdate $request)
{
$params = $request->only([
'status',
'commission_status'
]);
@@ -64,16 +126,6 @@ class OrderController extends Controller
abort(500, '订单不存在');
}
if (isset($params['status']) && (int)$params['status'] === 2) {
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
try {
$order->update($params);
} catch (\Exception $e) {
@@ -85,26 +137,6 @@ class OrderController extends Controller
]);
}
public function repair(Request $request)
{
if (empty($request->input('trade_no'))) {
abort(500, '参数错误');
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('status', 0)
->first();
if (!$order) {
abort(500, '订单不存在或订单已支付');
}
$order->status = 1;
if (!$order->save()) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function assign(OrderAssign $request)
{
$plan = Plan::find($request->input('plan_id'));
@@ -118,16 +150,21 @@ class OrderController extends Controller
abort(500, '该订阅不存在');
}
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($user->id)) {
abort(500, '该用户还有待支付的订单,无法分配');
}
DB::beginTransaction();
$order = new Order();
$orderService = new OrderService($order);
$order->user_id = $user->id;
$order->plan_id = $plan->id;
$order->cycle = $request->input('cycle');
$order->period = $request->input('period');
$order->trade_no = Helper::guid();
$order->total_amount = $request->input('total_amount');
if ($order->cycle === 'reset_price') {
if ($order->period === 'reset_price') {
$order->type = 4;
} else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) {
$order->type = 3;

View File

@@ -0,0 +1,133 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\PaymentSave;
use App\Models\Payment;
use App\Services\PaymentService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PaymentController extends Controller
{
public function getPaymentMethods()
{
$methods = [];
foreach (glob(base_path('app//Payments') . '/*.php') as $file) {
array_push($methods, pathinfo($file)['filename']);
}
return response([
'data' => $methods
]);
}
public function fetch()
{
$payments = Payment::orderBy('sort', 'ASC')->get();
foreach ($payments as $k => $v) {
$notifyUrl = url("/api/v1/guest/payment/notify/{$v->payment}/{$v->uuid}");
if ($v->notify_domain) {
$parseUrl = parse_url($notifyUrl);
$notifyUrl = $v->notify_domain . $parseUrl['path'];
}
$payments[$k]['notify_url'] = $notifyUrl;
}
return response([
'data' => $payments
]);
}
public function getPaymentForm(Request $request)
{
$paymentService = new PaymentService($request->input('payment'), $request->input('id'));
return response([
'data' => $paymentService->form()
]);
}
public function show(Request $request)
{
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
$payment->enable = !$payment->enable;
if (!$payment->save()) abort(500, '保存失败');
return response([
'data' => true
]);
}
public function save(Request $request)
{
if (!config('v2board.app_url')) {
abort(500, '请在站点配置中配置站点地址');
}
$params = $request->validate([
'name' => 'required',
'icon' => 'nullable',
'payment' => 'required',
'config' => 'required',
'notify_domain' => 'nullable|url',
'handling_fee_fixed' => 'nullable|integer',
'handling_fee_percent' => 'nullable|numeric|between:0.1,100'
], [
'name.required' => '显示名称不能为空',
'payment.required' => '网关参数不能为空',
'config.required' => '配置参数不能为空',
'notify_domain.url' => '自定义通知域名格式有误',
'handling_fee_fixed.integer' => '固定手续费格式有误',
'handling_fee_percent.between' => '百分比手续费范围须在0.1-100之间'
]);
if ($request->input('id')) {
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
try {
$payment->update($params);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
return response([
'data' => true
]);
}
$params['uuid'] = Helper::randomChar(8);
if (!Payment::create($params)) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
return response([
'data' => $payment->delete()
]);
}
public function sort(Request $request)
{
$request->validate([
'ids' => 'required|array'
], [
'ids.required' => '参数有误',
'ids.array' => '参数有误'
]);
DB::beginTransaction();
foreach ($request->input('ids') as $k => $v) {
if (!Payment::find($v)->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
}
DB::commit();
return response([
'data' => true
]);
}
}

View File

@@ -1,33 +1,23 @@
<?php
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\PlanSave;
use App\Http\Requests\Admin\PlanSort;
use App\Http\Requests\Admin\PlanUpdate;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Services\PlanService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PlanController extends Controller
{
public function fetch(Request $request)
{
$counts = User::select(
DB::raw("plan_id"),
DB::raw("count(*) as count")
)
->where('plan_id', '!=', NULL)
->where(function ($query) {
$query->where('expired_at', '>=', time())
->orWhere('expired_at', NULL);
})
->groupBy("plan_id")
->get();
$counts = PlanService::countActiveUsers();
$plans = Plan::orderBy('sort', 'ASC')->get();
foreach ($plans as $k => $v) {
$plans[$k]->count = 0;
@@ -51,10 +41,13 @@ class PlanController extends Controller
DB::beginTransaction();
// update user group id and transfer
try {
User::where('plan_id', $plan->id)->update([
'group_id' => $plan->group_id,
'transfer_enable' => $plan->transfer_enable * 1073741824
]);
if ($request->input('force_update')) {
User::where('plan_id', $plan->id)->update([
'group_id' => $params['group_id'],
'transfer_enable' => $params['transfer_enable'] * 1073741824,
'speed_limit' => $params['speed_limit']
]);
}
$plan->update($params);
} catch (\Exception $e) {
DB::rollBack();

View File

@@ -1,13 +1,14 @@
<?php
namespace App\Http\Controllers\Admin\Server;
namespace App\Http\Controllers\V1\Admin\Server;
use App\Models\Plan;
use App\Models\Server;
use App\Models\ServerGroup;
use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\ServerGroup;
use App\Models\ServerVmess;
use App\Models\User;
use App\Services\ServerService;
use Illuminate\Http\Request;
class GroupController extends Controller
{
@@ -18,8 +19,20 @@ class GroupController extends Controller
'data' => [ServerGroup::find($request->input('group_id'))]
]);
}
$serverGroups = ServerGroup::get();
$serverService = new ServerService();
$servers = $serverService->getAllServers();
foreach ($serverGroups as $k => $v) {
$serverGroups[$k]['user_count'] = User::where('group_id', $v['id'])->count();
$serverGroups[$k]['server_count'] = 0;
foreach ($servers as $server) {
if (in_array($v['id'], $server['group_id'])) {
$serverGroups[$k]['server_count'] = $serverGroups[$k]['server_count']+1;
}
}
}
return response([
'data' => ServerGroup::get()
'data' => $serverGroups
]);
}
@@ -50,10 +63,9 @@ class GroupController extends Controller
}
}
$servers = Server::all();
$servers = ServerVmess::all();
foreach ($servers as $server) {
$groupId = json_decode($server->group_id);
if (in_array($request->input('id'), $groupId)) {
if (in_array($request->input('id'), $server->group_id)) {
abort(500, '该组已被节点所使用,无法删除');
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Models\ServerHysteria;
use Illuminate\Http\Request;
class HysteriaController extends Controller
{
public function save(Request $request)
{
$params = $request->validate([
'show' => '',
'name' => 'required',
'group_id' => 'required|array',
'route_id' => 'nullable|array',
'parent_id' => 'nullable|integer',
'host' => 'required',
'port' => 'required',
'server_port' => 'required',
'tags' => 'nullable|array',
'rate' => 'required|numeric',
'up_mbps' => 'required|numeric|min:1',
'down_mbps' => 'required|numeric|min:1',
'server_name' => 'nullable',
'insecure' => 'required|in:0,1',
'obfs_type' => 'nullable|in:salamander',
'ignore_client_bandwidth' => 'required|in:0,1'
]);
if ($request->input('id')) {
$server = ServerHysteria::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!ServerHysteria::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerHysteria::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(Request $request)
{
$request->validate([
'show' => 'in:0,1'
], [
'show.in' => '显示状态格式不正确'
]);
$params = $request->only([
'show',
]);
$server = ServerHysteria::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function copy(Request $request)
{
$server = ServerHysteria::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!ServerHysteria::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ManageController extends Controller
{
public function getNodes(Request $request)
{
$serverService = new ServerService();
return response([
'data' => $serverService->getAllServers()
]);
}
public function sort(Request $request)
{
ini_set('post_max_size', '1m');
$params = $request->only(
'shadowsocks',
'vmess',
'trojan',
'hysteria'
) ?? [];
DB::beginTransaction();
foreach ($params as $k => $v) {
$model = 'App\\Models\\Server' . ucfirst($k);
foreach($v as $id => $sort) {
if (!$model::find($id)->update(['sort' => $sort])) {
DB::rollBack();
abort(500, '保存失败');
}
}
}
DB::commit();
return response([
'data' => true
]);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Models\ServerRoute;
use Illuminate\Http\Request;
class RouteController extends Controller
{
public function fetch(Request $request)
{
$routes = ServerRoute::get();
// TODO: remove on 1.8.0
foreach ($routes as $k => $route) {
$array = json_decode($route->match, true);
if (is_array($array)) $routes[$k]['match'] = $array;
}
// TODO: remove on 1.8.0
return [
'data' => $routes
];
}
public function save(Request $request)
{
$params = $request->validate([
'remarks' => 'required',
'match' => 'required|array',
'action' => 'required|in:block,dns',
'action_value' => 'nullable'
], [
'remarks.required' => '备注不能为空',
'match.required' => '匹配值不能为空',
'action.required' => '动作类型不能为空',
'action.in' => '动作类型参数有误'
]);
$params['match'] = array_filter($params['match']);
// TODO: remove on 1.8.0
$params['match'] = json_encode($params['match']);
// TODO: remove on 1.8.0
if ($request->input('id')) {
try {
$route = ServerRoute::find($request->input('id'));
$route->update($params);
return [
'data' => true
];
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
if (!ServerRoute::create($params)) abort(500, '创建失败');
return [
'data' => true
];
}
public function drop(Request $request)
{
$route = ServerRoute::find($request->input('id'));
if (!$route) abort(500, '路由不存在');
if (!$route->delete()) abort(500, '删除失败');
return [
'data' => true
];
}
}

View File

@@ -1,48 +1,18 @@
<?php
namespace App\Http\Controllers\Admin\Server;
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerShadowsocksSave;
use App\Http\Requests\Admin\ServerShadowsocksSort;
use App\Http\Requests\Admin\ServerShadowsocksUpdate;
use App\Models\ServerShadowsocks;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Server;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class ShadowsocksController extends Controller
{
public function fetch(Request $request)
{
$server = ServerShadowsocks::orderBy('sort', 'ASC')->get();
for ($i = 0; $i < count($server); $i++) {
if (!empty($server[$i]['tags'])) {
$server[$i]['tags'] = json_decode($server[$i]['tags']);
}
$server[$i]['group_id'] = json_decode($server[$i]['group_id']);
$server[$i]['online'] = Cache::get(CacheKey::get('SERVER_SHADOWSOCKS_ONLINE_USER', $server[$i]['parent_id'] ? $server[$i]['parent_id'] : $server[$i]['id']));
if ($server[$i]['parent_id']) {
$server[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server[$i]['parent_id']));
} else {
$server[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server[$i]['id']));
}
}
return response([
'data' => $server
]);
}
public function save(ServerShadowsocksSave $request)
{
$params = $request->validated();
$params['group_id'] = json_encode($params['group_id']);
if (isset($params['tags'])) {
$params['tags'] = json_encode($params['tags']);
}
if ($request->input('id')) {
$server = ServerShadowsocks::find($request->input('id'));
if (!$server) {
@@ -117,19 +87,4 @@ class ShadowsocksController extends Controller
'data' => true
]);
}
public function sort(ServerShadowsocksSort $request)
{
DB::beginTransaction();
foreach ($request->input('server_ids') as $k => $v) {
if (!ServerShadowsocks::find($v)->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
}
DB::commit();
return response([
'data' => true
]);
}
}

View File

@@ -1,48 +1,19 @@
<?php
namespace App\Http\Controllers\Admin\Server;
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Requests\Admin\ServerTrojanSave;
use App\Http\Requests\Admin\ServerTrojanSort;
use App\Http\Requests\Admin\ServerTrojanUpdate;
use App\Services\ServerService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerTrojanSave;
use App\Http\Requests\Admin\ServerTrojanUpdate;
use App\Models\ServerTrojan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use App\Services\ServerService;
use Illuminate\Http\Request;
class TrojanController extends Controller
{
public function fetch(Request $request)
{
$server = ServerTrojan::orderBy('sort', 'ASC')->get();
for ($i = 0; $i < count($server); $i++) {
if (!empty($server[$i]['tags'])) {
$server[$i]['tags'] = json_decode($server[$i]['tags']);
}
$server[$i]['group_id'] = json_decode($server[$i]['group_id']);
$server[$i]['online'] = Cache::get(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server[$i]['parent_id'] ? $server[$i]['parent_id'] : $server[$i]['id']));
if ($server[$i]['parent_id']) {
$server[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server[$i]['parent_id']));
} else {
$server[$i]['last_check_at'] = Cache::get(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server[$i]['id']));
}
}
return response([
'data' => $server
]);
}
public function save(ServerTrojanSave $request)
{
$params = $request->validated();
$params['group_id'] = json_encode($params['group_id']);
if (isset($params['tags'])) {
$params['tags'] = json_encode($params['tags']);
}
if ($request->input('id')) {
$server = ServerTrojan::find($request->input('id'));
if (!$server) {
@@ -117,22 +88,6 @@ class TrojanController extends Controller
'data' => true
]);
}
public function sort(ServerTrojanSort $request)
{
DB::beginTransaction();
foreach ($request->input('server_ids') as $k => $v) {
if (!ServerTrojan::find($v)->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
}
DB::commit();
return response([
'data' => true
]);
}
public function viewConfig(Request $request)
{
$serverService = new ServerService();

View File

@@ -0,0 +1,119 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Models\ServerVless;
use Illuminate\Http\Request;
use ParagonIE_Sodium_Compat as SodiumCompat;
use App\Utils\Helper;
class VlessController extends Controller
{
public function save(Request $request)
{
$params = $request->validate([
'group_id' => 'required',
'route_id' => 'nullable|array',
'name' => 'required',
'parent_id' => 'nullable|integer',
'host' => 'required',
'port' => 'required',
'server_port' => 'required',
'tls' => 'required|in:0,1,2',
'tls_settings' => 'nullable|array',
'flow' => 'nullable|in:xtls-rprx-vision',
'network' => 'required',
'network_settings' => 'nullable|array',
'tags' => 'nullable|array',
'rate' => 'required',
'show' => 'nullable|in:0,1',
'sort' => 'nullable'
]);
if (isset($params['tls']) && (int)$params['tls'] === 2) {
$keyPair = SodiumCompat::crypto_box_keypair();
$params['tls_settings'] = $params['tls_settings'] ?? [];
if (!isset($params['tls_settings']['public_key'])) {
$params['tls_settings']['public_key'] = Helper::base64EncodeUrlSafe(SodiumCompat::crypto_box_publickey($keyPair));
}
if (!isset($params['tls_settings']['private_key'])) {
$params['tls_settings']['private_key'] = Helper::base64EncodeUrlSafe(SodiumCompat::crypto_box_secretkey($keyPair));
}
}
if ($request->input('id')) {
$server = ServerVless::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!ServerVless::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerVless::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(Request $request)
{
$params = $request->validate([
'show' => 'nullable|in:0,1',
]);
$server = ServerVless::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function copy(Request $request)
{
$server = ServerVless::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!ServerVless::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers\V1\Admin\Server;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerVmessSave;
use App\Http\Requests\Admin\ServerVmessUpdate;
use App\Models\ServerVmess;
use Illuminate\Http\Request;
class VmessController extends Controller
{
public function save(ServerVmessSave $request)
{
$params = $request->validated();
if ($request->input('id')) {
$server = ServerVmess::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
if (!ServerVmess::create($params)) {
abort(500, '创建失败');
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if ($request->input('id')) {
$server = ServerVmess::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
}
return response([
'data' => $server->delete()
]);
}
public function update(ServerVmessUpdate $request)
{
$params = $request->only([
'show',
]);
$server = ServerVmess::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
}
try {
$server->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function copy(Request $request)
{
$server = ServerVmess::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!ServerVmess::create($server->toArray())) {
abort(500, '复制失败');
}
return response([
'data' => true
]);
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Models\ServerVmess;
use App\Models\Stat;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Models\Ticket;
use App\Models\User;
use App\Services\StatisticalService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
public function getOverride(Request $request)
{
return [
'data' => [
'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->whereNotIn('status', [0, 2])
->sum('total_amount'),
'month_register_total' => User::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->count(),
'ticket_pending_total' => Ticket::where('status', 0)
->count(),
'commission_pending_total' => Order::where('commission_status', 0)
->where('invite_user_id', '!=', NULL)
->whereNotIn('status', [0, 2])
->where('commission_balance', '>', 0)
->count(),
'day_income' => Order::where('created_at', '>=', strtotime(date('Y-m-d')))
->where('created_at', '<', time())
->whereNotIn('status', [0, 2])
->sum('total_amount'),
'last_month_income' => Order::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1'))))
->where('created_at', '<', strtotime(date('Y-m-1')))
->whereNotIn('status', [0, 2])
->sum('total_amount'),
'commission_month_payout' => CommissionLog::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->sum('get_amount'),
'commission_last_month_payout' => CommissionLog::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1'))))
->where('created_at', '<', strtotime(date('Y-m-1')))
->sum('get_amount'),
]
];
}
public function getOrder(Request $request)
{
$statistics = Stat::where('record_type', 'd')
->limit(31)
->orderBy('record_at', 'DESC')
->get()
->toArray();
$result = [];
foreach ($statistics as $statistic) {
$date = date('m-d', $statistic['record_at']);
$result[] = [
'type' => '收款金额',
'date' => $date,
'value' => $statistic['paid_total'] / 100
];
$result[] = [
'type' => '收款笔数',
'date' => $date,
'value' => $statistic['paid_count']
];
$result[] = [
'type' => '佣金金额(已发放)',
'date' => $date,
'value' => $statistic['commission_total'] / 100
];
$result[] = [
'type' => '佣金笔数(已发放)',
'date' => $date,
'value' => $statistic['commission_count']
];
}
$result = array_reverse($result);
return [
'data' => $result
];
}
public function getServerLastRank()
{
$servers = [
'shadowsocks' => ServerShadowsocks::where('parent_id', null)->get()->toArray(),
'v2ray' => ServerVmess::where('parent_id', null)->get()->toArray(),
'trojan' => ServerTrojan::where('parent_id', null)->get()->toArray(),
'vmess' => ServerVmess::where('parent_id', null)->get()->toArray()
];
$startAt = strtotime('-1 day', strtotime(date('Y-m-d')));
$endAt = strtotime(date('Y-m-d'));
$statistics = StatServer::select([
'server_id',
'server_type',
'u',
'd',
DB::raw('(u+d) as total')
])
->where('record_at', '>=', $startAt)
->where('record_at', '<', $endAt)
->where('record_type', 'd')
->limit(10)
->orderBy('total', 'DESC')
->get()
->toArray();
foreach ($statistics as $k => $v) {
foreach ($servers[$v['server_type']] as $server) {
if ($server['id'] === $v['server_id']) {
$statistics[$k]['server_name'] = $server['name'];
}
}
$statistics[$k]['total'] = $statistics[$k]['total'] / 1073741824;
}
array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics);
return [
'data' => $statistics
];
}
public function getStatUser(Request $request)
{
$request->validate([
'user_id' => 'required|integer'
]);
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$builder = StatUser::orderBy('record_at', 'DESC')->where('user_id', $request->input('user_id'));
$total = $builder->count();
$records = $builder->forPage($current, $pageSize)
->get();
return [
'data' => $records,
'total' => $total
];
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Models\Log as LogModel;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Laravel\Horizon\Contracts\JobRepository;
use Laravel\Horizon\Contracts\MasterSupervisorRepository;
use Laravel\Horizon\Contracts\MetricsRepository;
use Laravel\Horizon\Contracts\SupervisorRepository;
use Laravel\Horizon\Contracts\WorkloadRepository;
use Laravel\Horizon\WaitTimeCalculator;
class SystemController extends Controller
{
public function getSystemStatus()
{
return response([
'data' => [
'schedule' => $this->getScheduleStatus(),
'horizon' => $this->getHorizonStatus(),
'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null))
]
]);
}
public function getQueueWorkload(WorkloadRepository $workload)
{
return response([
'data' => collect($workload->get())->sortBy('name')->values()->toArray()
]);
}
protected function getScheduleStatus():bool
{
return (time() - 120) < Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null));
}
protected function getHorizonStatus():bool
{
if (! $masters = app(MasterSupervisorRepository::class)->all()) {
return false;
}
return collect($masters)->contains(function ($master) {
return $master->status === 'paused';
}) ? false : true;
}
public function getQueueStats()
{
return response([
'data' => [
'failedJobs' => app(JobRepository::class)->countRecentlyFailed(),
'jobsPerMinute' => app(MetricsRepository::class)->jobsProcessedPerMinute(),
'pausedMasters' => $this->totalPausedMasters(),
'periods' => [
'failedJobs' => config('horizon.trim.recent_failed', config('horizon.trim.failed')),
'recentJobs' => config('horizon.trim.recent'),
],
'processes' => $this->totalProcessCount(),
'queueWithMaxRuntime' => app(MetricsRepository::class)->queueWithMaximumRuntime(),
'queueWithMaxThroughput' => app(MetricsRepository::class)->queueWithMaximumThroughput(),
'recentJobs' => app(JobRepository::class)->countRecent(),
'status' => $this->getHorizonStatus(),
'wait' => collect(app(WaitTimeCalculator::class)->calculate())->take(1),
]
]);
}
/**
* Get the total process count across all supervisors.
*
* @return int
*/
protected function totalProcessCount()
{
$supervisors = app(SupervisorRepository::class)->all();
return collect($supervisors)->reduce(function ($carry, $supervisor) {
return $carry + collect($supervisor->processes)->sum();
}, 0);
}
/**
* Get the number of master supervisors that are currently paused.
*
* @return int
*/
protected function totalPausedMasters()
{
if (! $masters = app(MasterSupervisorRepository::class)->all()) {
return 0;
}
return collect($masters)->filter(function ($master) {
return $master->status === 'paused';
})->count();
}
public function getSystemLog(Request $request) {
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
$builder = LogModel::orderBy('created_at', 'DESC')
->setFilterAllowKeys('level');
$total = $builder->count();
$res = $builder->forPage($current, $pageSize)
->get();
return response([
'data' => $res,
'total' => $total
]);
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Services\ThemeService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
class ThemeController extends Controller
{
private $themes;
private $path;
public function __construct()
{
$this->path = $path = public_path('theme/');
$this->themes = array_map(function ($item) use ($path) {
return str_replace($path, '', $item);
}, glob($path . '*'));
}
public function getThemes()
{
$themeConfigs = [];
foreach ($this->themes as $theme) {
$themeConfigFile = $this->path . "{$theme}/config.json";
if (!File::exists($themeConfigFile)) continue;
$themeConfig = json_decode(File::get($themeConfigFile), true);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) continue;
$themeConfigs[$theme] = $themeConfig;
if (config("theme.{$theme}")) continue;
$themeService = new ThemeService($theme);
$themeService->init();
}
return response([
'data' => [
'themes' => $themeConfigs,
'active' => config('v2board.frontend_theme', 'v2board')
]
]);
}
public function getThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required|in:' . join(',', $this->themes)
]);
return response([
'data' => config("theme.{$payload['name']}")
]);
}
public function saveThemeConfig(Request $request)
{
$payload = $request->validate([
'name' => 'required|in:' . join(',', $this->themes),
'config' => 'required'
]);
$payload['config'] = json_decode(base64_decode($payload['config']), true);
if (!$payload['config'] || !is_array($payload['config'])) abort(500, '参数有误');
$themeConfigFile = public_path("theme/{$payload['name']}/config.json");
if (!File::exists($themeConfigFile)) abort(500, '主题不存在');
$themeConfig = json_decode(File::get($themeConfigFile), true);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) abort(500, '主题配置文件有误');
$validateFields = array_column($themeConfig['configs'], 'field_name');
$config = [];
foreach ($validateFields as $validateField) {
$config[$validateField] = isset($payload['config'][$validateField]) ? $payload['config'][$validateField] : '';
}
File::ensureDirectoryExists(base_path() . '/config/theme/');
$data = var_export($config, 1);
if (!File::put(base_path() . "/config/theme/{$payload['name']}.php", "<?php\n return $data ;")) {
abort(500, '修改失败');
}
try {
Artisan::call('config:cache');
// sleep(2);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => $config
]);
}
}

View File

@@ -1,14 +1,13 @@
<?php
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\V1\Admin;
use App\Jobs\SendEmailJob;
use App\Services\TicketService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Models\User;
use App\Models\TicketMessage;
use App\Models\User;
use App\Services\TicketService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
@@ -36,20 +35,20 @@ class TicketController extends Controller
}
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$model = Ticket::orderBy('created_at', 'DESC');
$model = Ticket::orderBy('updated_at', 'DESC');
if ($request->input('status') !== NULL) {
$model->where('status', $request->input('status'));
}
if ($request->input('reply_status') !== NULL) {
$model->whereIn('reply_status', $request->input('reply_status'));
}
if ($request->input('email') !== NULL) {
$user = User::where('email', $request->input('email'))->first();
if ($user) $model->where('user_id', $user->id);
}
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();
for ($i = 0; $i < count($res); $i++) {
if ($res[$i]['last_reply_user_id'] == $request->session()->get('id')) {
$res[$i]['reply_status'] = 0;
} else {
$res[$i]['reply_status'] = 1;
}
}
return response([
'data' => $res,
'total' => $total
@@ -68,7 +67,7 @@ class TicketController extends Controller
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->session()->get('id')
$request->user['id']
);
return response([
'data' => true

View File

@@ -1,39 +1,51 @@
<?php
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\V1\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UserFetch;
use App\Http\Requests\Admin\UserGenerate;
use App\Http\Requests\Admin\UserSendMail;
use App\Http\Requests\Admin\UserUpdate;
use App\Jobs\SendEmailJob;
use App\Models\Plan;
use App\Models\User;
use App\Services\AuthService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Models\User;
use App\Models\Plan;
use Illuminate\Support\Facades\DB;
class UserController extends Controller
{
public function resetSecret(Request $request)
{
$user = User::find($request->input('id'));
if (!$user) abort(500, '用户不存在');
$user->token = Helper::guid();
$user->uuid = Helper::guid(true);
return response([
'data' => $user->save()
]);
}
private function filter(Request $request, $builder)
{
if ($request->input('filter')) {
foreach ($request->input('filter') as $filter) {
if ($filter['key'] === 'invite_by_email') {
$user = User::where('email', $filter['value'])->first();
if (!$user) continue;
$builder->where('invite_user_id', $user->id);
continue;
$filters = $request->input('filter');
if ($filters) {
foreach ($filters as $k => $filter) {
if ($filter['condition'] === '模糊') {
$filter['condition'] = 'like';
$filter['value'] = "%{$filter['value']}%";
}
if ($filter['key'] === 'd' || $filter['key'] === 'transfer_enable') {
$filter['value'] = $filter['value'] * 1073741824;
}
if ($filter['condition'] === '模糊') {
$filter['condition'] = 'like';
$filter['value'] = "%{$filter['value']}%";
if ($filter['key'] === 'invite_by_email') {
$user = User::where('email', $filter['condition'], $filter['value'])->first();
$inviteUserId = isset($user->id) ? $user->id : 0;
$builder->where('invite_user_id', $inviteUserId);
unset($filters[$k]);
continue;
}
$builder->where($filter['key'], $filter['condition'], $filter['value']);
}
@@ -46,7 +58,11 @@ class UserController extends Controller
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$userModel = User::orderBy($sort, $sortType);
$userModel = User::select(
DB::raw('*'),
DB::raw('(u+d) as total_used')
)
->orderBy($sort, $sortType);
$this->filter($request, $userModel);
$total = $userModel->count();
$res = $userModel->forPage($current, $pageSize)
@@ -58,6 +74,7 @@ class UserController extends Controller
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
$res[$i]['subscribe_url'] = Helper::getSubscribeUrl($res[$i]['token']);
}
return response([
'data' => $res,
@@ -70,8 +87,12 @@ class UserController extends Controller
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$user = User::find($request->input('id'));
if ($user->invite_user_id) {
$user['invite_user'] = User::find($user->invite_user_id);
}
return response([
'data' => User::find($request->input('id'))
'data' => $user
]);
}
@@ -98,6 +119,19 @@ class UserController extends Controller
}
$params['group_id'] = $plan->group_id;
}
if ($request->input('invite_user_email')) {
$inviteUser = User::where('email', $request->input('invite_user_email'))->first();
if ($inviteUser) {
$params['invite_user_id'] = $inviteUser->id;
}
} else {
$params['invite_user_id'] = null;
}
if (isset($params['banned']) && (int)$params['banned'] === 1) {
$authService = new AuthService($user);
$authService->removeAllSession();
}
try {
$user->update($params);
@@ -124,7 +158,6 @@ class UserController extends Controller
}
$data = "邮箱,余额,推广佣金,总流量,剩余流量,套餐到期时间,订阅计划,订阅地址\r\n";
$baseUrl = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL')));
foreach($res as $user) {
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$balance = $user['balance'] / 100;
@@ -132,7 +165,7 @@ class UserController extends Controller
$transferEnable = $user['transfer_enable'] ? $user['transfer_enable'] / 1073741824 : 0;
$notUseFlow = (($user['transfer_enable'] - ($user['u'] + $user['d'])) / 1073741824) ?? 0;
$planName = $user['plan_name'] ?? '无订阅';
$subscribeUrl = $baseUrl . '/api/v1/client/subscribe?token=' . $user['token'];
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
$data .= "{$user['email']},{$balance},{$commissionBalance},{$transferEnable},{$notUseFlow},{$expireDate},{$planName},{$subscribeUrl}\r\n";
}
echo "\xEF\xBB\xBF" . $data;
@@ -156,6 +189,9 @@ class UserController extends Controller
'uuid' => Helper::guid(true),
'token' => Helper::guid()
];
if (User::where('email', $user['email'])->first()) {
abort(500, '邮箱已存在于系统中');
}
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
if (!User::create($user)) {
abort(500, '生成失败');
@@ -200,12 +236,11 @@ class UserController extends Controller
}
DB::commit();
$data = "账号,密码,过期时间,UUID,创建时间,订阅地址\r\n";
$baseUrl = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL')));
foreach($users as $user) {
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$createDate = date('Y-m-d H:i:s', $user['created_at']);
$password = $request->input('password') ?? $user['email'];
$subscribeUrl = $baseUrl . '/api/v1/client/subscribe?token=' . $user['token'];
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
$data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
}
echo $data;
@@ -228,7 +263,8 @@ class UserController extends Controller
'url' => config('v2board.app_url'),
'content' => $request->input('content')
]
]);
],
'send_email_mass');
}
return response([

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Http\Controllers\V1\Client;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Symfony\Component\Yaml\Yaml;
class AppController extends Controller
{
public function getConfig(Request $request)
{
$servers = [];
$user = $request->user;
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAvailableServers($user);
}
$defaultConfig = base_path() . '/resources/rules/app.clash.yaml';
$customConfig = base_path() . '/resources/rules/custom.app.clash.yaml';
if (File::exists($customConfig)) {
$config = Yaml::parseFile($customConfig);
} else {
$config = Yaml::parseFile($defaultConfig);
}
$proxy = [];
$proxies = [];
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks'
&& in_array($item['cipher'], [
'aes-128-gcm',
'aes-192-gcm',
'aes-256-gcm',
'chacha20-ietf-poly1305'
])
) {
array_push($proxy, \App\Protocols\Clash::buildShadowsocks($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'vmess') {
array_push($proxy, \App\Protocols\Clash::buildVmess($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'trojan') {
array_push($proxy, \App\Protocols\Clash::buildTrojan($user['uuid'], $item));
array_push($proxies, $item['name']);
}
}
$config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
foreach ($config['proxy-groups'] as $k => $v) {
$config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
}
die(Yaml::dump($config));
}
public function getVersion(Request $request)
{
if (strpos($request->header('user-agent'), 'tidalab/4.0.0') !== false
|| strpos($request->header('user-agent'), 'tunnelab/4.0.0') !== false
) {
if (strpos($request->header('user-agent'), 'Win64') !== false) {
return response([
'data' => [
'version' => config('v2board.windows_version'),
'download_url' => config('v2board.windows_download_url')
]
]);
} else {
return response([
'data' => [
'version' => config('v2board.macos_version'),
'download_url' => config('v2board.macos_download_url')
]
]);
}
return;
}
return response([
'data' => [
'windows_version' => config('v2board.windows_version'),
'windows_download_url' => config('v2board.windows_download_url'),
'macos_version' => config('v2board.macos_version'),
'macos_download_url' => config('v2board.macos_download_url'),
'android_version' => config('v2board.android_version'),
'android_download_url' => config('v2board.android_download_url')
]
]);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\V1\Client;
use App\Http\Controllers\Controller;
use App\Protocols\General;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
class ClientController extends Controller
{
public function subscribe(Request $request)
{
$flag = $request->input('flag')
?? ($_SERVER['HTTP_USER_AGENT'] ?? '');
$flag = strtolower($flag);
$user = $request->user;
// account not expired and is not banned.
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAvailableServers($user);
$this->setSubscribeInfoToServers($servers, $user);
if ($flag) {
foreach (array_reverse(glob(app_path('Protocols') . '/*.php')) as $file) {
$file = 'App\\Protocols\\' . basename($file, '.php');
$class = new $file($user, $servers);
if (strpos($flag, $class->flag) !== false) {
die($class->handle());
}
}
}
$class = new General($user, $servers);
die($class->handle());
}
}
private function setSubscribeInfoToServers(&$servers, $user)
{
if (!isset($servers[0])) return;
if (!(int)config('v2board.show_info_to_server_enable', 0)) return;
$useTraffic = $user['u'] + $user['d'];
$totalTraffic = $user['transfer_enable'];
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : '长期有效';
$userService = new UserService();
$resetDay = $userService->getResetDay($user);
array_unshift($servers, array_merge($servers[0], [
'name' => "套餐到期:{$expiredDate}",
]));
if ($resetDay) {
array_unshift($servers, array_merge($servers[0], [
'name' => "距离下次重置剩余:{$resetDay}",
]));
}
array_unshift($servers, array_merge($servers[0], [
'name' => "剩余流量:{$remainingTraffic}",
]));
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\V1\Guest;
use App\Http\Controllers\Controller;
use App\Utils\Dict;
use Illuminate\Support\Facades\Http;
class CommController extends Controller
{
public function config()
{
return response([
'data' => [
'tos_url' => config('v2board.tos_url'),
'is_email_verify' => (int)config('v2board.email_verify', 0) ? 1 : 0,
'is_invite_force' => (int)config('v2board.invite_force', 0) ? 1 : 0,
'email_whitelist_suffix' => (int)config('v2board.email_whitelist_enable', 0)
? $this->getEmailSuffix()
: 0,
'is_recaptcha' => (int)config('v2board.recaptcha_enable', 0) ? 1 : 0,
'recaptcha_site_key' => config('v2board.recaptcha_site_key'),
'app_description' => config('v2board.app_description'),
'app_url' => config('v2board.app_url'),
'logo' => config('v2board.logo'),
]
]);
}
private function getEmailSuffix()
{
$suffix = config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT);
if (!is_array($suffix)) {
return preg_split('/,/', $suffix);
}
return $suffix;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\V1\Guest;
use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\TelegramService;
use Illuminate\Http\Request;
class PaymentController extends Controller
{
public function notify($method, $uuid, Request $request)
{
try {
$paymentService = new PaymentService($method, null, $uuid);
$verify = $paymentService->notify($request->input());
if (!$verify) abort(500, 'verify error');
if (!$this->handle($verify['trade_no'], $verify['callback_no'])) {
abort(500, 'handle error');
}
die(isset($verify['custom_result']) ? $verify['custom_result'] : 'success');
} catch (\Exception $e) {
abort(500, 'fail');
}
}
private function handle($tradeNo, $callbackNo)
{
$order = Order::where('trade_no', $tradeNo)->first();
if (!$order) {
abort(500, 'order is not found');
}
if ($order->status !== 0) return true;
$orderService = new OrderService($order);
if (!$orderService->paid($callbackNo)) {
return false;
}
$telegramService = new TelegramService();
$message = sprintf(
"💰成功收款%s元\n———————————————\n订单号:%s",
$order->total_amount / 100,
$order->trade_no
);
$telegramService->sendMessageWithAdmin($message);
return true;
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace App\Http\Controllers\V1\Guest;
use App\Http\Controllers\Controller;
use App\Services\TelegramService;
use Illuminate\Http\Request;
class TelegramController extends Controller
{
protected $msg;
protected $commands = [];
protected $telegramService;
public function __construct(Request $request)
{
if ($request->input('access_token') !== md5(config('v2board.telegram_bot_token'))) {
abort(401);
}
$this->telegramService = new TelegramService();
}
public function webhook(Request $request)
{
$this->formatMessage($request->input());
$this->formatChatJoinRequest($request->input());
$this->handle();
}
public function handle()
{
if (!$this->msg) return;
$msg = $this->msg;
$commandName = explode('@', $msg->command);
// To reduce request, only commands contains @ will get the bot name
if (count($commandName) == 2) {
$botName = $this->getBotName();
if ($commandName[1] === $botName){
$msg->command = $commandName[0];
}
}
try {
foreach (glob(base_path('app//Plugins//Telegram//Commands') . '/*.php') as $file) {
$command = basename($file, '.php');
$class = '\\App\\Plugins\\Telegram\\Commands\\' . $command;
if (!class_exists($class)) continue;
$instance = new $class();
if ($msg->message_type === 'message') {
if (!isset($instance->command)) continue;
if ($msg->command !== $instance->command) continue;
$instance->handle($msg);
return;
}
if ($msg->message_type === 'reply_message') {
if (!isset($instance->regex)) continue;
if (!preg_match($instance->regex, $msg->reply_text, $match)) continue;
$instance->handle($msg, $match);
return;
}
}
} catch (\Exception $e) {
$this->telegramService->sendMessage($msg->chat_id, $e->getMessage());
}
}
public function getBotName()
{
$response = $this->telegramService->getMe();
return $response->result->username;
}
private function formatMessage(array $data)
{
if (!isset($data['message'])) return;
if (!isset($data['message']['text'])) return;
$obj = new \StdClass();
$text = explode(' ', $data['message']['text']);
$obj->command = $text[0];
$obj->args = array_slice($text, 1);
$obj->chat_id = $data['message']['chat']['id'];
$obj->message_id = $data['message']['message_id'];
$obj->message_type = 'message';
$obj->text = $data['message']['text'];
$obj->is_private = $data['message']['chat']['type'] === 'private';
if (isset($data['message']['reply_to_message']['text'])) {
$obj->message_type = 'reply_message';
$obj->reply_text = $data['message']['reply_to_message']['text'];
}
$this->msg = $obj;
}
private function formatChatJoinRequest(array $data)
{
if (!isset($data['chat_join_request'])) return;
if (!isset($data['chat_join_request']['from']['id'])) return;
if (!isset($data['chat_join_request']['chat']['id'])) return;
$user = \App\Models\User::where('telegram_id', $data['chat_join_request']['from']['id'])
->first();
if (!$user) {
$this->telegramService->declineChatJoinRequest(
$data['chat_join_request']['chat']['id'],
$data['chat_join_request']['from']['id']
);
return;
}
$userService = new \App\Services\UserService();
if (!$userService->isAvailable($user)) {
$this->telegramService->declineChatJoinRequest(
$data['chat_join_request']['chat']['id'],
$data['chat_join_request']['from']['id']
);
return;
}
$userService = new \App\Services\UserService();
$this->telegramService->approveChatJoinRequest(
$data['chat_join_request']['chat']['id'],
$data['chat_join_request']['from']['id']
);
}
}

View File

@@ -1,30 +1,93 @@
<?php
namespace App\Http\Controllers\Passport;
namespace App\Http\Controllers\V1\Passport;
use App\Http\Controllers\Controller;
use App\Http\Requests\Passport\AuthRegister;
use App\Http\Requests\Passport\AuthForget;
use App\Http\Requests\Passport\AuthLogin;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use App\Http\Requests\Passport\AuthRegister;
use App\Jobs\SendEmailJob;
use App\Models\InviteCode;
use App\Models\Plan;
use App\Models\User;
use App\Models\InviteCode;
use App\Utils\Helper;
use App\Utils\Dict;
use App\Services\AuthService;
use App\Utils\CacheKey;
use App\Utils\Dict;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use ReCaptcha\ReCaptcha;
class AuthController extends Controller
{
public function loginWithMailLink(Request $request)
{
if (!(int)config('v2board.login_with_mail_link_enable')) {
abort(404);
}
$params = $request->validate([
'email' => 'required|email:strict',
'redirect' => 'nullable'
]);
if (Cache::get(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $params['email']))) {
abort(500, __('Sending frequently, please try again later'));
}
$user = User::where('email', $params['email'])->first();
if (!$user) {
return response([
'data' => true
]);
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 300);
Cache::put(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $params['email']), time(), 60);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (config('v2board.app_url')) {
$link = config('v2board.app_url') . $redirect;
} else {
$link = url($redirect);
}
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => __('Login to :name', [
'name' => config('v2board.app_name', 'V2Board')
]),
'template_name' => 'login',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'link' => $link,
'url' => config('v2board.app_url')
]
]);
return response([
'data' => $link
]);
}
public function register(AuthRegister $request)
{
if ((int)config('v2board.register_limit_by_ip_enable', 0)) {
$registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0;
if ((int)$registerCountByIP >= (int)config('v2board.register_limit_count', 3)) {
abort(500, __('Register frequently, please try again after :minute minute', [
'minute' => config('v2board.register_limit_expire', 60)
]));
}
}
if ((int)config('v2board.recaptcha_enable', 0)) {
$recaptcha = new ReCaptcha(config('v2board.recaptcha_key'));
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
if (!$recaptchaResp->isSuccess()) {
abort(500, '验证码有误');
abort(500, __('Invalid code is incorrect'));
}
}
if ((int)config('v2board.email_whitelist_enable', 0)) {
@@ -32,36 +95,36 @@ class AuthController extends Controller
$request->input('email'),
config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT))
) {
abort(500, '邮箱后缀不处于白名单中');
abort(500, __('Email suffix is not in the Whitelist'));
}
}
if ((int)config('v2board.email_gmail_limit_enable', 0)) {
$prefix = explode('@', $request->input('email'))[0];
if (strpos($prefix, '.') !== false || strpos($prefix, '+') !== false) {
abort(500, '不支持Gmail别名邮箱');
abort(500, __('Gmail alias is not supported'));
}
}
if ((int)config('v2board.stop_register', 0)) {
abort(500, '本站已关闭注册');
abort(500, __('Registration has closed'));
}
if ((int)config('v2board.invite_force', 0)) {
if (empty($request->input('invite_code'))) {
abort(500, '必须使用邀请码才可以注册');
abort(500, __('You must use the invitation code to register'));
}
}
if ((int)config('v2board.email_verify', 0)) {
if (empty($request->input('email_code'))) {
abort(500, '邮箱验证码不能为空');
abort(500, __('Email verification code cannot be empty'));
}
if (Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== $request->input('email_code')) {
abort(500, '邮箱验证码有误');
if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) {
abort(500, __('Incorrect email verification code'));
}
}
$email = $request->input('email');
$password = $request->input('password');
$exist = User::where('email', $email)->first();
if ($exist) {
abort(500, '邮箱已存在系统中');
abort(500, __('Email already exists'));
}
$user = new User();
$user->email = $email;
@@ -74,7 +137,7 @@ class AuthController extends Controller
->first();
if (!$inviteCode) {
if ((int)config('v2board.invite_force', 0)) {
abort(500, '邀请码无效');
abort(500, __('Invalid invitation code'));
}
} else {
$user->invite_user_id = $inviteCode->user_id ? $inviteCode->user_id : null;
@@ -93,19 +156,32 @@ class AuthController extends Controller
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->expired_at = time() + (config('v2board.try_out_hour', 1) * 3600);
$user->speed_limit = $plan->speed_limit;
}
}
if (!$user->save()) {
abort(500, '注册失败');
abort(500, __('Register failed'));
}
if ((int)config('v2board.email_verify', 0)) {
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
}
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
$user->last_login_at = time();
$user->save();
if ((int)config('v2board.register_limit_by_ip_enable', 0)) {
Cache::put(
CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip()),
(int)$registerCountByIP + 1,
(int)config('v2board.register_limit_expire', 60) * 60
);
}
$authService = new AuthService($user);
return response()->json([
'data' => true
'data' => $authService->generateAuthData($request)
]);
}
@@ -114,37 +190,42 @@ class AuthController extends Controller
$email = $request->input('email');
$password = $request->input('password');
if ((int)config('v2board.password_limit_enable', 1)) {
$passwordErrorCount = (int)Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0);
if ($passwordErrorCount >= (int)config('v2board.password_limit_count', 5)) {
abort(500, __('There are too many password errors, please try again after :minute minutes.', [
'minute' => config('v2board.password_limit_expire', 60)
]));
}
}
$user = User::where('email', $email)->first();
if (!$user) {
abort(500, '用户名或密码错误');
abort(500, __('Incorrect email or password'));
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$user->password_salt,
$password,
$user->password)
) {
abort(500, '用户名或密码错误');
if ((int)config('v2board.password_limit_enable')) {
Cache::put(
CacheKey::get('PASSWORD_ERROR_LIMIT', $email),
(int)$passwordErrorCount + 1,
60 * (int)config('v2board.password_limit_expire', 60)
);
}
abort(500, __('Incorrect email or password'));
}
if ($user->banned) {
abort(500, '该账户已被停止使用');
abort(500, __('Your account has been suspended'));
}
$data = [
'token' => $user->token
];
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
if ($user->is_admin) {
$request->session()->put('is_admin', true);
$data['is_admin'] = true;
}
if ($user->is_staff) {
$request->session()->put('is_staff', true);
$data['is_staff'] = true;
}
$authService = new AuthService($user);
return response([
'data' => $data
'data' => $authService->generateAuthData($request)
]);
}
@@ -164,52 +245,34 @@ class AuthController extends Controller
$key = CacheKey::get('TEMP_TOKEN', $request->input('verify'));
$userId = Cache::get($key);
if (!$userId) {
abort(500, '令牌有误');
abort(500, __('Token error'));
}
$user = User::find($userId);
if (!$user) {
abort(500, '用户不存在');
abort(500, __('The user does not '));
}
if ($user->banned) {
abort(500, '该账户已被停止使用');
}
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
if ($user->is_admin) {
$request->session()->put('is_admin', true);
abort(500, __('Your account has been suspended'));
}
Cache::forget($key);
$authService = new AuthService($user);
return response([
'data' => true
'data' => $authService->generateAuthData($request)
]);
}
}
public function getTempToken(Request $request)
{
$user = User::where('token', $request->input('token'))->first();
if (!$user) {
abort(500, '令牌有误');
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 60);
return response([
'data' => $code
]);
}
public function getQuickLoginUrl(Request $request)
{
$user = User::where('token', $request->input('token'))->first();
if (!$user) {
abort(500, '令牌有误');
}
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if (!$authorization) abort(403, '未登录或登陆已过期');
$user = AuthService::decryptAuthData($authorization);
if (!$user) abort(403, '未登录或登陆已过期');
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 60);
Cache::put($key, $user['id'], 60);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (config('v2board.app_url')) {
$url = config('v2board.app_url') . $redirect;
@@ -221,37 +284,28 @@ class AuthController extends Controller
]);
}
public function check(Request $request)
{
$data = [
'is_login' => $request->session()->get('id') ? true : false
];
if ($request->session()->get('is_admin')) {
$data['is_admin'] = true;
}
return response([
'data' => $data
]);
}
public function forget(AuthForget $request)
{
if (Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== $request->input('email_code')) {
abort(500, '邮箱验证码有误');
$forgetRequestLimitKey = CacheKey::get('FORGET_REQUEST_LIMIT', $request->input('email'));
$forgetRequestLimit = (int)Cache::get($forgetRequestLimitKey);
if ($forgetRequestLimit >= 3) abort(500, __('Reset failed, Please try again later'));
if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) {
Cache::put($forgetRequestLimitKey, $forgetRequestLimit ? $forgetRequestLimit + 1 : 1, 300);
abort(500, __('Incorrect email verification code'));
}
$user = User::where('email', $request->input('email'))->first();
if (!$user) {
abort(500, '该邮箱不存在系统中');
abort(500, __('This email is not registered in the system'));
}
$user->password = password_hash($request->input('password'), PASSWORD_DEFAULT);
$user->password_algo = NULL;
$user->password_salt = NULL;
if (!$user->save()) {
abort(500, '重置失败');
abort(500, __('Reset failed'));
}
Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email')));
return response([
'data' => true
]);
}
}

View File

@@ -1,38 +1,22 @@
<?php
namespace App\Http\Controllers\Passport;
namespace App\Http\Controllers\V1\Passport;
use App\Http\Requests\Passport\CommSendEmailVerify;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\Mail;
use App\Utils\Helper;
use Illuminate\Support\Facades\Cache;
use App\Http\Requests\Passport\CommSendEmailVerify;
use App\Jobs\SendEmailJob;
use App\Models\InviteCode;
use App\Utils\Dict;
use App\Models\User;
use App\Utils\CacheKey;
use App\Utils\Dict;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use ReCaptcha\ReCaptcha;
class CommController extends Controller
{
public function config()
{
return response([
'data' => [
'isEmailVerify' => (int)config('v2board.email_verify', 0) ? 1 : 0,
'isInviteForce' => (int)config('v2board.invite_force', 0) ? 1 : 0,
'emailWhitelistSuffix' => (int)config('v2board.email_whitelist_enable', 0)
? $this->getEmailSuffix()
: 0,
'isRecaptcha' => (int)config('v2board.recaptcha_enable', 0) ? 1 : 0,
'recaptchaSiteKey' => config('v2board.recaptcha_site_key'),
'appDescription' => config('v2board.app_description')
]
]);
}
private function isEmailVerify()
{
return response([
@@ -46,15 +30,15 @@ class CommController extends Controller
$recaptcha = new ReCaptcha(config('v2board.recaptcha_key'));
$recaptchaResp = $recaptcha->verify($request->input('recaptcha_data'));
if (!$recaptchaResp->isSuccess()) {
abort(500, '验证码有误');
abort(500, __('Invalid code is incorrect'));
}
}
$email = $request->input('email');
if (Cache::get(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email))) {
abort(500, '验证码已发送,请过一会再请求');
abort(500, __('Email verification code has been sent, please request again later'));
}
$code = rand(100000, 999999);
$subject = config('v2board.app_name', 'V2Board') . '邮箱验证码';
$subject = config('v2board.app_name', 'V2Board') . __('Email verification code');
SendEmailJob::dispatch([
'email' => $email,

View File

@@ -0,0 +1,232 @@
<?php
namespace App\Http\Controllers\V1\Server;
use App\Http\Controllers\Controller;
use App\Models\ServerVmess;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/*
* V2ray Aurora
* Github: https://github.com/tokumeikoi/aurora
*/
class DeepbworkController extends Controller
{
CONST V2RAY_CONFIG = '{"log":{"loglevel":"debug","access":"access.log","error":"error.log"},"api":{"services":["HandlerService","StatsService"],"tag":"api"},"dns":{},"stats":{},"inbounds":[{"port":443,"protocol":"vmess","settings":{"clients":[]},"sniffing":{"enabled":true,"destOverride":["http","tls"]},"streamSettings":{"network":"tcp"},"tag":"proxy"},{"listen":"127.0.0.1","port":23333,"protocol":"dokodemo-door","settings":{"address":"0.0.0.0"},"tag":"api"}],"outbounds":[{"protocol":"freedom","settings":{}},{"protocol":"blackhole","settings":{},"tag":"block"}],"routing":{"rules":[{"type":"field","inboundTag":"api","outboundTag":"api"}]},"policy":{"levels":{"0":{"handshake":4,"connIdle":300,"uplinkOnly":5,"downlinkOnly":30,"statsUserUplink":true,"statsUserDownlink":true}}}}';
public function __construct(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
abort(500, 'token is null');
}
if ($token !== config('v2board.server_token')) {
abort(500, 'token is error');
}
}
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerVmess::find($nodeId);
if (!$server) {
abort(500, 'fail');
}
Cache::put(CacheKey::get('SERVER_VMESS_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers($server->group_id);
$result = [];
foreach ($users as $user) {
$user->v2ray_user = [
"uuid" => $user->uuid,
"email" => sprintf("%s@v2board.user", $user->uuid),
"alter_id" => 0,
"level" => 0,
];
unset($user['uuid']);
array_push($result, $user);
}
$eTag = sha1(json_encode($result));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
abort(304);
}
return response([
'msg' => 'ok',
'data' => $result,
])->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据
public function submit(Request $request)
{
// Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
$server = ServerVmess::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
'msg' => 'server is not found'
]);
}
$data = file_get_contents('php://input');
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_VMESS_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_VMESS_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
$formatData = [];
foreach ($data as $item) {
$formatData[$item['user_id']] = [$item['u'], $item['d']];
}
$userService->trafficFetch($server->toArray(), 'vmess', $formatData);
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
// 后端获取配置
public function config(Request $request)
{
$nodeId = $request->input('node_id');
$localPort = $request->input('local_port');
if (empty($nodeId) || empty($localPort)) {
abort(500, '参数错误');
}
try {
$json = $this->getV2RayConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
private function getV2RayConfig(int $nodeId, int $localPort)
{
$server = ServerVmess::find($nodeId);
if (!$server) {
abort(500, '节点不存在');
}
$json = json_decode(self::V2RAY_CONFIG);
$json->log->loglevel = (int)config('v2board.server_log_enable') ? 'debug' : 'none';
$json->inbounds[1]->port = (int)$localPort;
$json->inbounds[0]->port = (int)$server->server_port;
$json->inbounds[0]->streamSettings->network = $server->network;
$this->setDns($server, $json);
$this->setNetwork($server, $json);
$this->setRule($server, $json);
$this->setTls($server, $json);
return $json;
}
private function setDns(ServerVmess $server, object $json)
{
if ($server->dnsSettings) {
$dns = $server->dnsSettings;
if (isset($dns->servers)) {
array_push($dns->servers, '1.1.1.1');
array_push($dns->servers, 'localhost');
}
$json->dns = $dns;
$json->outbounds[0]->settings->domainStrategy = 'UseIP';
}
}
private function setNetwork(ServerVmess $server, object $json)
{
if ($server->networkSettings) {
switch ($server->network) {
case 'tcp':
$json->inbounds[0]->streamSettings->tcpSettings = $server->networkSettings;
break;
case 'kcp':
$json->inbounds[0]->streamSettings->kcpSettings = $server->networkSettings;
break;
case 'ws':
$json->inbounds[0]->streamSettings->wsSettings = $server->networkSettings;
break;
case 'http':
$json->inbounds[0]->streamSettings->httpSettings = $server->networkSettings;
break;
case 'domainsocket':
$json->inbounds[0]->streamSettings->dsSettings = $server->networkSettings;
break;
case 'quic':
$json->inbounds[0]->streamSettings->quicSettings = $server->networkSettings;
break;
case 'grpc':
$json->inbounds[0]->streamSettings->grpcSettings = $server->networkSettings;
break;
}
}
}
private function setRule(ServerVmess $server, object $json)
{
$domainRules = array_filter(explode(PHP_EOL, config('v2board.server_v2ray_domain')));
$protocolRules = array_filter(explode(PHP_EOL, config('v2board.server_v2ray_protocol')));
if ($server->ruleSettings) {
$ruleSettings = $server->ruleSettings;
// domain
if (isset($ruleSettings->domain)) {
$ruleSettings->domain = array_filter($ruleSettings->domain);
if (!empty($ruleSettings->domain)) {
$domainRules = array_merge($domainRules, $ruleSettings->domain);
}
}
// protocol
if (isset($ruleSettings->protocol)) {
$ruleSettings->protocol = array_filter($ruleSettings->protocol);
if (!empty($ruleSettings->protocol)) {
$protocolRules = array_merge($protocolRules, $ruleSettings->protocol);
}
}
}
if (!empty($domainRules)) {
$domainObj = new \StdClass();
$domainObj->type = 'field';
$domainObj->domain = $domainRules;
$domainObj->outboundTag = 'block';
array_push($json->routing->rules, $domainObj);
}
if (!empty($protocolRules)) {
$protocolObj = new \StdClass();
$protocolObj->type = 'field';
$protocolObj->protocol = $protocolRules;
$protocolObj->outboundTag = 'block';
array_push($json->routing->rules, $protocolObj);
}
if (empty($domainRules) && empty($protocolRules)) {
$json->inbounds[0]->sniffing->enabled = false;
}
}
private function setTls(ServerVMess $server, object $json)
{
if ((int)$server->tls) {
$tlsSettings = $server->tlsSettings;
$json->inbounds[0]->streamSettings->security = 'tls';
$tls = (object)[
'certificateFile' => '/root/.cert/server.crt',
'keyFile' => '/root/.cert/server.key'
];
$json->inbounds[0]->streamSettings->tlsSettings = new \StdClass();
if (isset($tlsSettings->serverName)) {
$json->inbounds[0]->streamSettings->tlsSettings->serverName = (string)$tlsSettings->serverName;
}
if (isset($tlsSettings->allowInsecure)) {
$json->inbounds[0]->streamSettings->tlsSettings->allowInsecure = (int)$tlsSettings->allowInsecure ? true : false;
}
$json->inbounds[0]->streamSettings->tlsSettings->certificates[0] = $tls;
}
}
}

View File

@@ -1,17 +1,13 @@
<?php
namespace App\Http\Controllers\Server;
namespace App\Http\Controllers\V1\Server;
use App\Http\Controllers\Controller;
use App\Models\ServerShadowsocks;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\ServerLog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
/*
@@ -34,6 +30,7 @@ class ShadowsocksTidalabController extends Controller
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerShadowsocks::find($nodeId);
if (!$server) {
@@ -41,7 +38,7 @@ class ShadowsocksTidalabController extends Controller
}
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers(json_decode($server->group_id));
$users = $serverService->getAvailableUsers($server->group_id);
$result = [];
foreach ($users as $user) {
array_push($result, [
@@ -51,9 +48,13 @@ class ShadowsocksTidalabController extends Controller
'secret' => $user->uuid
]);
}
$eTag = sha1(json_encode($result));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
abort(304);
}
return response([
'data' => $result
]);
])->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据
@@ -70,52 +71,18 @@ class ShadowsocksTidalabController extends Controller
$data = file_get_contents('php://input');
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_ONLINE_USER', $server->id), count($data), 3600);
$serverService = new ServerService();
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
DB::beginTransaction();
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
if (!$userService->trafficFetch((float)$u, (float)$d, (int)$item['user_id'])) {
DB::rollBack();
return response([
'ret' => 0,
'msg' => 'user fetch fail'
]);
}
$formatData = [];
$serverService->log(
$item['user_id'],
$request->input('node_id'),
$item['u'],
$item['d'],
$server->rate,
'shadowsocks'
);
foreach ($data as $item) {
$formatData[$item['user_id']] = [$item['u'], $item['d']];
}
DB::commit();
$userService->trafficFetch($server->toArray(), 'shadowsocks', $formatData);
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
// 后端获取配置
public function config(Request $request)
{
$nodeId = $request->input('node_id');
$localPort = $request->input('local_port');
if (empty($nodeId) || empty($localPort)) {
abort(500, '参数错误');
}
$serverService = new ServerService();
try {
$json = $serverService->getTrojanConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
}

View File

@@ -1,18 +1,16 @@
<?php
namespace App\Http\Controllers\Server;
namespace App\Http\Controllers\V1\Server;
use App\Http\Controllers\Controller;
use App\Models\ServerTrojan;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\ServerTrojan;
use App\Models\ServerLog;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
/*
* Tidal Lab Trojan
@@ -20,6 +18,7 @@ use Illuminate\Support\Facades\Cache;
*/
class TrojanTidalabController extends Controller
{
CONST TROJAN_CONFIG = '{"run_type":"server","local_addr":"0.0.0.0","local_port":443,"remote_addr":"www.taobao.com","remote_port":80,"password":[],"ssl":{"cert":"server.crt","key":"server.key","sni":"domain.com"},"api":{"enabled":true,"api_addr":"127.0.0.1","api_port":10000}}';
public function __construct(Request $request)
{
$token = $request->input('token');
@@ -34,6 +33,7 @@ class TrojanTidalabController extends Controller
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
$nodeId = $request->input('node_id');
$server = ServerTrojan::find($nodeId);
if (!$server) {
@@ -41,21 +41,23 @@ class TrojanTidalabController extends Controller
}
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers(json_decode($server->group_id));
$users = $serverService->getAvailableUsers($server->group_id);
$result = [];
foreach ($users as $user) {
$user->trojan_user = [
"password" => $user->uuid,
];
unset($user['uuid']);
unset($user['v2ray_alter_id']);
unset($user['v2ray_level']);
array_push($result, $user);
}
$eTag = sha1(json_encode($result));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
abort(304);
}
return response([
'msg' => 'ok',
'data' => $result,
]);
])->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据
@@ -72,30 +74,13 @@ class TrojanTidalabController extends Controller
$data = file_get_contents('php://input');
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server->id), count($data), 3600);
$serverService = new ServerService();
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
DB::beginTransaction();
$formatData = [];
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
if (!$userService->trafficFetch($u, $d, $item['user_id'])) {
DB::rollBack();
return response([
'ret' => 0,
'msg' => 'user fetch fail'
]);
}
$serverService->log(
$item['user_id'],
$request->input('node_id'),
$item['u'],
$item['d'],
$server->rate,
'trojan'
);
$formatData[$item['user_id']] = [$item['u'], $item['d']];
}
DB::commit();
$userService->trafficFetch($server->toArray(), 'trojan', $formatData);
return response([
'ret' => 1,
@@ -111,13 +96,28 @@ class TrojanTidalabController extends Controller
if (empty($nodeId) || empty($localPort)) {
abort(500, '参数错误');
}
$serverService = new ServerService();
try {
$json = $serverService->getTrojanConfig($nodeId, $localPort);
$json = $this->getTrojanConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}
die(json_encode($json, JSON_UNESCAPED_UNICODE));
}
private function getTrojanConfig(int $nodeId, int $localPort)
{
$server = ServerTrojan::find($nodeId);
if (!$server) {
abort(500, '节点不存在');
}
$json = json_decode(self::TROJAN_CONFIG);
$json->local_port = $server->server_port;
$json->ssl->sni = $server->server_name ? $server->server_name : $server->host;
$json->ssl->cert = "/root/.cert/server.crt";
$json->ssl->key = "/root/.cert/server.key";
$json->api->api_port = $localPort;
return $json;
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace App\Http\Controllers\V1\Server;
use App\Http\Controllers\Controller;
use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class UniProxyController extends Controller
{
private $nodeType;
private $nodeInfo;
private $nodeId;
private $serverService;
public function __construct(Request $request)
{
$token = $request->input('token');
if (empty($token)) {
abort(500, 'token is null');
}
if ($token !== config('v2board.server_token')) {
abort(500, 'token is error');
}
$this->nodeType = $request->input('node_type');
if ($this->nodeType === 'v2ray') $this->nodeType = 'vmess';
$this->nodeId = $request->input('node_id');
$this->serverService = new ServerService();
$this->nodeInfo = $this->serverService->getServer($this->nodeId, $this->nodeType);
if (!$this->nodeInfo) abort(500, 'server is not exist');
}
// 后端获取用户
public function user(Request $request)
{
ini_set('memory_limit', -1);
Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_LAST_CHECK_AT', $this->nodeInfo->id), time(), 3600);
$users = $this->serverService->getAvailableUsers($this->nodeInfo->group_id);
$users = $users->toArray();
$response['users'] = $users;
$eTag = sha1(json_encode($response));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
abort(304);
}
return response($response)->header('ETag', "\"{$eTag}\"");
}
// 后端提交数据
public function push(Request $request)
{
$data = file_get_contents('php://input');
$data = json_decode($data, true);
Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_ONLINE_USER', $this->nodeInfo->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_LAST_PUSH_AT', $this->nodeInfo->id), time(), 3600);
$userService = new UserService();
$userService->trafficFetch($this->nodeInfo->toArray(), $this->nodeType, $data);
return response([
'data' => true
]);
}
// 后端获取配置
public function config(Request $request)
{
switch ($this->nodeType) {
case 'shadowsocks':
$response = [
'server_port' => $this->nodeInfo->server_port,
'cipher' => $this->nodeInfo->cipher,
'obfs' => $this->nodeInfo->obfs,
'obfs_settings' => $this->nodeInfo->obfs_settings
];
if ($this->nodeInfo->cipher === '2022-blake3-aes-128-gcm') {
$response['server_key'] = Helper::getServerKey($this->nodeInfo->created_at, 16);
}
if ($this->nodeInfo->cipher === '2022-blake3-aes-256-gcm') {
$response['server_key'] = Helper::getServerKey($this->nodeInfo->created_at, 32);
}
break;
case 'vmess':
$response = [
'server_port' => $this->nodeInfo->server_port,
'network' => $this->nodeInfo->network,
'networkSettings' => $this->nodeInfo->networkSettings,
'tls' => $this->nodeInfo->tls
];
break;
case 'trojan':
$response = [
'host' => $this->nodeInfo->host,
'server_port' => $this->nodeInfo->server_port,
'server_name' => $this->nodeInfo->server_name,
];
break;
case 'hysteria':
$response = [
'host' => $this->nodeInfo->host,
'server_port' => $this->nodeInfo->server_port,
'server_name' => $this->nodeInfo->server_name,
'up_mbps' => $this->nodeInfo->up_mbps,
'down_mbps' => $this->nodeInfo->down_mbps,
'obfs' => Helper::getServerKey($this->nodeInfo->created_at, 16),
'obfs_type' => $this->nodeInfo->obfs_type,
'ignore_client_bandwidth' => !!$this->nodeInfo->ignore_client_bandwidth
];
break;
case "vless":
$response = [
'server_port' => $this->nodeInfo->server_port,
'network' => $this->nodeInfo->network,
'network_settings' => $this->nodeInfo->network_settings,
'tls' => $this->nodeInfo->tls,
'flow' => $this->nodeInfo->flow,
'tls_settings' => $this->nodeInfo->tls_settings
];
break;
}
$response['base_config'] = [
'push_interval' => (int)config('v2board.server_push_interval', 60),
'pull_interval' => (int)config('v2board.server_pull_interval', 60)
];
if ($this->nodeInfo['route_id']) {
$response['routes'] = $this->serverService->getRoutes($this->nodeInfo['route_id']);
}
$eTag = sha1(json_encode($response));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
abort(304);
}
return response($response)->header('ETag', "\"{$eTag}\"");
}
}

View File

@@ -1,11 +1,11 @@
<?php
namespace App\Http\Controllers\Admin;
namespace App\Http\Controllers\V1\Staff;
use App\Http\Requests\Admin\NoticeSave;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\NoticeSave;
use App\Models\Notice;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class NoticeController extends Controller

View File

@@ -1,15 +1,11 @@
<?php
namespace App\Http\Controllers\Staff;
namespace App\Http\Controllers\V1\Staff;
use App\Http\Requests\Admin\PlanSave;
use App\Http\Requests\Admin\PlanSort;
use App\Http\Requests\Admin\PlanUpdate;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\Order;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PlanController extends Controller

View File

@@ -1,12 +1,12 @@
<?php
namespace App\Http\Controllers\Staff;
namespace App\Http\Controllers\V1\Staff;
use App\Services\TicketService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Models\TicketMessage;
use App\Services\TicketService;
use Illuminate\Http\Request;
class TicketController extends Controller
{
@@ -39,13 +39,6 @@ class TicketController extends Controller
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();
for ($i = 0; $i < count($res); $i++) {
if ($res[$i]['last_reply_user_id'] == $request->session()->get('id')) {
$res[$i]['reply_status'] = 0;
} else {
$res[$i]['reply_status'] = 1;
}
}
return response([
'data' => $res,
'total' => $total
@@ -64,7 +57,7 @@ class TicketController extends Controller
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->session()->get('id')
$request->user['id']
);
return response([
'data' => true

View File

@@ -1,14 +1,14 @@
<?php
namespace App\Http\Controllers\Staff;
namespace App\Http\Controllers\V1\Staff;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\UserSendMail;
use App\Http\Requests\Staff\UserUpdate;
use App\Jobs\SendEmailJob;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Plan;
use App\Models\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
@@ -17,8 +17,13 @@ class UserController extends Controller
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$user = User::where('is_admin', 0)
->where('id', $request->input('id'))
->where('is_staff', 0)
->first();
if (!$user) abort(500, '用户不存在');
return response([
'data' => User::find($request->input('id'))
'data' => $user
]);
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\Payment;
use App\Utils\Dict;
use Illuminate\Http\Request;
class CommController extends Controller
{
public function config()
{
return response([
'data' => [
'is_telegram' => (int)config('v2board.telegram_bot_enable', 0),
'telegram_discuss_link' => config('v2board.telegram_discuss_link'),
'stripe_pk' => config('v2board.stripe_pk_live'),
'withdraw_methods' => config('v2board.commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
'withdraw_close' => (int)config('v2board.withdraw_close_enable', 0),
'currency' => config('v2board.currency', 'CNY'),
'currency_symbol' => config('v2board.currency_symbol', '¥'),
'commission_distribution_enable' => (int)config('v2board.commission_distribution_enable', 0),
'commission_distribution_l1' => config('v2board.commission_distribution_l1'),
'commission_distribution_l2' => config('v2board.commission_distribution_l2'),
'commission_distribution_l3' => config('v2board.commission_distribution_l3')
]
]);
}
public function getStripePublicKey(Request $request)
{
$payment = Payment::where('id', $request->input('id'))
->where('payment', 'StripeCredit')
->first();
if (!$payment) abort(500, 'payment is not found');
return response([
'data' => $payment->config['stripe_pk_live']
]);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Services\CouponService;
use Illuminate\Http\Request;
class CouponController extends Controller
{
public function check(Request $request)
{
if (empty($request->input('code'))) {
abort(500, __('Coupon cannot be empty'));
}
$couponService = new CouponService($request->input('code'));
$couponService->setPlanId($request->input('plan_id'));
$couponService->setUserId($request->user['id']);
$couponService->check();
return response([
'data' => $couponService->getCoupon()
]);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\CommissionLog;
use App\Models\InviteCode;
use App\Models\Order;
use App\Models\User;
use App\Utils\Helper;
use Illuminate\Http\Request;
class InviteController extends Controller
{
public function save(Request $request)
{
if (InviteCode::where('user_id', $request->user['id'])->where('status', 0)->count() >= config('v2board.invite_gen_limit', 5)) {
abort(500, __('The maximum number of creations has been reached'));
}
$inviteCode = new InviteCode();
$inviteCode->user_id = $request->user['id'];
$inviteCode->code = Helper::randomChar(8);
return response([
'data' => $inviteCode->save()
]);
}
public function details(Request $request)
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
$builder = CommissionLog::where('invite_user_id', $request->user['id'])
->where('get_amount', '>', 0)
->select([
'id',
'trade_no',
'order_amount',
'get_amount',
'created_at'
])
->orderBy('created_at', 'DESC');
$total = $builder->count();
$details = $builder->forPage($current, $pageSize)
->get();
return response([
'data' => $details,
'total' => $total
]);
}
public function fetch(Request $request)
{
$codes = InviteCode::where('user_id', $request->user['id'])
->where('status', 0)
->get();
$commission_rate = config('v2board.invite_commission', 10);
$user = User::find($request->user['id']);
if ($user->commission_rate) {
$commission_rate = $user->commission_rate;
}
$uncheck_commission_balance = (int)Order::where('status', 3)
->where('commission_status', 0)
->where('invite_user_id', $request->user['id'])
->sum('commission_balance');
if (config('v2board.commission_distribution_enable', 0)) {
$uncheck_commission_balance = $uncheck_commission_balance * (config('v2board.commission_distribution_l1') / 100);
}
$stat = [
//已注册用户数
(int)User::where('invite_user_id', $request->user['id'])->count(),
//有效的佣金
(int)CommissionLog::where('invite_user_id', $request->user['id'])
->sum('get_amount'),
//确认中的佣金
$uncheck_commission_balance,
//佣金比例
(int)$commission_rate,
//可用佣金
(int)$user->commission_balance
];
return response([
'data' => [
'codes' => $codes,
'stat' => $stat
]
]);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\Knowledge;
use App\Models\User;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
class KnowledgeController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$knowledge = Knowledge::where('id', $request->input('id'))
->where('show', 1)
->first()
->toArray();
if (!$knowledge) abort(500, __('Article does not exist'));
$user = User::find($request->user['id']);
$userService = new UserService();
if (!$userService->isAvailable($user)) {
$this->formatAccessData($knowledge['body']);
}
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
$knowledge['body'] = str_replace('{{siteName}}', config('v2board.app_name', 'V2Board'), $knowledge['body']);
$knowledge['body'] = str_replace('{{subscribeUrl}}', $subscribeUrl, $knowledge['body']);
$knowledge['body'] = str_replace('{{urlEncodeSubscribeUrl}}', urlencode($subscribeUrl), $knowledge['body']);
$knowledge['body'] = str_replace(
'{{safeBase64SubscribeUrl}}',
str_replace(
array('+', '/', '='),
array('-', '_', ''),
base64_encode($subscribeUrl)
),
$knowledge['body']
);
return response([
'data' => $knowledge
]);
}
$builder = Knowledge::select(['id', 'category', 'title', 'updated_at'])
->where('language', $request->input('language'))
->where('show', 1)
->orderBy('sort', 'ASC');
$keyword = $request->input('keyword');
if ($keyword) {
$builder = $builder->where(function ($query) use ($keyword) {
$query->where('title', 'LIKE', "%{$keyword}%")
->orWhere('body', 'LIKE', "%{$keyword}%");
});
}
$knowledges = $builder->get()
->groupBy('category');
return response([
'data' => $knowledges
]);
}
private function formatAccessData(&$body)
{
function getBetween($input, $start, $end){$substr = substr($input, strlen($start)+strpos($input, $start),(strlen($input) - strpos($input, $end))*(-1));return $start . $substr . $end;}
while (strpos($body, '<!--access start-->') !== false) {
$accessData = getBetween($body, '<!--access start-->', '<!--access end-->');
if ($accessData) {
$body = str_replace($accessData, '<div class="v2board-no-access">'. __('You must have a valid subscription to view content in this area') .'</div>', $body);
}
}
}
}

View File

@@ -1,11 +1,10 @@
<?php
namespace App\Http\Controllers\User;
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Notice;
use App\Utils\Helper;
use Illuminate\Http\Request;
class NoticeController extends Controller
{
@@ -13,7 +12,8 @@ class NoticeController extends Controller
{
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = 5;
$model = Notice::orderBy('created_at', 'DESC');
$model = Notice::orderBy('created_at', 'DESC')
->where('show', 1);
$total = $model->count();
$res = $model->forPage($current, $pageSize)
->get();

View File

@@ -0,0 +1,266 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\OrderSave;
use App\Models\Order;
use App\Models\Payment;
use App\Models\Plan;
use App\Models\User;
use App\Services\CouponService;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\PlanService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Library\BitpayX;
use Library\Epay;
use Library\MGate;
use Omnipay\Omnipay;
use Stripe\Source;
use Stripe\Stripe;
class OrderController extends Controller
{
public function fetch(Request $request)
{
$model = Order::where('user_id', $request->user['id'])
->orderBy('created_at', 'DESC');
if ($request->input('status') !== null) {
$model->where('status', $request->input('status'));
}
$order = $model->get();
$plan = Plan::get();
for ($i = 0; $i < count($order); $i++) {
for ($x = 0; $x < count($plan); $x++) {
if ($order[$i]['plan_id'] === $plan[$x]['id']) {
$order[$i]['plan'] = $plan[$x];
}
}
}
return response([
'data' => $order->makeHidden(['id', 'user_id'])
]);
}
public function detail(Request $request)
{
$order = Order::where('user_id', $request->user['id'])
->where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, __('Order does not exist or has been paid'));
}
$order['plan'] = Plan::find($order->plan_id);
$order['try_out_plan_id'] = (int)config('v2board.try_out_plan_id');
if (!$order['plan']) {
abort(500, __('Subscription plan does not exist'));
}
if ($order->surplus_order_ids) {
$order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
}
return response([
'data' => $order
]);
}
public function save(OrderSave $request)
{
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($request->user['id'])) {
abort(500, __('You have an unpaid or pending order, please try again later or cancel it'));
}
$planService = new PlanService($request->input('plan_id'));
$plan = $planService->plan;
$user = User::find($request->user['id']);
if (!$plan) {
abort(500, __('Subscription plan does not exist'));
}
if ($user->plan_id !== $plan->id && !$planService->haveCapacity() && $request->input('period') !== 'reset_price') {
abort(500, __('Current product is sold out'));
}
if ($plan[$request->input('period')] === NULL) {
abort(500, __('This payment period cannot be purchased, please choose another period'));
}
if ($request->input('period') === 'reset_price') {
if (!$userService->isAvailable($user) || $plan->id !== $user->plan_id) {
abort(500, __('Subscription has expired or no active subscription, unable to purchase Data Reset Package'));
}
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
if ($request->input('period') !== 'reset_price') {
abort(500, __('This subscription has been sold out, please choose another subscription'));
}
}
if (!$plan->renew && $user->plan_id == $plan->id && $request->input('period') !== 'reset_price') {
abort(500, __('This subscription cannot be renewed, please change to another subscription'));
}
if (!$plan->show && $plan->renew && !$userService->isAvailable($user)) {
abort(500, __('This subscription has expired, please change to another subscription'));
}
DB::beginTransaction();
$order = new Order();
$orderService = new OrderService($order);
$order->user_id = $request->user['id'];
$order->plan_id = $plan->id;
$order->period = $request->input('period');
$order->trade_no = Helper::generateOrderNo();
$order->total_amount = $plan[$request->input('period')];
if ($request->input('coupon_code')) {
$couponService = new CouponService($request->input('coupon_code'));
if (!$couponService->use($order)) {
DB::rollBack();
abort(500, __('Coupon failed'));
}
$order->coupon_id = $couponService->getId();
}
$orderService->setVipDiscount($user);
$orderService->setOrderType($user);
$orderService->setInvite($user);
if ($user->balance && $order->total_amount > 0) {
$remainingBalance = $user->balance - $order->total_amount;
$userService = new UserService();
if ($remainingBalance > 0) {
if (!$userService->addBalance($order->user_id, - $order->total_amount)) {
DB::rollBack();
abort(500, __('Insufficient balance'));
}
$order->balance_amount = $order->total_amount;
$order->total_amount = 0;
} else {
if (!$userService->addBalance($order->user_id, - $user->balance)) {
DB::rollBack();
abort(500, __('Insufficient balance'));
}
$order->balance_amount = $user->balance;
$order->total_amount = $order->total_amount - $user->balance;
}
}
if (!$order->save()) {
DB::rollback();
abort(500, __('Failed to create order'));
}
DB::commit();
return response([
'data' => $order->trade_no
]);
}
public function checkout(Request $request)
{
$tradeNo = $request->input('trade_no');
$method = $request->input('method');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->user['id'])
->where('status', 0)
->first();
if (!$order) {
abort(500, __('Order does not exist or has been paid'));
}
// free process
if ($order->total_amount <= 0) {
$orderService = new OrderService($order);
if (!$orderService->paid($order->trade_no)) abort(500, '');
return response([
'type' => -1,
'data' => true
]);
}
$payment = Payment::find($method);
if (!$payment || $payment->enable !== 1) abort(500, __('Payment method is not available'));
$paymentService = new PaymentService($payment->payment, $payment->id);
$order->handling_amount = NULL;
if ($payment->handling_fee_fixed || $payment->handling_fee_percent) {
$order->handling_amount = round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed);
}
$order->payment_id = $method;
if (!$order->save()) abort(500, __('Request failed, please try again later'));
$result = $paymentService->pay([
'trade_no' => $tradeNo,
'total_amount' => isset($order->handling_amount) ? ($order->total_amount + $order->handling_amount) : $order->total_amount,
'user_id' => $order->user_id,
'stripe_token' => $request->input('token')
]);
return response([
'type' => $result['type'],
'data' => $result['data']
]);
}
public function check(Request $request)
{
$tradeNo = $request->input('trade_no');
$order = Order::where('trade_no', $tradeNo)
->where('user_id', $request->user['id'])
->first();
if (!$order) {
abort(500, __('Order does not exist'));
}
return response([
'data' => $order->status
]);
}
public function getPaymentMethod()
{
$methods = Payment::select([
'id',
'name',
'payment',
'icon',
'handling_fee_fixed',
'handling_fee_percent'
])
->where('enable', 1)
->orderBy('sort', 'ASC')
->get();
return response([
'data' => $methods
]);
}
public function cancel(Request $request)
{
if (empty($request->input('trade_no'))) {
abort(500, __('Invalid parameter'));
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('user_id', $request->user['id'])
->first();
if (!$order) {
abort(500, __('Order does not exist'));
}
if ($order->status !== 0) {
abort(500, __('You can only cancel pending orders'));
}
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, __('Cancel failed'));
}
return response([
'data' => true
]);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\Plan;
use App\Models\User;
use App\Services\PlanService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PlanController extends Controller
{
public function fetch(Request $request)
{
$user = User::find($request->user['id']);
if ($request->input('id')) {
$plan = Plan::where('id', $request->input('id'))->first();
if (!$plan) {
abort(500, __('Subscription plan does not exist'));
}
if ((!$plan->show && !$plan->renew) || (!$plan->show && $user->plan_id !== $plan->id)) {
abort(500, __('Subscription plan does not exist'));
}
return response([
'data' => $plan
]);
}
$counts = PlanService::countActiveUsers();
$plans = Plan::where('show', 1)
->orderBy('sort', 'ASC')
->get();
foreach ($plans as $k => $v) {
if ($plans[$k]->capacity_limit === NULL) continue;
if (!isset($counts[$plans[$k]->id])) continue;
$plans[$k]->capacity_limit = $plans[$k]->capacity_limit - $counts[$plans[$k]->id]->count;
}
return response([
'data' => $plans
]);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\ServerService;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class ServerController extends Controller
{
public function fetch(Request $request)
{
$user = User::find($request->user['id']);
$servers = [];
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAvailableServers($user);
}
$eTag = sha1(json_encode(array_column($servers, 'cache_key')));
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
abort(304);
}
return response([
'data' => $servers
])->header('ETag', "\"{$eTag}\"");
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\StatUser;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
public function getTrafficLog(Request $request)
{
$builder = StatUser::select([
'u',
'd',
'record_at',
'user_id',
'server_rate'
])
->where('user_id', $request->user['id'])
->where('record_at', '>=', strtotime(date('Y-m-1')))
->orderBy('record_at', 'DESC');
return response([
'data' => $builder->get()
]);
}
}

View File

@@ -1,9 +1,11 @@
<?php
namespace App\Http\Controllers\User;
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\TelegramService;
use Illuminate\Http\Request;
class TelegramController extends Controller
{
@@ -17,4 +19,9 @@ class TelegramController extends Controller
]
]);
}
public function unbind(Request $request)
{
$user = User::where('user_id', $request->user['id'])->first();
}
}

View File

@@ -1,16 +1,17 @@
<?php
namespace App\Http\Controllers\User;
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\TicketSave;
use App\Http\Requests\User\TicketWithdraw;
use App\Jobs\SendTelegramJob;
use App\Models\User;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Models\Ticket;
use App\Models\TicketMessage;
use App\Models\User;
use App\Services\TelegramService;
use App\Services\TicketService;
use App\Utils\Dict;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TicketController extends Controller
@@ -19,10 +20,10 @@ class TicketController extends Controller
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->session()->get('id'))
->where('user_id', $request->user['id'])
->first();
if (!$ticket) {
abort(500, '工单不存在');
abort(500, __('Ticket does not exist'));
}
$ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get();
for ($i = 0; $i < count($ticket['message']); $i++) {
@@ -36,16 +37,9 @@ class TicketController extends Controller
'data' => $ticket
]);
}
$ticket = Ticket::where('user_id', $request->session()->get('id'))
$ticket = Ticket::where('user_id', $request->user['id'])
->orderBy('created_at', 'DESC')
->get();
for ($i = 0; $i < count($ticket); $i++) {
if ($ticket[$i]['last_reply_user_id'] == $request->session()->get('id')) {
$ticket[$i]['reply_status'] = 0;
} else {
$ticket[$i]['reply_status'] = 1;
}
}
return response([
'data' => $ticket
]);
@@ -54,31 +48,30 @@ class TicketController extends Controller
public function save(TicketSave $request)
{
DB::beginTransaction();
if ((int)Ticket::where('status', 0)->where('user_id', $request->session()->get('id'))->count()) {
abort(500, '存在其他工单尚未处理');
if ((int)Ticket::where('status', 0)->where('user_id', $request->user['id'])->lockForUpdate()->count()) {
abort(500, __('There are other unresolved tickets'));
}
$ticket = Ticket::create(array_merge($request->only([
'subject',
'level'
]), [
'user_id' => $request->session()->get('id'),
'last_reply_user_id' => $request->session()->get('id')
'user_id' => $request->user['id']
]));
if (!$ticket) {
DB::rollback();
abort(500, '工单创建失败');
abort(500, __('Failed to open ticket'));
}
$ticketMessage = TicketMessage::create([
'user_id' => $request->session()->get('id'),
'user_id' => $request->user['id'],
'ticket_id' => $ticket->id,
'message' => $request->input('message')
]);
if (!$ticketMessage) {
DB::rollback();
abort(500, '工单创建失败');
abort(500, __('Failed to open ticket'));
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
$this->sendNotify($ticket, $request->input('message'));
return response([
'data' => true
]);
@@ -87,36 +80,32 @@ class TicketController extends Controller
public function reply(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
abort(500, __('Invalid parameter'));
}
if (empty($request->input('message'))) {
abort(500, '消息不能为空');
abort(500, __('Message cannot be empty'));
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->session()->get('id'))
->where('user_id', $request->user['id'])
->first();
if (!$ticket) {
abort(500, '工单不存在');
abort(500, __('Ticket does not exist'));
}
if ($ticket->status) {
abort(500, '工单已关闭,无法回复');
abort(500, __('The ticket is closed and cannot be replied'));
}
if ($request->session()->get('id') == $this->getLastMessage($ticket->id)->user_id) {
abort(500, '请等待技术支持回复');
if ($request->user['id'] == $this->getLastMessage($ticket->id)->user_id) {
abort(500, __('Please wait for the technical enginneer to reply'));
}
DB::beginTransaction();
$ticketMessage = TicketMessage::create([
'user_id' => $request->session()->get('id'),
'ticket_id' => $ticket->id,
'message' => $request->input('message')
]);
$ticket->last_reply_user_id = $request->session()->get('id');
if (!$ticketMessage || !$ticket->save()) {
DB::rollback();
abort(500, '工单回复失败');
$ticketService = new TicketService();
if (!$ticketService->reply(
$ticket,
$request->input('message'),
$request->user['id']
)) {
abort(500, __('Ticket reply failed'));
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
$this->sendNotify($ticket, $request->input('message'));
return response([
'data' => true
]);
@@ -126,17 +115,17 @@ class TicketController extends Controller
public function close(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
abort(500, __('Invalid parameter'));
}
$ticket = Ticket::where('id', $request->input('id'))
->where('user_id', $request->session()->get('id'))
->where('user_id', $request->user['id'])
->first();
if (!$ticket) {
abort(500, '工单不存在');
abort(500, __('Ticket does not exist'));
}
$ticket->status = 1;
if (!$ticket->save()) {
abort(500, '关闭失败');
abort(500, __('Close failed'));
}
return response([
'data' => true
@@ -152,49 +141,57 @@ class TicketController extends Controller
public function withdraw(TicketWithdraw $request)
{
$user = User::find($request->session()->get('id'));
if ((int)config('v2board.withdraw_close_enable', 0)) {
abort(500, 'user.ticket.withdraw.not_support_withdraw');
}
if (!in_array(
$request->input('withdraw_method'),
config(
'v2board.commission_withdraw_method',
Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT
)
)) {
abort(500, __('Unsupported withdrawal method'));
}
$user = User::find($request->user['id']);
$limit = config('v2board.commission_withdraw_limit', 100);
if ($limit > ($user->commission_balance / 100)) {
abort(500, "当前系统要求的提现门槛佣金需为{$limit}CNY");
abort(500, __('The current required minimum withdrawal commission is :limit', ['limit' => $limit]));
}
DB::beginTransaction();
$subject = '[提现申请]本工单由系统发出';
$subject = __('[Commission Withdrawal Request] This ticket is opened by the system');
$ticket = Ticket::create([
'subject' => $subject,
'level' => 2,
'user_id' => $request->session()->get('id'),
'last_reply_user_id' => $request->session()->get('id')
'user_id' => $request->user['id']
]);
if (!$ticket) {
DB::rollback();
abort(500, '工单创建失败');
abort(500, __('Failed to open ticket'));
}
$methodText = [
'alipay' => '支付宝',
'paypal' => '贝宝(Paypal)',
'usdt' => 'USDT',
'btc' => '比特币'
];
$message = "提现方式:{$methodText[$request->input('withdraw_method')]}\r\n提现账号:{$request->input('withdraw_account')}\r\n";
$message = sprintf("%s\r\n%s",
__('Withdrawal method') . "" . $request->input('withdraw_method'),
__('Withdrawal account') . "" . $request->input('withdraw_account')
);
$ticketMessage = TicketMessage::create([
'user_id' => $request->session()->get('id'),
'user_id' => $request->user['id'],
'ticket_id' => $ticket->id,
'message' => $message
]);
if (!$ticketMessage) {
DB::rollback();
abort(500, '工单创建失败');
abort(500, __('Failed to open ticket'));
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
$this->sendNotify($ticket, $message);
return response([
'data' => true
]);
}
private function sendNotify(Ticket $ticket, TicketMessage $ticketMessage)
private function sendNotify(Ticket $ticket, string $message)
{
$telegramService = new TelegramService();
$telegramService->sendMessageWithAdmin("📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->subject}`\n内容:\n`{$ticketMessage->message}`", true);
$telegramService->sendMessageWithAdmin("📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->subject}`\n内容:\n`{$message}`", true);
}
}

View File

@@ -0,0 +1,239 @@
<?php
namespace App\Http\Controllers\V1\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\UserChangePassword;
use App\Http\Requests\User\UserTransfer;
use App\Http\Requests\User\UserUpdate;
use App\Models\Order;
use App\Models\Plan;
use App\Models\Ticket;
use App\Models\User;
use App\Services\AuthService;
use App\Services\UserService;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
public function getActiveSession(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
$authService = new AuthService($user);
return response([
'data' => $authService->getSessions()
]);
}
public function removeActiveSession(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
$authService = new AuthService($user);
return response([
'data' => $authService->removeSession($request->input('session_id'))
]);
}
public function checkLogin(Request $request)
{
$data = [
'is_login' => $request->user['id'] ? true : false
];
if ($request->user['is_admin']) {
$data['is_admin'] = true;
}
return response([
'data' => $data
]);
}
public function changePassword(UserChangePassword $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
if (!Helper::multiPasswordVerify(
$user->password_algo,
$user->password_salt,
$request->input('old_password'),
$user->password)
) {
abort(500, __('The old password is wrong'));
}
$user->password = password_hash($request->input('new_password'), PASSWORD_DEFAULT);
$user->password_algo = NULL;
$user->password_salt = NULL;
if (!$user->save()) {
abort(500, __('Save failed'));
}
return response([
'data' => true
]);
}
public function info(Request $request)
{
$user = User::where('id', $request->user['id'])
->select([
'email',
'transfer_enable',
'last_login_at',
'created_at',
'banned',
'remind_expire',
'remind_traffic',
'expired_at',
'balance',
'commission_balance',
'plan_id',
'discount',
'commission_rate',
'telegram_id',
'uuid'
])
->first();
if (!$user) {
abort(500, __('The user does not exist'));
}
$user['avatar_url'] = 'https://cdn.v2ex.com/gravatar/' . md5($user->email) . '?s=64&d=identicon';
return response([
'data' => $user
]);
}
public function getStat(Request $request)
{
$stat = [
Order::where('status', 0)
->where('user_id', $request->user['id'])
->count(),
Ticket::where('status', 0)
->where('user_id', $request->user['id'])
->count(),
User::where('invite_user_id', $request->user['id'])
->count()
];
return response([
'data' => $stat
]);
}
public function getSubscribe(Request $request)
{
$user = User::where('id', $request->user['id'])
->select([
'plan_id',
'token',
'expired_at',
'u',
'd',
'transfer_enable',
'email',
'uuid'
])
->first();
if (!$user) {
abort(500, __('The user does not exist'));
}
if ($user->plan_id) {
$user['plan'] = Plan::find($user->plan_id);
if (!$user['plan']) {
abort(500, __('Subscription plan does not exist'));
}
}
$user['subscribe_url'] = Helper::getSubscribeUrl($user['token']);
$userService = new UserService();
$user['reset_day'] = $userService->getResetDay($user);
return response([
'data' => $user
]);
}
public function resetSecurity(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
if (!$user->save()) {
abort(500, __('Reset failed'));
}
return response([
'data' => Helper::getSubscribeUrl($user['token'])
]);
}
public function update(UserUpdate $request)
{
$updateData = $request->only([
'remind_expire',
'remind_traffic'
]);
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
try {
$user->update($updateData);
} catch (\Exception $e) {
abort(500, __('Save failed'));
}
return response([
'data' => true
]);
}
public function transfer(UserTransfer $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
if ($request->input('transfer_amount') > $user->commission_balance) {
abort(500, __('Insufficient commission balance'));
}
$user->commission_balance = $user->commission_balance - $request->input('transfer_amount');
$user->balance = $user->balance + $request->input('transfer_amount');
if (!$user->save()) {
abort(500, __('Transfer failed'));
}
return response([
'data' => true
]);
}
public function getQuickLoginUrl(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
$code = Helper::guid();
$key = CacheKey::get('TEMP_TOKEN', $code);
Cache::put($key, $user->id, 60);
$redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard');
if (config('v2board.app_url')) {
$url = config('v2board.app_url') . $redirect;
} else {
$url = url($redirect);
}
return response([
'data' => $url
]);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Models\CommissionLog;
use App\Models\Order;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Models\ServerVmess;
use App\Models\Stat;
use App\Models\StatServer;
use App\Models\StatUser;
use App\Models\Ticket;
use App\Models\User;
use App\Services\StatisticalService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
public function override(Request $request)
{
$params = $request->validate([
'start_at' => '',
'end_at' => ''
]);
if (isset($params['start_at']) && isset($params['end_at'])) {
$stats = Stat::where('record_at', '>=', $params['start_at'])
->where('record_at', '<', $params['end_at'])
->get()
->makeHidden(['record_at', 'created_at', 'updated_at', 'id', 'record_type'])
->toArray();
} else {
$statisticalService = new StatisticalService();
return [
'data' => $statisticalService->generateStatData()
];
}
$stats = array_reduce($stats, function($carry, $item) {
foreach($item as $key => $value) {
if(isset($carry[$key]) && $carry[$key]) {
$carry[$key] += $value;
} else {
$carry[$key] = $value;
}
}
return $carry;
}, []);
return [
'data' => $stats
];
}
public function record(Request $request)
{
$request->validate([
'type' => 'required|in:paid_total,commission_total,register_count',
'start_at' => '',
'end_at' => ''
]);
$statisticalService = new StatisticalService();
$statisticalService->setStartAt($request->input('start_at'));
$statisticalService->setEndAt($request->input('end_at'));
return [
'data' => $statisticalService->getStatRecord($request->input('type'))
];
}
public function ranking(Request $request)
{
$request->validate([
'type' => 'required|in:server_traffic_rank,user_consumption_rank,invite_rank',
'start_at' => '',
'end_at' => '',
'limit' => 'nullable|integer'
]);
$statisticalService = new StatisticalService();
$statisticalService->setStartAt($request->input('start_at'));
$statisticalService->setEndAt($request->input('end_at'));
return [
'data' => $statisticalService->getRanking($request->input('type'), $request->input('limit') ?? 20)
];
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http;
use Fruitcake\Cors\HandleCors;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
@@ -14,6 +15,7 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
\App\Http\Middleware\CORS::class,
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
@@ -28,22 +30,21 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \App\Http\Middleware\EncryptCookies::class,
// \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// \Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
// \Illuminate\View\Middleware\ShareErrorsFromSession::class,
// \App\Http\Middleware\VerifyCsrfToken::class,
// \Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \App\Http\Middleware\EncryptCookies::class,
// \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// \Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\ForceJson::class,
\App\Http\Middleware\CORS::class,
'throttle:120,1',
\App\Http\Middleware\Language::class,
'bindings',
],
];
@@ -69,6 +70,7 @@ class Kernel extends HttpKernel
'admin' => \App\Http\Middleware\Admin::class,
'client' => \App\Http\Middleware\Client::class,
'staff' => \App\Http\Middleware\Staff::class,
'log' => \App\Http\Middleware\RequestLog::class
];
/**

View File

@@ -2,7 +2,9 @@
namespace App\Http\Middleware;
use App\Services\AuthService;
use Closure;
use Illuminate\Support\Facades\Cache;
class Admin
{
@@ -15,9 +17,14 @@ class Admin
*/
public function handle($request, Closure $next)
{
if (!$request->session()->get('is_admin')) {
abort(403, '权限不足');
}
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if (!$authorization) abort(403, '未登录或登陆已过期');
$user = AuthService::decryptAuthData($authorization);
if (!$user || !$user['is_admin']) abort(403, '未登录或登陆已过期');
$request->merge([
'user' => $user
]);
return $next($request);
}
}

View File

@@ -17,8 +17,8 @@ class CORS
}
$response = $next($request);
$response->header('Access-Control-Allow-Origin', trim($origin, '/'));
$response->header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
$response->header('Access-Control-Allow-Headers', 'Content-Type,X-Requested-With');
$response->header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS,HEAD');
$response->header('Access-Control-Allow-Headers', 'Origin,Content-Type,Accept,Authorization,X-Request-With');
$response->header('Access-Control-Allow-Credentials', 'true');
$response->header('Access-Control-Max-Age', 10080);

View File

@@ -2,8 +2,10 @@
namespace App\Http\Middleware;
use App\Utils\CacheKey;
use Closure;
use App\Models\User;
use Illuminate\Support\Facades\Cache;
class Client
{
@@ -24,7 +26,9 @@ class Client
if (!$user) {
abort(403, 'token is error');
}
$request->user = $user;
$request->merge([
'user' => $user
]);
return $next($request);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\App;
class Language
{
public function handle($request, Closure $next)
{
if ($request->header('content-language')) {
App::setLocale($request->header('content-language'));
}
return $next($request);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
class RequestLog
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if ($request->method() === 'POST') {
$path = $request->path();
info("POST {$path}");
};
return $next($request);
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Middleware;
use App\Services\AuthService;
use Closure;
class Staff
@@ -15,9 +16,14 @@ class Staff
*/
public function handle($request, Closure $next)
{
if (!$request->session()->get('is_staff')) {
abort(403, '权限不足');
}
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if (!$authorization) abort(403, '未登录或登陆已过期');
$user = AuthService::decryptAuthData($authorization);
if (!$user || !$user['is_staff']) abort(403, '未登录或登陆已过期');
$request->merge([
'user' => $user
]);
return $next($request);
}
}

View File

@@ -2,7 +2,9 @@
namespace App\Http\Middleware;
use App\Services\AuthService;
use Closure;
use Illuminate\Support\Facades\Cache;
class User
{
@@ -15,16 +17,14 @@ class User
*/
public function handle($request, Closure $next)
{
if ($request->input('access_token')) {
$user = \App\Models\User::where('token', $request->input('access_token'))->first();
if ($user) {
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
}
}
if (!$request->session()->get('id')) {
abort(403, '未登录或登陆已过期');
}
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if (!$authorization) abort(403, '未登录或登陆已过期');
$user = AuthService::decryptAuthData($authorization);
if (!$user) abort(403, '未登录或登陆已过期');
$request->merge([
'user' => $user
]);
return $next($request);
}
}

View File

@@ -6,6 +6,91 @@ use Illuminate\Foundation\Http\FormRequest;
class ConfigSave extends FormRequest
{
const RULES = [
// invite & commission
'invite_force' => 'in:0,1',
'invite_commission' => 'integer',
'invite_gen_limit' => 'integer',
'invite_never_expire' => 'in:0,1',
'commission_first_time_enable' => 'in:0,1',
'commission_auto_check_enable' => 'in:0,1',
'commission_withdraw_limit' => 'nullable|numeric',
'commission_withdraw_method' => 'nullable|array',
'withdraw_close_enable' => 'in:0,1',
'commission_distribution_enable' => 'in:0,1',
'commission_distribution_l1' => 'nullable|numeric',
'commission_distribution_l2' => 'nullable|numeric',
'commission_distribution_l3' => 'nullable|numeric',
// site
'logo' => 'nullable|url',
'force_https' => 'in:0,1',
'stop_register' => 'in:0,1',
'app_name' => '',
'app_description' => '',
'app_url' => 'nullable|url',
'subscribe_url' => 'nullable',
'subscribe_path' => 'nullable',
'try_out_enable' => 'in:0,1',
'try_out_plan_id' => 'integer',
'try_out_hour' => 'numeric',
'tos_url' => 'nullable|url',
'currency' => '',
'currency_symbol' => '',
// subscribe
'plan_change_enable' => 'in:0,1',
'reset_traffic_method' => 'in:0,1,2,3,4',
'new_order_event_id' => 'in:0,1',
'renew_order_event_id' => 'in:0,1',
'change_order_event_id' => 'in:0,1',
'show_info_to_server_enable' => 'in:0,1',
// server
'server_token' => 'nullable|min:16',
'server_pull_interval' => 'integer',
'server_push_interval' => 'integer',
// frontend
'frontend_theme' => '',
'frontend_theme_sidebar' => 'nullable|in:dark,light',
'frontend_theme_header' => 'nullable|in:dark,light',
'frontend_theme_color' => 'nullable|in:default,darkblue,black,green',
'frontend_background_url' => 'nullable|url',
// email
'email_template' => '',
'email_host' => '',
'email_port' => '',
'email_username' => '',
'email_password' => '',
'email_encryption' => '',
'email_from_address' => '',
// telegram
'telegram_bot_enable' => 'in:0,1',
'telegram_bot_token' => '',
'telegram_discuss_id' => '',
'telegram_channel_id' => '',
'telegram_discuss_link' => 'nullable|url',
// app
'windows_version' => '',
'windows_download_url' => '',
'macos_version' => '',
'macos_download_url' => '',
'android_version' => '',
'android_download_url' => '',
// safe
'email_whitelist_enable' => 'in:0,1',
'email_whitelist_suffix' => 'nullable|array',
'email_gmail_limit_enable' => 'in:0,1',
'recaptcha_enable' => 'in:0,1',
'recaptcha_key' => '',
'recaptcha_site_key' => '',
'email_verify' => 'in:0,1',
'safe_mode_enable' => 'in:0,1',
'register_limit_by_ip_enable' => 'in:0,1',
'register_limit_count' => 'integer',
'register_limit_expire' => 'integer',
'secure_path' => 'min:8|regex:/^[\w-]*$/',
'password_limit_enable' => 'in:0,1',
'password_limit_count' => 'integer',
'password_limit_expire' => 'integer',
];
/**
* Get the validation rules that apply to the request.
*
@@ -13,95 +98,7 @@ class ConfigSave extends FormRequest
*/
public function rules()
{
return [
// invite & commission
'safe_mode_enable' => 'in:0,1',
'invite_force' => 'in:0,1',
'invite_commission' => 'integer',
'invite_gen_limit' => 'integer',
'invite_never_expire' => 'in:0,1',
'commission_first_time_enable' => 'in:0,1',
'commission_auto_check_enable' => 'in:0,1',
'commission_withdraw_limit' => 'nullable|numeric',
// site
'stop_register' => 'in:0,1',
'email_verify' => 'in:0,1',
'app_name' => '',
'app_description' => '',
'app_url' => 'nullable|url',
'subscribe_url' => 'nullable|url',
'try_out_enable' => 'in:0,1',
'try_out_plan_id' => 'integer',
'try_out_hour' => 'numeric',
'email_whitelist_enable' => 'in:0,1',
'email_whitelist_suffix' => '',
'email_gmail_limit_enable' => 'in:0,1',
'recaptcha_enable' => 'in:0,1',
'recaptcha_key' => '',
'recaptcha_site_key' => '',
// subscribe
'plan_change_enable' => 'in:0,1',
'reset_traffic_method' => 'in:0,1',
'renew_reset_traffic_enable' => 'in:0,1',
'surplus_enable' => 'in:0,1',
// server
'server_token' => 'nullable|min:16',
'server_license' => 'nullable',
'server_log_enable' => 'in:0,1',
'server_v2ray_domain' => '',
'server_v2ray_protocol' => '',
// alipay
'alipay_enable' => 'in:0,1',
'alipay_appid' => 'nullable|integer|min:16',
'alipay_pubkey' => 'max:2048',
'alipay_privkey' => 'max:2048',
// stripe
'stripe_alipay_enable' => 'in:0,1',
'stripe_wepay_enable' => 'in:0,1',
'stripe_card_enable' => 'in:0,1',
'stripe_sk_live' => '',
'stripe_pk_live' => '',
'stripe_webhook_key' => '',
'stripe_currency' => 'in:hkd,usd,sgd,eur,gbp,jpy,cad',
// bitpayx
'bitpayx_name' => '',
'bitpayx_enable' => 'in:0,1',
'bitpayx_appsecret' => '',
// mGate
'mgate_name' => '',
'mgate_enable' => 'in:0,1',
'mgate_url' => 'nullable|url',
'mgate_app_id' => '',
'mgate_app_secret' => '',
// Epay
'epay_name' => '',
'epay_enable' => 'in:0,1',
'epay_url' => 'nullable|url',
'epay_pid' => '',
'epay_key' => '',
// frontend
'frontend_theme_sidebar' => 'in:dark,light',
'frontend_theme_header' => 'in:dark,light',
'frontend_theme_color' => 'in:default,darkblue,black',
'frontend_background_url' => 'nullable|url',
'frontend_admin_path' => '',
// tutorial
'apple_id' => 'nullable|email',
'apple_id_password' => '',
// email
'email_template' => '',
'email_host' => '',
'email_port' => '',
'email_username' => '',
'email_password' => '',
'email_encryption' => '',
'email_from_address' => '',
// telegram
'telegram_bot_enable' => 'in:0,1',
'telegram_bot_token' => '',
'telegram_discuss_id' => '',
'telegram_channel_id' => ''
];
return self::RULES;
}
public function messages()
@@ -109,7 +106,13 @@ class ConfigSave extends FormRequest
// illiteracy prompt
return [
'app_url.url' => '站点URL格式不正确必须携带http(s)://',
'subscribe_url.url' => '订阅URL格式不正确必须携带http(s)://'
'subscribe_url.url' => '订阅URL格式不正确必须携带http(s)://',
'server_token.min' => '通讯密钥长度必须大于16位',
'tos_url.url' => '服务条款URL格式不正确必须携带http(s)://',
'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式必须携带http(s)://',
'logo.url' => 'LOGO URL格式不正确必须携带https(s)://',
'secure_path.min' => '后台路径长度最小为8位',
'secure_path.regex' => '后台路径只能为字母或数字'
];
}
}

View File

@@ -21,7 +21,9 @@ class CouponGenerate extends FormRequest
'started_at' => 'required|integer',
'ended_at' => 'required|integer',
'limit_use' => 'nullable|integer',
'limit_use_with_user' => 'nullable|integer',
'limit_plan_ids' => 'nullable|array',
'limit_period' => 'nullable|array',
'code' => ''
];
}
@@ -40,8 +42,10 @@ class CouponGenerate extends FormRequest
'started_at.integer' => '开始时间格式有误',
'ended_at.required' => '结束时间不能为空',
'ended_at.integer' => '结束时间格式有误',
'limit_use.integer' => '使用次数格式有误',
'limit_plan_ids.array' => '指定订阅格式有误'
'limit_use.integer' => '最大使用次数格式有误',
'limit_use_with_user.integer' => '限制用户使用次数格式有误',
'limit_plan_ids.array' => '指定订阅格式有误',
'limit_period.array' => '指定周期格式有误'
];
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class CouponSave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required',
'type' => 'required|in:1,2',
'value' => 'required|integer',
'started_at' => 'required|integer',
'ended_at' => 'required|integer',
'limit_use' => 'nullable|integer',
'limit_plan_ids' => 'nullable|array',
'code' => ''
];
}
public function messages()
{
return [
'name.required' => '名称不能为空',
'type.required' => '类型不能为空',
'type.in' => '类型格式有误',
'value.required' => '金额或比例不能为空',
'value.integer' => '金额或比例格式有误',
'started_at.required' => '开始时间不能为空',
'started_at.integer' => '开始时间格式有误',
'ended_at.required' => '结束时间不能为空',
'ended_at.integer' => '结束时间格式有误',
'limit_use.integer' => '使用次数格式有误',
'limit_plan_ids.array' => '指定订阅格式有误'
];
}
}

View File

@@ -16,7 +16,8 @@ class NoticeSave extends FormRequest
return [
'title' => 'required',
'content' => 'required',
'img_url' => 'nullable|url'
'img_url' => 'nullable|url',
'tags' => 'nullable|array'
];
}
@@ -25,7 +26,8 @@ class NoticeSave extends FormRequest
return [
'title.required' => '标题不能为空',
'content.required' => '内容不能为空',
'img_url.url' => '图片URL格式不正确'
'img_url.url' => '图片URL格式不正确',
'tags.array' => '标签格式不正确'
];
}
}

View File

@@ -17,7 +17,7 @@ class OrderAssign extends FormRequest
'plan_id' => 'required',
'email' => 'required',
'total_amount' => 'required',
'cycle' => 'required|in:month_price,quarter_price,half_year_price,year_price,onetime_price,reset_price'
'period' => 'required|in:month_price,quarter_price,half_year_price,year_price,two_year_price,three_year_price,onetime_price,reset_price'
];
}
@@ -27,8 +27,8 @@ class OrderAssign extends FormRequest
'plan_id.required' => '订阅不能为空',
'email.required' => '邮箱不能为空',
'total_amount.required' => '支付金额不能为空',
'cycle.required' => '订阅周期不能为空',
'cycle.in' => '订阅周期格式有误'
'period.required' => '订阅周期不能为空',
'period.in' => '订阅周期格式有误'
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class OrderFetch extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'filter.*.key' => 'required|in:email,trade_no,status,commission_status,user_id,invite_user_id,callback_no,commission_balance',
'filter.*.condition' => 'required|in:>,<,=,>=,<=,模糊,!=',
'filter.*.value' => ''
];
}
public function messages()
{
return [
'filter.*.key.required' => '过滤键不能为空',
'filter.*.key.in' => '过滤键参数有误',
'filter.*.condition.required' => '过滤条件不能为空',
'filter.*.condition.in' => '过滤条件参数有误',
];
}
}

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