590 Commits
1.3.2 ... 1.5.4

Author SHA1 Message Date
339ab3925f Merge pull request #492 from v2board/dev
1.5.4
2022-01-01 00:02:25 +08:00
927d575255 update: fix admin dark mode 2022-01-01 00:01:06 +08:00
796bcc4ab3 Merge pull request #491 from v2board/dev
update: fix admin dark mode
2021-12-31 23:56:12 +08:00
552a80f5f9 update: fix admin dark mode 2021-12-31 23:55:23 +08:00
d78a8a2a9f Merge pull request #490 from v2board/dev
1.5.4
2021-12-31 23:30:59 +08:00
3fad649377 update: cache version 2021-12-31 23:30:34 +08:00
389db6fb97 Merge pull request #489 from v2board/dev
1.5.4
2021-12-31 23:10:22 +08:00
080af12f39 Merge pull request #487 from Mr-Sheep/master
fix issue with surge subscribe
2021-12-31 23:09:57 +08:00
ecbdd12a6d Merge pull request #488 from betaxab/updaterules
rules: update clash & surge rules
2021-12-31 23:08:34 +08:00
3ade2bf89c update: code optimization 2021-12-31 01:49:27 +08:00
45a5022d82 update: logic optimization 2021-12-31 01:48:21 +08:00
e1a8d6ca35 update: logic optimization 2021-12-30 00:20:30 +08:00
5df2504f73 update: fix 2021-12-28 15:12:25 +08:00
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
edb2e7956c update: sql 2021-12-28 12:04:30 +08:00
a557aa6d32 update: add coupon limit period 2021-12-28 01:59:59 +08:00
6ed9cc559e update: more 2021-12-28 01:49:33 +08:00
6718a61890 update: support stash 2021-12-27 00:13:27 +08:00
333611da48 update: support stash 2021-12-27 00:05:28 +08:00
8ee7839d00 update: admin comission manage 2021-12-26 20:13:53 +08:00
e29261a87f update: get reset day 2021-12-26 19:35:24 +08:00
ba75d6e993 update: add send mass mail queue 2021-12-26 19:21:29 +08:00
ce8b5dde28 update: fix telegram markdown 2021-12-23 14:25:05 +08:00
55357a1b0e update: fix markdown parse 2021-12-17 13:35:54 +08:00
0f777f1a77 update: add telegram discuss link & ui 2021-12-17 01:53:03 +08:00
8fa73c2d4b update: remove favicon 2021-12-15 22:13:06 +08:00
80ae5f17b7 update: invite copy link 2021-12-15 21:35:01 +08:00
de4f01f653 update: invite copy link 2021-12-15 21:23:06 +08:00
bbfabdb72f update: add clash regex filter 2021-12-13 14:26:05 +08:00
b392fa3345 update: payment icon 2021-12-13 14:24:53 +08:00
ecfb9ce8b0 Fix Surge Subscribing issue 2021-12-11 21:24:27 +08:00
a69eb4058b update: ui & payment icons 2021-12-07 16:34:44 +08:00
8d56377c8a update: fix user commission show 2021-12-02 14:03:39 +08:00
30aec3d8e9 update: add test send mail 2021-11-30 16:23:46 +08:00
05769ea591 update: fix some bug 2021-11-24 13:33:42 +08:00
fb8bcdcbe0 update: fix user filter 2021-11-24 13:15:28 +08:00
39c47a06eb update: fix darkreader 2021-11-23 23:05:55 +08:00
4c71eb5633 update: fix dark mode qrcode 2021-11-23 01:42:00 +08:00
b8df411ffe update: fix darkrader 2021-11-22 03:20:52 +08:00
66728f13ea update: ui components language 2021-11-15 02:35:13 +08:00
7b1a8ee5ff update: add custom currency 2021-11-14 02:43:41 +08:00
9d96d68f12 update: reset traffic 2021-11-11 01:25:45 +08:00
d0947d1aaa update: fix coupon use 2021-11-01 02:31:27 +08:00
6f67096fe3 update: order callback no filter & save notice 2021-10-31 21:08:14 +08:00
44b369d0e7 update: fix subscribe loading 2021-10-28 14:59:48 +08:00
04fcd6758d update: fix v2ray config edit 2021-10-27 12:59:25 +08:00
99f3004d6b update: remove get subscribe id 2021-10-25 01:44:51 +08:00
56c726b173 update: fix payment update 2021-10-22 00:00:49 +08:00
37a6f3861c update: add payment save request validate 2021-10-18 17:10:40 +08:00
90211c1018 update: fix user filter 2021-10-16 20:47:27 +08:00
cc3e3b3fd1 update: fix background 2021-10-15 15:14:25 +08:00
10d3feb57c update: check order 2021-10-14 15:45:42 +08:00
eb98d706ae update: shell 2021-10-13 21:23:18 +08:00
4996c317c9 update: install 2021-10-13 21:04:23 +08:00
f380bd3c0e update: install 2021-10-13 21:04:02 +08:00
8e7f0bbc44 update: shell 2021-10-13 21:00:21 +08:00
53011cb95c Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-10-11 21:46:45 +08:00
037e22aa2d update: getInfo show uuid 2021-10-11 21:39:55 +08:00
ffd5eb269d Merge pull request #485 from betaxab/update-surfboard 2021-10-11 00:28:09 +09:00
68def9b4de update: fix filter 2021-10-10 01:07:58 +08:00
a4d617608f update: fix filter 2021-10-09 23:21:39 +08:00
364e259188 update: fix v2ray config drawer 2021-10-08 20:29:55 +08:00
b18c810f7f update: fix v2ray config drawer 2021-10-08 20:00:34 +08:00
239eb2075c Protocols: Surfboard: The SS can now be directly supported 2021-10-07 23:01:39 +08:00
b40272a8fa update: fix user generate 2021-10-06 00:20:28 +08:00
a6d5a433b0 update: composer 2021-10-05 21:03:27 +08:00
fe0f0afa53 update: dark mode 2021-10-04 23:10:11 +08:00
9ff524fd15 update: fix context menu 2021-10-04 21:45:17 +08:00
f6d9e6e7f6 update: fix v2ray network settings check 2021-10-04 14:37:32 +08:00
d911069e18 update: fix table context menu 2021-10-04 12:07:55 +08:00
32c539d2c0 Merge pull request #484 from v2board/dev
1.5.3
2021-10-03 22:19:10 +09:00
52fa1ce6af update: language 2021-10-01 20:18:40 +09:00
82d2d91582 update: user 2021-10-01 19:30:49 +09:00
8085c2ba6a update: schedule 2021-10-01 17:37:49 +09:00
b60fde5762 update: fix charts 2021-09-27 14:58:58 +09:00
3b51f12ab1 update: rollback app rule 2021-09-26 22:09:19 +09:00
ab4e66a5b6 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-09-26 22:07:34 +09:00
1856e0e87e update: fix support safari 15 theme color 2021-09-26 22:07:24 +09:00
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
cfd528739a update: fix en-us 2021-09-26 20:44:47 +09:00
dd23609658 update: change charts component 2021-09-25 00:33:11 +09:00
c7f3cf9e67 update: support safari theme 2021-09-24 01:50:58 +09:00
ddf2f45d6c update: user filter 2021-09-22 21:13:20 +09:00
ba3de90733 update: fix commission 2021-09-22 18:04:16 +09:00
43f4ecce93 update: fix 2021-09-21 22:30:00 +09:00
30b3587771 update: fix 2021-09-21 19:11:14 +09:00
6ab9a4d54d update: email notice default off 2021-09-21 19:07:53 +09:00
7a4bd468a2 update: add plan reset method 2021-09-21 18:51:53 +09:00
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
25b3b11efd update: add commission distribution 2021-09-18 21:04:35 +09:00
edfc4043e8 update: add commission distribution 2021-09-18 20:53:34 +09:00
ab9abf5b93 update: crisp more data 2021-09-14 20:31:56 +09:00
6dd199631b update: crisp more data 2021-09-14 20:07:57 +09:00
4b863d681e update: crisp more data 2021-09-14 19:47:57 +09:00
5d6010045d update: support md5 with sha256 2021-09-14 13:12:44 +09:00
0374a03892 update: support md5 with sha256 2021-09-14 13:10:29 +09:00
ec00fc4496 update: support md5 with sha256 2021-09-14 02:38:24 +09:00
a365357770 update: fix stat 2021-09-07 12:49:35 +09:00
14579d1eea update: update alert 2021-09-07 03:49:04 +09:00
9f6d1ada93 update: v2board install 2021-09-07 03:18:45 +09:00
895976b830 update: horizon config 2021-09-07 01:47:29 +09:00
4b011225db Merge pull request #482 from betaxab/subs-domain-force-direct 2021-09-07 01:33:37 +09:00
243aed3f55 Merge pull request #480 from betaxab/horizon-metrics 2021-09-07 01:33:24 +09:00
d25d7ba69b Merge pull request #479 from betaxab/update 2021-09-07 01:33:16 +09:00
c0c84efb7c Protocols: force the current subscription domain to be a direct rule 2021-09-06 18:26:03 +08:00
91c7dfc0ed update: add no reset method 2021-09-06 02:31:45 +09:00
6830e6af38 update: push tag 2021-09-06 01:24:41 +09:00
cb75579772 update: fix push tag 2021-09-06 01:19:43 +09:00
e980c2d8f3 update: set horizon with mem 2021-09-05 18:05:25 +09:00
b6195494d3 Kernel: add horizon snapshot schedule 2021-09-05 10:32:33 +08:00
ccf3497241 update: register api 2021-09-04 21:41:57 +09:00
c80d93fa25 update: order queue 2021-09-04 16:31:25 +09:00
c2577e37c4 update: server log 2021-09-03 00:31:12 +09:00
c183462ef3 update: server log 2021-09-02 21:09:04 +09:00
2a5e9ef079 update: rollback 2021-09-02 20:28:24 +09:00
decbae1413 update: traffic fetch job 2021-09-02 19:58:04 +09:00
0c14652ff7 update: server log fetch 2021-09-02 18:42:36 +09:00
91418caf04 update: log record 2021-09-01 03:32:15 +09:00
607aa82f88 update: horizon config 2021-09-01 03:08:31 +09:00
adc2d02c49 update: horizon config 2021-09-01 03:08:07 +09:00
fabb49baea update: horizon auth 2021-09-01 02:11:19 +09:00
35e11a6816 update: add horizon 2021-09-01 01:56:16 +09:00
1d87a1b99a update: traffic fetch queue 2021-09-01 00:55:07 +09:00
fbced4d09b update: fix order assign modal 2021-08-31 20:59:56 +09:00
52914e354e update: order assign notice 2021-08-31 20:54:08 +09:00
d392d29b50 update: reset text 2021-08-30 19:11:16 +09:00
347a3bb4b0 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-08-30 01:18:17 +09:00
7ac1f69a71 update: fix css 2021-08-30 01:18:06 +09:00
6317c4a4f3 Protocols: Clash: update 2021-08-29 21:44:51 +08:00
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
85686df2e6 Protocols: Clash: fix Clash app_name not found 2021-08-29 21:32:46 +08:00
adb5d041e6 fix: remind expire 2021-08-29 12:56:55 +09:00
7ba0e9a4e0 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-08-29 12:55:08 +09:00
99a64f41e1 Merge pull request #477 from betaxab/abandon-clash-provider
Protocols: Clash: Rollback the previous config
2021-08-29 12:54:58 +09:00
62053bc30f Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-08-29 12:54:32 +09:00
e5e7a06514 update: fix coupon 2021-08-29 12:54:15 +09:00
905e2dacd1 Protocols: Clash: Rollback the previous configuration
because the provider cannot test the delay manually
2021-08-29 11:20:55 +08:00
8189a442bc Merge pull request #475 from WayChan/update_clash_subscribe
update Clash Subscribe
2021-08-29 02:52:51 +09:00
b38f7979d6 update: coupon 2021-08-28 16:34:22 +09:00
5a3b897c57 update: add coupon per user limit 2021-08-28 16:32:55 +09:00
2d5fb03937 update: remind mail i18n 2021-08-28 15:39:10 +09:00
982c47d0b4 update: remind mail i18n 2021-08-28 15:32:38 +09:00
4cebeed2d7 update: server log 2021-08-28 15:22:51 +09:00
c95d374cc0 update: order manual 2021-08-28 15:11:59 +09:00
23462e2753 update: add more tips 2021-08-27 00:54:35 +09:00
b26a3e0e70 update: fix order update 2021-08-25 18:06:08 +09:00
3600c9a166 update: rollback traffic fetch 2021-08-24 16:53:53 +09:00
8a58a1ad88 update 2021-08-24 16:50:45 +09:00
968d55e2e3 update: readme 2021-08-22 15:46:49 +09:00
98ac5cb680 Fix Clash Json decode error 2021-08-22 00:09:37 +08:00
0288d2df4b update: order success handle 2021-08-21 20:52:00 +09:00
cb8e34fa2e change profile-update-interval time 2021-08-21 17:59:58 +08:00
b0c818c661 update 2021-08-21 16:20:16 +09:00
303d4a1c66 update: short payment generate key 2021-08-20 23:42:07 +09:00
e6416712f0 Merge pull request #471 from betaxab/add-trojan-surfboard
Protocols: Surfboard: add trojan protocol support
2021-08-20 22:32:08 +09:00
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
9b0a487c69 update: add knowledge jump func 2021-08-20 22:30:18 +09:00
27a6cc98d1 update Clash Subscribe 2021-08-20 00:54:00 +08:00
e05f6116b6 Protocols: AnXray: change AnXray flag to axxray 2021-08-19 15:47:18 +08:00
a9db11877f Protocols: Surfboard: add trojan protocol support 2021-08-19 08:10:36 +08:00
abe1ebccae update: new generate order number method 2021-08-19 00:58:51 +09:00
c957a4ca83 update: fix ticket message length support utf8mb4 2021-08-18 21:23:45 +09:00
c9cd307cc4 update: auto open knowledge by id 2021-08-18 18:54:40 +09:00
31cdcef3aa update: remove apple id config 2021-08-12 17:28:15 +09:00
9f75d4cbde update: default theme add green color 2021-08-12 17:17:36 +09:00
de5f80b5a3 update: script 2021-08-10 13:37:37 +09:00
474df5e18f update: payment config format 2021-08-08 16:56:50 +09:00
dfa75f49bd update: fix tg 2021-08-07 19:00:33 +09:00
f2c7d092ac update: fix model 2021-08-06 23:41:41 +09:00
2c389ebe8c update: v2board update 2021-08-06 23:20:10 +09:00
25e19446e6 update: user tags 2021-08-06 17:33:16 +09:00
a8761e9d4d update: opt editor 2021-08-06 14:02:47 +09:00
5f4e9c0301 update: cache version 2021-08-06 13:51:55 +09:00
b6e9260464 update: fix editor 2021-08-06 13:51:30 +09:00
6aa96fe856 update: fix editor 2021-08-06 13:01:17 +09:00
ab02935fd7 update: fix v2board udpate 2021-08-06 02:40:31 +09:00
b275ad469b update: cache version 2021-08-06 01:53:51 +09:00
e6a7c2c11c update: opt code 2021-08-06 01:43:01 +09:00
0f0f726269 update: fix code support php8 2021-08-05 15:22:15 +09:00
00cd3e26be update: fix admin editor 2021-08-05 14:48:24 +09:00
d95974019a update: fix 2021-08-02 03:00:01 +09:00
7a80950ab5 update: laravel 7 2021-08-01 23:56:11 +09:00
73a6d3236a update: fix reset traffic maybe failure issue 2021-08-01 16:48:00 +09:00
59dd34674e update: rollback ui 2021-07-31 16:24:12 +09:00
5dda531c2b update: ui 2021-07-31 15:15:05 +09:00
7234ccf4c1 update: ui 2021-07-31 14:17:48 +09:00
448b5382b9 Merge pull request #468 from betaxab/fix-grpc-protocol 2021-07-31 13:17:06 +09:00
bd2b056fbf protocols: fix gRPC protocol serviceName field
fix anXray serverName filed
2021-07-31 12:14:40 +08:00
ad8e2b8e80 update: api 2021-07-31 03:36:49 +09:00
8a20a70513 update: fix ui 2021-07-31 02:34:58 +09:00
06adaa2124 update: fix order statistics 2021-07-31 02:21:49 +09:00
ebf98d42a8 update: new feature 2021-07-31 02:05:39 +09:00
1adb1bcfa0 update: fix default app_url with subscribe_url 2021-07-30 23:53:37 +09:00
bb8da6e2c5 update: fix payment verify app_url 2021-07-30 16:00:20 +09:00
c426aabbcf update: theme 2021-07-29 21:29:53 +09:00
18bb1bd962 update: theme 2021-07-29 21:24:37 +09:00
07e9377417 update: theme 2021-07-29 21:22:07 +09:00
c0d3150461 update: add payment app_url alert 2021-07-29 21:12:11 +09:00
1a9b8b09bb Merge pull request #467 from v2board/dev
1.5.2
2021-07-29 20:59:52 +09:00
cb621a93ae update: version 2021-07-29 20:56:28 +09:00
fb449b490e Merge pull request #466 from v2board/dev
1.5.2
2021-07-29 20:15:19 +09:00
e35ec76f81 update: add subscribe anxray 2021-07-29 20:13:01 +09:00
5a60380765 update: add theme config 2021-07-29 13:38:13 +09:00
f409d89c4a update: add config 2021-07-29 02:41:31 +09:00
de045c79f5 update: custom theme 2021-07-29 02:36:51 +09:00
6a336d4253 update: web route 2021-07-29 02:29:15 +09:00
0ce3948c12 update: theme directory 2021-07-29 02:21:09 +09:00
7338c4a294 update: user 2021-07-27 19:16:49 +09:00
9417623fcf fix: email suffix select 2021-07-27 18:32:29 +09:00
4f29264de9 update: fix email suffix 2021-07-26 13:50:04 +09:00
34d8f0d5f0 update: statistics check time 2021-07-24 01:31:23 +09:00
b585038916 update: user 2021-07-23 20:43:09 +09:00
5abb642277 update: user 2021-07-22 22:36:48 +09:00
01cf486137 update: comm config 2021-07-22 22:32:42 +09:00
cb811bda5a update: user 2021-07-22 21:12:09 +09:00
2306e38d0c update: remove aliyun repo 2021-07-22 01:22:14 +09:00
2798c1df06 update: user language 2021-07-20 21:16:46 +09:00
a727745b43 update: order ui 2021-07-20 18:22:26 +09:00
af901291a5 update: quick login url 2021-07-19 21:17:34 +09:00
42d755c7d9 update: get comm config api 2021-07-19 18:44:40 +09:00
56a4ce5fe4 fix: surplus 2021-07-18 22:27:23 +09:00
67ab0d1f22 update: user 2021-07-16 18:06:11 +09:00
230470c037 fix: knowledge sort 2021-07-16 17:46:36 +09:00
77aec7d553 update: commission type and opt knowledge sort timestamp 2021-07-14 20:08:30 +09:00
88948eb8ee fix: change plan refund surplus amount 2021-07-11 01:13:09 +09:00
4d38ce3968 Merge pull request #463 from betaxab/fix-vmss-tls 2021-07-09 20:24:25 +09:00
a88121626b fix: knowledge subscribe url 2021-07-09 00:01:13 +09:00
90409b1107 update: add default subscribe, 1.5.3 remove this 2021-07-07 19:54:31 +09:00
04615688f7 update: subscribe url random 2021-07-06 18:04:59 +09:00
f48de9f07d Protocols: fix vmess tls sni field 2021-07-05 21:01:28 +08:00
4722379e79 update: add new client protocol 2021-07-04 13:21:10 +09:00
9aed2554ee Merge pull request #462 from betaxab/routerprotocol
Protocols: add Passwall & SSRPlus subscription support
2021-07-04 13:08:07 +09:00
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
567acdd03b Merge pull request #461 from betaxab/v2rayprotocol
Protocols: add V2ray Client Protocol
2021-07-03 23:56:00 +09:00
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
1c419283c0 Merge pull request #460 from betaxab/p3
Protocols: fix QuantumultX subscription
2021-07-03 14:26:42 +09:00
d25f6bff65 Protocols: fix QuantumultX subscription 2021-07-03 12:53:22 +08:00
0b97dd0995 fix: app subscribe 2021-07-03 03:31:28 +09:00
6f90c6b878 update: code 2021-07-02 23:35:10 +09:00
5b8591fde9 update: code 2021-07-02 23:34:42 +09:00
dfeec044ea fix: statsistics 2021-07-02 22:43:22 +09:00
70b47ec4b0 update: remove origin subscribe method 2021-07-02 22:24:39 +09:00
cd85fba9c7 restore: origin subscribe method 2021-07-02 22:14:07 +09:00
e01e951f7f update: code 2021-07-02 22:07:54 +09:00
0284e47155 fix: shadowrocket grpc 2021-07-02 22:05:12 +09:00
a4e1ba4016 fix: shadowrocket grpc 2021-07-02 21:57:00 +09:00
f95deb3f16 fix: shadowrocket grpc 2021-07-02 21:52:13 +09:00
dfef6d2d94 update: remove origin subscribe method 2021-07-02 21:25:26 +09:00
efc8419eb5 update: grpc 2021-07-02 20:46:16 +09:00
aecfa85efd update 2021-07-01 23:25:59 +09:00
9db5de09f2 update: order process event 2021-07-01 22:04:22 +09:00
b174403a2a remove: soft delete 2021-07-01 20:06:09 +09:00
0f488540f4 update: middleware 2021-07-01 18:16:27 +09:00
d00ad94c46 update: sql 2021-07-01 18:14:49 +09:00
5fa7c534ff update: middleware 2021-07-01 18:11:23 +09:00
386c1339f5 fix: plan update 2021-07-01 18:02:13 +09:00
6509091e4f update: check order php env memory limit 2021-07-01 12:41:34 +09:00
078dfbf339 fix: safe delete sql 2021-07-01 03:01:26 +09:00
de6ff1dca9 update: add user softdelete api 2021-07-01 00:35:22 +09:00
044d1e9b7f update: message box 2021-06-30 00:48:28 +09:00
9c711b4ea6 update: middleware 2021-06-25 22:35:14 +09:00
760d248e4e update: ui 2021-06-23 19:36:40 +09:00
1a2e8e2966 fix: ui event 2021-06-23 19:24:30 +09:00
b00b58d73d update: ui 2021-06-23 19:15:19 +09:00
43027660be update: ui 2021-06-23 19:11:56 +09:00
fe69a5976b update: ui 2021-06-23 19:06:48 +09:00
ecebba1d51 fix: language change 2021-06-23 18:02:35 +09:00
efc43dc45e update: ui 2021-06-23 16:32:25 +09:00
c5d88bfbfc update: ui 2021-06-23 16:25:55 +09:00
6928fd3fef update readme 2021-06-23 02:31:50 +09:00
e4f178e167 add: wechatpay native 2021-06-14 22:29:31 +09:00
343c93ad8e add: wechatpay native 2021-06-14 22:27:29 +09:00
f80af8efdc update: dev version 2021-06-12 16:49:34 +09:00
e5b998ee8d fix: user update 2021-06-12 16:46:58 +09:00
d510064732 update: support grpc for clash & shadowrocket 2021-06-12 15:42:44 +09:00
c5e2ec1d12 update: gitignore 2021-06-12 01:57:49 +09:00
ee2ca23487 update: language more 2021-06-12 01:56:39 +09:00
a5532490ba update: telegram ticket replay notify 2021-06-10 22:24:54 +09:00
2f175323a3 update: css 2021-06-08 20:30:36 +09:00
f896370e64 update: sort leave alert 2021-06-08 20:07:10 +09:00
0313c35dbe update: user edit 2021-06-04 00:56:28 +09:00
232cb18a25 update: frontend 2021-06-02 21:10:16 +09:00
04a05a6ba4 Merge pull request #445 from betaxab/p1
ClientController: clash: add subscription-userinfo header
2021-05-26 23:26:35 +09:00
e2b4df592d update: frontend 2021-05-26 23:15:18 +09:00
668663f978 update: user frontend 2021-05-26 22:11:03 +09:00
2e7544c71c ClientController: clash: add subscription-userinfo header 2021-05-26 17:07:38 +08:00
1fb3f62cff update: user frontend 2021-05-26 17:35:57 +09:00
02a1728bff update: payment service 2021-05-26 01:38:57 +09:00
4ea71d85be update: user frontend 2021-05-24 22:49:57 +09:00
afe8bb3171 update: check order 2021-05-24 21:10:54 +09:00
ef17be2046 fix: ticket filter 2021-05-22 14:18:27 +09:00
53dea06a6c update: user 2021-05-21 02:40:31 +09:00
229a3022ac Merge pull request #439 from v2board/dev
1.5.1
2021-05-18 21:00:54 +09:00
554dc4c12b update: version 2021-05-18 21:00:01 +09:00
6175e59e6f remove: old route 2021-05-17 02:30:39 +09:00
9df6e52438 update: composer2 2021-05-17 02:30:00 +09:00
63acb4c581 fix: remark input 2021-05-16 23:16:33 +09:00
f54c82dcf9 remove: old code 2021-05-16 03:11:24 +09:00
b92b38a635 fix: stripe notify 2021-05-15 02:59:46 +09:00
ddab443d75 fix: payment input 2021-05-14 22:53:07 +09:00
8f806e6de7 update: chart smooth 2021-05-13 18:18:06 +09:00
f1c62e2732 knowledge: add access area 2021-05-13 18:08:09 +09:00
3275b96a0a fix: user tos target 2021-05-09 20:26:00 +09:00
240555104a Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-05-09 00:46:31 +09:00
30ff71bd11 payment: support epay 2021-05-09 00:46:21 +09:00
b17bbad745 Merge pull request #417 from ColetteContreras/patch-2 2021-05-07 14:16:51 +09:00
a301b8461e update: user 2021-05-07 12:07:20 +09:00
3e354bf5af update: admin 2021-05-07 01:27:54 +09:00
b9796f462c update: payment 2021-05-07 01:18:38 +09:00
dc6317db97 update 2021-05-07 01:17:55 +09:00
b3b0988730 update: tos config 2021-05-07 01:06:21 +09:00
4070761cd0 update: add tos 2021-05-07 00:56:54 +09:00
2c408a2f56 update 2021-05-07 00:15:30 +09:00
2cfaeb2fb9 add: tos url 2021-05-07 00:12:58 +09:00
ef1c0b6091 add: payment drop 2021-05-07 00:10:05 +09:00
8e1a313709 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-05-07 00:08:00 +09:00
d5cf1bc735 Merge pull request #407 from betaxab/p1 2021-05-03 13:32:19 +09:00
e1c63bd556 Merge pull request #412 from betaxab/p3 2021-05-03 13:31:07 +09:00
324e218767 update: frontent 2021-05-02 22:38:22 +09:00
04b5d16457 add: alipay f2f 2021-05-01 00:27:02 +09:00
2431ffaba7 update: payment 2021-04-29 01:39:45 +09:00
6b31c39e6e update: payment 2021-04-29 01:33:54 +09:00
2769a6adf4 update: stripe wepay 2021-04-29 01:27:24 +09:00
aa78761115 fix: payment 2021-04-29 01:25:11 +09:00
3e60cbf968 add: stripe wepay 2021-04-29 01:22:40 +09:00
84583bd384 update: payment 2021-04-28 18:48:18 +09:00
0333d62e6f fix: payment 2021-04-28 18:29:28 +09:00
3bfa2180e6 update 2021-04-28 18:00:22 +09:00
22ee741200 rewrite: payment 2021-04-28 17:56:08 +09:00
58b27cdd50 fix: hidden order fetch params 2021-04-15 22:41:29 +09:00
743311f2f7 Update PoseidonController.php
Fix server status
2021-04-06 07:03:45 +08:00
9c04f75b85 update: exchange api 2021-04-02 01:45:11 +09:00
c7d7dfda28 fix: more 2021-03-28 22:58:35 +09:00
35362689c4 update: config 2021-03-26 02:20:53 +09:00
b740998760 feature: customer service system 2021-03-25 18:26:22 +09:00
488eafdd67 feature: order filter 2021-03-25 17:59:28 +09:00
ff30d3dcb2 feature: server push status 2021-03-24 20:57:06 +09:00
cbe5882e01 feature: server push status 2021-03-24 16:04:09 +09:00
4e2e3cd2a0 rm: remove server name check 2021-03-22 02:33:33 +09:00
326fb5d918 fix: server name repeat 2021-03-20 02:24:55 +09:00
a49faf3e1f V2boardStatistics: comply with psr-4 autoloading standard 2021-03-19 18:28:58 +08:00
e4b76a705f fix: stat 2021-03-13 01:03:53 +09:00
a03125ee6a fix: stat 2021-03-12 01:24:50 +09:00
58a63ae819 add: server name check 2021-03-10 19:52:51 +09:00
ab8ebd593f URLSchemes: let's support standard v2ray vmess URL Schemes 2021-03-08 19:12:25 +08:00
11b6c8448a fix: user filter 2021-03-07 02:44:39 +09:00
9ce9af4917 fix: statistics 2021-03-06 01:22:37 +09:00
550f0fee37 update: admin user update 2021-03-02 21:12:32 +09:00
ab9c6c85bb fix: server rank 2021-02-26 23:54:38 +09:00
0e6f6358d8 opt v2board statistics 2021-02-23 14:49:07 +09:00
b0ddf7d45f opt 2021-02-19 01:15:37 +09:00
f8a851d464 Merge pull request #401 from v2board/dev
1.5.0
2021-02-17 17:20:51 +09:00
da8bff5609 update: frontend 2021-02-17 17:15:41 +09:00
d3150cadac update: version 2021-02-17 17:11:45 +09:00
cdddbae19a fix: something 2021-02-17 17:11:17 +09:00
7c40a146a9 update: app config 2021-02-16 01:53:37 +09:00
f81426b2c4 Merge pull request #399 from v2board/dev
1.4.3
2021-02-13 17:34:45 +09:00
e581d08f95 feature: more language switch 2021-02-06 00:03:58 +09:00
2ba924e578 update: withdraw close 2021-02-03 19:27:21 +09:00
77f6b3f289 feature: close withdraw 2021-02-03 19:17:32 +09:00
2979003b6f fix: get reset day 2021-02-03 19:03:28 +09:00
2be497b04d Merge pull request #388 from betaxab/p1 2021-02-03 13:08:17 +09:00
f55fc86547 fix: order chart 2021-02-01 19:04:03 +09:00
bc9b3b6e76 fix: server charts 2021-01-26 22:22:47 +09:00
fcafb65ce9 fix: config save 2021-01-26 22:20:27 +09:00
34be1b9579 fix: stat charts 2021-01-26 19:33:11 +09:00
627ff98882 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2021-01-21 23:29:34 +09:00
007b10d925 update: user remarks 2021-01-21 23:29:15 +09:00
f855ecc758 remove: test line 2021-01-21 15:52:12 +09:00
af8b2b61d4 language: update English translation
- minor Chinese entry adjustments
2021-01-21 00:50:56 +08:00
f7a7c21c16 update: backend language and order result page 2021-01-20 19:51:55 +09:00
edee396c9b fix: coupon generate 2021-01-15 00:35:36 +09:00
54d1226c8b fix: coupon generate 2021-01-15 00:33:30 +09:00
1ce019cfa5 update: commission 2021-01-13 12:24:22 +09:00
fe736df68d update: crisp and tawk info 2021-01-13 00:47:41 +09:00
72bf45e4d5 update: user 2021-01-12 00:49:59 +09:00
c5639a9bef update: other 2021-01-10 03:25:31 +09:00
3f1c505922 update: user 2021-01-08 14:21:13 +09:00
ec0fbabd07 update 2021-01-08 02:00:18 +09:00
eca105b7d1 update: frontend 2021-01-08 01:58:44 +09:00
a55647ab7a update: config 2021-01-08 01:33:11 +09:00
633b9ad912 Merge pull request #377 from wloot/patch-6
[security] Fix user info leak in getSubscribe()
2021-01-07 17:16:07 +09:00
be146bc98b Merge pull request #373 from betaxab/p1
subs: following shadowsocks SIP008 new changes
2021-01-07 17:11:11 +09:00
79cbe6ce39 Merge pull request #364 from wloot/patch-2
QuantumultX: keep expected behavior for host
2021-01-07 17:10:25 +09:00
f8185b51b7 update: more feature 2021-01-02 22:35:41 +09:00
b4d74a016a fix: traffic show 2021-01-02 12:07:28 +09:00
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
4958c94bf2 update: user frontend 2020-12-28 17:23:56 +08:00
5766ec1951 update: statistics 2020-12-25 00:53:46 +08:00
faeaa02a84 compress 2020-12-24 23:49:52 +08:00
66c547d0ac update: more feature 2020-12-24 23:41:32 +08:00
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
b8b7033132 update: sql 2020-12-22 15:21:18 +08:00
9a465d9bb4 update: statistics 2020-12-21 17:11:48 +08:00
bcea2e7a54 update: statistics 2020-12-20 21:06:23 +08:00
7e5e696c70 update: statistics 2020-12-20 16:57:33 +08:00
1d70382aee update: db 2020-12-20 02:49:37 +08:00
8af862633f fix: frontend user edit 2020-12-19 21:33:11 +08:00
c0e60978ce fix: frontend user edit 2020-12-19 19:16:37 +08:00
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
a0d1b10614 fix: quantumultx tls-host key error 2020-12-16 23:15:20 +08:00
5fbefc7a98 Merge branch 'dev' 2020-12-14 11:03:57 +08:00
5a5491591b update: version 2020-12-14 11:03:32 +08:00
77e5eeea1b Merge branch 'dev' 2020-12-14 11:00:46 +08:00
da86b8cba9 update 2020-12-14 11:00:11 +08:00
91dfa5069c Merge pull request #361 from v2board/dev
1.4.2
2020-12-13 23:58:49 +08:00
2393ccbec7 update: server manage 2020-12-13 23:53:20 +08:00
962cfa9bea Merge pull request #359 from v2board/dev
1.4.2
2020-12-13 23:19:33 +08:00
8df4b6869a update: frontend 2020-12-13 23:15:47 +08:00
2159ab568a update: admin copy subscribe url 2020-12-13 23:09:32 +08:00
ef077cd095 update: frontend 2020-12-13 14:49:36 +08:00
17be643ce2 update: server manage 2020-12-13 14:16:07 +08:00
3b969c2c52 update: server manage 2020-12-13 12:17:48 +08:00
67d257c7b2 update: server manage 2020-12-13 12:16:05 +08:00
bc709a9c94 update: user filter 2020-12-11 22:32:10 +08:00
06f1ea0b81 update: user filter 2020-12-11 21:56:30 +08:00
0b425c1a01 update: admin 2020-12-11 20:45:54 +08:00
d072dc3e32 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-12-11 20:40:43 +08:00
db3e2abbbf update: user filter 2020-12-11 20:40:27 +08:00
c72e6cf4ea Merge pull request #356 from v2board/dev
1.4.1
2020-12-11 16:09:12 +08:00
b58df71db4 Merge pull request #355 from wloot/patch-1
Patch 1
2020-12-11 16:08:37 +08:00
91eb83388c Fix allowInsecure 2020-12-11 01:11:26 +08:00
16cf1f135a Fix allowInsecure 2020-12-11 01:11:04 +08:00
f3ef9e457e Fix allowInsecure 2020-12-11 01:09:49 +08:00
be1d22b423 Merge pull request #352 from betaxab/p1
rules: update default clash & surfboard & surge rules
2020-12-10 23:50:37 +08:00
ba2c92866c fix: mail knowledge link 2020-12-10 23:40:34 +08:00
91b016c231 update: log 2020-12-09 11:46:37 +08:00
d1b2e315ce update: log 2020-12-09 11:44:51 +08:00
8f0e3ce27d update: frontend 2020-12-08 18:59:07 +08:00
a47bac5ebc fix: ipad ua 2020-12-07 18:31:47 +08:00
d84af96535 update: app rules 2020-12-07 18:05:22 +08:00
29e7be855c rules: update default clash & surfboard & surge rules
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-12-05 21:25:11 +08:00
1ca9899437 Merge pull request #345 from wloot/buildvmess
clean up buildvmess()
2020-12-05 17:16:41 +08:00
254fdf6049 update: reset day 2020-12-04 21:17:25 +08:00
896d9eb030 update: frontend 2020-12-03 21:29:13 +08:00
501986bf53 update: frontend 2020-12-03 14:16:47 +08:00
c83c7478b7 update: frontend 2020-11-30 14:07:04 +08:00
e0e9187655 update: frontend 2020-11-30 12:11:06 +08:00
8a894a9a32 update: reset day 2020-11-30 12:04:00 +08:00
f9e55a3905 update: remove coupon log 2020-11-28 23:04:56 +08:00
f81ecbea5d Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-11-28 23:04:45 +08:00
487bece1bb update: ui 2020-11-28 23:04:11 +08:00
5f0e62f43c Merge pull request #344 from betaxab/p1
subscription: add shadowsocks SIP008 subscription support
2020-11-27 01:48:50 +08:00
a8b6ebdc60 Merge pull request #333 from betaxab/p2
rules: update default clash & surfboard & surge rules
2020-11-27 01:48:00 +08:00
9af98f72fd update: check commission 2020-11-26 15:07:47 +08:00
a54f64b698 update: check commission 2020-11-26 14:44:00 +08:00
3e97b26593 rules: update default clash & surfboard & surge rules
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-11-25 17:38:39 +08:00
cb2dd63e98 update: ui 2020-11-24 14:58:04 +08:00
512c06f15d update: ui 2020-11-24 00:57:27 +08:00
d645e9b98a update: app version 2020-11-22 16:25:56 +08:00
49c00c7a63 update: app version 2020-11-22 02:01:44 +08:00
7fe8a5b239 clean up buildvmess() 2020-11-20 00:53:53 +08:00
589eec9392 clean up buildvmess() 2020-11-20 00:30:49 +08:00
cada8ae9e8 update: app version 2020-11-19 21:30:27 +08:00
3ec90c4c52 update: app version 2020-11-19 21:27:53 +08:00
c1c6dccbfa update: app version 2020-11-19 21:24:14 +08:00
baf719fcff Merge remote-tracking branch 'origin/dev' into dev 2020-11-19 21:21:39 +08:00
7a9527d48a update: app version 2020-11-19 21:21:21 +08:00
7f12a79d07 subscription: add shadowsocks SIP008 subscription support
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-11-19 13:05:49 +08:00
a0dcf51c28 Merge pull request #343 from ColetteContreras/fixPoseidon
Fix Poseidon, add ETag
2020-11-19 12:24:19 +08:00
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
d2bce02d4e Add ETag for caching 2020-11-18 05:10:32 -05:00
2b5ef08fe0 Fix alterid 2020-11-18 04:56:27 -05:00
732edcad57 update: fix opcache return null 2020-11-18 11:12:38 +08:00
e030ed8d80 update: custom v2ray alter id 2020-11-17 23:02:12 +08:00
717043c96a update: custom v2ray alter id 2020-11-17 22:18:00 +08:00
04250fb7ac update: custom v2ray alter id 2020-11-17 22:12:51 +08:00
d1e831e961 update: custom v2ray alter id 2020-11-17 21:24:42 +08:00
fb9465b5a6 update: custom v2ray alter id 2020-11-17 21:23:16 +08:00
ad98651f93 update: admin ui 2020-11-17 17:30:28 +08:00
9035afaa78 update: commission process 2020-11-17 17:01:02 +08:00
92e3e4e01f update: fix status 2020-11-17 16:40:13 +08:00
be19a93efb update: fix commission first 2020-11-17 16:34:10 +08:00
3aabeff5de update: fix guest 2020-11-16 22:22:23 +08:00
5889d527ad update: server 2020-11-16 11:58:34 +08:00
2063e3c51d update: quantumult 2020-11-15 23:42:58 +08:00
592b751e2c update: server 2020-11-15 17:10:32 +08:00
b4212e7af4 update: server 2020-11-15 15:25:32 +08:00
c7aa63759e update: frontend 2020-11-15 15:20:51 +08:00
b3f075ae42 update: server 2020-11-15 15:14:47 +08:00
6fd7b48d6f update: server 2020-11-15 15:14:19 +08:00
f1fd300a35 update: version 2020-11-15 12:45:35 +08:00
204982e4c5 update: frontend 2020-11-15 12:44:23 +08:00
5df67aef16 update: app 2020-11-14 23:04:11 +08:00
ad8dee359f fix: v2ray advance config 2020-11-14 23:02:49 +08:00
6ccb3fa7be update: frontend 2020-11-14 21:33:53 +08:00
3bf11a136a update: server sort 2020-11-14 17:41:26 +08:00
2fc7c8c07c clear: files 2020-11-14 17:27:54 +08:00
5b79ea569c update: server sort 2020-11-14 17:26:17 +08:00
9fa53efc0c update: sql 2020-11-13 02:17:19 +08:00
fb257c999f update: comm 2020-11-11 17:15:07 +08:00
cefb2b3a27 update: app 2020-11-10 21:20:18 +08:00
7263b6d0ed update: user filter 2020-11-10 21:08:57 +08:00
e6d5aadbd2 update: server submit 2020-11-09 20:44:03 +08:00
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
1788250dc8 update: server submit 2020-11-09 00:15:20 +08:00
9917e21a61 update: server submit 2020-11-09 00:12:19 +08:00
59a8429456 update: user manage 2020-11-07 18:01:23 +08:00
9345bf7979 update: order 2020-11-07 17:23:47 +08:00
7042dce0a1 update: traffic submit 2020-11-07 16:38:57 +08:00
675353bbe1 fix: bug 2020-11-07 16:05:26 +08:00
93c90ddaf0 fix: bug 2020-11-07 16:04:42 +08:00
1d92d6b2f9 fix: more bug 2020-11-07 15:44:57 +08:00
34b8b666f4 Merge branch 'master' into dev 2020-11-04 02:12:03 +08:00
1f9a8c807f fix: install sql 2020-11-04 02:11:44 +08:00
6d9ab7dfe2 Merge branch 'dev' of https://github.com/v2board/v2board into dev 2020-11-02 22:22:48 +08:00
ebce59a0d1 fix: stat 2020-11-02 22:22:18 +08:00
c6be6b2fbc Merge pull request #327 from v2board/dev
1.4
2020-11-01 16:50:50 +08:00
e6f1bae7e9 Merge pull request #325 from betaxab/p1
ShadowsocksTidalabController: update name
2020-10-29 14:53:58 +08:00
8c777807a9 ShadowsocksTidalabController: update name
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-10-29 14:48:37 +08:00
2ee8a234a9 Merge pull request #316 from betaxab/p2
Utils: adjust QuantumultX & Surge & Surfboard
2020-10-29 14:42:09 +08:00
5cba4f2517 Merge pull request #324 from v2board/revert-323-patch-1
Revert "Fix Shadowsocks of Surfboard"
2020-10-29 14:41:57 +08:00
0d750af0cb Revert "Fix Shadowsocks of Surfboard" 2020-10-29 14:41:38 +08:00
0342ed69b6 Merge pull request #315 from betaxab/p1
dumpCSV: add BOM to prevent Chinese garbled in Excel
2020-10-29 14:40:12 +08:00
655b1dd49a Merge pull request #323 from Mazeorz/patch-1
Fix Shadowsocks of Surfboard
2020-10-29 14:39:31 +08:00
8935989b06 update: commission withdraw limit 2020-10-29 01:01:49 +08:00
9dc19baeb6 update: commission withdraw limit 2020-10-29 01:00:22 +08:00
c02db9d957 update: commission withdraw limit 2020-10-29 00:37:37 +08:00
5d6fc44281 Utils: adjust QuantumultX & Surge & Surfboard & Clash
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-10-27 09:37:02 +08:00
2bc77526fd Fix Shadowsocks of Surfboard
Modified the generated parameter format
2020-10-26 16:49:16 +08:00
06d22e7585 fix: reset password 2020-10-26 16:29:48 +08:00
645638feb9 fix: reset passwordk 2020-10-26 16:22:38 +08:00
ebec80a75c fix: expired buy reset issue 2020-10-26 15:50:37 +08:00
d654e01f95 fix: expired buy reset issue 2020-10-26 15:39:06 +08:00
be18727de1 update: staff 2020-10-25 14:18:48 +08:00
e82c5bc5ff update: config 2020-10-25 03:56:43 +08:00
69caf0d61c update: knowledge var 2020-10-25 00:41:27 +08:00
eab3cc48bd update: coupon 2020-10-25 00:04:36 +08:00
bf4e63ed9f update: add subscribe flag 2020-10-24 23:43:45 +08:00
4901cbfea5 update: coupon generate 2020-10-24 23:26:59 +08:00
1a94a48cf4 update: coupon generate 2020-10-24 23:08:14 +08:00
4ca1b9e8ff update: telegram notify 2020-10-20 23:11:59 +08:00
f2e7ada947 update: frontend 2020-10-20 11:28:45 +08:00
07cc8275d8 feature: multiple ban 2020-10-20 02:09:32 +08:00
16ae59c992 feature: new send mail 2020-10-19 00:37:27 +08:00
bbdda28197 update: knowledge 2020-10-18 20:07:26 +08:00
550a787c1a update: user filter 2020-10-18 19:21:21 +08:00
392c3677bc update: knowledge 2020-10-18 18:57:21 +08:00
28262568b2 update: knowledge 2020-10-18 16:38:11 +08:00
a2877b9a78 feature: knowledge base & surplus switch 2020-10-18 03:19:16 +08:00
5c1558beb9 feature: knowledge base & surplus switch 2020-10-18 02:53:56 +08:00
9041ee2a37 feature: knowledge base & surplus switch 2020-10-18 02:51:32 +08:00
be3e808551 update: dump csv 2020-10-14 19:42:08 +08:00
1116c7ef28 fix: token2login not working 2020-10-14 16:21:20 +08:00
eecb01c24e update: shell 2020-10-14 15:27:11 +08:00
7b4aec55b8 update: ja-jp 2020-10-14 00:52:47 +08:00
393680c963 feature: i18n 2020-10-13 23:55:17 +08:00
c984fa2e0b update: order service 2020-10-11 01:22:43 +08:00
8213dd3a73 update: admin frontend 2020-10-11 00:54:29 +08:00
56714617ea fix: reset traffic 2020-10-11 00:28:09 +08:00
00c5016d5a update: shadowsocks more cipher 2020-10-09 23:52:25 +08:00
ec4cd8acef update: google recaptcha 2020-10-09 23:34:34 +08:00
11adc63a3e update: google recaptcha 2020-10-09 23:30:12 +08:00
54ab44c3fd feature: google recaptcha 2020-10-09 23:27:36 +08:00
575bbe5174 dumpCSV: add BOM to prevent Chinese garbled in Excel
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-10-09 08:29:42 +08:00
3d26fda064 feature: dump csv 2020-10-09 00:50:10 +08:00
2ede0f3f17 feature: dump csv 2020-10-08 22:42:26 +08:00
a00eca2fda feature: dump csv 2020-10-08 22:35:07 +08:00
7a2967f41c feature: user filter & user generate 2020-10-08 12:13:47 +08:00
90545a333f Merge pull request #310 from betaxab/p1
Add Quantumult X & Surge Shadowsocks support
2020-10-08 12:12:40 +08:00
c6bf19ea75 Merge pull request #311 from betaxab/p2
subs: add Surfboard Shadowsocks support
2020-10-08 12:12:21 +08:00
c326d0ab5c subs: add Surfboard Shadowsocks support
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-10-04 23:57:28 +08:00
1acf64db44 subs: add Quantumult X & Surge Shadowsocks support
Signed-off-by: Beta Soft <betaxab@gmail.com>
2020-10-04 22:55:40 +08:00
87a9f6727b fix: shadowsocks server port 2020-10-04 16:26:28 +08:00
6fd577d2c8 update: support shadowsocks app subscribe 2020-10-04 16:07:19 +08:00
63adb9c4a8 fix: trojan urlencode issue 2020-10-04 15:35:42 +08:00
3be39043cb update: server 2020-10-04 14:52:33 +08:00
1f4778eaff update: server 2020-10-04 14:34:48 +08:00
ba2e0a6b66 feature: shadowsocks and more 2020-10-04 14:21:09 +08:00
ac48f90678 fix: remind traffic 2020-09-23 23:15:43 +08:00
76f0b4d4d0 fix: remind traffic 2020-09-23 22:40:20 +08:00
29fc4206c0 update: opcache fail abort 2020-09-23 14:37:24 +08:00
c8e6c79dd0 feature: staff permission 2020-09-20 16:41:48 +08:00
f0f636c722 feature: staff permission 2020-09-19 22:52:05 +08:00
d500769bd7 feature: fuzzy search 2020-09-18 14:18:56 +08:00
644aedb999 fix: order surplus issue 2020-09-16 20:44:50 +08:00
c8f1a23358 fix: order surplus issue 2020-09-16 20:31:30 +08:00
bb1a59291f fix: reset traffic 2020-09-14 17:43:47 +08:00
0cfa6a0676 fix: reset traffic 2020-09-14 17:41:26 +08:00
8aa3fb2f09 optimization: code 2020-09-11 01:22:55 +08:00
fe0332a9f9 optimization: code 2020-09-11 01:02:31 +08:00
e1cac79318 fix: cycle sort 2020-09-10 22:15:07 +08:00
85c4e477d2 feature: more cycle 2020-09-10 21:48:44 +08:00
8a44ccb3fc fix: onetime reset package 2020-09-10 13:08:12 +08:00
136c5cf9e9 fix: onetime change cycle invalid 2020-09-10 13:01:55 +08:00
2402a59b57 fix: tutorial apple id not null 2020-09-08 17:14:23 +08:00
c97451276a fix: var name 2020-09-08 13:58:58 +08:00
2a53e93444 readme: thanks jetbrains 2020-09-04 13:49:58 +08:00
20911fb669 fix: telegram multiple notification issues 2020-09-01 15:41:01 +08:00
31a222f3d8 fix: clash proxies merge 2020-09-01 10:08:42 +08:00
247 changed files with 9711 additions and 3266 deletions

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,7 +49,8 @@ class CheckCommission extends Command
{
if ((int)config('v2board.commission_auto_check_enable', 1)) {
Order::where('commission_status', 0)
->where('status', 3)
->where('invite_user_id', '!=', NULL)
->whereNotIn('status', [0, 2])
->where('updated_at', '<=', strtotime('-3 day', time()))
->update([
'commission_status' => 1
@ -57,20 +60,66 @@ 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)
{
if ((int)config('v2board.commission_distribution_enable', 0)) {
$level = 3;
$commissionShareLevels = [
0 => (int)config('v2board.commission_distribution_l1'),
1 => (int)config('v2board.commission_distribution_l2'),
2 => (int)config('v2board.commission_distribution_l3')
];
} else {
$level = 3;
$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 ((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;
}
return true;
}
}

View File

@ -2,13 +2,12 @@
namespace App\Console\Commands;
use App\Jobs\OrderHandleJob;
use App\Services\OrderService;
use Illuminate\Console\Command;
use App\Models\Order;
use App\Models\User;
use App\Models\Plan;
use App\Utils\Helper;
use App\Models\Coupon;
use Illuminate\Support\Facades\DB;
class CheckOrder extends Command
@ -44,115 +43,12 @@ class CheckOrder extends Command
*/
public function handle()
{
$orders = Order::get();
foreach ($orders as $item) {
switch ($item->status) {
// cancel
case 0:
if (strtotime($item->created_at) <= (time() - 1800)) {
$orderService = new OrderService($item);
$orderService->cancel();
}
break;
case 1:
$this->orderHandle($item);
break;
}
}
}
private function orderHandle(Order $order)
{
$user = User::find($order->user_id);
$plan = Plan::find($order->plan_id);
if ($order->refund_amount) {
$user->balance = $user->balance + $order->refund_amount;
}
DB::beginTransaction();
if ($order->surplus_order_ids) {
try {
Order::whereIn('id', json_decode($order->surplus_order_ids))->update([
'status' => 4
]);
} catch (\Exception $e) {
DB::rollback();
abort(500, '开通失败');
}
}
switch ((string)$order->cycle) {
case 'onetime_price':
$this->buyByOneTime($order, $user, $plan);
break;
case 'reset_price':
$this->buyReset($user);
break;
default:
$this->buyByCycle($order, $user, $plan);
}
if (!$user->save()) {
DB::rollBack();
abort(500, '开通失败');
}
$order->status = 3;
if (!$order->save()) {
DB::rollBack();
abort(500, '开通失败');
}
DB::commit();
}
private function buyReset(User $user)
{
$user->u = 0;
$user->d = 0;
}
private function buyByCycle(Order $order, User $user, Plan $plan)
{
// change plan process
if ((int)$order->type === 3) {
$user->expired_at = time();
}
$user->transfer_enable = $plan->transfer_enable * 1073741824;
// 续费重置&类型=续费
if ((int)config('v2board.renew_reset_traffic_enable', 1) && $order->type === 2) $this->buyReset($user);
// 购买前用户过期为NULL一次性
if ($user->expired_at === NULL) $this->buyReset($user);
// 新购
if ($order->type === 1) $this->buyReset($user);
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->expired_at = $this->getTime($order->cycle, $user->expired_at);
}
private function buyByOneTime(Order $order, User $user, Plan $plan)
{
$user->transfer_enable = $plan->transfer_enable * 1073741824;
$user->u = 0;
$user->d = 0;
$user->plan_id = $plan->id;
$user->group_id = $plan->group_id;
$user->expired_at = NULL;
}
private function getTime($str, $timestamp)
{
if ($timestamp < time()) {
$timestamp = time();
}
switch ($str) {
case 'month_price':
return strtotime('+1 month', $timestamp);
case 'quarter_price':
return strtotime('+3 month', $timestamp);
case 'half_year_price':
return strtotime('+6 month', $timestamp);
case 'year_price':
return strtotime('+12 month', $timestamp);
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

@ -2,12 +2,14 @@
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
{
protected $user;
protected $builder;
/**
* The name and signature of the console command.
*
@ -30,7 +32,7 @@ class ResetTraffic extends Command
public function __construct()
{
parent::__construct();
$this->user = User::where('expired_at', '!=', NULL)
$this->builder = User::where('expired_at', '!=', NULL)
->where('expired_at', '>', time());
}
@ -41,36 +43,58 @@ 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);
foreach (Plan::get() as $plan) {
switch ($plan->reset_traffic_method) {
case null: {
$resetTrafficMethod = config('v2board.reset_traffic_method', 0);
switch ((int)$resetTrafficMethod) {
// month first day
case 0:
$this->resetByMonthFirstDay($this->builder);
break;
// expire day
case 1:
$this->resetByExpireDay($this->builder);
break;
// no action
case 2:
break;
}
break;
}
case 0: {
$builder = with(clone($this->builder))->where('plan_id', $plan->id);
$this->resetByMonthFirstDay($builder);
break;
}
case 1: {
$builder = with(clone($this->builder))->where('plan_id', $plan->id);
$this->resetByExpireDay($builder);
break;
}
case 2: {
break;
}
}
}
}
private function resetByMonthFirstDay($user):void
private function resetByMonthFirstDay($builder):void
{
$user = $this->user;
if ((string)date('d') === '01') {
$user->update([
$builder->update([
'u' => 0,
'd' => 0
]);
}
}
private function resetByExpireDay():void
private function resetByExpireDay($builder):void
{
$user = $this->user;
$lastDay = date('d', strtotime('last day of +0 months'));
$users = [];
foreach ($user->get() as $item) {
foreach ($builder->get() as $item) {
$expireDay = date('d', $item->expired_at);
$today = date('d');
if ($expireDay === $today) {

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,51 +42,9 @@ class SendRemindMail extends Command
public function handle()
{
$users = User::all();
$mailService = new MailService();
foreach ($users as $user) {
if ($user->remind_expire) $this->remindExpire($user);
if ($user->remind_traffic) $this->remindTraffic($user);
if ($user->remind_expire) $mailService->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')
]
]);
}
}
private function remindTraffic($user)
{
if ($this->remindTrafficIsWarnValue(($user->u + $user->d), $user->transfer_enable)) {
$sendCount = MailLog::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('template_name', 'like', '%remindTraffic%')
->count();
if ($sendCount > 0) return;
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => '在' . config('v2board.app_name', 'V2board') . '的流量使用已达到80%',
'template_name' => 'remindTraffic',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url')
]
]);
}
}
private function remindTrafficIsWarnValue($ud, $transfer_enable)
{
if ($ud <= 0) return false;
if (($ud / $transfer_enable * 100) < 80) return false;
return true;
}
}

View File

@ -2,7 +2,13 @@
namespace App\Console\Commands;
use App\Models\Order;
use App\Models\User;
use App\Utils\CacheKey;
use App\Utils\Helper;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Matriphe\Larinfo;
class Test extends Command
{

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,12 @@ 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')) {
abort(500, 'V2board 已安装,如需重新安装请删除目录下.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')),
@ -100,7 +99,6 @@ class V2boardInstall extends Command
$this->info('一切就绪');
$this->info('访问 http(s)://你的站点/admin 进入管理面板');
\File::put(base_path() . '/.lock', time());
} catch (\Exception $e) {
$this->error($e->getMessage());
}

View File

@ -0,0 +1,100 @@
<?php
namespace App\Console\Commands;
use App\Jobs\StatServerJob;
use Illuminate\Console\Command;
use App\Models\Order;
use App\Models\StatOrder;
use App\Models\ServerLog;
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()
{
$this->statOrder();
$this->statServer();
}
private function statOrder()
{
$endAt = strtotime(date('Y-m-d'));
$startAt = strtotime('-1 day', $endAt);
$builder = Order::where('paid_at', '>=', $startAt)
->where('paid_at', '<', $endAt)
->whereNotIn('status', [0, 2]);
$orderCount = $builder->count();
$orderAmount = $builder->sum('total_amount');
$builder = $builder->where('commission_balance', '!=', 0);
$commissionCount = $builder->count();
$commissionAmount = $builder->sum('commission_balance');
$data = [
'order_count' => $orderCount,
'order_amount' => $orderAmount,
'commission_count' => $commissionCount,
'commission_amount' => $commissionAmount,
'record_type' => 'd',
'record_at' => $startAt
];
$statistic = StatOrder::where('record_at', $startAt)
->where('record_type', 'd')
->first();
if ($statistic) {
$statistic->update($data);
return;
}
StatOrder::create($data);
}
private function statServer()
{
$endAt = strtotime(date('Y-m-d'));
$startAt = strtotime('-1 day', $endAt);
$statistics = ServerLog::select([
'server_id',
'method as server_type',
DB::raw("sum(u) as u"),
DB::raw("sum(d) as d"),
])
->where('log_at', '>=', $startAt)
->where('log_at', '<', $endAt)
->groupBy('server_id', 'method')
->get()
->toArray();
foreach ($statistics as $statistic) {
$statistic['record_type'] = 'd';
$statistic['record_at'] = $startAt;
StatServerJob::dispatch($statistic);
}
}
}

View File

@ -51,11 +51,12 @@ class V2boardUpdate extends Command
}
$this->info('正在导入数据库请稍等...');
foreach ($sql as $item) {
if (!$item) continue;
try {
DB::select(DB::raw($item));
} catch (\Exception $e) {
}
}
$this->info('更新完毕');
$this->info('更新完毕,请重新启动队列服务。');
}
}

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

@ -25,15 +25,17 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule)
{
// v2board
$schedule->command('v2board:cache')->hourly();
$schedule->command('v2board:statistics')->dailyAt('0:10');
// check
$schedule->command('check:order')->everyMinute();
$schedule->command('check:commission')->everyMinute();
// reset
$schedule->command('reset:traffic')->daily();
$schedule->command('reset:serverLog')->quarterly();
$schedule->command('reset:serverLog')->quarterly()->at('0:15');
// send
$schedule->command('send:remindMail')->dailyAt('11:30');
// horizon metrics
$schedule->command('horizon:snapshot')->everyFiveMinutes();
}
/**

View File

@ -2,8 +2,8 @@
namespace App\Exceptions;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
@ -29,10 +29,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 +42,14 @@ 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, '请求频繁,请稍后再试');
}
return parent::render($request, $exception);
}
}

View File

@ -3,10 +3,12 @@
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\ConfigSave;
use App\Jobs\SendEmailJob;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Utils\Dict;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Mail;
class ConfigController extends Controller
{
@ -21,6 +23,35 @@ class ConfigController extends Controller
]);
}
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->session()->get('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)
{
$telegramService = new TelegramService($request->input('telegram_bot_token'));
@ -46,7 +77,14 @@ class ConfigController extends Controller
'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_auto_check_enable' => config('v2board.commission_auto_check_enable', 1),
'commission_withdraw_limit' => config('v2board.commission_withdraw_limit', 100),
'commission_withdraw_method' => config('v2board.commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
'withdraw_close_enable' => config('v2board.withdraw_close_enable', 0),
'commission_distribution_enable' => 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')
],
'site' => [
'safe_mode_enable' => (int)config('v2board.safe_mode_enable', 0),
@ -60,12 +98,21 @@ class ConfigController extends Controller
'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)
'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'),
'tos_url' => config('v2board.tos_url'),
'currency' => config('v2board.currency', 'CNY'),
'currency_symbol' => config('v2board.currency_symbol', '¥')
],
'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', 1)
'surplus_enable' => (int)config('v2board.surplus_enable', 1),
'new_order_event_id' => (int)config('v2board.new_order_event_id', 0),
'renew_order_event_id' => (int)config('v2board.renew_order_event_id', 0),
'change_order_event_id' => (int)config('v2board.change_order_event_id', 0),
],
'pay' => [
// alipay
@ -99,11 +146,14 @@ class ConfigController extends Controller
'epay_key' => config('v2board.epay_key'),
],
'frontend' => [
'frontend_theme' => config('v2board.frontend_theme', 'v2board'),
'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')
'frontend_admin_path' => config('v2board.frontend_admin_path', 'admin'),
'frontend_customer_service_method' => config('v2board.frontend_customer_service_method', 0),
'frontend_customer_service_id' => config('v2board.frontend_customer_service_id'),
],
'server' => [
'server_token' => config('v2board.server_token'),
@ -112,9 +162,6 @@ class ConfigController extends Controller
'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'),
@ -126,7 +173,16 @@ class ConfigController extends Controller
],
'telegram' => [
'telegram_bot_enable' => config('v2board.telegram_bot_enable', 0),
'telegram_bot_token' => config('v2board.telegram_bot_token')
'telegram_bot_token' => config('v2board.telegram_bot_token'),
'telegram_discuss_link' => config('v2board.telegram_discuss_link')
],
'app' => [
'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')
]
]
]);
@ -134,7 +190,7 @@ class ConfigController extends Controller
public function save(ConfigSave $request)
{
$data = $request->input();
$data = $request->validated();
$array = \Config::get('v2board');
foreach ($data as $k => $v) {
if (!in_array($k, array_keys($request->validated()))) {
@ -147,7 +203,9 @@ class ConfigController extends Controller
abort(500, '修改失败');
}
if (function_exists('opcache_reset')) {
opcache_reset();
if (opcache_reset() === false) {
abort(500, '缓存清除失败请卸载或检查opcache配置状态');
}
}
\Artisan::call('config:cache');
return response([

View File

@ -3,30 +3,41 @@
namespace App\Http\Controllers\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\Models\Coupon;
use App\Utils\Helper;
use Illuminate\Support\Facades\DB;
class CouponController extends Controller
{
public function fetch(Request $request)
{
$coupons = Coupon::all();
foreach ($coupons as $k => $v) {
if ($coupons[$k]['limit_plan_ids']) $coupons[$k]['limit_plan_ids'] = json_decode($coupons[$k]['limit_plan_ids']);
}
$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') : 'id';
$builder = Coupon::orderBy($sort, $sortType);
$total = $builder->count();
$coupons = $builder->forPage($current, $pageSize)
->get();
return response([
'data' => $coupons
'data' => $coupons,
'total' => $total
]);
}
public function save(CouponSave $request)
public function generate(CouponGenerate $request)
{
$params = $request->validated();
if (isset($params['limit_plan_ids'])) {
$params['limit_plan_ids'] = json_encode($params['limit_plan_ids']);
if ($request->input('generate_count')) {
$this->multiGenerate($request);
return;
}
$params = $request->validated();
if (!$request->input('id')) {
if (!isset($params['code'])) {
$params['code'] = Helper::randomChar(8);
@ -47,6 +58,38 @@ class CouponController extends Controller
]);
}
private function multiGenerate(CouponGenerate $request)
{
$coupons = [];
$coupon = $request->validated();
$coupon['created_at'] = $coupon['updated_at'] = time();
$coupon['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']);
$coupon['limit_period'] = json_encode($coupon['limit_period']);
unset($coupon['generate_count']);
for ($i = 0;$i < $request->input('generate_count');$i++) {
$coupon['code'] = Helper::randomChar(8);
array_push($coupons, $coupon);
}
DB::beginTransaction();
if (!Coupon::insert($coupons)) {
DB::rollBack();
abort(500, '生成失败');
}
DB::commit();
$data = "名称,类型,金额或比例,开始时间,结束时间,可用次数,可用于订阅,券码,生成时间\r\n";
foreach($coupons as $coupon) {
$type = ['', '金额', '比例'][$coupon['type']];
$value = ['', ($coupon['value'] / 100),$coupon['value']][$coupon['type']];
$startTime = date('Y-m-d H:i:s', $coupon['started_at']);
$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']);
$limitPlanIds = implode("/", json_decode($coupon['limit_plan_ids'], true)) ?? '不限制';
$data .= "{$coupon['name']},{$type},{$value},{$startTime},{$endTime},{$limitUse},{$limitPlanIds},{$coupon['code']},{$createTime}\r\n";
}
echo $data;
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {

View File

@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\Admin;
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
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$knowledge = Knowledge::find($request->input('id'))->toArray();
if (!$knowledge) abort(500, '知识不存在');
return response([
'data' => $knowledge
]);
}
return response([
'data' => Knowledge::select(['title', 'id', 'updated_at', 'category', 'show'])
->orderBy('sort', 'ASC')
->get()
]);
}
public function getCategory(Request $request)
{
return response([
'data' => array_keys(Knowledge::get()->groupBy('category')->toArray())
]);
}
public function save(KnowledgeSave $request)
{
$params = $request->validated();
if (!$request->input('id')) {
if (!Knowledge::create($params)) {
abort(500, '创建失败');
}
} else {
try {
Knowledge::find($request->input('id'))->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
return response([
'data' => true
]);
}
public function show(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$knowledge = Knowledge::find($request->input('id'));
if (!$knowledge) {
abort(500, '知识不存在');
}
$knowledge->show = $knowledge->show ? 0 : 1;
if (!$knowledge->save()) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function sort(KnowledgeSort $request)
{
DB::beginTransaction();
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([
'data' => true
]);
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$knowledge = Knowledge::find($request->input('id'));
if (!$knowledge) {
abort(500, '知识不存在');
}
if (!$knowledge->delete()) {
abort(500, '删除失败');
}
return response([
'data' => true
]);
}
}

View File

@ -1,47 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\MailSend;
use App\Services\UserService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Jobs\SendEmailJob;
class MailController extends Controller
{
public function send(MailSend $request)
{
$userService = new UserService();
$users = [];
switch ($request->input('type')) {
case 1: $users = $userService->getAllUsers();
break;
case 2: $users = $userService->getUsersByIds($request->input('receiver'));
break;
// available users
case 3: $users = $userService->getAvailableUsers();
break;
// un available users
case 4: $users = $userService->getUnAvailbaleUsers();
break;
}
foreach ($users as $user) {
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => $request->input('subject'),
'template_name' => 'notify',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url'),
'content' => $request->input('content')
]
]);
}
return response([
'data' => true
]);
}
}

View File

@ -4,7 +4,9 @@ namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\OrderAssign;
use App\Http\Requests\Admin\OrderUpdate;
use App\Http\Requests\Admin\OrderFetch;
use App\Services\OrderService;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
@ -15,25 +17,36 @@ 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 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 +64,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 +112,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 +123,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 +136,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,92 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\PaymentSave;
use App\Services\PaymentService;
use App\Utils\Helper;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Payment;
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::all();
foreach ($payments as $k => $v) {
$payments[$k]['notify_url'] = url("/api/v1/guest/payment/notify/{$v->payment}/{$v->uuid}");
}
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 save(Request $request)
{
if (!config('v2board.app_url')) {
abort(500, '请在站点配置中配置站点地址');
}
if ($request->input('id')) {
$payment = Payment::find($request->input('id'));
if (!$payment) abort(500, '支付方式不存在');
try {
$payment->update($request->input());
} catch (\Exception $e) {
abort(500, '更新失败');
}
return response([
'data' => true
]);
}
$request->validate([
'name' => 'required',
'payment' => 'required',
'config' => 'required'
], [
'name.required' => '显示名称不能为空',
'payment.required' => '网关参数不能为空',
'config.required' => '配置参数不能为空'
]);
if (!Payment::create([
'name' => $request->input('name'),
'icon' => $request->input('icon'),
'payment' => $request->input('payment'),
'config' => $request->input('config'),
'uuid' => Helper::randomChar(8)
])) {
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()
]);
}
}

View File

@ -52,8 +52,8 @@ class PlanController extends Controller
// 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
'group_id' => $params['group_id'],
'transfer_enable' => $params['transfer_enable'] * 1073741824
]);
$plan->update($params);
} catch (\Exception $e) {

View File

@ -3,9 +3,12 @@
namespace App\Http\Controllers\Admin\Server;
use App\Models\Plan;
use App\Models\Server;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Models\ServerV2ray;
use App\Models\ServerGroup;
use App\Models\User;
use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
@ -18,8 +21,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 +65,9 @@ class GroupController extends Controller
}
}
$servers = Server::all();
$servers = ServerV2ray::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,54 @@
<?php
namespace App\Http\Controllers\Admin\Server;
use App\Models\ServerV2ray;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
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');
DB::beginTransaction();
foreach ($request->input('sorts') as $k => $v) {
switch ($v['key']) {
case 'shadowsocks':
if (!ServerShadowsocks::find($v['value'])->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
break;
case 'v2ray':
if (!ServerV2ray::find($v['value'])->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
break;
case 'trojan':
if (!ServerTrojan::find($v['value'])->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
break;
}
}
DB::commit();
return response([
'data' => true
]);
}
}

View File

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

View File

@ -3,46 +3,17 @@
namespace App\Http\Controllers\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\Models\ServerTrojan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
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

@ -3,72 +3,20 @@
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;
use App\Models\ServerV2ray;
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'));
$server = ServerV2ray::find($request->input('id'));
if (!$server) {
abort(500, '服务器不存在');
}
@ -82,7 +30,7 @@ class V2rayController extends Controller
]);
}
if (!Server::create($params)) {
if (!ServerV2ray::create($params)) {
abort(500, '创建失败');
}
@ -94,7 +42,7 @@ class V2rayController extends Controller
public function drop(Request $request)
{
if ($request->input('id')) {
$server = Server::find($request->input('id'));
$server = ServerV2ray::find($request->input('id'));
if (!$server) {
abort(500, '节点ID不存在');
}
@ -110,7 +58,7 @@ class V2rayController extends Controller
'show',
]);
$server = Server::find($request->input('id'));
$server = ServerV2ray::find($request->input('id'));
if (!$server) {
abort(500, '该服务器不存在');
@ -128,12 +76,12 @@ class V2rayController extends Controller
public function copy(Request $request)
{
$server = Server::find($request->input('id'));
$server = ServerV2ray::find($request->input('id'));
$server->show = 0;
if (!$server) {
abort(500, '服务器不存在');
}
if (!Server::create($server->toArray())) {
if (!ServerV2ray::create($server->toArray())) {
abort(500, '复制失败');
}
@ -145,24 +93,9 @@ class V2rayController extends Controller
public function viewConfig(Request $request)
{
$serverService = new ServerService();
$config = $serverService->getVmessConfig($request->input('node_id'), 23333);
$config = $serverService->getV2RayConfig($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

@ -2,15 +2,21 @@
namespace App\Http\Controllers\Admin;
use App\Models\ServerShadowsocks;
use App\Models\ServerTrojan;
use App\Services\ServerService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\ServerGroup;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\Plan;
use App\Models\User;
use App\Models\Ticket;
use App\Models\Order;
use App\Models\StatOrder;
use App\Models\StatServer;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class StatController extends Controller
{
@ -20,7 +26,7 @@ class StatController extends Controller
'data' => [
'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
->whereIn('status', [3, 4])
->whereNotIn('status', [0, 2])
->sum('total_amount'),
'month_register_total' => User::where('created_at', '>=', strtotime(date('Y-m-1')))
->where('created_at', '<', time())
@ -29,18 +35,91 @@ class StatController extends Controller
->count(),
'commission_pendding_total' => Order::where('commission_status', 0)
->where('invite_user_id', '!=', NULL)
->where('status', 3)
->whereNotIn('status', [0, 2])
->where('commission_balance', '>', 0)
->count(),
'day_income' => Order::where('created_at', '>=', strtotime(date('Y-m-d')))
->where('created_at', '<', time())
->where('status', 3)
->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')))
->where('status', 3)
->whereNotIn('status', [0, 2])
->sum('total_amount')
]
]);
}
public function getOrder(Request $request)
{
$statistics = StatOrder::where('record_type', 'd')
->limit(31)
->orderBy('record_at', 'DESC')
->get()
->toArray();
$result = [];
foreach ($statistics as $statistic) {
$date = date('m-d', $statistic['record_at']);
array_push($result, [
'type' => '收款金额',
'date' => $date,
'value' => $statistic['order_amount'] / 100
]);
array_push($result, [
'type' => '收款笔数',
'date' => $date,
'value' => $statistic['order_count']
]);
array_push($result, [
'type' => '佣金金额',
'date' => $date,
'value' => $statistic['commission_amount'] / 100
]);
array_push($result, [
'type' => '佣金笔数',
'date' => $date,
'value' => $statistic['commission_count']
]);
}
$result = array_reverse($result);
return response([
'data' => $result
]);
}
public function getServerLastRank()
{
$servers = [
'shadowsocks' => ServerShadowsocks::where('parent_id', null)->get()->toArray(),
'vmess' => ServerV2ray::where('parent_id', null)->get()->toArray(),
'trojan' => ServerTrojan::where('parent_id', null)->get()->toArray()
];
$timestamp = strtotime('-1 day', strtotime(date('Y-m-d')));
$statistics = StatServer::select([
'server_id',
'server_type',
'u',
'd',
DB::raw('(u+d) as total')
])
->where('record_at', '>=', $timestamp)
->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 response([
'data' => $statistics
]);
}
}

View File

@ -1,93 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Requests\Admin\TutorialSave;
use App\Http\Requests\Admin\TutorialSort;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Tutorial;
use Illuminate\Support\Facades\DB;
class TutorialController extends Controller
{
public function fetch(Request $request)
{
return response([
'data' => Tutorial::orderBy('sort', 'ASC')->get()
]);
}
public function save(TutorialSave $request)
{
$params = $request->validated();
if (!$request->input('id')) {
if (!Tutorial::create($params)) {
abort(500, '创建失败');
}
} else {
try {
Tutorial::find($request->input('id'))->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
return response([
'data' => true
]);
}
public function show(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$tutorial = Tutorial::find($request->input('id'));
if (!$tutorial) {
abort(500, '教程不存在');
}
$tutorial->show = $tutorial->show ? 0 : 1;
if (!$tutorial->save()) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function sort(TutorialSort $request)
{
DB::beginTransaction();
foreach ($request->input('tutorial_ids') as $k => $v) {
if (!Tutorial::find($v)->update(['sort' => $k + 1])) {
DB::rollBack();
abort(500, '保存失败');
}
}
DB::commit();
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数有误');
}
$tutorial = Tutorial::find($request->input('id'));
if (!$tutorial) {
abort(500, '教程不存在');
}
if (!$tutorial->delete()) {
abort(500, '删除失败');
}
return response([
'data' => true
]);
}
}

View File

@ -2,28 +2,64 @@
namespace App\Http\Controllers\Admin;
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\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 UserController extends Controller
{
public function fetch(Request $request)
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)
{
$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['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']);
}
}
}
public function fetch(UserFetch $request)
{
$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';
$userModel = User::orderBy($sort, $sortType);
if ($request->input('email')) {
$userModel->where('email', $request->input('email'));
}
if ($request->input('invite_user_id')) {
$userModel->where('invite_user_id', $request->input('invite_user_id'));
}
$this->filter($request, $userModel);
$total = $userModel->count();
$res = $userModel->forPage($current, $pageSize)
->get();
@ -34,6 +70,7 @@ class UserController extends Controller
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
$res[$i]['subscribe_url'] = Helper::getSubscribeHost() . '/api/v1/client/subscribe?token=' . $res[$i]['token'];
}
return response([
'data' => $res,
@ -46,8 +83,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
]);
}
@ -74,6 +115,14 @@ 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;
}
try {
$user->update($params);
@ -84,4 +133,154 @@ class UserController extends Controller
'data' => true
]);
}
public function dumpCSV(Request $request)
{
$userModel = User::orderBy('id', 'asc');
$this->filter($request, $userModel);
$res = $userModel->get();
$plan = Plan::get();
for ($i = 0; $i < count($res); $i++) {
for ($k = 0; $k < count($plan); $k++) {
if ($plan[$k]['id'] == $res[$i]['plan_id']) {
$res[$i]['plan_name'] = $plan[$k]['name'];
}
}
}
$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;
$commissionBalance = $user['commission_balance'] / 100;
$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'];
$data .= "{$user['email']},{$balance},{$commissionBalance},{$transferEnable},{$notUseFlow},{$expireDate},{$planName},{$subscribeUrl}\r\n";
}
echo "\xEF\xBB\xBF" . $data;
}
public function generate(UserGenerate $request)
{
if ($request->input('email_prefix')) {
if ($request->input('plan_id')) {
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
abort(500, '订阅计划不存在');
}
}
$user = [
'email' => $request->input('email_prefix') . '@' . $request->input('email_suffix'),
'plan_id' => isset($plan->id) ? $plan->id : NULL,
'group_id' => isset($plan->group_id) ? $plan->group_id : NULL,
'transfer_enable' => isset($plan->transfer_enable) ? $plan->transfer_enable * 1073741824 : 0,
'expired_at' => $request->input('expired_at') ?? NULL,
'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, '生成失败');
}
return response([
'data' => true
]);
}
if ($request->input('generate_count')) {
$this->multiGenerate($request);
}
}
private function multiGenerate(Request $request)
{
if ($request->input('plan_id')) {
$plan = Plan::find($request->input('plan_id'));
if (!$plan) {
abort(500, '订阅计划不存在');
}
}
$users = [];
for ($i = 0;$i < $request->input('generate_count');$i++) {
$user = [
'email' => Helper::randomChar(6) . '@' . $request->input('email_suffix'),
'plan_id' => isset($plan->id) ? $plan->id : NULL,
'group_id' => isset($plan->group_id) ? $plan->group_id : NULL,
'transfer_enable' => isset($plan->transfer_enable) ? $plan->transfer_enable * 1073741824 : 0,
'expired_at' => $request->input('expired_at') ?? NULL,
'uuid' => Helper::guid(true),
'token' => Helper::guid(),
'created_at' => time(),
'updated_at' => time()
];
$user['password'] = password_hash($request->input('password') ?? $user['email'], PASSWORD_DEFAULT);
array_push($users, $user);
}
DB::beginTransaction();
if (!User::insert($users)) {
DB::rollBack();
abort(500, '生成失败');
}
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'];
$data .= "{$user['email']},{$password},{$expireDate},{$user['uuid']},{$createDate},{$subscribeUrl}\r\n";
}
echo $data;
}
public function sendMail(UserSendMail $request)
{
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType);
$this->filter($request, $builder);
$users = $builder->get();
foreach ($users as $user) {
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => $request->input('subject'),
'template_name' => 'notify',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url'),
'content' => $request->input('content')
]
],
'send_email_mass');
}
return response([
'data' => true
]);
}
public function ban(Request $request)
{
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType);
$this->filter($request, $builder);
try {
$builder->update([
'banned' => 1
]);
} catch (\Exception $e) {
abort(500, '处理失败');
}
return response([
'data' => true
]);
}
}

View File

@ -7,7 +7,8 @@ use App\Services\ServerService;
use App\Services\UserService;
use App\Utils\Clash;
use Illuminate\Http\Request;
use App\Models\Server;
use App\Models\ServerV2ray;
use Illuminate\Support\Facades\File;
use Symfony\Component\Yaml\Yaml;
class AppController extends Controller
@ -18,25 +19,36 @@ class AppController extends Controller
public function getConfig(Request $request)
{
$server = [];
$servers = [];
$user = $request->user;
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAllServers($user);
$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);
}
$config = Yaml::parseFile(base_path() . '/resources/rules/app.clash.yaml');
$proxy = [];
$proxies = [];
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);
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
array_push($proxy, Protocols\Clash::buildShadowsocks($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'v2ray') {
array_push($proxy, Protocols\Clash::buildVmess($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'trojan') {
array_push($proxy, Protocols\Clash::buildTrojan($user['uuid'], $item));
array_push($proxies, $item['name']);
}
}
$config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
@ -46,71 +58,37 @@ class AppController extends Controller
die(Yaml::dump($config));
}
public function getVersion()
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' => [
'version' => '4.0.0',
'download_url' => ''
'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')
]
]);
}
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

@ -2,224 +2,40 @@
namespace App\Http\Controllers\Client;
use App\Http\Controllers\Client\Protocols\V2rayN;
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 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 (isset($_SERVER['HTTP_USER_AGENT'])) {
$_SERVER['HTTP_USER_AGENT'] = strtolower($_SERVER['HTTP_USER_AGENT']);
if (strpos($_SERVER['HTTP_USER_AGENT'], 'quantumult%20x') !== false) {
die($this->quantumultX($user, $servers['vmess'], $servers['trojan']));
}
if (strpos($_SERVER['HTTP_USER_AGENT'], 'quantumult') !== false) {
die($this->quantumult($user, $servers['vmess']));
}
if (strpos($_SERVER['HTTP_USER_AGENT'], 'clash') !== false) {
die($this->clash($user, $servers['vmess'], $servers['trojan']));
}
if (strpos($_SERVER['HTTP_USER_AGENT'], 'surfboard') !== false) {
die($this->surfboard($user, $servers['vmess']));
}
if (strpos($_SERVER['HTTP_USER_AGENT'], 'surge') !== false) {
die($this->surge($user, $servers['vmess'], $servers['trojan']));
}
if (strpos($_SERVER['HTTP_USER_AGENT'], 'shadowrocket') !== false) {
die($this->shadowrocket($user, $servers['vmess'], $servers['trojan']));
$servers = $serverService->getAvailableServers($user);
if ($flag) {
foreach (glob(app_path('Http//Controllers//Client//Protocols') . '/*.php') as $file) {
$file = 'App\\Http\\Controllers\\Client\\Protocols\\' . basename($file, '.php');
$class = new $file($user, $servers);
if (strpos($flag, $class->flag) !== false) {
die($class->handle());
}
}
}
die($this->origin($user, $servers['vmess'], $servers['trojan']));
// todo 1.5.3 remove
$class = new V2rayN($user, $servers);
die($class->handle());
die('该客户端暂不支持进行订阅');
}
}
// 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, $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 ($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, $vmess = [], $trojan = [])
{
$uri = '';
header("subscription-userinfo: upload={$user->u}; download={$user->d}; total={$user->transfer_enable}; expire={$user->expired_at}");
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, $vmess = [], $trojan = [])
{
$uri = '';
foreach ($vmess as $item) {
$uri .= Helper::buildVmessLink($item, $user);
}
foreach ($trojan as $item) {
$uri .= Helper::buildTrojanLink($item, $user);
}
return base64_encode($uri);
}
private function surge($user, $vmess = [], $trojan = [])
{
$proxies = '';
$proxyGroup = '';
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, $vmess = [])
{
$proxies = '';
$proxyGroup = '';
foreach ($vmess as $item) {
// [Proxy]
$proxies .= $item->name . ' = vmess, ' . $item->host . ', ' . $item->port . ', username=' . $user->uuid;
if ($item->tls) {
$tlsSettings = json_decode($item->tlsSettings);
$proxies .= ', tls=' . ($item->tls ? "true" : "false");
if (isset($tlsSettings->allowInsecure)) {
$proxies .= ', skip-cert-verify=' . ($tlsSettings->allowInsecure ? "true" : "false");
}
}
if ($item->network == 'ws') {
$proxies .= ', ws=true';
if ($item->networkSettings) {
$wsSettings = json_decode($item->networkSettings);
if (isset($wsSettings->path)) $proxies .= ', ws-path=' . $wsSettings->path;
if (isset($wsSettings->headers->Host)) $proxies .= ', ws-headers=host:' . $wsSettings->headers->Host;
}
}
$proxies .= "\r\n";
// [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, $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 ($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) {
$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

@ -0,0 +1,99 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class AnXray
{
public $flag = 'axxray';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
foreach ($servers as $item) {
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($uuid, $server)
{
$name = rawurlencode($server['name']);
$str = str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode("{$server['cipher']}:{$uuid}")
);
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
public static function buildShadowsocksSIP008($uuid, $server)
{
$config = [
"id" => $server['id'],
"remarks" => $server['name'],
"server" => $server['host'],
"server_port" => $server['port'],
"password" => $uuid,
"method" => $server['cipher']
];
return $config;
}
public static function buildVmess($uuid, $server)
{
$config = [
"encryption" => "none",
"type" => urlencode($server['network']),
"security" => $server['tls'] ? "tls" : "",
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = urlencode($tlsSettings['serverName']);
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = urlencode($wsSettings['path']);
if (isset($wsSettings['headers']['Host'])) $config['host'] = urlencode($wsSettings['headers']['Host']);
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['serviceName'] = urlencode($grpcSettings['serviceName']);
}
return "vmess://" . $uuid . "@" . $server['host'] . ":" . $server['port'] . "?" . http_build_query($config) . "#" . urlencode($server['name']) . "\r\n";
}
public static function buildTrojan($uuid, $server)
{
$name = rawurlencode($server['name']);
$query = http_build_query([
'allowInsecure' => $server['allow_insecure'],
'peer' => $server['server_name'],
'sni' => $server['server_name']
]);
$uri = "trojan://{$uuid}@{$server['host']}:{$server['port']}?{$query}#{$name}";
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,158 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
use Symfony\Component\Yaml\Yaml;
class Clash
{
public $flag = 'clash';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$appName = config('v2board.app_name', 'V2Board');
header("subscription-userinfo: upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
header('profile-update-interval: 24');
header("content-disposition: filename={$appName}");
$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 ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
array_push($proxy, self::buildShadowsocks($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'v2ray') {
array_push($proxy, self::buildVmess($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'trojan') {
array_push($proxy, self::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;
$isFilter = false;
foreach ($config['proxy-groups'][$k]['proxies'] as $src) {
foreach ($proxies as $dst) {
if ($this->isMatch($src, $dst)) {
$isFilter = true;
$config['proxy-groups'][$k]['proxies'] = array_diff($config['proxy-groups'][$k]['proxies'], [$src]);
array_push($config['proxy-groups'][$k]['proxies'], $dst);
}
}
}
if (!$isFilter) {
$config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
}
}
// Force the current subscription domain to be a direct rule
$subsDomain = $_SERVER['SERVER_NAME'];
$subsDomainRule = "DOMAIN,{$subsDomain},DIRECT";
array_unshift($config['rules'], $subsDomainRule);
$yaml = Yaml::dump($config);
$yaml = str_replace('$app_name', config('v2board.app_name', 'V2Board'), $yaml);
return $yaml;
}
public static function buildShadowsocks($uuid, $server)
{
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'ss';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['cipher'] = $server['cipher'];
$array['password'] = $uuid;
$array['udp'] = true;
return $array;
}
public static function buildVmess($uuid, $server)
{
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'vmess';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['uuid'] = $uuid;
$array['alterId'] = $server['alter_id'];
$array['cipher'] = 'auto';
$array['udp'] = true;
if ($server['tls']) {
$array['tls'] = true;
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
$array['skip-cert-verify'] = ($tlsSettings['allowInsecure'] ? true : false);
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$array['servername'] = $tlsSettings['serverName'];
}
}
if ($server['network'] === 'ws') {
$array['network'] = 'ws';
if ($server['networkSettings']) {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
$array['ws-path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
$array['ws-headers'] = ['Host' => $wsSettings['headers']['Host']];
}
}
if ($server['network'] === 'grpc') {
$array['network'] = 'grpc';
if ($server['networkSettings']) {
$grpcObject = $server['networkSettings'];
$array['grpc-opts'] = [];
$array['grpc-opts']['grpc-service-name'] = $grpcObject['serviceName'];
}
}
return $array;
}
public static function buildTrojan($password, $server)
{
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'trojan';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['password'] = $password;
$array['udp'] = true;
if (!empty($server['server_name'])) $array['sni'] = $server['server_name'];
if (!empty($server['allow_insecure'])) $array['skip-cert-verify'] = ($server['allow_insecure'] ? true : false);
return $array;
}
private function isMatch($exp, $str)
{
try {
return preg_match($exp, $str);
} catch (\Exception $e) {
return false;
}
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class Passwall
{
public $flag = 'passwall';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
foreach ($servers as $item) {
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($password, $server)
{
$name = rawurlencode($server['name']);
$str = str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode("{$server['cipher']}:{$password}")
);
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
public static function buildVmess($uuid, $server)
{
$config = [
"v" => "2",
"ps" => $server['name'],
"add" => $server['host'],
"port" => (string)$server['port'],
"id" => $uuid,
"aid" => (string)$server['alter_id'],
"net" => $server['network'],
"type" => "none",
"host" => "",
"path" => "",
"tls" => $server['tls'] ? "tls" : "",
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = $tlsSettings['serverName'];
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
}
return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
}
public static function buildTrojan($password, $server)
{
$name = rawurlencode($server['name']);
$query = http_build_query([
'allowInsecure' => $server['allow_insecure'],
'peer' => $server['server_name'],
'sni' => $server['server_name']
]);
$uri = "trojan://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class QuantumultX
{
public $flag = 'quantumult%20x';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
header("subscription-userinfo: upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($password, $server)
{
$config = [
"shadowsocks={$server['host']}:{$server['port']}",
"method={$server['cipher']}",
"password={$password}",
'fast-open=true',
'udp-relay=true',
"tag={$server['name']}"
];
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
public static function buildVmess($uuid, $server)
{
$config = [
"vmess={$server['host']}:{$server['port']}",
'method=chacha20-poly1305',
"password={$uuid}",
'fast-open=true',
'udp-relay=true',
"tag={$server['name']}"
];
if ($server['tls']) {
if ($server['network'] === 'tcp')
array_push($config, 'obfs=over-tls');
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
array_push($config, 'tls-verification=' . ($tlsSettings['allowInsecure'] ? 'false' : 'true'));
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$host = $tlsSettings['serverName'];
}
}
if ($server['network'] === 'ws') {
if ($server['tls'])
array_push($config, 'obfs=wss');
else
array_push($config, 'obfs=ws');
if ($server['networkSettings']) {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
array_push($config, "obfs-uri={$wsSettings['path']}");
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']) && !isset($host))
$host = $wsSettings['headers']['Host'];
}
}
if (isset($host)) {
array_push($config, "obfs-host={$host}");
}
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
public static function buildTrojan($password, $server)
{
$config = [
"trojan={$server['host']}:{$server['port']}",
"password={$password}",
'over-tls=true',
$server['server_name'] ? "tls-host={$server['server_name']}" : "",
// Tips: allowInsecure=false = tls-verification=true
$server['allow_insecure'] ? 'tls-verification=false' : 'tls-verification=true',
'fast-open=true',
'udp-relay=true',
"tag={$server['name']}"
];
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class SSRPlus
{
public $flag = 'ssrplus';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
foreach ($servers as $item) {
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($password, $server)
{
$name = rawurlencode($server['name']);
$str = str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode("{$server['cipher']}:{$password}")
);
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
public static function buildVmess($uuid, $server)
{
$config = [
"v" => "2",
"ps" => $server['name'],
"add" => $server['host'],
"port" => (string)$server['port'],
"id" => $uuid,
"aid" => (string)$server['alter_id'],
"net" => $server['network'],
"type" => "none",
"host" => "",
"path" => "",
"tls" => $server['tls'] ? "tls" : "",
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = $tlsSettings['serverName'];
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
}
return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
}
public static function buildTrojan($password, $server)
{
$name = rawurlencode($server['name']);
$query = http_build_query([
'allowInsecure' => $server['allow_insecure'],
'peer' => $server['server_name'],
'sni' => $server['server_name']
]);
$uri = "trojan://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class Shadowrocket
{
public $flag = 'shadowrocket';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$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 ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($password, $server)
{
$name = rawurlencode($server['name']);
$str = str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode("{$server['cipher']}:{$password}")
);
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
public static function buildVmess($uuid, $server)
{
$userinfo = base64_encode('auto:' . $uuid . '@' . $server['host'] . ':' . $server['port']);
$config = [
'tfo' => 1,
'remark' => $server['name'],
'alterId' => $server['alter_id']
];
if ($server['tls']) {
$config['tls'] = 1;
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
$config['allowInsecure'] = (int)$tlsSettings['allowInsecure'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['peer'] = $tlsSettings['serverName'];
}
}
if ($server['network'] === 'ws') {
$config['obfs'] = "websocket";
if ($server['networkSettings']) {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
$config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
$config['obfsParam'] = $wsSettings['headers']['Host'];
}
}
if ($server['network'] === 'grpc') {
$config['obfs'] = "grpc";
if ($server['networkSettings']) {
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName']) && !empty($grpcSettings['serviceName']))
$config['path'] = $grpcSettings['serviceName'];
}
if (isset($tlsSettings)) {
$config['host'] = $tlsSettings['serverName'];
} else {
$config['host'] = $server['host'];
}
}
$query = http_build_query($config, '', '&', PHP_QUERY_RFC3986);
$uri = "vmess://{$userinfo}?{$query}";
$uri .= "\r\n";
return $uri;
}
public static function buildTrojan($password, $server)
{
$name = rawurlencode($server['name']);
$query = http_build_query([
'allowInsecure' => $server['allow_insecure'],
'peer' => $server['server_name']
]);
$uri = "trojan://{$password}@{$server['host']}:{$server['port']}?{$query}&tfo=1#{$name}";
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class Shadowsocks
{
public $flag = 'shadowsocks';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$configs = [];
$subs = [];
$subs['servers'] = [];
$subs['bytes_used'] = '';
$subs['bytes_remaining'] = '';
$bytesUsed = $user['u'] + $user['d'];
$bytesRemaining = $user['transfer_enable'] - $bytesUsed;
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
array_push($configs, self::SIP008($item, $user));
}
}
$subs['version'] = 1;
$subs['bytes_used'] = $bytesUsed;
$subs['bytes_remaining'] = $bytesRemaining;
$subs['servers'] = array_merge($subs['servers'] ? $subs['servers'] : [], $configs);
return json_encode($subs, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT);
}
public static function SIP008($server, $user)
{
$config = [
"id" => $server['id'],
"remarks" => $server['name'],
"server" => $server['host'],
"server_port" => $server['port'],
"password" => $user['uuid'],
"method" => $server['cipher']
];
return $config;
}
}

View File

@ -0,0 +1,159 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
use Symfony\Component\Yaml\Yaml;
class Stash
{
public $flag = 'stash';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$appName = config('v2board.app_name', 'V2Board');
header("subscription-userinfo: upload={$user['u']}; download={$user['d']}; total={$user['transfer_enable']}; expire={$user['expired_at']}");
header('profile-update-interval: 24');
header("content-disposition: filename={$appName}");
// 暂时使用clash配置文件后续根据Stash更新情况更新
$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 ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
array_push($proxy, self::buildShadowsocks($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'v2ray') {
array_push($proxy, self::buildVmess($user['uuid'], $item));
array_push($proxies, $item['name']);
}
if ($item['type'] === 'trojan') {
array_push($proxy, self::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;
$isFilter = false;
foreach ($config['proxy-groups'][$k]['proxies'] as $src) {
foreach ($proxies as $dst) {
if ($this->isMatch($src, $dst)) {
$isFilter = true;
$config['proxy-groups'][$k]['proxies'] = array_diff($config['proxy-groups'][$k]['proxies'], [$src]);
array_push($config['proxy-groups'][$k]['proxies'], $dst);
}
}
}
if (!$isFilter) {
$config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
}
}
// Force the current subscription domain to be a direct rule
$subsDomain = $_SERVER['SERVER_NAME'];
$subsDomainRule = "DOMAIN,{$subsDomain},DIRECT";
array_unshift($config['rules'], $subsDomainRule);
$yaml = Yaml::dump($config);
$yaml = str_replace('$app_name', config('v2board.app_name', 'V2Board'), $yaml);
return $yaml;
}
public static function buildShadowsocks($uuid, $server)
{
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'ss';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['cipher'] = $server['cipher'];
$array['password'] = $uuid;
$array['udp'] = true;
return $array;
}
public static function buildVmess($uuid, $server)
{
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'vmess';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['uuid'] = $uuid;
$array['alterId'] = $server['alter_id'];
$array['cipher'] = 'auto';
$array['udp'] = true;
if ($server['tls']) {
$array['tls'] = true;
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
$array['skip-cert-verify'] = ($tlsSettings['allowInsecure'] ? true : false);
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$array['servername'] = $tlsSettings['serverName'];
}
}
if ($server['network'] === 'ws') {
$array['network'] = 'ws';
if ($server['networkSettings']) {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
$array['ws-path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
$array['ws-headers'] = ['Host' => $wsSettings['headers']['Host']];
}
}
if ($server['network'] === 'grpc') {
$array['network'] = 'grpc';
if ($server['networkSettings']) {
$grpcObject = $server['networkSettings'];
$array['grpc-opts'] = [];
$array['grpc-opts']['grpc-service-name'] = $grpcObject['serviceName'];
}
}
return $array;
}
public static function buildTrojan($password, $server)
{
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'trojan';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['password'] = $password;
$array['udp'] = true;
if (!empty($server['server_name'])) $array['sni'] = $server['server_name'];
if (!empty($server['allow_insecure'])) $array['skip-cert-verify'] = ($server['allow_insecure'] ? true : false);
return $array;
}
private function isMatch($exp, $str)
{
try {
return preg_match($exp, $str);
} catch (\Exception $e) {
return false;
}
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class Surfboard
{
public $flag = 'surfboard';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$proxies = '';
$proxyGroup = '';
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
// [Proxy]
$proxies .= self::buildShadowsocks($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'v2ray') {
// [Proxy]
$proxies .= self::buildVmess($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'trojan') {
// [Proxy]
$proxies .= self::buildTrojan($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'];
$subsDomain = $_SERVER['SERVER_NAME'];
$config = str_replace('$subs_link', $subsURL, $config);
$config = str_replace('$subs_domain', $subsDomain, $config);
$config = str_replace('$proxies', $proxies, $config);
$config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
return $config;
}
public static function buildShadowsocks($password, $server)
{
$config = [
"{$server['name']}=ss",
"{$server['host']}",
"{$server['port']}",
"encrypt-method={$server['cipher']}",
"password={$password}",
'tfo=true',
'udp-relay=true'
];
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
public static function buildVmess($uuid, $server)
{
$config = [
"{$server['name']}=vmess",
"{$server['host']}",
"{$server['port']}",
"username={$uuid}",
'tfo=true',
'udp-relay=true'
];
if ($server['tls']) {
array_push($config, 'tls=true');
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
array_push($config, 'skip-cert-verify=' . ($tlsSettings['allowInsecure'] ? 'true' : 'false'));
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
array_push($config, "sni={$tlsSettings['serverName']}");
}
}
if ($server['network'] === 'ws') {
array_push($config, 'ws=true');
if ($server['networkSettings']) {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
array_push($config, "ws-path={$wsSettings['path']}");
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
array_push($config, "ws-headers=Host:{$wsSettings['headers']['Host']}");
}
}
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
public static function buildTrojan($password, $server)
{
$config = [
"{$server['name']}=trojan",
"{$server['host']}",
"{$server['port']}",
"password={$password}",
$server['server_name'] ? "sni={$server['server_name']}" : "",
'tfo=true',
'udp-relay=true'
];
if (!empty($server['allow_insecure'])) {
array_push($config, $server['allow_insecure'] ? 'skip-cert-verify=true' : 'skip-cert-verify=false');
}
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class Surge
{
public $flag = 'surge';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$proxies = '';
$proxyGroup = '';
foreach ($servers as $item) {
if ($item['type'] === 'shadowsocks') {
// [Proxy]
$proxies .= self::buildShadowsocks($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'v2ray') {
// [Proxy]
$proxies .= self::buildVmess($user['uuid'], $item);
// [Proxy Group]
$proxyGroup .= $item['name'] . ', ';
}
if ($item['type'] === 'trojan') {
// [Proxy]
$proxies .= self::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
$subsDomain = $_SERVER['SERVER_NAME'];
$subsURL = 'https://' . $subsDomain . '/api/v1/client/subscribe?token=' . $user['token'];
$config = str_replace('$subs_link', $subsURL, $config);
$config = str_replace('$subs_domain', $subsDomain, $config);
$config = str_replace('$proxies', $proxies, $config);
$config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
return $config;
}
public static function buildShadowsocks($password, $server)
{
$config = [
"{$server['name']}=ss",
"{$server['host']}",
"{$server['port']}",
"encrypt-method={$server['cipher']}",
"password={$password}",
'tfo=true',
'udp-relay=true'
];
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
public static function buildVmess($uuid, $server)
{
$config = [
"{$server['name']}=vmess",
"{$server['host']}",
"{$server['port']}",
"username={$uuid}",
'tfo=true',
'udp-relay=true'
];
if ($server['tls']) {
array_push($config, 'tls=true');
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['allowInsecure']) && !empty($tlsSettings['allowInsecure']))
array_push($config, 'skip-cert-verify=' . ($tlsSettings['allowInsecure'] ? 'true' : 'false'));
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
array_push($config, "sni={$tlsSettings['serverName']}");
}
}
if ($server['network'] === 'ws') {
array_push($config, 'ws=true');
if ($server['networkSettings']) {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
array_push($config, "ws-path={$wsSettings['path']}");
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
array_push($config, "ws-headers=Host:{$wsSettings['headers']['Host']}");
}
}
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
public static function buildTrojan($password, $server)
{
$config = [
"{$server['name']}=trojan",
"{$server['host']}",
"{$server['port']}",
"password={$password}",
$server['server_name'] ? "sni={$server['server_name']}" : "",
'tfo=true',
'udp-relay=true'
];
if (!empty($server['allow_insecure'])) {
array_push($config, $server['allow_insecure'] ? 'skip-cert-verify=true' : 'skip-cert-verify=false');
}
$config = array_filter($config);
$uri = implode(',', $config);
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class V2rayN
{
public $flag = 'v2rayn';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
foreach ($servers as $item) {
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($password, $server)
{
$name = rawurlencode($server['name']);
$str = str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode("{$server['cipher']}:{$password}")
);
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
public static function buildVmess($uuid, $server)
{
$config = [
"v" => "2",
"ps" => $server['name'],
"add" => $server['host'],
"port" => (string)$server['port'],
"id" => $uuid,
"aid" => (string)$server['alter_id'],
"net" => $server['network'],
"type" => "none",
"host" => "",
"path" => "",
"tls" => $server['tls'] ? "tls" : "",
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = $tlsSettings['serverName'];
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
}
return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
}
public static function buildTrojan($password, $server)
{
$name = rawurlencode($server['name']);
$query = http_build_query([
'allowInsecure' => $server['allow_insecure'],
'peer' => $server['server_name'],
'sni' => $server['server_name']
]);
$uri = "trojan://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
class V2rayNG
{
public $flag = 'v2rayng';
private $servers;
private $user;
public function __construct($user, $servers)
{
$this->user = $user;
$this->servers = $servers;
}
public function handle()
{
$servers = $this->servers;
$user = $this->user;
$uri = '';
foreach ($servers as $item) {
if ($item['type'] === 'v2ray') {
$uri .= self::buildVmess($user['uuid'], $item);
}
if ($item['type'] === 'shadowsocks') {
$uri .= self::buildShadowsocks($user['uuid'], $item);
}
if ($item['type'] === 'trojan') {
$uri .= self::buildTrojan($user['uuid'], $item);
}
}
return base64_encode($uri);
}
public static function buildShadowsocks($password, $server)
{
$name = rawurlencode($server['name']);
$str = str_replace(
['+', '/', '='],
['-', '_', ''],
base64_encode("{$server['cipher']}:{$password}")
);
return "ss://{$str}@{$server['host']}:{$server['port']}#{$name}\r\n";
}
public static function buildVmess($uuid, $server)
{
$config = [
"v" => "2",
"ps" => $server['name'],
"add" => $server['host'],
"port" => (string)$server['port'],
"id" => $uuid,
"aid" => (string)$server['alter_id'],
"net" => $server['network'],
"type" => "none",
"host" => "",
"path" => "",
"tls" => $server['tls'] ? "tls" : "",
];
if ($server['tls']) {
if ($server['tlsSettings']) {
$tlsSettings = $server['tlsSettings'];
if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName']))
$config['sni'] = $tlsSettings['serverName'];
}
}
if ((string)$server['network'] === 'ws') {
$wsSettings = $server['networkSettings'];
if (isset($wsSettings['path'])) $config['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host'];
}
if ((string)$server['network'] === 'grpc') {
$grpcSettings = $server['networkSettings'];
if (isset($grpcSettings['serviceName'])) $config['path'] = $grpcSettings['serviceName'];
}
return "vmess://" . base64_encode(json_encode($config)) . "\r\n";
}
public static function buildTrojan($password, $server)
{
$name = rawurlencode($server['name']);
$query = http_build_query([
'allowInsecure' => $server['allow_insecure'],
'peer' => $server['server_name'],
'sni' => $server['server_name']
]);
$uri = "trojan://{$password}@{$server['host']}:{$server['port']}?{$query}#{$name}";
$uri .= "\r\n";
return $uri;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\Guest;
use App\Utils\Dict;
use App\Http\Controllers\Controller;
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')
]
]);
}
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

@ -1,176 +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) {
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

@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Guest;
use App\Models\Order;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\TelegramService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
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($paymentService->customResult) ? $paymentService->customResult : '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 === 1) 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

@ -73,7 +73,7 @@ class TelegramController extends Controller
$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']) ? 'send' : 'reply';
$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'];
@ -184,7 +184,7 @@ class TelegramController extends Controller
abort(500, '用户不存在');
}
$ticketService = new TicketService();
if ($user->is_admin) {
if ($user->is_admin || $user->is_staff) {
$ticketService->replyByAdmin(
$ticketId,
$msg->text,
@ -193,5 +193,8 @@ class TelegramController extends Controller
}
$telegramService = new TelegramService();
$telegramService->sendMessage($msg->chat_id, "#`{$ticketId}` 的工单已回复成功", 'markdown');
$telegramService->sendMessageWithAdmin("#`{$ticketId}` 的工单已由 {$user->email} 进行回复", true);
}
}

View File

@ -14,46 +14,54 @@ use App\Models\InviteCode;
use App\Utils\Helper;
use App\Utils\Dict;
use App\Utils\CacheKey;
use ReCaptcha\ReCaptcha;
class AuthController extends Controller
{
public function register(AuthRegister $request)
{
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, __('Invalid code is incorrect'));
}
}
if ((int)config('v2board.email_whitelist_enable', 0)) {
if (!Helper::emailSuffixVerify(
$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, '邮箱验证码有误');
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;
@ -66,7 +74,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;
@ -89,15 +97,20 @@ class AuthController extends Controller
}
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')));
}
$data = [
'token' => $user->token,
'auth_data' => base64_encode("{$user->email}:{$user->password}")
];
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
return response()->json([
'data' => true
'data' => $data
]);
}
@ -108,22 +121,24 @@ class AuthController extends Controller
$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, '用户名或密码错误');
abort(500, __('Incorrect email or password'));
}
if ($user->banned) {
abort(500, '该账户已被停止使用');
abort(500, __('Your account has been suspended'));
}
$data = [
'token' => $user->token
'token' => $user->token,
'auth_data' => base64_encode("{$user->email}:{$user->password}")
];
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
@ -131,12 +146,15 @@ class AuthController extends Controller
$request->session()->put('is_admin', true);
$data['is_admin'] = true;
}
if ($user->is_staff) {
$request->session()->put('is_staff', true);
$data['is_staff'] = true;
}
return response([
'data' => $data
]);
}
// 准备废弃
public function token2Login(Request $request)
{
if ($request->input('token')) {
@ -146,21 +164,21 @@ class AuthController extends Controller
} else {
$location = url($redirect);
}
return header('Location:' . $location);
return redirect()->to($location)->send();
}
if ($request->input('verify')) {
$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, '该账户已被停止使用');
abort(500, __('Your account has been suspended'));
}
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
@ -178,7 +196,7 @@ class AuthController extends Controller
{
$user = User::where('token', $request->input('token'))->first();
if (!$user) {
abort(500, '用户不存在');
abort(500, __('Token error'));
}
$code = Helper::guid();
@ -189,6 +207,31 @@ class AuthController extends Controller
]);
}
public function getQuickLoginUrl(Request $request)
{
$authData = explode(':', base64_decode($request->input('auth_data')));
if (!isset($authData[0])) abort(403, __('Token error'));
$user = User::where('email', $authData[0])
->where('password', $authData[1])
->first();
if (!$user) {
abort(500, __('Token error'));
}
$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
]);
}
public function check(Request $request)
{
$data = [
@ -205,16 +248,17 @@ class AuthController extends Controller
public function forget(AuthForget $request)
{
if (Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== $request->input('email_code')) {
abort(500, '邮箱验证码有误');
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([

View File

@ -13,9 +13,11 @@ use App\Jobs\SendEmailJob;
use App\Models\InviteCode;
use App\Utils\Dict;
use App\Utils\CacheKey;
use ReCaptcha\ReCaptcha;
class CommController extends Controller
{
// TODO: remove on 1.5.5
public function config()
{
return response([
@ -24,7 +26,11 @@ class CommController extends Controller
'isInviteForce' => (int)config('v2board.invite_force', 0) ? 1 : 0,
'emailWhitelistSuffix' => (int)config('v2board.email_whitelist_enable', 0)
? $this->getEmailSuffix()
: 0
: 0,
'isRecaptcha' => (int)config('v2board.recaptcha_enable', 0) ? 1 : 0,
'recaptchaSiteKey' => config('v2board.recaptcha_site_key'),
'appDescription' => config('v2board.app_description'),
'appUrl' => config('v2board.app_url')
]
]);
}
@ -38,12 +44,19 @@ class CommController extends Controller
public function sendEmailVerify(CommSendEmailVerify $request)
{
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, __('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

@ -8,7 +8,7 @@ use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\ServerLog;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@ -35,24 +35,23 @@ class DeepbworkController extends Controller
public function user(Request $request)
{
$nodeId = $request->input('node_id');
$server = Server::find($nodeId);
$server = ServerV2ray::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));
$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" => $user->v2ray_alter_id,
"level" => $user->v2ray_level,
"alter_id" => $server->alter_id,
"level" => 0,
];
unset($user['uuid']);
unset($user['v2ray_alter_id']);
unset($user['v2ray_level']);
unset($user['email']);
array_push($result, $user);
}
return response([
@ -64,8 +63,8 @@ class DeepbworkController extends Controller
// 后端提交数据
public function submit(Request $request)
{
// Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
$server = Server::find($request->input('node_id'));
// Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
$server = ServerV2ray::find($request->input('node_id'));
if (!$server) {
return response([
'ret' => 0,
@ -75,26 +74,12 @@ class DeepbworkController extends Controller
$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();
Cache::put(CacheKey::get('SERVER_V2RAY_LAST_PUSH_AT', $server->id), time(), 3600);
$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'
);
$userService->trafficFetch($u, $d, $item['user_id'], $server, 'vmess');
}
return response([
@ -113,7 +98,7 @@ class DeepbworkController extends Controller
}
$serverService = new ServerService();
try {
$json = $serverService->getVmessConfig($nodeId, $localPort);
$json = $serverService->getV2RayConfig($nodeId, $localPort);
} catch (\Exception $e) {
abort(500, $e->getMessage());
}

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

@ -0,0 +1,82 @@
<?php
namespace App\Http\Controllers\Server;
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 Illuminate\Support\Facades\Cache;
/*
* Tidal Lab Shadowsocks
* Github: https://github.com/tokumeikoi/tidalab-ss
*/
class ShadowsocksTidalabController 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 = ServerShadowsocks::find($nodeId);
if (!$server) {
abort(500, 'fail');
}
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600);
$serverService = new ServerService();
$users = $serverService->getAvailableUsers($server->group_id);
$result = [];
foreach ($users as $user) {
array_push($result, [
'id' => $user->id,
'port' => $server->server_port,
'cipher' => $server->cipher,
'secret' => $user->uuid
]);
}
return response([
'data' => $result
]);
}
// 后端提交数据
public function submit(Request $request)
{
// Log::info('serverSubmitData:' . $request->input('node_id') . ':' . file_get_contents('php://input'));
$server = ServerShadowsocks::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_SHADOWSOCKS_ONLINE_USER', $server->id), count($data), 3600);
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_PUSH_AT', $server->id), time(), 3600);
$userService = new UserService();
foreach ($data as $item) {
$u = $item['u'] * $server->rate;
$d = $item['d'] * $server->rate;
$userService->trafficFetch($u, $d, $item['user_id'], $server, 'shadowsocks');
}
return response([
'ret' => 1,
'msg' => 'ok'
]);
}
}

View File

@ -41,15 +41,14 @@ 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']);
unset($user['email']);
array_push($result, $user);
}
return response([
@ -72,26 +71,12 @@ 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();
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,
'trojan'
);
$userService->trafficFetch($u, $d, $item['user_id'], $server, 'trojan');
}
return response([

View File

@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Staff;
use App\Http\Requests\Admin\NoticeSave;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Notice;
use Illuminate\Support\Facades\Cache;
class NoticeController extends Controller
{
public function fetch(Request $request)
{
return response([
'data' => Notice::orderBy('id', 'DESC')->get()
]);
}
public function save(NoticeSave $request)
{
$data = $request->only([
'title',
'content',
'img_url'
]);
if (!$request->input('id')) {
if (!Notice::create($data)) {
abort(500, '保存失败');
}
} else {
try {
Notice::find($request->input('id'))->update($data);
} catch (\Exception $e) {
abort(500, '保存失败');
}
}
return response([
'data' => true
]);
}
public function drop(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$notice = Notice::find($request->input('id'));
if (!$notice) {
abort(500, '公告不存在');
}
if (!$notice->delete()) {
abort(500, '删除失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\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\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();
$plans = Plan::orderBy('sort', 'ASC')->get();
foreach ($plans as $k => $v) {
$plans[$k]->count = 0;
foreach ($counts as $kk => $vv) {
if ($plans[$k]->id === $counts[$kk]->plan_id) $plans[$k]->count = $counts[$kk]->count;
}
}
return response([
'data' => $plans
]);
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\Staff;
use App\Services\TicketService;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Models\TicketMessage;
class TicketController extends Controller
{
public function fetch(Request $request)
{
if ($request->input('id')) {
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
}
$ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get();
for ($i = 0; $i < count($ticket['message']); $i++) {
if ($ticket['message'][$i]['user_id'] !== $ticket->user_id) {
$ticket['message'][$i]['is_me'] = true;
} else {
$ticket['message'][$i]['is_me'] = false;
}
}
return response([
'data' => $ticket
]);
}
$current = $request->input('current') ? $request->input('current') : 1;
$pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10;
$model = Ticket::orderBy('created_at', 'DESC');
if ($request->input('status') !== NULL) {
$model->where('status', $request->input('status'));
}
$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
]);
}
public function reply(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
if (empty($request->input('message'))) {
abort(500, '消息不能为空');
}
$ticketService = new TicketService();
$ticketService->replyByAdmin(
$request->input('id'),
$request->input('message'),
$request->session()->get('id')
);
return response([
'data' => true
]);
}
public function close(Request $request)
{
if (empty($request->input('id'))) {
abort(500, '参数错误');
}
$ticket = Ticket::where('id', $request->input('id'))
->first();
if (!$ticket) {
abort(500, '工单不存在');
}
$ticket->status = 1;
if (!$ticket->save()) {
abort(500, '关闭失败');
}
return response([
'data' => true
]);
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers\Staff;
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;
class UserController extends Controller
{
public function getUserInfoById(Request $request)
{
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
]);
}
public function update(UserUpdate $request)
{
$params = $request->validated();
$user = User::find($request->input('id'));
if (!$user) {
abort(500, '用户不存在');
}
if (User::where('email', $params['email'])->first() && $user->email !== $params['email']) {
abort(500, '邮箱已被使用');
}
if (isset($params['password'])) {
$params['password'] = password_hash($params['password'], PASSWORD_DEFAULT);
$params['password_algo'] = NULL;
} else {
unset($params['password']);
}
if (isset($params['plan_id'])) {
$plan = Plan::find($params['plan_id']);
if (!$plan) {
abort(500, '订阅计划不存在');
}
$params['group_id'] = $plan->group_id;
}
try {
$user->update($params);
} catch (\Exception $e) {
abort(500, '保存失败');
}
return response([
'data' => true
]);
}
public function sendMail(UserSendMail $request)
{
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType);
$this->filter($request, $builder);
$users = $builder->get();
foreach ($users as $user) {
SendEmailJob::dispatch([
'email' => $user->email,
'subject' => $request->input('subject'),
'template_name' => 'notify',
'template_value' => [
'name' => config('v2board.app_name', 'V2Board'),
'url' => config('v2board.app_url'),
'content' => $request->input('content')
]
]);
}
return response([
'data' => true
]);
}
public function ban(Request $request)
{
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
$builder = User::orderBy($sort, $sortType);
$this->filter($request, $builder);
try {
$builder->update([
'banned' => 1
]);
} catch (\Exception $e) {
abort(500, '处理失败');
}
return response([
'data' => true
]);
}
}

View File

@ -2,6 +2,8 @@
namespace App\Http\Controllers\User;
use App\Models\Payment;
use App\Utils\Dict;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
@ -11,9 +13,25 @@ class CommController extends Controller
{
return response([
'data' => [
'isTelegram' => (int)config('v2board.telegram_bot_enable', 0),
'stripePk' => config('v2board.stripe_pk_live')
'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', '¥')
]
]);
}
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

@ -3,6 +3,7 @@
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Services\CouponService;
use Illuminate\Http\Request;
use App\Models\Coupon;
@ -11,30 +12,14 @@ 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, '这个计划无法使用该优惠码');
}
abort(500, __('Coupon cannot be empty'));
}
$couponService = new CouponService($request->input('code'));
$couponService->setPlanId($request->input('plan_id'));
$couponService->setUserId($request->session()->get('id'));
$couponService->check();
return response([
'data' => $coupon
'data' => $couponService->getCoupon()
]);
}
}

View File

@ -14,7 +14,7 @@ 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, '已达到创建数量上限');
abort(500, __('The maximum number of creations has been reached'));
}
$inviteCode = new InviteCode();
$inviteCode->user_id = $request->session()->get('id');
@ -29,7 +29,7 @@ class InviteController extends Controller
return response([
'data' => Order::where('invite_user_id', $request->session()->get('id'))
->where('commission_balance', '>', 0)
->where('status', 3)
->whereIn('status', [3, 4])
->select([
'id',
'commission_status',

View File

@ -0,0 +1,72 @@
<?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, __('Article does not exist'));
$user = User::find($request->session()->get('id'));
$userService = new UserService();
if ($userService->isAvailable($user)) {
$appleId = config('v2board.apple_id');
$appleIdPassword = config('v2board.apple_id_password');
} else {
$appleId = __('No active subscription. Unable to use our provided Apple ID');
$appleIdPassword = __('No active subscription. Unable to use our provided Apple ID');
$this->formatAccessData($knowledge['body']);
}
$subscribeUrl = config('v2board.app_url', env('APP_URL'));
$subscribeUrls = explode(',', config('v2board.subscribe_url'));
if ($subscribeUrls) {
$subscribeUrl = $subscribeUrls[rand(0, count($subscribeUrls) - 1)];
}
$subscribeUrl = "{$subscribeUrl}/api/v1/client/subscribe?token={$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
]);
}
$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
]);
}
private function formatAccessData(&$body)
{
function getBetween($input, $start, $end){$substr = substr($input, strlen($start)+strpos($input, $start),(strlen($input) - strpos($input, $end))*(-1));return $substr;}
$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

@ -4,8 +4,10 @@ namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\OrderSave;
use App\Models\Payment;
use App\Services\CouponService;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
@ -40,7 +42,7 @@ class OrderController extends Controller
}
}
return response([
'data' => $order
'data' => $order->makeHidden(['id', 'user_id'])
]);
}
@ -50,12 +52,12 @@ class OrderController extends Controller
->where('trade_no', $request->input('trade_no'))
->first();
if (!$order) {
abort(500, '订单不存在');
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, '订阅不存在');
abort(500, __('Subscription plan does not exist'));
}
return response([
'data' => $order
@ -66,39 +68,39 @@ class OrderController extends Controller
{
$userService = new UserService();
if ($userService->isNotCompleteOrderByUserId($request->session()->get('id'))) {
abort(500, '您有未付款或开通中的订单,请稍后或取消再试');
abort(500, __('You have an unpaid or pending order, please try again later or cancel it'));
}
$plan = Plan::find($request->input('plan_id'));
$user = User::find($request->session()->get('id'));
if (!$plan) {
abort(500, '该订阅不存在');
abort(500, __('Subscription plan does not exist'));
}
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 ($user->expired_at <= time() || !$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('cycle') !== 'reset_price') {
abort(500, '该订阅已售罄');
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) {
abort(500, '该订阅无法续费,请更换其他订阅');
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[$request->input('cycle')] === NULL) {
if ($request->input('cycle') === 'reset_price') {
abort(500, '该订阅当前不支持重置流量');
}
abort(500, '该订阅周期无法进行购买,请选择其他周期');
}
if ($request->input('cycle') === 'reset_price' && !$user->plan_id) {
abort(500, '必须存在订阅才可以购买流量重置包');
}
if ($request->input('cycle') === 'reset_price' && $user->expired_at <= time()) {
abort(500, '当前订阅已过期,无法购买重置包');
if (!$plan->show && $plan->renew && !$userService->isAvailable($user)) {
abort(500, __('This subscription has expired, please change to another subscription'));
}
DB::beginTransaction();
@ -106,16 +108,17 @@ class OrderController extends Controller
$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')];
$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, '优惠券使用失败');
abort(500, __('Coupon failed'));
}
$order->coupon_id = $couponService->getId();
}
$orderService->setVipDiscount($user);
@ -128,14 +131,14 @@ class OrderController extends Controller
if ($remainingBalance > 0) {
if (!$userService->addBalance($order->user_id, - $order->total_amount)) {
DB::rollBack();
abort(500, '余额不足');
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, '余额不足');
abort(500, __('Insufficient balance'));
}
$order->balance_amount = $user->balance;
$order->total_amount = $order->total_amount - $user->balance;
@ -144,7 +147,7 @@ class OrderController extends Controller
if (!$order->save()) {
DB::rollback();
abort(500, '订单创建失败');
abort(500, __('Failed to create order'));
}
DB::commit();
@ -163,83 +166,31 @@ class OrderController extends Controller
->where('status', 0)
->first();
if (!$order) {
abort(500, '订单不存在或已支付');
abort(500, __('Order does not exist or has been paid'));
}
// free process
if ($order->total_amount <= 0) {
$order->total_amount = 0;
$order->status = 1;
$order->save();
$orderService = new OrderService($order);
if (!$orderService->paid($order->trade_no)) abort(500, '');
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, '支付方式不存在');
}
$payment = Payment::find($method);
if (!$payment || $payment->enable !== 1) abort(500, __('Payment method is not available'));
$paymentService = new PaymentService($payment->payment, $payment->id);
$result = $paymentService->pay([
'trade_no' => $tradeNo,
'total_amount' => $order->total_amount,
'user_id' => $order->user_id,
'stripe_token' => $request->input('token')
]);
$order->update(['payment_id' => $method]);
return response([
'type' => $result['type'],
'data' => $result['data']
]);
}
public function check(Request $request)
@ -249,7 +200,7 @@ class OrderController extends Controller
->where('user_id', $request->session()->get('id'))
->first();
if (!$order) {
abort(500, '订单不存在');
abort(500, __('Order does not exist'));
}
return response([
'data' => $order->status
@ -258,241 +209,39 @@ class OrderController extends Controller
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);
}
$methods = Payment::select([
'id',
'name',
'payment',
'icon'
])
->where('enable', 1)->get();
return response([
'data' => $data
'data' => $methods
]);
}
public function cancel(Request $request)
{
if (empty($request->input('trade_no'))) {
abort(500, '参数有误');
abort(500, __('Invalid parameter'));
}
$order = Order::where('trade_no', $request->input('trade_no'))
->where('user_id', $request->session()->get('id'))
->first();
if (!$order) {
abort(500, '订单不存在');
abort(500, __('Order does not exist'));
}
if ($order->status !== 0) {
abort(500, '只可以取消待支付订单');
abort(500, __('You can only cancel pending orders'));
}
$orderService = new OrderService($order);
if (!$orderService->cancel()) {
abort(500, '取消失败');
abort(500, __('Cancel failed'));
}
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

@ -14,7 +14,7 @@ class PlanController extends Controller
$plan = Plan::where('id', $request->input('id'))
->first();
if (!$plan) {
abort(500, '该订阅不存在');
abort(500, __('Subscription plan does not exist'));
}
return response([
'data' => $plan

View File

@ -8,7 +8,7 @@ use App\Services\UserService;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\ServerLog;
use App\Models\User;
@ -23,8 +23,7 @@ class ServerController extends Controller
$userService = new UserService();
if ($userService->isAvailable($user)) {
$serverService = new ServerService();
$servers = $serverService->getAllServers($user);
$servers = array_merge($servers['vmess'], $servers['trojan']);
$servers = $serverService->getAvailableServers($user);
}
return response([
'data' => $servers
@ -37,16 +36,16 @@ class ServerController extends Controller
$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');
->orderBy('log_at', 'DESC');
switch ($type) {
case 0:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-d')));
$serverLogModel->where('log_at', '>=', strtotime(date('Y-m-d')));
break;
case 1:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-d')) - 604800);
$serverLogModel->where('log_at', '>=', strtotime(date('Y-m-d')) - 604800);
break;
case 2:
$serverLogModel->where('created_at', '>=', strtotime(date('Y-m-1')));
$serverLogModel->where('log_at', '>=', strtotime(date('Y-m-1')));
}
$total = $serverLogModel->count();
$res = $serverLogModel->forPage($current, $pageSize)

View File

@ -3,7 +3,9 @@
namespace App\Http\Controllers\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->session()->get('id'))->first();
}
}

View File

@ -8,6 +8,7 @@ use App\Http\Requests\User\TicketWithdraw;
use App\Jobs\SendTelegramJob;
use App\Models\User;
use App\Services\TelegramService;
use App\Utils\Dict;
use Illuminate\Http\Request;
use App\Models\Ticket;
use App\Models\TicketMessage;
@ -22,7 +23,7 @@ class TicketController extends Controller
->where('user_id', $request->session()->get('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++) {
@ -55,7 +56,7 @@ class TicketController extends Controller
{
DB::beginTransaction();
if ((int)Ticket::where('status', 0)->where('user_id', $request->session()->get('id'))->count()) {
abort(500, '存在其他工单尚未处理');
abort(500, __('There are other unresolved tickets'));
}
$ticket = Ticket::create(array_merge($request->only([
'subject',
@ -66,7 +67,7 @@ class TicketController extends Controller
]));
if (!$ticket) {
DB::rollback();
abort(500, '工单创建失败');
abort(500, __('Failed to open ticket'));
}
$ticketMessage = TicketMessage::create([
'user_id' => $request->session()->get('id'),
@ -75,7 +76,7 @@ class TicketController extends Controller
]);
if (!$ticketMessage) {
DB::rollback();
abort(500, '工单创建失败');
abort(500, __('Failed to open ticket'));
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
@ -87,22 +88,22 @@ 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'))
->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, '请等待技术支持回复');
abort(500, __('Please wait for the technical enginneer to reply'));
}
DB::beginTransaction();
$ticketMessage = TicketMessage::create([
@ -113,7 +114,7 @@ class TicketController extends Controller
$ticket->last_reply_user_id = $request->session()->get('id');
if (!$ticketMessage || !$ticket->save()) {
DB::rollback();
abort(500, '工单回复失败');
abort(500, __('Ticket reply failed'));
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
@ -126,17 +127,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'))
->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,8 +153,25 @@ class TicketController extends Controller
public function withdraw(TicketWithdraw $request)
{
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->session()->get('id'));
$limit = config('v2board.commission_withdraw_limit', 100);
if ($limit > ($user->commission_balance / 100)) {
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,
@ -162,15 +180,12 @@ class TicketController extends Controller
]);
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'),
'ticket_id' => $ticket->id,
@ -178,7 +193,7 @@ class TicketController extends Controller
]);
if (!$ticketMessage) {
DB::rollback();
abort(500, '工单创建失败');
abort(500, __('Failed to open ticket'));
}
DB::commit();
$this->sendNotify($ticket, $ticketMessage);
@ -190,6 +205,6 @@ class TicketController extends Controller
private function sendNotify(Ticket $ticket, TicketMessage $ticketMessage)
{
$telegramService = new TelegramService();
$telegramService->sendMessageWithAdmin("📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->subject}`\n内容:\n`{$ticketMessage->message}`");
$telegramService->sendMessageWithAdmin("📮工单提醒 #{$ticket->id}\n———————————————\n主题:\n`{$ticket->subject}`\n内容:\n`{$ticketMessage->message}`", true);
}
}

View File

@ -1,82 +0,0 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Tutorial;
use Illuminate\Support\Facades\DB;
class TutorialController extends Controller
{
public function getSubscribeUrl(Request $request)
{
$user = User::find($request->session()->get('id'));
return response([
'data' => [
'subscribe_url' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token']
]
]);
}
public function getAppleID(Request $request)
{
$user = User::find($request->session()->get('id'));
if ($user->expired_at < time()) {
return response([
'data' => [
]
]);
}
return response([
'data' => [
'apple_id' => config('v2board.apple_id'),
'apple_id_password' => config('v2board.apple_id_password')
]
]);
}
public function fetch(Request $request)
{
if ($request->input('id')) {
$tutorial = Tutorial::where('show', 1)
->where('id', $request->input('id'))
->first();
if (!$tutorial) {
abort(500, '教程不存在');
}
return response([
'data' => $tutorial
]);
}
$tutorial = Tutorial::select(['id', 'category_id', 'title'])
->where('show', 1)
->orderBy('sort', 'ASC')
->get()
->groupBy('category_id');
$user = User::find($request->session()->get('id'));
$response = [
'data' => [
'tutorials' => $tutorial,
'safe_area_var' => [
'subscribe_url' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'],
'app_name' => config('v2board.app_name', 'V2board'),
'apple_id' => $user->expired_at > time() || $user->expired_at === NULL ? config('v2board.apple_id', '本站暂无提供AppleID信息') : '账号过期或未订阅',
'apple_id_password' => $user->expired_at > time() || $user->expired_at === NULL ? config('v2board.apple_id_password', '本站暂无提供AppleID信息') : '账号过期或未订阅'
]
]
];
// fuck support shadowrocket urlsafeb64 subscribe
$response['data']['safe_area_var']['b64_subscribe_url'] = str_replace(
array('+', '/', '='),
array('-', '_', ''),
base64_encode($response['data']['safe_area_var']['subscribe_url'])
);
// end
// fuck support surge urlencode subscribe
$response['data']['safe_area_var']['ue_subscribe_url'] = urlencode($response['data']['safe_area_var']['subscribe_url']);
// end
return response($response);
}
}

View File

@ -3,16 +3,19 @@
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Http\Requests\User\UserTransfer;
use App\Http\Requests\User\UserUpdate;
use App\Http\Requests\User\UserChangePassword;
use App\Utils\CacheKey;
use Illuminate\Http\Request;
use App\Models\User;
use App\Models\Plan;
use App\Models\Server;
use App\Models\ServerV2ray;
use App\Models\Ticket;
use App\Utils\Helper;
use App\Models\Order;
use App\Models\ServerLog;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
@ -27,17 +30,22 @@ class UserController extends Controller
public function changePassword(UserChangePassword $request)
{
$user = User::find($request->session()->get('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, '旧密码有误');
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, '保存失败');
abort(500, __('Save failed'));
}
$request->session()->flush();
return response([
@ -54,7 +62,6 @@ class UserController extends Controller
'last_login_at',
'created_at',
'banned',
'is_admin',
'remind_expire',
'remind_traffic',
'expired_at',
@ -63,9 +70,13 @@ class UserController extends Controller
'plan_id',
'discount',
'commission_rate',
'telegram_id'
'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
@ -91,14 +102,27 @@ class UserController extends Controller
public function getSubscribe(Request $request)
{
$user = User::find($request->session()->get('id'));
$user = User::where('id', $request->session()->get('id'))
->select([
'plan_id',
'token',
'expired_at',
'u',
'd',
'transfer_enable',
'email'
])
->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, '订阅计划不存在');
abort(500, __('Subscription plan does not exist'));
}
}
$user['subscribe_url'] = config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user['token'];
$user['subscribe_url'] = Helper::getSubscribeHost() . "/api/v1/client/subscribe?token={$user['token']}";
$user['reset_day'] = $this->getResetDay($user);
return response([
'data' => $user
@ -108,10 +132,13 @@ class UserController extends Controller
public function resetSecurity(Request $request)
{
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, __('The user does not exist'));
}
$user->uuid = Helper::guid(true);
$user->token = Helper::guid();
if (!$user->save()) {
abort(500, '重置失败');
abort(500, __('Reset failed'));
}
return response([
'data' => config('v2board.subscribe_url', config('v2board.app_url', env('APP_URL'))) . '/api/v1/client/subscribe?token=' . $user->token
@ -127,12 +154,12 @@ class UserController extends Controller
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, '该用户不存在');
abort(500, __('The user does not exist'));
}
try {
$user->update($updateData);
} catch (\Exception $e) {
abort(500, '保存失败');
abort(500, __('Save failed'));
}
return response([
@ -140,22 +167,19 @@ class UserController extends Controller
]);
}
public function transfer(Request $request)
public function transfer(UserTransfer $request)
{
$user = User::find($request->session()->get('id'));
if (!$user) {
abort(500, '该用户不存在');
}
if ($request->input('transfer_amount') <= 0) {
abort(500, '参数错误');
abort(500, __('The user does not exist'));
}
if ($request->input('transfer_amount') > $user->commission_balance) {
abort(500, '推广佣金余额不足');
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, '划转失败');
abort(500, __('Transfer failed'));
}
return response([
'data' => true
@ -165,6 +189,8 @@ class UserController extends Controller
private function getResetDay(User $user)
{
if ($user->expired_at <= time() || $user->expired_at === NULL) return null;
// if reset method is not reset
if (isset($user->plan->reset_traffic_method) && $user->plan->reset_traffic_method === 2) return null;
$day = date('d', $user->expired_at);
$today = date('d');
$lastDay = date('d', strtotime('last day of +0 months'));
@ -173,6 +199,9 @@ class UserController extends Controller
return $lastDay - $today;
}
if ((int)config('v2board.reset_traffic_method') === 1) {
if ((int)$day >= (int)$today && (int)$day >= (int)$lastDay) {
return $lastDay - $today;
}
if ((int)$day >= (int)$today) {
return $day - $today;
} else {
@ -181,4 +210,26 @@ class UserController extends Controller
}
return null;
}
public function getQuickLoginUrl(Request $request)
{
$user = User::find($request->session()->get('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

@ -35,6 +35,7 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\CORS::class,
],
'api' => [
@ -43,7 +44,7 @@ class Kernel extends HttpKernel
\Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\ForceJson::class,
\App\Http\Middleware\CORS::class,
'throttle:120,1',
\App\Http\Middleware\Language::class,
'bindings',
],
];
@ -68,7 +69,7 @@ class Kernel extends HttpKernel
'user' => \App\Http\Middleware\User::class,
'admin' => \App\Http\Middleware\Admin::class,
'client' => \App\Http\Middleware\Client::class,
'server' => \App\Http\Middleware\Server::class,
'staff' => \App\Http\Middleware\Staff::class,
];
/**

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,23 @@
<?php
namespace App\Http\Middleware;
use Closure;
class Staff
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (!$request->session()->get('is_staff')) {
abort(403, '权限不足');
}
return $next($request);
}
}

View File

@ -15,12 +15,16 @@ 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);
}
$authorization = $request->input('auth_data') ?? $request->header('authorization');
if ($authorization) {
$authData = explode(':', base64_decode($authorization));
if (!isset($authData[1]) || !isset($authData[0])) abort(403, '鉴权失败,请重新登入');
$user = \App\Models\User::where('password', $authData[1])
->where('email', $authData[0])
->first();
if (!$user) abort(403, '鉴权失败,请重新登入');
$request->session()->put('email', $user->email);
$request->session()->put('id', $user->id);
}
if (!$request->session()->get('id')) {
abort(403, '未登录或登陆已过期');

View File

@ -22,23 +22,39 @@ class ConfigSave extends FormRequest
'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
'stop_register' => 'in:0,1',
'email_verify' => 'in:0,1',
'app_name' => '',
'app_description' => '',
'app_url' => 'nullable|url',
'subscribe_url' => 'nullable|url',
'subscribe_url' => 'nullable',
'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_whitelist_suffix' => 'nullable|array',
'email_gmail_limit_enable' => 'in:0,1',
'recaptcha_enable' => 'in:0,1',
'recaptcha_key' => '',
'recaptcha_site_key' => '',
'tos_url' => 'nullable|url',
'currency' => '',
'currency_symbol' => '',
// subscribe
'plan_change_enable' => 'in:0,1',
'reset_traffic_method' => 'in:0,1',
'renew_reset_traffic_enable' => 'in:0,1',
'reset_traffic_method' => 'in:0,1,2',
'surplus_enable' => 'in:0,1',
'new_order_event_id' => 'in:0,1',
'renew_order_event_id' => 'in:0,1',
'change_order_event_id' => 'in:0,1',
// server
'server_token' => 'nullable|min:16',
'server_license' => 'nullable',
@ -75,14 +91,14 @@ class ConfigSave extends FormRequest
'epay_pid' => '',
'epay_key' => '',
// frontend
'frontend_theme' => '',
'frontend_theme_sidebar' => 'in:dark,light',
'frontend_theme_header' => 'in:dark,light',
'frontend_theme_color' => 'in:default,darkblue,black',
'frontend_theme_color' => 'in:default,darkblue,black,green',
'frontend_background_url' => 'nullable|url',
'frontend_admin_path' => '',
// tutorial
'apple_id' => 'email',
'apple_id_password' => '',
'frontend_customer_service_method' => '',
'frontend_customer_service_id' => '',
// email
'email_template' => '',
'email_host' => '',
@ -95,7 +111,15 @@ class ConfigSave extends FormRequest
'telegram_bot_enable' => 'in:0,1',
'telegram_bot_token' => '',
'telegram_discuss_id' => '',
'telegram_channel_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' => ''
];
}
@ -104,7 +128,10 @@ 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)://'
];
}
}

View File

@ -4,7 +4,7 @@ namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class CouponSave extends FormRequest
class CouponGenerate extends FormRequest
{
/**
* Get the validation rules that apply to the request.
@ -14,13 +14,16 @@ class CouponSave extends FormRequest
public function rules()
{
return [
'generate_count' => 'nullable|integer|max:500',
'name' => 'required',
'type' => 'required|in:1,2',
'value' => 'required|integer',
'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' => ''
];
}
@ -28,6 +31,8 @@ class CouponSave extends FormRequest
public function messages()
{
return [
'generate_count.integer' => '生成数量必须为数字',
'generate_count.max' => '生成数量最大为500个',
'name.required' => '名称不能为空',
'type.required' => '类型不能为空',
'type.in' => '类型格式有误',
@ -37,8 +42,10 @@ class CouponSave 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

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class KnowledgeCategorySave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'required',
'language' => 'required'
];
}
public function messages()
{
return [
'name.required' => '分类名称不能为空',
'language.required' => '分类语言不能为空'
];
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class KnowledgeCategorySort extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'knowledge_category_ids' => 'required|array'
];
}
public function messages()
{
return [
'knowledge_category_ids.required' => '分类不能为空',
'knowledge_category_ids.array' => '分类格式有误'
];
}
}

View File

@ -4,7 +4,7 @@ namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class TutorialSave extends FormRequest
class KnowledgeSave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
@ -14,10 +14,10 @@ class TutorialSave extends FormRequest
public function rules()
{
return [
'category' => 'required',
'language' => 'required',
'title' => 'required',
// 1:windows 2:macos 3:ios 4:android 5:linux 6:router
'category_id' => 'required|in:1,2,3,4,5,6',
'steps' => 'required'
'body' => 'required'
];
}
@ -25,9 +25,9 @@ class TutorialSave extends FormRequest
{
return [
'title.required' => '标题不能为空',
'category_id.required' => '分类不能为空',
'category_id.in' => '分类格式不正确',
'steps.required' => '教程步骤不能为空'
'category.required' => '分类不能为空',
'body.required' => '内容不能为空',
'language.required' => '语言不能为空'
];
}
}

View File

@ -4,7 +4,7 @@ namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerTrojanSort extends FormRequest
class KnowledgeSort extends FormRequest
{
/**
* Get the validation rules that apply to the request.
@ -14,15 +14,15 @@ class ServerTrojanSort extends FormRequest
public function rules()
{
return [
'server_ids' => 'required|array'
'knowledge_ids' => 'required|array'
];
}
public function messages()
{
return [
'server_ids.required' => '服务器ID不能为空',
'server_ids.array' => '服务器ID格式有误'
'knowledge_ids.required' => '知识ID不能为空',
'knowledge_ids.array' => '知识ID格式有误'
];
}
}

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',
'filter.*.condition' => 'required|in:>,<,=,>=,<=,模糊,!=',
'filter.*.value' => ''
];
}
public function messages()
{
return [
'filter.*.key.required' => '过滤键不能为空',
'filter.*.key.in' => '过滤键参数有误',
'filter.*.condition.required' => '过滤条件不能为空',
'filter.*.condition.in' => '过滤条件参数有误',
];
}
}

View File

@ -15,7 +15,7 @@ class OrderUpdate extends FormRequest
{
return [
'status' => 'in:0,1,2,3',
'commission_status' => 'in:0,1,2'
'commission_status' => 'in:0,1,3'
];
}

View File

@ -22,8 +22,11 @@ class PlanSave extends FormRequest
'quarter_price' => 'nullable|integer',
'half_year_price' => 'nullable|integer',
'year_price' => 'nullable|integer',
'two_year_price' => 'nullable|integer',
'three_year_price' => 'nullable|integer',
'onetime_price' => 'nullable|integer',
'reset_price' => 'nullable|integer'
'reset_price' => 'nullable|integer',
'reset_traffic_method' => 'nullable|integer|in:0,1,2'
];
}
@ -39,8 +42,12 @@ class PlanSave extends FormRequest
'quarter_price.integer' => '季付金额格式有误',
'half_year_price.integer' => '半年付金额格式有误',
'year_price.integer' => '年付金额格式有误',
'two_year_price.integer' => '两年付金额格式有误',
'three_year_price.integer' => '三年付金额格式有误',
'onetime_price.integer' => '一次性金额有误',
'reset_price.integer' => '流量重置包金额有误'
'reset_price.integer' => '流量重置包金额有误',
'reset_traffic_method.integer' => '流量重置方式格式有误',
'reset_traffic_method.in' => '流量重置方式格式有误'
];
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerShadowsocksSave extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'show' => '',
'name' => 'required',
'group_id' => 'required|array',
'parent_id' => 'nullable|integer',
'host' => 'required',
'port' => 'required',
'server_port' => 'required',
'cipher' => 'required|in:aes-128-gcm,aes-256-gcm,chacha20-ietf-poly1305',
'tags' => 'nullable|array',
'rate' => 'required|numeric'
];
}
public function messages()
{
return [
'name.required' => '节点名称不能为空',
'group_id.required' => '权限组不能为空',
'group_id.array' => '权限组格式不正确',
'parent_id.integer' => '父节点格式不正确',
'host.required' => '节点地址不能为空',
'port.required' => '连接端口不能为空',
'server_port.required' => '后端服务端口不能为空',
'cipher.required' => '加密方式不能为空',
'tags.array' => '标签格式不正确',
'rate.required' => '倍率不能为空',
'rate.numeric' => '倍率格式不正确'
];
}
}

View File

@ -4,25 +4,25 @@ namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class TutorialSort extends FormRequest
class ServerShadowsocksUpdate extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'tutorial_ids' => 'required|array'
'show' => 'in:0,1'
];
}
public function messages()
{
return [
'tutorial_ids.required' => '教程ID不能为空',
'tutorial_ids.array' => '教程ID格式有误'
'show.in' => '显示状态格式不正确'
];
}
}

View File

@ -24,11 +24,12 @@ class ServerV2raySave extends FormRequest
'tls' => 'required',
'tags' => 'nullable|array',
'rate' => 'required|numeric',
'network' => 'required|in:tcp,kcp,ws,http,domainsocket,quic',
'networkSettings' => '',
'ruleSettings' => '',
'tlsSettings' => '',
'dnsSettings' => ''
'alter_id' => 'required|integer',
'network' => 'required|in:tcp,kcp,ws,http,domainsocket,quic,grpc',
'networkSettings' => 'nullable|array',
'ruleSettings' => 'nullable|array',
'tlsSettings' => 'nullable|array',
'dnsSettings' => 'nullable|array'
];
}
@ -47,7 +48,11 @@ class ServerV2raySave extends FormRequest
'rate.required' => '倍率不能为空',
'rate.numeric' => '倍率格式不正确',
'network.required' => '传输协议不能为空',
'network.in' => '传输协议格式不正确'
'network.in' => '传输协议格式不正确',
'networkSettings.array' => '传输协议配置有误',
'ruleSettings.array' => '规则配置有误',
'tlsSettings.array' => 'tls配置有误',
'dnsSettings.array' => 'dns配置有误'
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class UserFetch extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'filter.*.key' => 'required|in:id,email,transfer_enable,d,expired_at,uuid,token,invite_by_email,invite_user_id,plan_id,banned,remarks',
'filter.*.condition' => 'required|in:>,<,=,>=,<=,模糊,!=',
'filter.*.value' => 'required'
];
}
public function messages()
{
return [
'filter.*.key.required' => '过滤键不能为空',
'filter.*.key.in' => '过滤键参数有误',
'filter.*.condition.required' => '过滤条件不能为空',
'filter.*.condition.in' => '过滤条件参数有误',
'filter.*.value.required' => '过滤值不能为空'
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class UserGenerate extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'generate_count' => 'nullable|integer|max:500',
'expired_at' => 'nullable|integer',
'plan_id' => 'nullable|integer',
'email_prefix' => 'nullable',
'email_suffix' => 'required',
'password' => 'nullable'
];
}
public function messages()
{
return [
'generate_count.integer' => '生成数量必须为数字',
'generate_count.max' => '生成数量最大为500个'
];
}
}

View File

@ -4,7 +4,7 @@ namespace App\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
class ServerV2raySort extends FormRequest
class UserSendMail extends FormRequest
{
/**
* Get the validation rules that apply to the request.
@ -14,15 +14,16 @@ class ServerV2raySort extends FormRequest
public function rules()
{
return [
'server_ids' => 'required|array'
'subject' => 'required',
'content' => 'required',
];
}
public function messages()
{
return [
'server_ids.required' => '服务器ID不能为空',
'server_ids.array' => '服务器ID格式有误'
'subject.required' => '主题不能为空',
'content.required' => '发送内容不能为空'
];
}
}

View File

@ -15,7 +15,7 @@ class UserUpdate extends FormRequest
{
return [
'email' => 'required|email',
'password' => 'nullable',
'password' => 'nullable|min:8',
'transfer_enable' => 'numeric',
'expired_at' => 'nullable|integer',
'banned' => 'required|in:0,1',
@ -23,10 +23,13 @@ class UserUpdate extends FormRequest
'commission_rate' => 'nullable|integer|min:0|max:100',
'discount' => 'nullable|integer|min:0|max:100',
'is_admin' => 'required|in:0,1',
'is_staff' => 'required|in:0,1',
'u' => 'integer',
'd' => 'integer',
'balance' => 'integer',
'commission_balance' => 'integer'
'commission_type' => 'integer',
'commission_balance' => 'integer',
'remarks' => 'nullable'
];
}
@ -41,6 +44,8 @@ class UserUpdate extends FormRequest
'banned.in' => '是否封禁格式不正确',
'is_admin.required' => '是否管理员不能为空',
'is_admin.in' => '是否管理员格式不正确',
'is_staff.required' => '是否员工不能为空',
'is_staff.in' => '是否员工格式不正确',
'plan_id.integer' => '订阅计划格式不正确',
'commission_rate.integer' => '推荐返利比例格式不正确',
'commission_rate.nullable' => '推荐返利比例格式不正确',
@ -53,7 +58,8 @@ class UserUpdate extends FormRequest
'u.integer' => '上行流量格式不正确',
'd.integer' => '下行流量格式不正确',
'balance.integer' => '余额格式不正确',
'commission_balance.integer' => '佣金格式不正确'
'commission_balance.integer' => '佣金格式不正确',
'password.min' => '密码长度最小8位'
];
}
}

View File

@ -23,11 +23,11 @@ class AuthForget extends FormRequest
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确',
'password.required' => '密码不能为空',
'password.min' => '密码必须大于8位数',
'email_code.required' => '邮箱验证码不能为空'
'email.required' => __('Email can not be empty'),
'email.email' => __('Email format is incorrect'),
'password.required' => __('Password can not be empty'),
'password.min' => __('Password must be greater than 8 digits'),
'email_code.required' => __('Email verification code cannot be empty')
];
}
}

View File

@ -22,10 +22,10 @@ class AuthLogin extends FormRequest
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确',
'password.required' => '密码不能为空',
'password.min' => '密码必须大于8位数'
'email.required' => __('Email can not be empty'),
'email.email' => __('Email format is incorrect'),
'password.required' => __('Password can not be empty'),
'password.min' => __('Password must be greater than 8 digits')
];
}
}

View File

@ -22,10 +22,10 @@ class AuthRegister extends FormRequest
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确',
'password.required' => '密码不能为空',
'password.min' => '密码必须大于8位数'
'email.required' => __('Email can not be empty'),
'email.email' => __('Email format is incorrect'),
'password.required' => __('Password can not be empty'),
'password.min' => __('Password must be greater than 8 digits')
];
}
}

View File

@ -21,8 +21,8 @@ class CommSendEmailVerify extends FormRequest
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确'
'email.required' => __('Email can not be empty'),
'email.email' => __('Email format is incorrect')
];
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Requests\Staff;
use Illuminate\Foundation\Http\FormRequest;
class UserUpdate extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required|email',
'password' => 'nullable',
'transfer_enable' => 'numeric',
'expired_at' => 'nullable|integer',
'banned' => 'required|in:0,1',
'plan_id' => 'nullable|integer',
'commission_rate' => 'nullable|integer|min:0|max:100',
'discount' => 'nullable|integer|min:0|max:100',
'u' => 'integer',
'd' => 'integer',
'balance' => 'integer',
'commission_balance' => 'integer'
];
}
public function messages()
{
return [
'email.required' => '邮箱不能为空',
'email.email' => '邮箱格式不正确',
'transfer_enable.numeric' => '流量格式不正确',
'expired_at.integer' => '到期时间格式不正确',
'banned.required' => '是否封禁不能为空',
'banned.in' => '是否封禁格式不正确',
'plan_id.integer' => '订阅计划格式不正确',
'commission_rate.integer' => '推荐返利比例格式不正确',
'commission_rate.nullable' => '推荐返利比例格式不正确',
'commission_rate.min' => '推荐返利比例最小为0',
'commission_rate.max' => '推荐返利比例最大为100',
'discount.integer' => '专属折扣比例格式不正确',
'discount.nullable' => '专属折扣比例格式不正确',
'discount.min' => '专属折扣比例最小为0',
'discount.max' => '专属折扣比例最大为100',
'u.integer' => '上行流量格式不正确',
'd.integer' => '下行流量格式不正确',
'balance.integer' => '余额格式不正确',
'commission_balance.integer' => '佣金格式不正确'
];
}
}

View File

@ -15,16 +15,16 @@ class OrderSave extends FormRequest
{
return [
'plan_id' => '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'
];
}
public function messages()
{
return [
'plan_id.required' => '套餐ID不能为空',
'cycle.required' => '套餐周期不能为空',
'cycle.in' => '套餐周期有误'
'plan_id.required' => __('Plan ID cannot be empty'),
'period.required' => __('Plan period cannot be empty'),
'period.in' => __('Wrong plan period')
];
}
}

View File

@ -23,10 +23,10 @@ class TicketSave extends FormRequest
public function messages()
{
return [
'subject.required' => '工单主题不能为空',
'level.required' => '工单级别不能为空',
'level.in' => '工单级别格式不正确',
'message.required' => '消息不能为空'
'subject.required' => __('Ticket subject cannot be empty'),
'level.required' => __('Ticket level cannot be empty'),
'level.in' => __('Incorrect ticket level format'),
'message.required' => __('Message cannot be empty')
];
}
}

View File

@ -14,7 +14,7 @@ class TicketWithdraw extends FormRequest
public function rules()
{
return [
'withdraw_method' => 'required|in:alipay,paypal,usdt,btc',
'withdraw_method' => 'required',
'withdraw_account' => 'required'
];
}
@ -22,9 +22,8 @@ class TicketWithdraw extends FormRequest
public function messages()
{
return [
'withdraw_method.required' => '提现方式不能为空',
'withdraw_method.in' => '提现方式不支持',
'withdraw_account.required' => '提现账号不能为空'
'withdraw_method.required' => __('The withdrawal method cannot be empty'),
'withdraw_account.required' => __('The withdrawal account cannot be empty')
];
}
}

View File

@ -22,9 +22,9 @@ class UserChangePassword extends FormRequest
public function messages()
{
return [
'old_password.required' => '旧密码不能为空',
'new_password.required' => '新密码不能为空',
'new_password.min' => '密码必须大于8位数'
'old_password.required' => __('Old password cannot be empty'),
'new_password.required' => __('New password cannot be empty'),
'new_password.min' => __('Password must be greater than 8 digits')
];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
class UserTransfer extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'transfer_amount' => 'required|integer|min:1'
];
}
public function messages()
{
return [
'transfer_amount.required' => __('The transfer amount cannot be empty'),
'transfer_amount.integer' => __('The transfer amount parameter is wrong'),
'transfer_amount.min' => __('The transfer amount parameter is wrong')
];
}
}

View File

@ -22,8 +22,8 @@ class UserUpdate extends FormRequest
public function messages()
{
return [
'show.in' => '过期提醒格式不正确',
'renew.in' => '流量提醒格式不正确'
'show.in' => __('Incorrect format of expiration reminder'),
'renew.in' => __('Incorrect traffic alert format')
];
}
}

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