1
0
mirror of https://github.com/v2board/v2board.git synced 2025-04-22 09:32:35 +08:00

Merge pull request from v2board/dev

1.7.2
This commit is contained in:
tokumeikoi 2022-12-24 18:36:15 +08:00 committed by GitHub
commit 6b235e592d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 415 additions and 162 deletions

@ -42,6 +42,9 @@ class ResetUser extends Command
*/ */
public function handle() public function handle()
{ {
if (!$this->confirm("确定要重置所有用户安全信息吗?")) {
return;
}
ini_set('memory_limit', -1); ini_set('memory_limit', -1);
$users = User::all(); $users = User::all();
foreach ($users as $user) foreach ($users as $user)

@ -48,8 +48,8 @@ class V2boardInstall extends Command
$this->info(" \ V / / __/| |_) | (_) | (_| | | | (_| | "); $this->info(" \ V / / __/| |_) | (_) | (_| | | | (_| | ");
$this->info(" \_/ |_____|____/ \___/ \__,_|_| \__,_| "); $this->info(" \_/ |_____|____/ \___/ \__,_|_| \__,_| ");
if (\File::exists(base_path() . '/.env')) { if (\File::exists(base_path() . '/.env')) {
$defaultSecurePath = hash('crc32b', config('app.key')); $securePath = config('v2board.secure_path', config('v2board.frontend_admin_path', hash('crc32b', config('app.key'))));
$this->info("访问 http(s)://你的站点/{$defaultSecurePath} 进入管理面板,你可以在用户中心修改你的密码。"); $this->info("访问 http(s)://你的站点/{$securePath} 进入管理面板,你可以在用户中心修改你的密码。");
abort(500, '如需重新安装请删除目录下.env文件'); abort(500, '如需重新安装请删除目录下.env文件');
} }

@ -57,8 +57,7 @@ class V2boardUpdate extends Command
} catch (\Exception $e) { } catch (\Exception $e) {
} }
} }
$this->info('更新完毕,请重新启动队列服务。'); \Artisan::call('horizon:terminate');
\Artisan::call('cache:clear'); $this->info('更新完毕,队列服务已重启,你无需进行任何操作。');
\Artisan::call('config:cache');
} }
} }

@ -87,28 +87,16 @@ class ConfigController extends Controller
'site' => [ 'site' => [
'logo' => config('v2board.logo'), 'logo' => config('v2board.logo'),
'force_https' => (int)config('v2board.force_https', 0), 'force_https' => (int)config('v2board.force_https', 0),
'safe_mode_enable' => (int)config('v2board.safe_mode_enable', 0),
'stop_register' => (int)config('v2board.stop_register', 0), 'stop_register' => (int)config('v2board.stop_register', 0),
'email_verify' => (int)config('v2board.email_verify', 0),
'app_name' => config('v2board.app_name', 'V2Board'), 'app_name' => config('v2board.app_name', 'V2Board'),
'app_description' => config('v2board.app_description', 'V2Board is best!'), 'app_description' => config('v2board.app_description', 'V2Board is best!'),
'app_url' => config('v2board.app_url'), 'app_url' => config('v2board.app_url'),
'subscribe_url' => config('v2board.subscribe_url'), 'subscribe_url' => config('v2board.subscribe_url'),
'try_out_plan_id' => (int)config('v2board.try_out_plan_id', 0), 'try_out_plan_id' => (int)config('v2board.try_out_plan_id', 0),
'try_out_hour' => (int)config('v2board.try_out_hour', 1), 'try_out_hour' => (int)config('v2board.try_out_hour', 1),
'email_whitelist_enable' => (int)config('v2board.email_whitelist_enable', 0),
'email_whitelist_suffix' => config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
'email_gmail_limit_enable' => config('v2board.email_gmail_limit_enable', 0),
'recaptcha_enable' => (int)config('v2board.recaptcha_enable', 0),
'recaptcha_key' => config('v2board.recaptcha_key'),
'recaptcha_site_key' => config('v2board.recaptcha_site_key'),
'tos_url' => config('v2board.tos_url'), 'tos_url' => config('v2board.tos_url'),
'currency' => config('v2board.currency', 'CNY'), 'currency' => config('v2board.currency', 'CNY'),
'currency_symbol' => config('v2board.currency_symbol', '¥'), 'currency_symbol' => config('v2board.currency_symbol', '¥'),
'register_limit_by_ip_enable' => (int)config('v2board.register_limit_by_ip_enable', 0),
'register_limit_count' => config('v2board.register_limit_count', 3),
'register_limit_expire' => config('v2board.register_limit_expire', 60),
'secure_path' => config('v2board.secure_path', config('v2board.frontend_admin_path', hash('crc32b', config('app.key'))))
], ],
'subscribe' => [ 'subscribe' => [
'plan_change_enable' => (int)config('v2board.plan_change_enable', 1), 'plan_change_enable' => (int)config('v2board.plan_change_enable', 1),
@ -152,6 +140,23 @@ class ConfigController extends Controller
'macos_download_url' => config('v2board.macos_download_url'), 'macos_download_url' => config('v2board.macos_download_url'),
'android_version' => config('v2board.android_version'), 'android_version' => config('v2board.android_version'),
'android_download_url' => config('v2board.android_download_url') 'android_download_url' => config('v2board.android_download_url')
],
'safe' => [
'email_verify' => (int)config('v2board.email_verify', 0),
'safe_mode_enable' => (int)config('v2board.safe_mode_enable', 0),
'secure_path' => config('v2board.secure_path', config('v2board.frontend_admin_path', hash('crc32b', config('app.key')))),
'email_whitelist_enable' => (int)config('v2board.email_whitelist_enable', 0),
'email_whitelist_suffix' => config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
'email_gmail_limit_enable' => config('v2board.email_gmail_limit_enable', 0),
'recaptcha_enable' => (int)config('v2board.recaptcha_enable', 0),
'recaptcha_key' => config('v2board.recaptcha_key'),
'recaptcha_site_key' => config('v2board.recaptcha_site_key'),
'register_limit_by_ip_enable' => (int)config('v2board.register_limit_by_ip_enable', 0),
'register_limit_count' => config('v2board.register_limit_count', 3),
'register_limit_expire' => config('v2board.register_limit_expire', 60),
'password_limit_enable' => (int)config('v2board.password_limit_enable', 1),
'password_limit_count' => config('v2board.password_limit_count', 5),
'password_limit_expire' => config('v2board.password_limit_expire', 60)
] ]
]; ];
if ($key && isset($data[$key])) { if ($key && isset($data[$key])) {

@ -15,6 +15,12 @@ class RouteController extends Controller
public function fetch(Request $request) public function fetch(Request $request)
{ {
$routes = ServerRoute::get(); $routes = ServerRoute::get();
// TODO: remove on 1.8.0
foreach ($routes as $k => $route) {
$array = json_decode($route->match, true);
if (is_array($array)) $routes[$k]['match'] = $array;
}
// TODO: remove on 1.8.0
return [ return [
'data' => $routes 'data' => $routes
]; ];
@ -24,10 +30,19 @@ class RouteController extends Controller
{ {
$params = $request->validate([ $params = $request->validate([
'remarks' => 'required', 'remarks' => 'required',
'match' => 'required', 'match' => 'required|array',
'action' => 'required', 'action' => 'required|in:block,dns',
'action_value' => 'nullable' 'action_value' => 'nullable'
], [
'remarks.required' => '备注不能为空',
'match.required' => '匹配值不能为空',
'action.required' => '动作类型不能为空',
'action.in' => '动作类型参数有误'
]); ]);
$params['match'] = array_filter($params['match']);
// TODO: remove on 1.8.0
$params['match'] = json_encode($params['match']);
// TODO: remove on 1.8.0
if ($request->input('id')) { if ($request->input('id')) {
try { try {
$route = ServerRoute::find($request->input('id')); $route = ServerRoute::find($request->input('id'));

@ -25,9 +25,9 @@ class ThemeController extends Controller
{ {
$themeConfigs = []; $themeConfigs = [];
foreach ($this->themes as $theme) { foreach ($this->themes as $theme) {
$themeConfigFile = $this->path . "{$theme}/config.php"; $themeConfigFile = $this->path . "{$theme}/config.json";
if (!File::exists($themeConfigFile)) continue; if (!File::exists($themeConfigFile)) continue;
$themeConfig = include($themeConfigFile); $themeConfig = json_decode(File::get($themeConfigFile), true);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) continue; if (!isset($themeConfig['configs']) || !is_array($themeConfig)) continue;
$themeConfigs[$theme] = $themeConfig; $themeConfigs[$theme] = $themeConfig;
if (config("theme.{$theme}")) continue; if (config("theme.{$theme}")) continue;
@ -60,9 +60,10 @@ class ThemeController extends Controller
]); ]);
$payload['config'] = json_decode(base64_decode($payload['config']), true); $payload['config'] = json_decode(base64_decode($payload['config']), true);
if (!$payload['config'] || !is_array($payload['config'])) abort(500, '参数有误'); if (!$payload['config'] || !is_array($payload['config'])) abort(500, '参数有误');
$themeConfigFile = public_path("theme/{$payload['name']}/config.php"); $themeConfigFile = public_path("theme/{$payload['name']}/config.json");
if (!File::exists($themeConfigFile)) abort(500, '主题不存在'); if (!File::exists($themeConfigFile)) abort(500, '主题不存在');
$themeConfig = include($themeConfigFile); $themeConfig = json_decode(File::get($themeConfigFile), true);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) abort(500, '主题配置文件有误');
$validateFields = array_column($themeConfig['configs'], 'field_name'); $validateFields = array_column($themeConfig['configs'], 'field_name');
$config = []; $config = [];
foreach ($validateFields as $validateField) { foreach ($validateFields as $validateField) {

@ -0,0 +1,176 @@
<?php
namespace App\Http\Controllers\Client\Protocols;
use App\Utils\Helper;
use Symfony\Component\Yaml\Yaml;
class ClashMeta
{
public $flag = 'clashmeta';
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:attachment;filename*=UTF-8''".rawurlencode($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'])) $config['proxy-groups'][$k]['proxies'] = [];
$isFilter = false;
foreach ($config['proxy-groups'][$k]['proxies'] as $src) {
foreach ($proxies as $dst) {
if (!$this->isRegex($src)) continue;
$isFilter = true;
$config['proxy-groups'][$k]['proxies'] = array_values(array_diff($config['proxy-groups'][$k]['proxies'], [$src]));
if ($this->isMatch($src, $dst)) {
array_push($config['proxy-groups'][$k]['proxies'], $dst);
}
}
if ($isFilter) continue;
}
if ($isFilter) continue;
$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($password, $server)
{
if ($server['cipher'] === '2022-blake3-aes-128-gcm') {
$serverKey = Helper::getShadowsocksServerKey($server['created_at'], 16);
$userKey = Helper::uuidToBase64($password, 16);
$password = "{$serverKey}:{$userKey}";
}
if ($server['cipher'] === '2022-blake3-aes-256-gcm') {
$serverKey = Helper::getShadowsocksServerKey($server['created_at'], 32);
$userKey = Helper::uuidToBase64($password, 32);
$password = "{$serverKey}:{$userKey}";
}
$array = [];
$array['name'] = $server['name'];
$array['type'] = 'ss';
$array['server'] = $server['host'];
$array['port'] = $server['port'];
$array['cipher'] = $server['cipher'];
$array['password'] = $password;
$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'] = 0;
$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'];
$array['ws-opts'] = [];
if (isset($wsSettings['path']) && !empty($wsSettings['path']))
$array['ws-opts']['path'] = $wsSettings['path'];
if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host']))
$array['ws-opts']['headers'] = ['Host' => $wsSettings['headers']['Host']];
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']) {
$grpcSettings = $server['networkSettings'];
$array['grpc-opts'] = [];
if (isset($grpcSettings['serviceName'])) $array['grpc-opts']['grpc-service-name'] = $grpcSettings['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)
{
return @preg_match($exp, $str);
}
private function isRegex($exp)
{
return @preg_match($exp, null) !== false;
}
}

@ -71,6 +71,15 @@ class Surfboard
$config = str_replace('$subs_domain', $subsDomain, $config); $config = str_replace('$subs_domain', $subsDomain, $config);
$config = str_replace('$proxies', $proxies, $config); $config = str_replace('$proxies', $proxies, $config);
$config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config); $config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
$upload = round($user['u'] / (1024*1024*1024), 2);
$download = round($user['d'] / (1024*1024*1024), 2);
$useTraffic = $upload + $download;
$totalTraffic = round($user['transfer_enable'] / (1024*1024*1024), 2);
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$subscribeInfo = "title={$appName}订阅信息, content=上传流量:{$upload}GB\\n下载流量{$download}GB\\n剩余流量{$useTraffic}GB\\n套餐流量{$totalTraffic}GB\\n到期时间{$expireDate}";
$config = str_replace('$subscribe_info', $subscribeInfo, $config);
return $config; return $config;
} }

@ -72,6 +72,15 @@ class Surge
$config = str_replace('$subs_domain', $subsDomain, $config); $config = str_replace('$subs_domain', $subsDomain, $config);
$config = str_replace('$proxies', $proxies, $config); $config = str_replace('$proxies', $proxies, $config);
$config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config); $config = str_replace('$proxy_group', rtrim($proxyGroup, ', '), $config);
$upload = round($user['u'] / (1024*1024*1024), 2);
$download = round($user['d'] / (1024*1024*1024), 2);
$useTraffic = $upload + $download;
$totalTraffic = round($user['transfer_enable'] / (1024*1024*1024), 2);
$expireDate = $user['expired_at'] === NULL ? '长期有效' : date('Y-m-d H:i:s', $user['expired_at']);
$subscribeInfo = "title={$appName}订阅信息, content=上传流量:{$upload}GB\\n下载流量{$download}GB\\n剩余流量{$useTraffic}GB\\n套餐流量{$totalTraffic}GB\\n到期时间{$expireDate}";
$config = str_replace('$subscribe_info', $subscribeInfo, $config);
return $config; return $config;
} }

@ -3,6 +3,8 @@
namespace App\Http\Controllers\Client\Protocols; namespace App\Http\Controllers\Client\Protocols;
use App\Utils\Helper;
class V2rayN class V2rayN
{ {
public $flag = 'v2rayn'; public $flag = 'v2rayn';
@ -37,6 +39,16 @@ class V2rayN
public static function buildShadowsocks($password, $server) public static function buildShadowsocks($password, $server)
{ {
if ($server['cipher'] === '2022-blake3-aes-128-gcm') {
$serverKey = Helper::getShadowsocksServerKey($server['created_at'], 16);
$userKey = Helper::uuidToBase64($password, 16);
$password = "{$serverKey}:{$userKey}";
}
if ($server['cipher'] === '2022-blake3-aes-256-gcm') {
$serverKey = Helper::getShadowsocksServerKey($server['created_at'], 32);
$userKey = Helper::uuidToBase64($password, 32);
$password = "{$serverKey}:{$userKey}";
}
$name = rawurlencode($server['name']); $name = rawurlencode($server['name']);
$str = str_replace( $str = str_replace(
['+', '/', '='], ['+', '/', '='],

@ -23,19 +23,6 @@ class CommController extends Controller
'app_description' => config('v2board.app_description'), 'app_description' => config('v2board.app_description'),
'app_url' => config('v2board.app_url'), 'app_url' => config('v2board.app_url'),
'logo' => config('v2board.logo'), 'logo' => config('v2board.logo'),
// TODO:REMOVE:1.7.0
'tosUrl' => config('v2board.tos_url'),
'isEmailVerify' => (int)config('v2board.email_verify', 0) ? 1 : 0,
'isInviteForce' => (int)config('v2board.invite_force', 0) ? 1 : 0,
'emailWhitelistSuffix' => (int)config('v2board.email_whitelist_enable', 0)
? $this->getEmailSuffix()
: 0,
'isRecaptcha' => (int)config('v2board.recaptcha_enable', 0) ? 1 : 0,
'recaptchaSiteKey' => config('v2board.recaptcha_site_key'),
'appDescription' => config('v2board.app_description'),
'appUrl' => config('v2board.app_url'),
] ]
]); ]);
} }

@ -156,6 +156,7 @@ class AuthController extends Controller
$user->plan_id = $plan->id; $user->plan_id = $plan->id;
$user->group_id = $plan->group_id; $user->group_id = $plan->group_id;
$user->expired_at = time() + (config('v2board.try_out_hour', 1) * 3600); $user->expired_at = time() + (config('v2board.try_out_hour', 1) * 3600);
$user->speed_limit = $plan->speed_limit;
} }
} }
@ -189,10 +190,13 @@ class AuthController extends Controller
$email = $request->input('email'); $email = $request->input('email');
$password = $request->input('password'); $password = $request->input('password');
$passwordErrorCount = (int)Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0); if ((int)config('v2board.password_limit_enable', 1)) {
$passwordErrorCount = (int)Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0);
if ($passwordErrorCount >= 5) { if ($passwordErrorCount >= (int)config('v2board.password_limit_count', 5)) {
abort(500, __('There are too many password errors, please try again after 30 minutes.')); abort(500, __('There are too many password errors, please try again after :minute minutes.', [
'minute' => config('v2board.password_limit_expire', 60)
]));
}
} }
$user = User::where('email', $email)->first(); $user = User::where('email', $email)->first();
@ -205,11 +209,13 @@ class AuthController extends Controller
$password, $password,
$user->password) $user->password)
) { ) {
Cache::put( if ((int)config('v2board.password_limit_enable')) {
CacheKey::get('PASSWORD_ERROR_LIMIT', $email), Cache::put(
(int)$passwordErrorCount + 1, CacheKey::get('PASSWORD_ERROR_LIMIT', $email),
30 * 60 (int)$passwordErrorCount + 1,
); 60 * (int)config('v2board.password_limit_expire', 60)
);
}
abort(500, __('Incorrect email or password')); abort(500, __('Incorrect email or password'));
} }

@ -109,8 +109,8 @@ class UniProxyController extends Controller
break; break;
} }
$response['base_config'] = [ $response['base_config'] = [
'push_interval' => config('v2board.server_push_interval', 60), 'push_interval' => (int)config('v2board.server_push_interval', 60),
'pull_interval' => config('v2board.server_pull_interval', 60) 'pull_interval' => (int)config('v2board.server_pull_interval', 60)
]; ];
if ($this->nodeInfo['route_id']) { if ($this->nodeInfo['route_id']) {
$response['routes'] = $this->serverService->getRoutes($this->nodeInfo['route_id']); $response['routes'] = $this->serverService->getRoutes($this->nodeInfo['route_id']);

@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\User\UserTransfer; use App\Http\Requests\User\UserTransfer;
use App\Http\Requests\User\UserUpdate; use App\Http\Requests\User\UserUpdate;
use App\Http\Requests\User\UserChangePassword; use App\Http\Requests\User\UserChangePassword;
use App\Services\AuthService;
use App\Services\UserService; use App\Services\UserService;
use App\Utils\CacheKey; use App\Utils\CacheKey;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -18,6 +19,30 @@ use Illuminate\Support\Facades\Cache;
class UserController extends Controller class UserController extends Controller
{ {
public function getActiveSession(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
$authService = new AuthService($user);
return response([
'data' => $authService->getSessions()
]);
}
public function removeActiveSession(Request $request)
{
$user = User::find($request->user['id']);
if (!$user) {
abort(500, __('The user does not exist'));
}
$authService = new AuthService($user);
return response([
'data' => $authService->delSession($request->input('session_id'))
]);
}
public function checkLogin(Request $request) public function checkLogin(Request $request)
{ {
$data = [ $data = [

@ -24,9 +24,7 @@ class ConfigSave extends FormRequest
// site // site
'logo' => 'nullable|url', 'logo' => 'nullable|url',
'force_https' => 'in:0,1', 'force_https' => 'in:0,1',
'safe_mode_enable' => 'in:0,1',
'stop_register' => 'in:0,1', 'stop_register' => 'in:0,1',
'email_verify' => 'in:0,1',
'app_name' => '', 'app_name' => '',
'app_description' => '', 'app_description' => '',
'app_url' => 'nullable|url', 'app_url' => 'nullable|url',
@ -34,19 +32,9 @@ class ConfigSave extends FormRequest
'try_out_enable' => 'in:0,1', 'try_out_enable' => 'in:0,1',
'try_out_plan_id' => 'integer', 'try_out_plan_id' => 'integer',
'try_out_hour' => 'numeric', 'try_out_hour' => 'numeric',
'email_whitelist_enable' => 'in:0,1',
'email_whitelist_suffix' => 'nullable|array',
'email_gmail_limit_enable' => 'in:0,1',
'recaptcha_enable' => 'in:0,1',
'recaptcha_key' => '',
'recaptcha_site_key' => '',
'tos_url' => 'nullable|url', 'tos_url' => 'nullable|url',
'currency' => '', 'currency' => '',
'currency_symbol' => '', 'currency_symbol' => '',
'register_limit_by_ip_enable' => 'in:0,1',
'register_limit_count' => 'integer',
'register_limit_expire' => 'integer',
'secure_path' => '',
// subscribe // subscribe
'plan_change_enable' => 'in:0,1', 'plan_change_enable' => 'in:0,1',
'reset_traffic_method' => 'in:0,1,2,3,4', 'reset_traffic_method' => 'in:0,1,2,3,4',
@ -85,7 +73,23 @@ class ConfigSave extends FormRequest
'macos_version' => '', 'macos_version' => '',
'macos_download_url' => '', 'macos_download_url' => '',
'android_version' => '', 'android_version' => '',
'android_download_url' => '' 'android_download_url' => '',
// safe
'email_whitelist_enable' => 'in:0,1',
'email_whitelist_suffix' => 'nullable|array',
'email_gmail_limit_enable' => 'in:0,1',
'recaptcha_enable' => 'in:0,1',
'recaptcha_key' => '',
'recaptcha_site_key' => '',
'email_verify' => 'in:0,1',
'safe_mode_enable' => 'in:0,1',
'register_limit_by_ip_enable' => 'in:0,1',
'register_limit_count' => 'integer',
'register_limit_expire' => 'integer',
'secure_path' => 'min:8|regex:/^[\w-]*$/',
'password_limit_enable' => 'in:0,1',
'password_limit_count' => 'integer',
'password_limit_expire' => 'integer',
]; ];
/** /**
* Get the validation rules that apply to the request. * Get the validation rules that apply to the request.
@ -106,7 +110,9 @@ class ConfigSave extends FormRequest
'server_token.min' => '通讯密钥长度必须大于16位', 'server_token.min' => '通讯密钥长度必须大于16位',
'tos_url.url' => '服务条款URL格式不正确必须携带http(s)://', 'tos_url.url' => '服务条款URL格式不正确必须携带http(s)://',
'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式必须携带http(s)://', 'telegram_discuss_link.url' => 'Telegram群组地址必须为URL格式必须携带http(s)://',
'logo.url' => 'LOGO URL格式不正确必须携带https(s)://' 'logo.url' => 'LOGO URL格式不正确必须携带https(s)://',
'secure_path.min' => '后台路径长度最小为8位',
'secure_path.regex' => '后台路径只能为字母或数字'
]; ];
} }
} }

@ -21,11 +21,12 @@ class UserRoute
$router->get ('/checkLogin', 'User\\UserController@checkLogin'); $router->get ('/checkLogin', 'User\\UserController@checkLogin');
$router->post('/transfer', 'User\\UserController@transfer'); $router->post('/transfer', 'User\\UserController@transfer');
$router->post('/getQuickLoginUrl', 'User\\UserController@getQuickLoginUrl'); $router->post('/getQuickLoginUrl', 'User\\UserController@getQuickLoginUrl');
$router->get ('/getActiveSession', 'User\\UserController@getActiveSession');
$router->post('/removeActiveSession', 'User\\UserController@removeActiveSession');
// Order // Order
$router->post('/order/save', 'User\\OrderController@save'); $router->post('/order/save', 'User\\OrderController@save');
$router->post('/order/checkout', 'User\\OrderController@checkout'); $router->post('/order/checkout', 'User\\OrderController@checkout');
$router->get ('/order/check', 'User\\OrderController@check'); $router->get ('/order/check', 'User\\OrderController@check');
$router->get ('/order/details', 'User\\OrderController@detail'); // TODO: 1.7.0 remove
$router->get ('/order/detail', 'User\\OrderController@detail'); $router->get ('/order/detail', 'User\\OrderController@detail');
$router->get ('/order/fetch', 'User\\OrderController@fetch'); $router->get ('/order/fetch', 'User\\OrderController@fetch');
$router->get ('/order/getPaymentMethod', 'User\\OrderController@getPaymentMethod'); $router->get ('/order/getPaymentMethod', 'User\\OrderController@getPaymentMethod');

@ -28,6 +28,11 @@ class AlipayF2F {
'label' => '支付宝公钥', 'label' => '支付宝公钥',
'description' => '', 'description' => '',
'type' => 'input', 'type' => 'input',
],
'product_name' => [
'label' => '自定义商品名称',
'description' => '将会体现在支付宝账单中',
'type' => 'input'
] ]
]; ];
} }
@ -42,7 +47,7 @@ class AlipayF2F {
$gateway->setAlipayPublicKey($this->config['public_key']); // 可以是路径,也可以是密钥内容 $gateway->setAlipayPublicKey($this->config['public_key']); // 可以是路径,也可以是密钥内容
$gateway->setNotifyUrl($order['notify_url']); $gateway->setNotifyUrl($order['notify_url']);
$gateway->setBizContent([ $gateway->setBizContent([
'subject' => config('v2board.app_name', 'V2Board') . ' - 订阅', 'subject' => $this->config['product_name'] ?? (config('v2board.app_name', 'V2Board') . ' - 订阅'),
'out_trade_no' => $order['trade_no'], 'out_trade_no' => $order['trade_no'],
'total_amount' => $order['total_amount'] / 100 'total_amount' => $order['total_amount'] / 100
]); ]);

@ -14,7 +14,7 @@ class AuthService
{ {
private $user; private $user;
public function __construct($user) public function __construct(User $user)
{ {
$this->user = $user; $this->user = $user;
} }
@ -27,7 +27,9 @@ class AuthService
'session' => $guid, 'session' => $guid,
], config('app.key'), 'HS256'); ], config('app.key'), 'HS256');
self::addSession($this->user->id, $guid, [ self::addSession($this->user->id, $guid, [
'ip' => $request->ip() 'ip' => $request->ip(),
'login_at' => time(),
'ua' => $request->userAgent()
]); ]);
return [ return [
'token' => $this->user->token, 'token' => $this->user->token,
@ -74,10 +76,23 @@ class AuthService
$cacheKey, $cacheKey,
$sessions $sessions
)) return false; )) return false;
return true;
} }
public function getSessions() public function getSessions()
{ {
return (array)Cache::get(CacheKey::get("USER_SESSIONS", $this->user->id), []); return (array)Cache::get(CacheKey::get("USER_SESSIONS", $this->user->id), []);
} }
public function delSession($sessionId)
{
$cacheKey = CacheKey::get("USER_SESSIONS", $this->user->id);
$sessions = (array)Cache::get($cacheKey, []);
unset($sessions[$sessionId]);
if (!Cache::put(
$cacheKey,
$sessions
)) return false;
return true;
}
} }

@ -221,7 +221,14 @@ class ServerService
public function getRoutes(array $routeIds) public function getRoutes(array $routeIds)
{ {
return ServerRoute::select(['id', 'match', 'action', 'action_value'])->whereIn('id', $routeIds)->get(); $routes = ServerRoute::select(['id', 'match', 'action', 'action_value'])->whereIn('id', $routeIds)->get();
// TODO: remove on 1.8.0
foreach ($routes as $k => $route) {
$array = json_decode($route->match, true);
if (is_array($array)) $routes[$k]['match'] = $array;
}
// TODO: remove on 1.8.0
return $routes;
} }
public function getServer($serverId, $serverType) public function getServer($serverId, $serverType)

@ -18,9 +18,10 @@ class ThemeService
public function init() public function init()
{ {
$themeConfigFile = $this->path . "{$this->theme}/config.php"; $themeConfigFile = $this->path . "{$this->theme}/config.json";
if (!File::exists($themeConfigFile)) return; if (!File::exists($themeConfigFile)) abort(500, "{$this->theme}主题不存在");
$themeConfig = include($themeConfigFile); $themeConfig = json_decode(File::get($themeConfigFile), true);
if (!isset($themeConfig['configs']) || !is_array($themeConfig)) abort(500, "{$this->theme}主题配置文件有误");
$configs = $themeConfig['configs']; $configs = $themeConfig['configs'];
$data = []; $data = [];
foreach ($configs as $config) { foreach ($configs as $config) {

@ -2,17 +2,12 @@
namespace App\Services; namespace App\Services;
use App\Jobs\ServerLogJob;
use App\Jobs\StatServerJob; use App\Jobs\StatServerJob;
use App\Jobs\StatUserJob; use App\Jobs\StatUserJob;
use App\Jobs\TrafficFetchJob; use App\Jobs\TrafficFetchJob;
use App\Models\InviteCode;
use App\Models\Order; use App\Models\Order;
use App\Models\Plan; use App\Models\Plan;
use App\Models\ServerV2ray;
use App\Models\Ticket;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\DB;
class UserService class UserService
{ {
@ -33,9 +28,9 @@ class UserService
} }
if ((int)$day >= (int)$today) { if ((int)$day >= (int)$today) {
return $day - $today; return $day - $today;
} else {
return $lastDay - $today + $day;
} }
return $lastDay - $today + $day;
} }
private function calcResetDayByYearFirstDay(): int private function calcResetDayByYearFirstDay(): int

@ -237,5 +237,5 @@ return [
| The only modification by laravel config | The only modification by laravel config
| |
*/ */
'version' => '1.7.1.1671082585916' 'version' => '1.7.2.1671471846226'
]; ];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,49 @@
{
"name": "v2board",
"description": "v2board",
"version": "1.7.2",
"images": "https://images.unsplash.com/photo-1515405295579-ba7b45403062?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2160&q=80",
"configs": [{
"label": "主题色",
"placeholder": "请选择主题颜色",
"field_name": "theme_color",
"field_type": "select",
"select_options": {
"default": "默认(蓝色)",
"green": "奶绿色",
"black": "黑色",
"darkblue": "暗蓝色"
},
"default_value": "default"
}, {
"label": "背景",
"placeholder": "请输入背景图片URL",
"field_name": "background_url",
"field_type": "input"
}, {
"label": "边栏风格",
"placeholder": "请选择边栏风格",
"field_name": "theme_sidebar",
"field_type": "select",
"select_options": {
"light": "亮",
"dark": "暗"
},
"default_value": "light"
}, {
"label": "顶部风格",
"placeholder": "请选择顶部风格",
"field_name": "theme_header",
"field_type": "select",
"select_options": {
"light": "亮",
"dark": "暗"
},
"default_value": "dark"
}, {
"label": "自定义页脚HTML",
"placeholder": "可以实现客服JS代码的加入等",
"field_name": "custom_html",
"field_type": "textarea"
}]
}

@ -1,53 +0,0 @@
<?php
return [
'name' => 'V2board',
'description' => 'V2board默认主题',
'version' => '1.5.6',
'images' => 'https://images.unsplash.com/photo-1515405295579-ba7b45403062?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2160&q=80',
'configs' => [
[
'label' => '主题色', // 标签
'placeholder' => '请选择主题颜色', // 描述
'field_name' => 'theme_color', // 字段名 作为数据key使用
'field_type' => 'select', // 字段类型: select,input,switch
'select_options' => [ // 当字段类型为select时有效
'default' => '默认(蓝色)',
'green' => '奶绿色',
'black' => '黑色',
'darkblue' => '暗蓝色',
],
'default_value' => 'default' // 字段默认值,将会在首次进行初始化
], [
'label' => '背景',
'placeholder' => '请输入背景图片URL',
'field_name' => 'background_url',
'field_type' => 'input'
], [
'label' => '边栏风格',
'placeholder' => '请选择边栏风格',
'field_name' => 'theme_sidebar',
'field_type' => 'select',
'select_options' => [
'light' => '亮',
'dark' => '暗'
],
'default_value' => 'light'
], [
'label' => '顶部风格',
'placeholder' => '请选择顶部风格',
'field_name' => 'theme_header',
'field_type' => 'select',
'select_options' => [
'light' => '亮',
'dark' => '暗'
],
'default_value' => 'dark'
], [
'label' => '自定义页脚HTML',
'placeholder' => '可以实现客服JS代码的加入等',
'field_name' => 'custom_html',
'field_type' => 'textarea'
]
]
];

@ -59,19 +59,6 @@
<script src="/theme/{{$theme}}/assets/vendors.async.js?v={{$version}}"></script> <script src="/theme/{{$theme}}/assets/vendors.async.js?v={{$version}}"></script>
<script src="/theme/{{$theme}}/assets/components.async.js?v={{$version}}"></script> <script src="/theme/{{$theme}}/assets/components.async.js?v={{$version}}"></script>
<script src="/theme/{{$theme}}/assets/umi.js?v={{$version}}"></script> <script src="/theme/{{$theme}}/assets/umi.js?v={{$version}}"></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-P1E9Z5LRRK"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-P1E9Z5LRRK');
</script>
@if (file_exists(public_path("/theme/{$theme}/assets/custom.js"))) @if (file_exists(public_path("/theme/{$theme}/assets/custom.js")))
<script src="/theme/{{$theme}}/assets/custom.js?v={{$version}}"></script> <script src="/theme/{{$theme}}/assets/custom.js?v={{$version}}"></script>
@endif @endif

@ -94,5 +94,5 @@
"Login to :name": "Login to :name", "Login to :name": "Login to :name",
"Sending frequently, please try again later": "Sending frequently, please try again later", "Sending frequently, please try again later": "Sending frequently, please try again later",
"Current product is sold out": "Current product is sold out", "Current product is sold out": "Current product is sold out",
"There are too many password errors, please try again after 30 minutes.": "There are too many password errors, please try again after 30 minutes." "There are too many password errors, please try again after :minute minutes.": "There are too many password errors, please try again after :minute minutes."
} }

@ -94,5 +94,5 @@
"Login to :name": "登入到 :name", "Login to :name": "登入到 :name",
"Sending frequently, please try again later": "发送频繁,请稍后再试", "Sending frequently, please try again later": "发送频繁,请稍后再试",
"Current product is sold out": "当前商品已售罄", "Current product is sold out": "当前商品已售罄",
"There are too many password errors, please try again after 30 minutes.": "密码错误次数过多,请 30 分钟后再试" "There are too many password errors, please try again after :minute minutes.": "密码错误次数过多,请 :minute 分钟后再试"
} }

@ -12,6 +12,9 @@ test-timeout = 5
internet-test-url = http://bing.com internet-test-url = http://bing.com
proxy-test-url = http://bing.com proxy-test-url = http://bing.com
[Panel]
SubscribeInfo = $subscribe_info, style=info
# Surfboard 的服务器和策略组配置方式与 Surge 类似, 可以参考 Surge 的规则配置手册: https://manual.nssurge.com/ # Surfboard 的服务器和策略组配置方式与 Surge 类似, 可以参考 Surge 的规则配置手册: https://manual.nssurge.com/
[Proxy] [Proxy]

@ -36,6 +36,9 @@ hide-crashlytics-request = true
use-keyword-filter = false use-keyword-filter = false
hide-udp = false hide-udp = false
[Panel]
SubscribeInfo = $subscribe_info, style=info
# ----------------------------- # -----------------------------
# Surge 的几种策略配置规范,请参考 https://manual.nssurge.com/policy/proxy.html # Surge 的几种策略配置规范,请参考 https://manual.nssurge.com/policy/proxy.html
# 不同的代理策略有*很多*可选参数,请参考上方连接的 Parameters 一段,根据需求自行添加参数。 # 不同的代理策略有*很多*可选参数,请参考上方连接的 Parameters 一段,根据需求自行添加参数。

@ -31,19 +31,6 @@
<script src="/assets/admin/vendors.async.js?v={{$version}}"></script> <script src="/assets/admin/vendors.async.js?v={{$version}}"></script>
<script src="/assets/admin/components.async.js?v={{$version}}"></script> <script src="/assets/admin/components.async.js?v={{$version}}"></script>
<script src="/assets/admin/umi.js?v={{$version}}"></script> <script src="/assets/admin/umi.js?v={{$version}}"></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-P1E9Z5LRRK"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-P1E9Z5LRRK');
</script>
</body> </body>
</html> </html>