Merge pull request #604 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

View File

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

View File

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

View File

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

View File

@ -87,28 +87,16 @@ class ConfigController extends Controller
'site' => [
'logo' => config('v2board.logo'),
'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),
'email_verify' => (int)config('v2board.email_verify', 0),
'app_name' => config('v2board.app_name', 'V2Board'),
'app_description' => config('v2board.app_description', 'V2Board is best!'),
'app_url' => config('v2board.app_url'),
'subscribe_url' => config('v2board.subscribe_url'),
'try_out_plan_id' => (int)config('v2board.try_out_plan_id', 0),
'try_out_hour' => (int)config('v2board.try_out_hour', 1),
'email_whitelist_enable' => (int)config('v2board.email_whitelist_enable', 0),
'email_whitelist_suffix' => config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT),
'email_gmail_limit_enable' => config('v2board.email_gmail_limit_enable', 0),
'recaptcha_enable' => (int)config('v2board.recaptcha_enable', 0),
'recaptcha_key' => config('v2board.recaptcha_key'),
'recaptcha_site_key' => config('v2board.recaptcha_site_key'),
'tos_url' => config('v2board.tos_url'),
'currency' => config('v2board.currency', 'CNY'),
'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' => [
'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'),
'android_version' => config('v2board.android_version'),
'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])) {

View File

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

View File

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

View File

@ -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;
}
}

View File

@ -71,6 +71,15 @@ class Surfboard
$config = str_replace('$subs_domain', $subsDomain, $config);
$config = str_replace('$proxies', $proxies, $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;
}

View File

@ -72,6 +72,15 @@ class Surge
$config = str_replace('$subs_domain', $subsDomain, $config);
$config = str_replace('$proxies', $proxies, $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;
}

View File

@ -3,6 +3,8 @@
namespace App\Http\Controllers\Client\Protocols;
use App\Utils\Helper;
class V2rayN
{
public $flag = 'v2rayn';
@ -37,6 +39,16 @@ class V2rayN
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']);
$str = str_replace(
['+', '/', '='],

View File

@ -23,19 +23,6 @@ class CommController extends Controller
'app_description' => config('v2board.app_description'),
'app_url' => config('v2board.app_url'),
'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'),
]
]);
}

View File

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

View File

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

View File

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

View File

@ -24,9 +24,7 @@ class ConfigSave extends FormRequest
// site
'logo' => 'nullable|url',
'force_https' => 'in:0,1',
'safe_mode_enable' => 'in:0,1',
'stop_register' => 'in:0,1',
'email_verify' => 'in:0,1',
'app_name' => '',
'app_description' => '',
'app_url' => 'nullable|url',
@ -34,19 +32,9 @@ class ConfigSave extends FormRequest
'try_out_enable' => 'in:0,1',
'try_out_plan_id' => 'integer',
'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',
'currency' => '',
'currency_symbol' => '',
'register_limit_by_ip_enable' => 'in:0,1',
'register_limit_count' => 'integer',
'register_limit_expire' => 'integer',
'secure_path' => '',
// subscribe
'plan_change_enable' => 'in:0,1',
'reset_traffic_method' => 'in:0,1,2,3,4',
@ -85,7 +73,23 @@ class ConfigSave extends FormRequest
'macos_version' => '',
'macos_download_url' => '',
'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.
@ -106,7 +110,9 @@ class ConfigSave extends FormRequest
'server_token.min' => '通讯密钥长度必须大于16位',
'tos_url.url' => '服务条款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' => '后台路径只能为字母或数字'
];
}
}

View File

@ -21,11 +21,12 @@ class UserRoute
$router->get ('/checkLogin', 'User\\UserController@checkLogin');
$router->post('/transfer', 'User\\UserController@transfer');
$router->post('/getQuickLoginUrl', 'User\\UserController@getQuickLoginUrl');
$router->get ('/getActiveSession', 'User\\UserController@getActiveSession');
$router->post('/removeActiveSession', 'User\\UserController@removeActiveSession');
// Order
$router->post('/order/save', 'User\\OrderController@save');
$router->post('/order/checkout', 'User\\OrderController@checkout');
$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/fetch', 'User\\OrderController@fetch');
$router->get ('/order/getPaymentMethod', 'User\\OrderController@getPaymentMethod');

View File

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

View File

@ -14,7 +14,7 @@ class AuthService
{
private $user;
public function __construct($user)
public function __construct(User $user)
{
$this->user = $user;
}
@ -27,7 +27,9 @@ class AuthService
'session' => $guid,
], config('app.key'), 'HS256');
self::addSession($this->user->id, $guid, [
'ip' => $request->ip()
'ip' => $request->ip(),
'login_at' => time(),
'ua' => $request->userAgent()
]);
return [
'token' => $this->user->token,
@ -74,10 +76,23 @@ class AuthService
$cacheKey,
$sessions
)) return false;
return true;
}
public function getSessions()
{
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;
}
}

View File

@ -221,7 +221,14 @@ class ServerService
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)

View File

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

View File

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

View File

@ -237,5 +237,5 @@ return [
| 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

View File

@ -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"
}]
}

View File

@ -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'
]
]
];

View File

@ -59,19 +59,6 @@
<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/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")))
<script src="/theme/{{$theme}}/assets/custom.js?v={{$version}}"></script>
@endif

View File

@ -94,5 +94,5 @@
"Login to :name": "Login to :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.": "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."
}

View File

@ -94,5 +94,5 @@
"Login to :name": "登入到 :name",
"Sending frequently, please try again later": "发送频繁,请稍后再试",
"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 分钟后再试"
}

View File

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

View File

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

View File

@ -31,19 +31,6 @@
<script src="/assets/admin/vendors.async.js?v={{$version}}"></script>
<script src="/assets/admin/components.async.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>
</html>