diff --git a/.env.example b/.env.example new file mode 100755 index 0000000..4f0fede --- /dev/null +++ b/.env.example @@ -0,0 +1,48 @@ +APP_NAME=V2Board +APP_ENV=local +APP_KEY= +APP_DEBUG=false +APP_URL=http://localhost + +LOG_CHANNEL=stack + +DB_CONNECTION=mysql +DB_HOST=localhost +DB_PORT=3306 +DB_DATABASE=laravel +DB_USERNAME=root +DB_PASSWORD=123456 + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +QUEUE_CONNECTION=redis +SESSION_DRIVER=redis +SESSION_LIFETIME=120 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_DRIVER=smtp +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS=null +MAIL_FROM_NAME=null +MAILGUN_DOMAIN= +MAILGUN_SECRET= + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= +PUSHER_APP_CLUSTER=mt1 + +MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..967315d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto +*.css linguist-vendored +*.scss linguist-vendored +*.js linguist-vendored +CHANGELOG.md export-ignore diff --git a/.gitignore b/.gitignore index 297959a..98b07c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,21 @@ -/vendor/ -node_modules/ -npm-debug.log -yarn-error.log - -# Laravel 4 specific -bootstrap/compiled.php -app/storage/ - -# Laravel 5 & Lumen specific -public/storage -public/hot - -# Laravel 5 & Lumen specific with changed public path -public_html/storage -public_html/hot - -storage/*.key +/node_modules +/config/v2board.php +/public/hot +/public/storage +/public/env.example.js +/storage/*.key +/vendor .env -Homestead.yaml -Homestead.json -/.vagrant +.env.backup .phpunit.result.cache +.idea +.lock +Homestead.json +Homestead.yaml +npm-debug.log +yarn-error.log +composer.phar +composer.lock +yarn.lock +docker-compose.yml +.DS_Store diff --git a/LICENSE b/LICENSE index 71e76fb..b89b3f6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 lotusnetwork +Copyright (c) 2019 Tokumeikoi and LotusNetwork Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e768c2f..f0b6390 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ -# lotusboard -NeoBranch Only! | Enhanced V2board +# Lotusboard + +The enhanced v2board + +### UserManual | 用戶手冊 + +[Documents](https://lotusnetwork.github.io) + +Hysteria + - Multiple bugs fixed + +VLESS + - Add vless support + - Multi GUN mode on grpc + - other stuffs that Vmess has + - XTLS supported + +Vmess + - TLS fingerprint, firefox by default + - Websocket ed4096(0rtt enabled for xray) + - Subscription info was translated into English + - Auto zero encryption when TLS enabled + + +Subscription: + + - ClashVPN mode profile (Proxy all traffic except local and icmp), add &flag=gclh to fetch it + + - Simplified the default clash config diff --git a/app/Console/Commands/CheckCommission.php b/app/Console/Commands/CheckCommission.php new file mode 100644 index 0000000..d956363 --- /dev/null +++ b/app/Console/Commands/CheckCommission.php @@ -0,0 +1,127 @@ +autoCheck(); + $this->autoPayCommission(); + } + + public function autoCheck() + { + if ((int)config('v2board.commission_auto_check_enable', 1)) { + Order::where('commission_status', 0) + ->where('invite_user_id', '!=', NULL) + ->where('status', 3) + ->where('updated_at', '<=', strtotime('-3 day', time())) + ->update([ + 'commission_status' => 1 + ]); + } + } + + public function autoPayCommission() + { + $orders = Order::where('commission_status', 1) + ->where('invite_user_id', '!=', NULL) + ->get(); + foreach ($orders as $order) { + DB::beginTransaction(); + if (!$this->payHandle($order->invite_user_id, $order)) { + DB::rollBack(); + continue; + } + $order->commission_status = 2; + if (!$order->save()) { + DB::rollBack(); + continue; + } + DB::commit(); + } + } + + public function payHandle($inviteUserId, Order $order) + { + $level = 3; + if ((int)config('v2board.commission_distribution_enable', 0)) { + $commissionShareLevels = [ + 0 => (int)config('v2board.commission_distribution_l1'), + 1 => (int)config('v2board.commission_distribution_l2'), + 2 => (int)config('v2board.commission_distribution_l3') + ]; + } else { + $commissionShareLevels = [ + 0 => 100 + ]; + } + for ($l = 0; $l < $level; $l++) { + $inviter = User::find($inviteUserId); + if (!$inviter) continue; + if (!isset($commissionShareLevels[$l])) continue; + $commissionBalance = $order->commission_balance * ($commissionShareLevels[$l] / 100); + if (!$commissionBalance) continue; + if ((int)config('v2board.withdraw_close_enable', 0)) { + $inviter->balance = $inviter->balance + $commissionBalance; + } else { + $inviter->commission_balance = $inviter->commission_balance + $commissionBalance; + } + if (!$inviter->save()) { + DB::rollBack(); + return false; + } + if (!CommissionLog::create([ + 'invite_user_id' => $inviteUserId, + 'user_id' => $order->user_id, + 'trade_no' => $order->trade_no, + 'order_amount' => $order->total_amount, + 'get_amount' => $commissionBalance + ])) { + DB::rollBack(); + return false; + } + $inviteUserId = $inviter->invite_user_id; + // update order actual commission balance + $order->actual_commission_balance = $order->actual_commission_balance + $commissionBalance; + } + return true; + } + +} diff --git a/app/Console/Commands/CheckOrder.php b/app/Console/Commands/CheckOrder.php new file mode 100755 index 0000000..fabdfe8 --- /dev/null +++ b/app/Console/Commands/CheckOrder.php @@ -0,0 +1,54 @@ +orderBy('created_at', 'ASC') + ->get(); + foreach ($orders as $order) { + OrderHandleJob::dispatch($order->trade_no); + } + } +} diff --git a/app/Console/Commands/CheckServer.php b/app/Console/Commands/CheckServer.php new file mode 100644 index 0000000..3e94ea7 --- /dev/null +++ b/app/Console/Commands/CheckServer.php @@ -0,0 +1,65 @@ +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)); + } + } + } +} diff --git a/app/Console/Commands/CheckTicket.php b/app/Console/Commands/CheckTicket.php new file mode 100644 index 0000000..a4ea978 --- /dev/null +++ b/app/Console/Commands/CheckTicket.php @@ -0,0 +1,52 @@ +where('updated_at', '<=', time() - 24 * 3600) + ->where('reply_status', 0) + ->get(); + foreach ($tickets as $ticket) { + if ($ticket->user_id === $ticket->last_reply_user_id) continue; + $ticket->status = 1; + $ticket->save(); + } + } +} diff --git a/app/Console/Commands/ClearUser.php b/app/Console/Commands/ClearUser.php new file mode 100644 index 0000000..30eb338 --- /dev/null +++ b/app/Console/Commands/ClearUser.php @@ -0,0 +1,51 @@ +where('transfer_enable', 0) + ->where('expired_at', 0) + ->where('last_login_at', NULL); + $count = $builder->count(); + if ($builder->delete()) { + $this->info("已删除${count}位没有任何数据的用户"); + } + } +} diff --git a/app/Console/Commands/ResetLog.php b/app/Console/Commands/ResetLog.php new file mode 100644 index 0000000..8342b5c --- /dev/null +++ b/app/Console/Commands/ResetLog.php @@ -0,0 +1,52 @@ +delete(); + StatServer::where('record_at', '<', strtotime('-2 month', time()))->delete(); + Log::where('created_at', '<', strtotime('-1 month', time()))->delete(); + } +} diff --git a/app/Console/Commands/ResetPassword.php b/app/Console/Commands/ResetPassword.php new file mode 100644 index 0000000..37bc4f8 --- /dev/null +++ b/app/Console/Commands/ResetPassword.php @@ -0,0 +1,54 @@ +argument('email'))->first(); + if (!$user) abort(500, '邮箱不存在'); + $password = Helper::guid(false); + $user->password = password_hash($password, PASSWORD_DEFAULT); + $user->password_algo = null; + if (!$user->save()) abort(500, '重置失败'); + $this->info("!!!重置成功!!!"); + $this->info("新密码为:{$password},请尽快修改密码。"); + } +} diff --git a/app/Console/Commands/ResetTraffic.php b/app/Console/Commands/ResetTraffic.php new file mode 100644 index 0000000..157e288 --- /dev/null +++ b/app/Console/Commands/ResetTraffic.php @@ -0,0 +1,164 @@ +builder = User::where('expired_at', '!=', NULL) + ->where('expired_at', '>', time()); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + ini_set('memory_limit', -1); + $resetMethods = Plan::select( + DB::raw("GROUP_CONCAT(`id`) as plan_ids"), + DB::raw("reset_traffic_method as method") + ) + ->groupBy('reset_traffic_method') + ->get() + ->toArray(); + foreach ($resetMethods as $resetMethod) { + $planIds = explode(',', $resetMethod['plan_ids']); + switch (true) { + case ($resetMethod['method'] === NULL): { + $resetTrafficMethod = config('v2board.reset_traffic_method', 0); + $builder = with(clone($this->builder))->whereIn('plan_id', $planIds); + switch ((int)$resetTrafficMethod) { + // month first day + case 0: + $this->resetByMonthFirstDay($builder); + break; + // expire day + case 1: + $this->resetByExpireDay($builder); + break; + // no action + case 2: + break; + // year first day + case 3: + $this->resetByYearFirstDay($builder); + // year expire day + case 4: + $this->resetByExpireYear($builder); + } + break; + } + case ($resetMethod['method'] === 0): { + $builder = with(clone($this->builder))->whereIn('plan_id', $planIds); + $this->resetByMonthFirstDay($builder); + break; + } + case ($resetMethod['method'] === 1): { + $builder = with(clone($this->builder))->whereIn('plan_id', $planIds); + $this->resetByExpireDay($builder); + break; + } + case ($resetMethod['method'] === 2): { + break; + } + case ($resetMethod['method'] === 3): { + $builder = with(clone($this->builder))->whereIn('plan_id', $planIds); + $this->resetByYearFirstDay($builder); + break; + } + case ($resetMethod['method'] === 4): { + $builder = with(clone($this->builder))->whereIn('plan_id', $planIds); + $this->resetByExpireYear($builder); + break; + } + } + } + } + + private function resetByExpireYear($builder):void + { + $users = []; + foreach ($builder->get() as $item) { + $expireDay = date('m-d', $item->expired_at); + $today = date('m-d'); + if ($expireDay === $today) { + array_push($users, $item->id); + } + } + User::whereIn('id', $users)->update([ + 'u' => 0, + 'd' => 0 + ]); + } + + private function resetByYearFirstDay($builder):void + { + if ((string)date('md') === '0101') { + $builder->update([ + 'u' => 0, + 'd' => 0 + ]); + } + } + + private function resetByMonthFirstDay($builder):void + { + if ((string)date('d') === '01') { + $builder->update([ + 'u' => 0, + 'd' => 0 + ]); + } + } + + private function resetByExpireDay($builder):void + { + $lastDay = date('d', strtotime('last day of +0 months')); + $users = []; + foreach ($builder->get() as $item) { + $expireDay = date('d', $item->expired_at); + $today = date('d'); + if ($expireDay === $today) { + array_push($users, $item->id); + } + + if (($today === $lastDay) && $expireDay >= $lastDay) { + array_push($users, $item->id); + } + } + User::whereIn('id', $users)->update([ + 'u' => 0, + 'd' => 0 + ]); + } +} diff --git a/app/Console/Commands/ResetUser.php b/app/Console/Commands/ResetUser.php new file mode 100644 index 0000000..51197ae --- /dev/null +++ b/app/Console/Commands/ResetUser.php @@ -0,0 +1,58 @@ +confirm("确定要重置所有用户安全信息吗?")) { + return; + } + ini_set('memory_limit', -1); + $users = User::all(); + foreach ($users as $user) + { + $user->token = Helper::guid(); + $user->uuid = Helper::guid(true); + $user->save(); + $this->info("已重置用户{$user->email}的安全信息"); + } + } +} diff --git a/app/Console/Commands/SendRemindMail.php b/app/Console/Commands/SendRemindMail.php new file mode 100644 index 0000000..8a069fb --- /dev/null +++ b/app/Console/Commands/SendRemindMail.php @@ -0,0 +1,51 @@ +remind_expire) $mailService->remindExpire($user); + if ($user->remind_traffic) $mailService->remindTraffic($user); + } + } +} diff --git a/app/Console/Commands/Test.php b/app/Console/Commands/Test.php new file mode 100644 index 0000000..667e616 --- /dev/null +++ b/app/Console/Commands/Test.php @@ -0,0 +1,41 @@ +info("__ ______ ____ _ "); + $this->info("\ \ / /___ \| __ ) ___ __ _ _ __ __| | "); + $this->info(" \ \ / / __) | _ \ / _ \ / _` | '__/ _` | "); + $this->info(" \ V / / __/| |_) | (_) | (_| | | | (_| | "); + $this->info(" \_/ |_____|____/ \___/ \__,_|_| \__,_| "); + if (\File::exists(base_path() . '/.env')) { + $securePath = config('v2board.secure_path', config('v2board.frontend_admin_path', hash('crc32b', config('app.key')))); + $this->info("访问 http(s)://你的站点/{$securePath} 进入管理面板,你可以在用户中心修改你的密码。"); + abort(500, '如需重新安装请删除目录下.env文件'); + } + + if (!copy(base_path() . '/.env.example', base_path() . '/.env')) { + abort(500, '复制环境文件失败,请检查目录权限'); + } + $this->saveToEnv([ + 'APP_KEY' => 'base64:' . base64_encode(Encrypter::generateKey('AES-256-CBC')), + 'DB_HOST' => $this->ask('请输入数据库地址(默认:localhost)', 'localhost'), + 'DB_DATABASE' => $this->ask('请输入数据库名'), + 'DB_USERNAME' => $this->ask('请输入数据库用户名'), + 'DB_PASSWORD' => $this->ask('请输入数据库密码') + ]); + \Artisan::call('config:clear'); + \Artisan::call('config:cache'); + try { + DB::connection()->getPdo(); + } catch (\Exception $e) { + abort(500, '数据库连接失败'); + } + $file = \File::get(base_path() . '/database/install.sql'); + if (!$file) { + abort(500, '数据库文件不存在'); + } + $sql = str_replace("\n", "", $file); + $sql = preg_split("/;/", $sql); + if (!is_array($sql)) { + abort(500, '数据库文件格式有误'); + } + $this->info('正在导入数据库请稍等...'); + foreach ($sql as $item) { + try { + DB::select(DB::raw($item)); + } catch (\Exception $e) { + } + } + $this->info('数据库导入完成'); + $email = ''; + while (!$email) { + $email = $this->ask('请输入管理员邮箱?'); + } + $password = Helper::guid(false); + if (!$this->registerAdmin($email, $password)) { + abort(500, '管理员账号注册失败,请重试'); + } + + $this->info('一切就绪'); + $this->info("管理员邮箱:{$email}"); + $this->info("管理员密码:{$password}"); + + $defaultSecurePath = hash('crc32b', config('app.key')); + $this->info("访问 http(s)://你的站点/{$defaultSecurePath} 进入管理面板,你可以在用户中心修改你的密码。"); + } catch (\Exception $e) { + $this->error($e->getMessage()); + } + } + + private function registerAdmin($email, $password) + { + $user = new User(); + $user->email = $email; + if (strlen($password) < 8) { + abort(500, '管理员密码长度最小为8位字符'); + } + $user->password = password_hash($password, PASSWORD_DEFAULT); + $user->uuid = Helper::guid(true); + $user->token = Helper::guid(); + $user->is_admin = 1; + return $user->save(); + } + + private function saveToEnv($data = []) + { + function set_env_var($key, $value) + { + if (! is_bool(strpos($value, ' '))) { + $value = '"' . $value . '"'; + } + $key = strtoupper($key); + + $envPath = app()->environmentFilePath(); + $contents = file_get_contents($envPath); + + preg_match("/^{$key}=[^\r\n]*/m", $contents, $matches); + + $oldValue = count($matches) ? $matches[0] : ''; + + if ($oldValue) { + $contents = str_replace("{$oldValue}", "{$key}={$value}", $contents); + } else { + $contents = $contents . "\n{$key}={$value}\n"; + } + + $file = fopen($envPath, 'w'); + fwrite($file, $contents); + return fclose($file); + } + foreach($data as $key => $value) { + set_env_var($key, $value); + } + return true; + } +} diff --git a/app/Console/Commands/V2boardStatistics.php b/app/Console/Commands/V2boardStatistics.php new file mode 100644 index 0000000..a455aaf --- /dev/null +++ b/app/Console/Commands/V2boardStatistics.php @@ -0,0 +1,131 @@ +statUser(); + $this->statServer(); + $this->stat(); + $this->info('耗时' . (microtime(true) - $startAt)); + } + + private function statServer() + { + $createdAt = time(); + $recordAt = strtotime('-1 day', strtotime(date('Y-m-d'))); + $statService = new StatisticalService(); + $statService->setStartAt($recordAt); + $statService->setServerStats(); + $stats = $statService->getStatServer(); + DB::beginTransaction(); + foreach ($stats as $stat) { + if (!StatServer::insert([ + 'server_id' => $stat['server_id'], + 'server_type' => $stat['server_type'], + 'u' => $stat['u'], + 'd' => $stat['d'], + 'created_at' => $createdAt, + 'updated_at' => $createdAt, + 'record_type' => 'd', + 'record_at' => $recordAt + ])) { + DB::rollback(); + throw new \Exception('stat server fail'); + } + } + DB::commit(); + $statService->clearStatServer(); + } + + private function statUser() + { + $createdAt = time(); + $recordAt = strtotime('-1 day', strtotime(date('Y-m-d'))); + $statService = new StatisticalService(); + $statService->setStartAt($recordAt); + $statService->setUserStats(); + $stats = $statService->getStatUser(); + DB::beginTransaction(); + foreach ($stats as $stat) { + if (!StatUser::insert([ + 'user_id' => $stat['user_id'], + 'u' => $stat['u'], + 'd' => $stat['d'], + 'server_rate' => $stat['server_rate'], + 'created_at' => $createdAt, + 'updated_at' => $createdAt, + 'record_type' => 'd', + 'record_at' => $recordAt + ])) { + DB::rollback(); + throw new \Exception('stat user fail'); + } + } + DB::commit(); + $statService->clearStatUser(); + } + + private function stat() + { + $endAt = strtotime(date('Y-m-d')); + $startAt = strtotime('-1 day', $endAt); + $statisticalService = new StatisticalService(); + $statisticalService->setStartAt($startAt); + $statisticalService->setEndAt($endAt); + $data = $statisticalService->generateStatData(); + $data['record_at'] = $startAt; + $data['record_type'] = 'd'; + $statistic = Stat::where('record_at', $startAt) + ->where('record_type', 'd') + ->first(); + if ($statistic) { + $statistic->update($data); + return; + } + Stat::create($data); + } +} diff --git a/app/Console/Commands/V2boardUpdate.php b/app/Console/Commands/V2boardUpdate.php new file mode 100644 index 0000000..e39d92b --- /dev/null +++ b/app/Console/Commands/V2boardUpdate.php @@ -0,0 +1,63 @@ +getPdo(); + $file = \File::get(base_path() . '/database/update.sql'); + if (!$file) { + abort(500, '数据库文件不存在'); + } + $sql = str_replace("\n", "", $file); + $sql = preg_split("/;/", $sql); + if (!is_array($sql)) { + abort(500, '数据库文件格式有误'); + } + $this->info('正在导入数据库请稍等...'); + foreach ($sql as $item) { + if (!$item) continue; + try { + DB::select(DB::raw($item)); + } catch (\Exception $e) { + } + } + \Artisan::call('horizon:terminate'); + $this->info('更新完毕,队列服务已重启,你无需进行任何操作。'); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php new file mode 100644 index 0000000..abd5517 --- /dev/null +++ b/app/Console/Kernel.php @@ -0,0 +1,56 @@ +command('v2board:statistics')->dailyAt('0:10'); + // check + $schedule->command('check:order')->everyMinute(); + $schedule->command('check:commission')->everyMinute(); + $schedule->command('check:ticket')->everyMinute(); + // reset + $schedule->command('reset:traffic')->daily(); + $schedule->command('reset:log')->daily(); + // send + $schedule->command('send:remindMail')->dailyAt('11:30'); + // horizon metrics + $schedule->command('horizon:snapshot')->everyFiveMinutes(); + } + + /** + * Register the commands for the application. + * + * @return void + */ + protected function commands() + { + $this->load(__DIR__ . '/Commands'); + + require base_path('routes/console.php'); + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100755 index 0000000..96e3a68 --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,77 @@ + $e->getMessage(), + 'exception' => get_class($e), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => collect($e->getTrace())->map(function ($trace) { + return Arr::except($trace, ['args']); + })->all(), + ] : [ + 'message' => $this->isHttpException($e) ? $e->getMessage() : __("Uh-oh, we've had some problems, we're working on it."), + ]; + } +} diff --git a/app/Http/Controllers/Admin/ConfigController.php b/app/Http/Controllers/Admin/ConfigController.php new file mode 100755 index 0000000..1abc8cd --- /dev/null +++ b/app/Http/Controllers/Admin/ConfigController.php @@ -0,0 +1,202 @@ + $files + ]); + } + + public function getThemeTemplate() + { + $path = public_path('theme/'); + $files = array_map(function ($item) use ($path) { + return str_replace($path, '', $item); + }, glob($path . '*')); + return response([ + 'data' => $files + ]); + } + + public function testSendMail(Request $request) + { + $obj = new SendEmailJob([ + 'email' => $request->user['email'], + 'subject' => 'This is v2board test email', + 'template_name' => 'notify', + 'template_value' => [ + 'name' => config('v2board.app_name', 'V2Board'), + 'content' => 'This is v2board test email', + 'url' => config('v2board.app_url') + ] + ]); + return response([ + 'data' => true, + 'log' => $obj->handle() + ]); + } + + public function setTelegramWebhook(Request $request) + { + $hookUrl = url(config('v2board.app_url') . '/api/v1/guest/telegram/webhook?access_token=' . md5(config('v2board.telegram_bot_token', $request->input('telegram_bot_token')))); + $telegramService = new TelegramService($request->input('telegram_bot_token')); + $telegramService->getMe(); + $telegramService->setWebhook($hookUrl); + return response([ + 'data' => true + ]); + } + + public function fetch(Request $request) + { + $key = $request->input('key'); + $data = [ + 'invite' => [ + 'invite_force' => (int)config('v2board.invite_force', 0), + 'invite_commission' => config('v2board.invite_commission', 10), + 'invite_gen_limit' => config('v2board.invite_gen_limit', 5), + 'invite_never_expire' => config('v2board.invite_never_expire', 0), + 'commission_first_time_enable' => config('v2board.commission_first_time_enable', 1), + 'commission_auto_check_enable' => config('v2board.commission_auto_check_enable', 1), + 'commission_withdraw_limit' => config('v2board.commission_withdraw_limit', 100), + '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' => [ + 'logo' => config('v2board.logo'), + 'force_https' => (int)config('v2board.force_https', 0), + 'stop_register' => (int)config('v2board.stop_register', 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), + '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), + '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), + 'show_info_to_server_enable' => (int)config('v2board.show_info_to_server_enable', 0) + ], + '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'), + ], + 'server' => [ + 'server_token' => config('v2board.server_token'), + 'server_pull_interval' => config('v2board.server_pull_interval', 60), + 'server_push_interval' => config('v2board.server_push_interval', 60), + ], + 'email' => [ + 'email_template' => config('v2board.email_template', 'default'), + 'email_host' => config('v2board.email_host'), + 'email_port' => config('v2board.email_port'), + 'email_username' => config('v2board.email_username'), + 'email_password' => config('v2board.email_password'), + 'email_encryption' => config('v2board.email_encryption'), + 'email_from_address' => config('v2board.email_from_address') + ], + 'telegram' => [ + 'telegram_bot_enable' => config('v2board.telegram_bot_enable', 0), + 'telegram_bot_token' => config('v2board.telegram_bot_token'), + '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') + ], + '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])) { + return response([ + 'data' => [ + $key => $data[$key] + ] + ]); + }; + // TODO: default should be in Dict + return response([ + 'data' => $data + ]); + } + + public function save(ConfigSave $request) + { + $data = $request->validated(); + $config = config('v2board'); + foreach (ConfigSave::RULES as $k => $v) { + if (!in_array($k, array_keys(ConfigSave::RULES))) { + unset($config[$k]); + continue; + } + if (array_key_exists($k, $data)) { + $config[$k] = $data[$k]; + } + } + $data = var_export($config, 1); + if (!File::put(base_path() . '/config/v2board.php', " true + ]); + } +} diff --git a/app/Http/Controllers/Admin/CouponController.php b/app/Http/Controllers/Admin/CouponController.php new file mode 100644 index 0000000..2a1561c --- /dev/null +++ b/app/Http/Controllers/Admin/CouponController.php @@ -0,0 +1,137 @@ +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, + 'total' => $total + ]); + } + + public function show(Request $request) + { + if (empty($request->input('id'))) { + abort(500, '参数有误'); + } + $coupon = Coupon::find($request->input('id')); + if (!$coupon) { + abort(500, '优惠券不存在'); + } + $coupon->show = $coupon->show ? 0 : 1; + if (!$coupon->save()) { + abort(500, '保存失败'); + } + + return response([ + 'data' => true + ]); + } + + public function generate(CouponGenerate $request) + { + 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); + } + if (!Coupon::create($params)) { + abort(500, '创建失败'); + } + } else { + try { + Coupon::find($request->input('id'))->update($params); + } catch (\Exception $e) { + abort(500, '保存失败'); + } + } + + return response([ + 'data' => true + ]); + } + + private function multiGenerate(CouponGenerate $request) + { + $coupons = []; + $coupon = $request->validated(); + $coupon['created_at'] = $coupon['updated_at'] = time(); + $coupon['show'] = 1; + unset($coupon['generate_count']); + for ($i = 0;$i < $request->input('generate_count');$i++) { + $coupon['code'] = Helper::randomChar(8); + array_push($coupons, $coupon); + } + DB::beginTransaction(); + if (!Coupon::insert(array_map(function ($item) use ($coupon) { + // format data + if (isset($item['limit_plan_ids']) && is_array($item['limit_plan_ids'])) { + $item['limit_plan_ids'] = json_encode($coupon['limit_plan_ids']); + } + if (isset($item['limit_period']) && is_array($item['limit_period'])) { + $item['limit_period'] = json_encode($coupon['limit_period']); + } + return $item; + }, $coupons))) { + DB::rollBack(); + abort(500, '生成失败'); + } + 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 = isset($coupon['limit_plan_ids']) ? implode("/", $coupon['limit_plan_ids']) : '不限制'; + $data .= "{$coupon['name']},{$type},{$value},{$startTime},{$endTime},{$limitUse},{$limitPlanIds},{$coupon['code']},{$createTime}\r\n"; + } + echo $data; + } + + public function drop(Request $request) + { + if (empty($request->input('id'))) { + abort(500, '参数有误'); + } + $coupon = Coupon::find($request->input('id')); + if (!$coupon) { + abort(500, '优惠券不存在'); + } + if (!$coupon->delete()) { + abort(500, '删除失败'); + } + + return response([ + 'data' => true + ]); + } +} diff --git a/app/Http/Controllers/Admin/KnowledgeController.php b/app/Http/Controllers/Admin/KnowledgeController.php new file mode 100644 index 0000000..aa5cb66 --- /dev/null +++ b/app/Http/Controllers/Admin/KnowledgeController.php @@ -0,0 +1,113 @@ +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 + ]); + } +} diff --git a/app/Http/Controllers/Admin/NoticeController.php b/app/Http/Controllers/Admin/NoticeController.php new file mode 100644 index 0000000..5664b99 --- /dev/null +++ b/app/Http/Controllers/Admin/NoticeController.php @@ -0,0 +1,81 @@ + Notice::orderBy('id', 'DESC')->get() + ]); + } + + public function save(NoticeSave $request) + { + $data = $request->only([ + 'title', + 'content', + 'img_url', + 'tags' + ]); + 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 show(Request $request) + { + if (empty($request->input('id'))) { + abort(500, '参数有误'); + } + $notice = Notice::find($request->input('id')); + if (!$notice) { + abort(500, '公告不存在'); + } + $notice->show = $notice->show ? 0 : 1; + if (!$notice->save()) { + abort(500, '保存失败'); + } + + return response([ + 'data' => true + ]); + } + + public function drop(Request $request) + { + if (empty($request->input('id'))) { + abort(500, '参数错误'); + } + $notice = Notice::find($request->input('id')); + if (!$notice) { + abort(500, '公告不存在'); + } + if (!$notice->delete()) { + abort(500, '删除失败'); + } + return response([ + 'data' => true + ]); + } +} diff --git a/app/Http/Controllers/Admin/OrderController.php b/app/Http/Controllers/Admin/OrderController.php new file mode 100644 index 0000000..c743809 --- /dev/null +++ b/app/Http/Controllers/Admin/OrderController.php @@ -0,0 +1,190 @@ +input('filter')) { + foreach ($request->input('filter') as $filter) { + if ($filter['key'] === 'email') { + $user = User::where('email', "%{$filter['value']}%")->first(); + if (!$user) continue; + $builder->where('user_id', $user->id); + continue; + } + if ($filter['condition'] === '模糊') { + $filter['condition'] = 'like'; + $filter['value'] = "%{$filter['value']}%"; + } + $builder->where($filter['key'], $filter['condition'], $filter['value']); + } + } + } + + public function detail(Request $request) + { + $order = Order::find($request->input('id')); + if (!$order) abort(500, '订单不存在'); + $order['commission_log'] = CommissionLog::where('trade_no', $order->trade_no)->get(); + if ($order->surplus_order_ids) { + $order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get(); + } + return response([ + 'data' => $order + ]); + } + + public function fetch(OrderFetch $request) + { + $current = $request->input('current') ? $request->input('current') : 1; + $pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10; + $orderModel = Order::orderBy('created_at', 'DESC'); + if ($request->input('is_commission')) { + $orderModel->where('invite_user_id', '!=', NULL); + $orderModel->whereNotIn('status', [0, 2]); + $orderModel->where('commission_balance', '>', 0); + } + $this->filter($request, $orderModel); + $total = $orderModel->count(); + $res = $orderModel->forPage($current, $pageSize) + ->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']; + } + } + } + return response([ + 'data' => $res, + 'total' => $total + ]); + } + + 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([ + 'commission_status' + ]); + + $order = Order::where('trade_no', $request->input('trade_no')) + ->first(); + if (!$order) { + abort(500, '订单不存在'); + } + + try { + $order->update($params); + } catch (\Exception $e) { + abort(500, '更新失败'); + } + + return response([ + 'data' => true + ]); + } + + public function assign(OrderAssign $request) + { + $plan = Plan::find($request->input('plan_id')); + $user = User::where('email', $request->input('email'))->first(); + + if (!$user) { + abort(500, '该用户不存在'); + } + + if (!$plan) { + 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->period = $request->input('period'); + $order->trade_no = Helper::guid(); + $order->total_amount = $request->input('total_amount'); + + if ($order->period === 'reset_price') { + $order->type = 4; + } else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id) { + $order->type = 3; + } else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) { + $order->type = 2; + } else { + $order->type = 1; + } + + $orderService->setInvite($user); + + if (!$order->save()) { + DB::rollback(); + abort(500, '订单创建失败'); + } + + DB::commit(); + + return response([ + 'data' => $order->trade_no + ]); + } +} diff --git a/app/Http/Controllers/Admin/PaymentController.php b/app/Http/Controllers/Admin/PaymentController.php new file mode 100644 index 0000000..8b340cf --- /dev/null +++ b/app/Http/Controllers/Admin/PaymentController.php @@ -0,0 +1,133 @@ + $methods + ]); + } + + public function fetch() + { + $payments = Payment::orderBy('sort', 'ASC')->get(); + foreach ($payments as $k => $v) { + $notifyUrl = url("/api/v1/guest/payment/notify/{$v->payment}/{$v->uuid}"); + if ($v->notify_domain) { + $parseUrl = parse_url($notifyUrl); + $notifyUrl = $v->notify_domain . $parseUrl['path']; + } + $payments[$k]['notify_url'] = $notifyUrl; + } + return response([ + 'data' => $payments + ]); + } + + public function getPaymentForm(Request $request) + { + $paymentService = new PaymentService($request->input('payment'), $request->input('id')); + return response([ + 'data' => $paymentService->form() + ]); + } + + public function show(Request $request) + { + $payment = Payment::find($request->input('id')); + if (!$payment) abort(500, '支付方式不存在'); + $payment->enable = !$payment->enable; + if (!$payment->save()) abort(500, '保存失败'); + return response([ + 'data' => true + ]); + } + + public function save(Request $request) + { + if (!config('v2board.app_url')) { + abort(500, '请在站点配置中配置站点地址'); + } + $params = $request->validate([ + 'name' => 'required', + 'icon' => 'nullable', + 'payment' => 'required', + 'config' => 'required', + 'notify_domain' => 'nullable|url', + 'handling_fee_fixed' => 'nullable|integer', + 'handling_fee_percent' => 'nullable|numeric|between:0.1,100' + ], [ + 'name.required' => '显示名称不能为空', + 'payment.required' => '网关参数不能为空', + 'config.required' => '配置参数不能为空', + 'notify_domain.url' => '自定义通知域名格式有误', + 'handling_fee_fixed.integer' => '固定手续费格式有误', + 'handling_fee_percent.between' => '百分比手续费范围须在0.1-100之间' + ]); + if ($request->input('id')) { + $payment = Payment::find($request->input('id')); + if (!$payment) abort(500, '支付方式不存在'); + try { + $payment->update($params); + } catch (\Exception $e) { + abort(500, $e->getMessage()); + } + return response([ + 'data' => true + ]); + } + $params['uuid'] = Helper::randomChar(8); + if (!Payment::create($params)) { + abort(500, '保存失败'); + } + return response([ + 'data' => true + ]); + } + + public function drop(Request $request) + { + $payment = Payment::find($request->input('id')); + if (!$payment) abort(500, '支付方式不存在'); + return response([ + 'data' => $payment->delete() + ]); + } + + + public function sort(Request $request) + { + $request->validate([ + 'ids' => 'required|array' + ], [ + 'ids.required' => '参数有误', + 'ids.array' => '参数有误' + ]); + DB::beginTransaction(); + foreach ($request->input('ids') as $k => $v) { + if (!Payment::find($v)->update(['sort' => $k + 1])) { + DB::rollBack(); + abort(500, '保存失败'); + } + } + DB::commit(); + return response([ + 'data' => true + ]); + } +} diff --git a/app/Http/Controllers/Admin/PlanController.php b/app/Http/Controllers/Admin/PlanController.php new file mode 100755 index 0000000..72659d4 --- /dev/null +++ b/app/Http/Controllers/Admin/PlanController.php @@ -0,0 +1,125 @@ +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 + ]); + } + + public function save(PlanSave $request) + { + $params = $request->validated(); + if ($request->input('id')) { + $plan = Plan::find($request->input('id')); + if (!$plan) { + abort(500, '该订阅不存在'); + } + DB::beginTransaction(); + // update user group id and transfer + try { + if ($request->input('force_update')) { + User::where('plan_id', $plan->id)->update([ + 'group_id' => $params['group_id'], + 'transfer_enable' => $params['transfer_enable'] * 1073741824, + 'speed_limit' => $params['speed_limit'] + ]); + } + $plan->update($params); + } catch (\Exception $e) { + DB::rollBack(); + abort(500, '保存失败'); + } + DB::commit(); + return response([ + 'data' => true + ]); + } + if (!Plan::create($params)) { + abort(500, '创建失败'); + } + return response([ + 'data' => true + ]); + } + + public function drop(Request $request) + { + if (Order::where('plan_id', $request->input('id'))->first()) { + abort(500, '该订阅下存在订单无法删除'); + } + if (User::where('plan_id', $request->input('id'))->first()) { + abort(500, '该订阅下存在用户无法删除'); + } + if ($request->input('id')) { + $plan = Plan::find($request->input('id')); + if (!$plan) { + abort(500, '该订阅ID不存在'); + } + } + return response([ + 'data' => $plan->delete() + ]); + } + + public function update(PlanUpdate $request) + { + $updateData = $request->only([ + 'show', + 'renew' + ]); + + $plan = Plan::find($request->input('id')); + if (!$plan) { + abort(500, '该订阅不存在'); + } + + try { + $plan->update($updateData); + } catch (\Exception $e) { + abort(500, '保存失败'); + } + + return response([ + 'data' => true + ]); + } + + public function sort(PlanSort $request) + { + DB::beginTransaction(); + foreach ($request->input('plan_ids') as $k => $v) { + if (!Plan::find($v)->update(['sort' => $k + 1])) { + DB::rollBack(); + abort(500, '保存失败'); + } + } + DB::commit(); + return response([ + 'data' => true + ]); + } +} diff --git a/app/Http/Controllers/Admin/Server/GroupController.php b/app/Http/Controllers/Admin/Server/GroupController.php new file mode 100644 index 0000000..0d50077 --- /dev/null +++ b/app/Http/Controllers/Admin/Server/GroupController.php @@ -0,0 +1,85 @@ +input('group_id')) { + return response([ + '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' => $serverGroups + ]); + } + + public function save(Request $request) + { + if (empty($request->input('name'))) { + abort(500, '组名不能为空'); + } + + if ($request->input('id')) { + $serverGroup = ServerGroup::find($request->input('id')); + } else { + $serverGroup = new ServerGroup(); + } + + $serverGroup->name = $request->input('name'); + return response([ + 'data' => $serverGroup->save() + ]); + } + + public function drop(Request $request) + { + if ($request->input('id')) { + $serverGroup = ServerGroup::find($request->input('id')); + if (!$serverGroup) { + abort(500, '组不存在'); + } + } + + $servers = ServerVmess::all(); + foreach ($servers as $server) { + if (in_array($request->input('id'), $server->group_id)) { + abort(500, '该组已被节点所使用,无法删除'); + } + } + + if (Plan::where('group_id', $request->input('id'))->first()) { + abort(500, '该组已被订阅所使用,无法删除'); + } + if (User::where('group_id', $request->input('id'))->first()) { + abort(500, '该组已被用户所使用,无法删除'); + } + return response([ + 'data' => $serverGroup->delete() + ]); + } +} diff --git a/app/Http/Controllers/Admin/Server/HysteriaController.php b/app/Http/Controllers/Admin/Server/HysteriaController.php new file mode 100644 index 0000000..3094831 --- /dev/null +++ b/app/Http/Controllers/Admin/Server/HysteriaController.php @@ -0,0 +1,113 @@ +validate([ + 'show' => '', + 'name' => 'required', + 'group_id' => 'required|array', + 'route_id' => 'nullable|array', + 'parent_id' => 'nullable|integer', + 'host' => 'required', + 'port' => 'required', + 'server_port' => 'required', + 'tags' => 'nullable|array', + 'rate' => 'required|numeric', + 'up_mbps' => 'required|numeric|min:1', + 'down_mbps' => 'required|numeric|min:1', + 'server_name' => 'nullable', + 'insecure' => 'required|in:0,1' + ]); + + if ($request->input('id')) { + $server = ServerHysteria::find($request->input('id')); + if (!$server) { + abort(500, '服务器不存在'); + } + try { + $server->update($params); + } catch (\Exception $e) { + abort(500, '保存失败'); + } + return response([ + 'data' => true + ]); + } + + if (!ServerHysteria::create($params)) { + abort(500, '创建失败'); + } + + return response([ + 'data' => true + ]); + } + + public function drop(Request $request) + { + if ($request->input('id')) { + $server = ServerHysteria::find($request->input('id')); + if (!$server) { + abort(500, '节点ID不存在'); + } + } + return response([ + 'data' => $server->delete() + ]); + } + + public function update(Request $request) + { + $request->validate([ + 'show' => 'in:0,1' + ], [ + 'show.in' => '显示状态格式不正确' + ]); + $params = $request->only([ + 'show', + ]); + + $server = ServerHysteria::find($request->input('id')); + + if (!$server) { + abort(500, '该服务器不存在'); + } + try { + $server->update($params); + } catch (\Exception $e) { + abort(500, '保存失败'); + } + + return response([ + 'data' => true + ]); + } + + public function copy(Request $request) + { + $server = ServerHysteria::find($request->input('id')); + $server->show = 0; + if (!$server) { + abort(500, '服务器不存在'); + } + if (!ServerHysteria::create($server->toArray())) { + abort(500, '复制失败'); + } + + return response([ + 'data' => true + ]); + } +} diff --git a/app/Http/Controllers/Admin/Server/ManageController.php b/app/Http/Controllers/Admin/Server/ManageController.php new file mode 100644 index 0000000..d901f85 --- /dev/null +++ b/app/Http/Controllers/Admin/Server/ManageController.php @@ -0,0 +1,44 @@ + $serverService->getAllServers() + ]); + } + + public function sort(Request $request) + { + ini_set('post_max_size', '1m'); + $params = $request->only( + 'shadowsocks', + 'vmess', + 'trojan', + 'hysteria' + ) ?? []; + DB::beginTransaction(); + foreach ($params as $k => $v) { + $model = 'App\\Models\\Server' . ucfirst($k); + foreach($v as $id => $sort) { + if (!$model::find($id)->update(['sort' => $sort])) { + DB::rollBack(); + abort(500, '保存失败'); + } + } + } + DB::commit(); + return response([ + 'data' => true + ]); + } +} diff --git a/app/Http/Controllers/Admin/Server/RouteController.php b/app/Http/Controllers/Admin/Server/RouteController.php new file mode 100644 index 0000000..c305270 --- /dev/null +++ b/app/Http/Controllers/Admin/Server/RouteController.php @@ -0,0 +1,72 @@ + $route) { + $array = json_decode($route->match, true); + if (is_array($array)) $routes[$k]['match'] = $array; + } + // TODO: remove on 1.8.0 + return [ + 'data' => $routes + ]; + } + + public function save(Request $request) + { + $params = $request->validate([ + 'remarks' => 'required', + 'match' => 'required|array', + 'action' => 'required|in:block,dns', + 'action_value' => 'nullable' + ], [ + 'remarks.required' => '备注不能为空', + 'match.required' => '匹配值不能为空', + 'action.required' => '动作类型不能为空', + 'action.in' => '动作类型参数有误' + ]); + $params['match'] = array_filter($params['match']); + // TODO: remove on 1.8.0 + $params['match'] = json_encode($params['match']); + // TODO: remove on 1.8.0 + if ($request->input('id')) { + try { + $route = ServerRoute::find($request->input('id')); + $route->update($params); + return [ + 'data' => true + ]; + } catch (\Exception $e) { + abort(500, '保存失败'); + } + } + if (!ServerRoute::create($params)) abort(500, '创建失败'); + return [ + 'data' => true + ]; + } + + public function drop(Request $request) + { + $route = ServerRoute::find($request->input('id')); + if (!$route) abort(500, '路由不存在'); + if (!$route->delete()) abort(500, '删除失败'); + return [ + 'data' => true + ]; + } +} diff --git a/app/Http/Controllers/Admin/Server/ShadowsocksController.php b/app/Http/Controllers/Admin/Server/ShadowsocksController.php new file mode 100644 index 0000000..5ac1261 --- /dev/null +++ b/app/Http/Controllers/Admin/Server/ShadowsocksController.php @@ -0,0 +1,91 @@ +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 + ]); + } +} diff --git a/app/Http/Controllers/Admin/Server/TrojanController.php b/app/Http/Controllers/Admin/Server/TrojanController.php new file mode 100644 index 0000000..68a318c --- /dev/null +++ b/app/Http/Controllers/Admin/Server/TrojanController.php @@ -0,0 +1,99 @@ +validated(); + if ($request->input('id')) { + $server = ServerTrojan::find($request->input('id')); + if (!$server) { + abort(500, '服务器不存在'); + } + try { + $server->update($params); + } catch (\Exception $e) { + abort(500, '保存失败'); + } + return response([ + 'data' => true + ]); + } + + if (!ServerTrojan::create($params)) { + abort(500, '创建失败'); + } + + return response([ + 'data' => true + ]); + } + + public function drop(Request $request) + { + if ($request->input('id')) { + $server = ServerTrojan::find($request->input('id')); + if (!$server) { + abort(500, '节点ID不存在'); + } + } + return response([ + 'data' => $server->delete() + ]); + } + + public function update(ServerTrojanUpdate $request) + { + $params = $request->only([ + 'show', + ]); + + $server = ServerTrojan::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 = ServerTrojan::find($request->input('id')); + $server->show = 0; + if (!$server) { + abort(500, '服务器不存在'); + } + if (!ServerTrojan::create($server->toArray())) { + abort(500, '复制失败'); + } + + return response([ + 'data' => true + ]); + } + public function viewConfig(Request $request) + { + $serverService = new ServerService(); + $config = $serverService->getTrojanConfig($request->input('node_id'), 23333); + return response([ + 'data' => $config + ]); + } +} diff --git a/app/Http/Controllers/Admin/Server/VmessController.php b/app/Http/Controllers/Admin/Server/VmessController.php new file mode 100644 index 0000000..f09565e --- /dev/null +++ b/app/Http/Controllers/Admin/Server/VmessController.php @@ -0,0 +1,92 @@ +validated(); + + if ($request->input('id')) { + $server = ServerVmess::find($request->input('id')); + if (!$server) { + abort(500, '服务器不存在'); + } + try { + $server->update($params); + } catch (\Exception $e) { + abort(500, '保存失败'); + } + return response([ + 'data' => true + ]); + } + + if (!ServerVmess::create($params)) { + abort(500, '创建失败'); + } + + return response([ + 'data' => true + ]); + } + + public function drop(Request $request) + { + if ($request->input('id')) { + $server = ServerVmess::find($request->input('id')); + if (!$server) { + abort(500, '节点ID不存在'); + } + } + return response([ + 'data' => $server->delete() + ]); + } + + public function update(ServerVmessUpdate $request) + { + $params = $request->only([ + 'show', + ]); + + $server = ServerVmess::find($request->input('id')); + + if (!$server) { + abort(500, '该服务器不存在'); + } + try { + $server->update($params); + } catch (\Exception $e) { + abort(500, '保存失败'); + } + + return response([ + 'data' => true + ]); + } + + public function copy(Request $request) + { + $server = ServerVmess::find($request->input('id')); + $server->show = 0; + if (!$server) { + abort(500, '服务器不存在'); + } + if (!ServerVmess::create($server->toArray())) { + abort(500, '复制失败'); + } + + return response([ + 'data' => true + ]); + } +} diff --git a/app/Http/Controllers/Admin/StatController.php b/app/Http/Controllers/Admin/StatController.php new file mode 100644 index 0000000..dccb00f --- /dev/null +++ b/app/Http/Controllers/Admin/StatController.php @@ -0,0 +1,227 @@ +validate([ + 'start_at' => '', + 'end_at' => '' + ]); + + if (isset($params['start_at']) && isset($params['end_at'])) { + $stats = Stat::where('record_at', '>=', $params['start_at']) + ->where('record_at', '<', $params['end_at']) + ->get() + ->makeHidden(['record_at', 'created_at', 'updated_at', 'id', 'record_type']) + ->toArray(); + } else { + $statisticalService = new StatisticalService(); + return [ + 'data' => $statisticalService->generateStatData() + ]; + } + + $stats = array_reduce($stats, function($carry, $item) { + foreach($item as $key => $value) { + if(isset($carry[$key]) && $carry[$key]) { + $carry[$key] += $value; + } else { + $carry[$key] = $value; + } + } + return $carry; + }, []); + + return [ + 'data' => $stats + ]; + } + + public function getStatRecord(Request $request) + { + $request->validate([ + 'type' => 'required|in:paid_total,commission_total,register_count', + 'start_at' => '', + 'end_at' => '' + ]); + + $statisticalService = new StatisticalService(); + $statisticalService->setStartAt($request->input('start_at')); + $statisticalService->setEndAt($request->input('end_at')); + return [ + 'data' => $statisticalService->getStatRecord($request->input('type')) + ]; + } + + public function getRanking(Request $request) + { + $request->validate([ + 'type' => 'required|in:server_traffic_rank,user_consumption_rank,invite_rank', + 'start_at' => '', + 'end_at' => '', + 'limit' => 'nullable|integer' + ]); + + $statisticalService = new StatisticalService(); + $statisticalService->setStartAt($request->input('start_at')); + $statisticalService->setEndAt($request->input('end_at')); + return [ + 'data' => $statisticalService->getRanking($request->input('type'), $request->input('limit') ?? 20) + ]; + } + + public function getOverride(Request $request) + { + return [ + 'data' => [ + 'month_income' => Order::where('created_at', '>=', strtotime(date('Y-m-1'))) + ->where('created_at', '<', time()) + ->whereNotIn('status', [0, 2]) + ->sum('total_amount'), + 'month_register_total' => User::where('created_at', '>=', strtotime(date('Y-m-1'))) + ->where('created_at', '<', time()) + ->count(), + 'ticket_pending_total' => Ticket::where('status', 0) + ->count(), + 'commission_pending_total' => Order::where('commission_status', 0) + ->where('invite_user_id', '!=', NULL) + ->whereNotIn('status', [0, 2]) + ->where('commission_balance', '>', 0) + ->count(), + 'day_income' => Order::where('created_at', '>=', strtotime(date('Y-m-d'))) + ->where('created_at', '<', time()) + ->whereNotIn('status', [0, 2]) + ->sum('total_amount'), + 'last_month_income' => Order::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1')))) + ->where('created_at', '<', strtotime(date('Y-m-1'))) + ->whereNotIn('status', [0, 2]) + ->sum('total_amount'), + 'commission_month_payout' => CommissionLog::where('created_at', '>=', strtotime(date('Y-m-1'))) + ->where('created_at', '<', time()) + ->sum('get_amount'), + 'commission_last_month_payout' => CommissionLog::where('created_at', '>=', strtotime('-1 month', strtotime(date('Y-m-1')))) + ->where('created_at', '<', strtotime(date('Y-m-1'))) + ->sum('get_amount'), + ] + ]; + } + + public function getOrder(Request $request) + { + $statistics = Stat::where('record_type', 'd') + ->limit(31) + ->orderBy('record_at', 'DESC') + ->get() + ->toArray(); + $result = []; + foreach ($statistics as $statistic) { + $date = date('m-d', $statistic['record_at']); + $result[] = [ + 'type' => '收款金额', + 'date' => $date, + 'value' => $statistic['paid_total'] / 100 + ]; + $result[] = [ + 'type' => '收款笔数', + 'date' => $date, + 'value' => $statistic['paid_count'] + ]; + $result[] = [ + 'type' => '佣金金额(已发放)', + 'date' => $date, + 'value' => $statistic['commission_total'] / 100 + ]; + $result[] = [ + 'type' => '佣金笔数(已发放)', + 'date' => $date, + 'value' => $statistic['commission_count'] + ]; + } + $result = array_reverse($result); + return [ + 'data' => $result + ]; + } + + public function getServerLastRank() + { + $servers = [ + 'shadowsocks' => ServerShadowsocks::where('parent_id', null)->get()->toArray(), + 'v2ray' => ServerVmess::where('parent_id', null)->get()->toArray(), + 'trojan' => ServerTrojan::where('parent_id', null)->get()->toArray(), + 'vmess' => ServerVmess::where('parent_id', null)->get()->toArray(), + 'hysteria' => ServerHysteria::where('parent_id', null)->get()->toArray() + ]; + $startAt = strtotime('-1 day', strtotime(date('Y-m-d'))); + $endAt = strtotime(date('Y-m-d')); + $statistics = StatServer::select([ + 'server_id', + 'server_type', + 'u', + 'd', + DB::raw('(u+d) as total') + ]) + ->where('record_at', '>=', $startAt) + ->where('record_at', '<', $endAt) + ->where('record_type', 'd') + ->limit(10) + ->orderBy('total', 'DESC') + ->get() + ->toArray(); + foreach ($statistics as $k => $v) { + foreach ($servers[$v['server_type']] as $server) { + if ($server['id'] === $v['server_id']) { + $statistics[$k]['server_name'] = $server['name']; + } + } + $statistics[$k]['total'] = $statistics[$k]['total'] / 1073741824; + } + array_multisort(array_column($statistics, 'total'), SORT_DESC, $statistics); + return [ + 'data' => $statistics + ]; + } + + public function getStatUser(Request $request) + { + $request->validate([ + 'user_id' => 'required|integer' + ]); + $current = $request->input('current') ? $request->input('current') : 1; + $pageSize = $request->input('pageSize') >= 10 ? $request->input('pageSize') : 10; + $builder = StatUser::orderBy('record_at', 'DESC')->where('user_id', $request->input('user_id')); + + $total = $builder->count(); + $records = $builder->forPage($current, $pageSize) + ->get(); + return [ + 'data' => $records, + 'total' => $total + ]; + } + +} + diff --git a/app/Http/Controllers/Admin/SystemController.php b/app/Http/Controllers/Admin/SystemController.php new file mode 100644 index 0000000..cf33b84 --- /dev/null +++ b/app/Http/Controllers/Admin/SystemController.php @@ -0,0 +1,105 @@ + [ + 'schedule' => $this->getScheduleStatus(), + 'horizon' => $this->getHorizonStatus(), + 'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null)) + ] + ]); + } + + public function getQueueWorkload(WorkloadRepository $workload) + { + return response([ + 'data' => collect($workload->get())->sortBy('name')->values()->toArray() + ]); + } + + protected function getScheduleStatus():bool + { + return (time() - 120) < Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null)); + } + + protected function getHorizonStatus():bool + { + if (! $masters = app(MasterSupervisorRepository::class)->all()) { + return false; + } + + return collect($masters)->contains(function ($master) { + return $master->status === 'paused'; + }) ? false : true; + } + + public function getQueueStats() + { + return response([ + 'data' => [ + 'failedJobs' => app(JobRepository::class)->countRecentlyFailed(), + 'jobsPerMinute' => app(MetricsRepository::class)->jobsProcessedPerMinute(), + 'pausedMasters' => $this->totalPausedMasters(), + 'periods' => [ + 'failedJobs' => config('horizon.trim.recent_failed', config('horizon.trim.failed')), + 'recentJobs' => config('horizon.trim.recent'), + ], + 'processes' => $this->totalProcessCount(), + 'queueWithMaxRuntime' => app(MetricsRepository::class)->queueWithMaximumRuntime(), + 'queueWithMaxThroughput' => app(MetricsRepository::class)->queueWithMaximumThroughput(), + 'recentJobs' => app(JobRepository::class)->countRecent(), + 'status' => $this->getHorizonStatus(), + 'wait' => collect(app(WaitTimeCalculator::class)->calculate())->take(1), + ] + ]); + } + + /** + * Get the total process count across all supervisors. + * + * @return int + */ + protected function totalProcessCount() + { + $supervisors = app(SupervisorRepository::class)->all(); + + return collect($supervisors)->reduce(function ($carry, $supervisor) { + return $carry + collect($supervisor->processes)->sum(); + }, 0); + } + + /** + * Get the number of master supervisors that are currently paused. + * + * @return int + */ + protected function totalPausedMasters() + { + if (! $masters = app(MasterSupervisorRepository::class)->all()) { + return 0; + } + + return collect($masters)->filter(function ($master) { + return $master->status === 'paused'; + })->count(); + } +} + diff --git a/app/Http/Controllers/Admin/ThemeController.php b/app/Http/Controllers/Admin/ThemeController.php new file mode 100644 index 0000000..7db9d44 --- /dev/null +++ b/app/Http/Controllers/Admin/ThemeController.php @@ -0,0 +1,91 @@ +path = $path = public_path('theme/'); + $this->themes = array_map(function ($item) use ($path) { + return str_replace($path, '', $item); + }, glob($path . '*')); + } + + public function getThemes() + { + $themeConfigs = []; + foreach ($this->themes as $theme) { + $themeConfigFile = $this->path . "{$theme}/config.json"; + if (!File::exists($themeConfigFile)) continue; + $themeConfig = json_decode(File::get($themeConfigFile), true); + if (!isset($themeConfig['configs']) || !is_array($themeConfig)) continue; + $themeConfigs[$theme] = $themeConfig; + if (config("theme.{$theme}")) continue; + $themeService = new ThemeService($theme); + $themeService->init(); + } + return response([ + 'data' => [ + 'themes' => $themeConfigs, + 'active' => config('v2board.frontend_theme', 'v2board') + ] + ]); + } + + public function getThemeConfig(Request $request) + { + $payload = $request->validate([ + 'name' => 'required|in:' . join(',', $this->themes) + ]); + return response([ + 'data' => config("theme.{$payload['name']}") + ]); + } + + public function saveThemeConfig(Request $request) + { + $payload = $request->validate([ + 'name' => 'required|in:' . join(',', $this->themes), + 'config' => 'required' + ]); + $payload['config'] = json_decode(base64_decode($payload['config']), true); + if (!$payload['config'] || !is_array($payload['config'])) abort(500, '参数有误'); + $themeConfigFile = public_path("theme/{$payload['name']}/config.json"); + if (!File::exists($themeConfigFile)) abort(500, '主题不存在'); + $themeConfig = json_decode(File::get($themeConfigFile), true); + if (!isset($themeConfig['configs']) || !is_array($themeConfig)) abort(500, '主题配置文件有误'); + $validateFields = array_column($themeConfig['configs'], 'field_name'); + $config = []; + foreach ($validateFields as $validateField) { + $config[$validateField] = isset($payload['config'][$validateField]) ? $payload['config'][$validateField] : ''; + } + + File::ensureDirectoryExists(base_path() . '/config/theme/'); + + $data = var_export($config, 1); + if (!File::put(base_path() . "/config/theme/{$payload['name']}.php", " $config + ]); + } +} diff --git a/app/Http/Controllers/Admin/TicketController.php b/app/Http/Controllers/Admin/TicketController.php new file mode 100644 index 0000000..18b2926 --- /dev/null +++ b/app/Http/Controllers/Admin/TicketController.php @@ -0,0 +1,96 @@ +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('updated_at', 'DESC'); + if ($request->input('status') !== NULL) { + $model->where('status', $request->input('status')); + } + if ($request->input('reply_status') !== NULL) { + $model->whereIn('reply_status', $request->input('reply_status')); + } + if ($request->input('email') !== NULL) { + $user = User::where('email', $request->input('email'))->first(); + if ($user) $model->where('user_id', $user->id); + } + $total = $model->count(); + $res = $model->forPage($current, $pageSize) + ->get(); + 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->user['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 + ]); + } +} diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php new file mode 100644 index 0000000..d4184ab --- /dev/null +++ b/app/Http/Controllers/Admin/UserController.php @@ -0,0 +1,294 @@ +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::select( + DB::raw('*'), + DB::raw('(u+d) as total_used') + ) + ->orderBy($sort, $sortType); + $this->filter($request, $userModel); + $total = $userModel->count(); + $res = $userModel->forPage($current, $pageSize) + ->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']; + } + } + $res[$i]['subscribe_url'] = Helper::getSubscribeUrl('/api/v1/client/subscribe?token=' . $res[$i]['token']); + } + return response([ + 'data' => $res, + 'total' => $total + ]); + } + + public function getUserInfoById(Request $request) + { + 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 + ]); + } + + 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; + } + if ($request->input('invite_user_email')) { + $inviteUser = User::where('email', $request->input('invite_user_email'))->first(); + if ($inviteUser) { + $params['invite_user_id'] = $inviteUser->id; + } + } else { + $params['invite_user_id'] = null; + } + + if (isset($params['banned']) && (int)$params['banned'] === 1) { + $authService = new AuthService($user); + $authService->removeAllSession(); + } + + try { + $user->update($params); + } catch (\Exception $e) { + abort(500, '保存失败'); + } + return response([ + '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"; + 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 = Helper::getSubscribeUrl('/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"; + 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 = Helper::getSubscribeUrl('/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 + ]); + } +} diff --git a/app/Http/Controllers/Client/ClientController.php b/app/Http/Controllers/Client/ClientController.php new file mode 100644 index 0000000..a994e3e --- /dev/null +++ b/app/Http/Controllers/Client/ClientController.php @@ -0,0 +1,62 @@ +input('flag') + ?? ($_SERVER['HTTP_USER_AGENT'] ?? ''); + $flag = strtolower($flag); + $user = $request->user; + // account not expired and is not banned. + $userService = new UserService(); + if ($userService->isAvailable($user)) { + $serverService = new ServerService(); + $servers = $serverService->getAvailableServers($user); + $this->setSubscribeInfoToServers($servers, $user); + if ($flag) { + foreach (array_reverse(glob(app_path('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()); + } + } + } + $class = new General($user, $servers); + die($class->handle()); + } + } + + private function setSubscribeInfoToServers(&$servers, $user) + { + if (!isset($servers[0])) return; + if (!(int)config('v2board.show_info_to_server_enable', 0)) return; + $useTraffic = $user['u'] + $user['d']; + $totalTraffic = $user['transfer_enable']; + $remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic); + $expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : 'No expiration'; + $userService = new UserService(); + $resetDay = $userService->getResetDay($user); + array_unshift($servers, array_merge($servers[0], [ + 'name' => "Expired at {$expiredDate}", + ])); + if ($resetDay) { + array_unshift($servers, array_merge($servers[0], [ + 'name' => "Reset usage after {$resetDay} day(s)", + ])); + } + array_unshift($servers, array_merge($servers[0], [ + 'name' => "Remain: {$remainingTraffic}", + ])); + } +} diff --git a/app/Http/Controllers/Client/Protocols/Clash.php b/app/Http/Controllers/Client/Protocols/Clash.php new file mode 100644 index 0000000..1d53250 --- /dev/null +++ b/app/Http/Controllers/Client/Protocols/Clash.php @@ -0,0 +1,272 @@ +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'] === 'vmess') { + if (is_array($item['tags']) && in_array("VLESS", $item['tags'])) { + array_push($proxy, self::buildVless($user['uuid'], $item)); + array_push($proxies, $item['name']); + } else { + 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']); + } + if ($item['type'] === 'hysteria') { + array_push($proxy, self::buildHysteria($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); + } + $config['proxy-groups'] = array_filter($config['proxy-groups'], function($group) { + return $group['proxies']; + }); + $config['proxy-groups'] = array_values($config['proxy-groups']); + // Force the current subscription domain to be a direct rule + $subsDomain = $_SERVER['HTTP_HOST']; + if ($subsDomain) { + array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT"); + } + + $yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); + $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::getServerKey($server['created_at'], 16); + $userKey = Helper::uuidToBase64($password, 16); + $password = "{$serverKey}:{$userKey}"; + } + if ($server['cipher'] === '2022-blake3-aes-256-gcm') { + $serverKey = Helper::getServerKey($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; + $array['cipher'] = 'zero'; + 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'] === 'tcp') { + $tcpSettings = $server['networkSettings']; + if (isset($tcpSettings['header']['type'])) $array['network'] = $tcpSettings['header']['type']; + if (isset($tcpSettings['header']['request']['path'][0])) $array['http-opts']['path'] = $tcpSettings['header']['request']['path'][0]; + } + 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']}?ed=4096"; + 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']}?ed=4096"; + if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host'])) + $array['ws-headers'] = ['Host' => $wsSettings['headers']['Host']]; + } + $array['max-early-data'] = 4096; + $array['early-data-header-name'] = 'Sec-WebSocket-Protocol'; + } + 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; + } + + public static function buildHysteria($password, $server) + { + $array = []; + $array['name'] = $server['name']; + $array['type'] = 'hysteria'; + $array['server'] = $server['host']; + $array['port'] = $server['port']; + $array['auth_str'] = $password; +// $array['obfs'] = $server['server_key']; + $array['protocol'] = 'udp'; + $array['up'] = $server['up_mbps']; + $array['down'] = $server['down_mbps']; + if (!empty($server['server_name'])) $array['sni'] = $server['server_name']; + $array['skip-cert-verify'] = !empty($server['insecure']) ? true : false; + return $array; + } + + public static function buildVless($uuid, $server) + { + $array = []; + $array['name'] = $server['name']; + $array['type'] = 'vless'; + $array['server'] = $server['host']; + $array['port'] = $server['port']; + $array['uuid'] = $uuid; + $array['udp'] = true; + + if ($server['tls']) { + $array['tls'] = true; + if (is_array($server['tags']) && in_array("VLESS", $server['tags']) && in_array("XTLS", $server['tags'])) { + $array['flow'] = "xtls-rprx-vision-udp443"; + } + 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'] === 'tcp') { + $tcpSettings = $server['networkSettings']; + if (isset($tcpSettings['header']['type'])) $array['network'] = $tcpSettings['header']['type']; + if (isset($tcpSettings['header']['request']['path'][0])) $array['http-opts']['path'] = $tcpSettings['header']['request']['path'][0]; + } + 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']}?ed=4096"; + 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']}?ed=4096"; + if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host'])) + $array['ws-headers'] = ['Host' => $wsSettings['headers']['Host']]; + } + $array['max-early-data'] = 4096; + $array['early-data-header-name'] = 'Sec-WebSocket-Protocol'; + } + 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; + } + + private function isMatch($exp, $str) + { + return @preg_match($exp, $str); + } + + private function isRegex($exp) + { + return @preg_match($exp, null) !== false; + } +} diff --git a/app/Http/Controllers/Client/Protocols/GCLH.php b/app/Http/Controllers/Client/Protocols/GCLH.php new file mode 100644 index 0000000..0f21b9e --- /dev/null +++ b/app/Http/Controllers/Client/Protocols/GCLH.php @@ -0,0 +1,272 @@ +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/vpn.clash.yaml'; + $customConfig = base_path() . '/resources/rules/customvpn.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'] === 'vmess') { + if (is_array($item['tags']) && in_array("VLESS", $item['tags'])) { + array_push($proxy, self::buildVless($user['uuid'], $item)); + array_push($proxies, $item['name']); + } else { + 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']); + } + if ($item['type'] === 'hysteria') { + array_push($proxy, self::buildHysteria($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); + } + $config['proxy-groups'] = array_filter($config['proxy-groups'], function($group) { + return $group['proxies']; + }); + $config['proxy-groups'] = array_values($config['proxy-groups']); + // Force the current subscription domain to be a direct rule + $subsDomain = $_SERVER['HTTP_HOST']; + if ($subsDomain) { + array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT"); + } + + $yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); + $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::getServerKey($server['created_at'], 16); + $userKey = Helper::uuidToBase64($password, 16); + $password = "{$serverKey}:{$userKey}"; + } + if ($server['cipher'] === '2022-blake3-aes-256-gcm') { + $serverKey = Helper::getServerKey($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; + $array['cipher'] = 'zero'; + 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'] === 'tcp') { + $tcpSettings = $server['networkSettings']; + if (isset($tcpSettings['header']['type'])) $array['network'] = $tcpSettings['header']['type']; + if (isset($tcpSettings['header']['request']['path'][0])) $array['http-opts']['path'] = $tcpSettings['header']['request']['path'][0]; + } + 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']}?ed=4096"; + 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']}?ed=4096"; + if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host'])) + $array['ws-headers'] = ['Host' => $wsSettings['headers']['Host']]; + } + $array['max-early-data'] = 4096; + $array['early-data-header-name'] = 'Sec-WebSocket-Protocol'; + } + 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; + } + + public static function buildHysteria($password, $server) + { + $array = []; + $array['name'] = $server['name']; + $array['type'] = 'hysteria'; + $array['server'] = $server['host']; + $array['port'] = $server['port']; + $array['auth_str'] = $password; +// $array['obfs'] = $server['server_key']; + $array['protocol'] = 'udp'; + $array['up'] = $server['up_mbps']; + $array['down'] = $server['down_mbps']; + if (!empty($server['server_name'])) $array['sni'] = $server['server_name']; + $array['skip-cert-verify'] = !empty($server['insecure']) ? true : false; + return $array; + } + + public static function buildVless($uuid, $server) + { + $array = []; + $array['name'] = $server['name']; + $array['type'] = 'vless'; + $array['server'] = $server['host']; + $array['port'] = $server['port']; + $array['uuid'] = $uuid; + $array['udp'] = true; + + if ($server['tls']) { + $array['tls'] = true; + if (is_array($server['tags']) && in_array("VLESS", $server['tags']) && in_array("XTLS", $server['tags'])) { + $array['flow'] = "xtls-rprx-vision-udp443"; + } + 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'] === 'tcp') { + $tcpSettings = $server['networkSettings']; + if (isset($tcpSettings['header']['type'])) $array['network'] = $tcpSettings['header']['type']; + if (isset($tcpSettings['header']['request']['path'][0])) $array['http-opts']['path'] = $tcpSettings['header']['request']['path'][0]; + } + 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']}?ed=4096"; + 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']}?ed=4096"; + if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host'])) + $array['ws-headers'] = ['Host' => $wsSettings['headers']['Host']]; + } + $array['max-early-data'] = 4096; + $array['early-data-header-name'] = 'Sec-WebSocket-Protocol'; + } + 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; + } + + private function isMatch($exp, $str) + { + return @preg_match($exp, $str); + } + + private function isRegex($exp) + { + return @preg_match($exp, null) !== false; + } +} diff --git a/app/Http/Controllers/Client/Protocols/General.php b/app/Http/Controllers/Client/Protocols/General.php new file mode 100644 index 0000000..675a05a --- /dev/null +++ b/app/Http/Controllers/Client/Protocols/General.php @@ -0,0 +1,182 @@ +user = $user; + $this->servers = $servers; + } + + public function handle() + { + $servers = $this->servers; + $user = $this->user; + $uri = ''; + + foreach ($servers as $item) { + if ($item['type'] === 'vmess') { + if (is_array($item['tags']) && in_array("VLESS", $item['tags'])) { + $uri .= self::buildVless($user['uuid'], $item); + } else { + $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); + } + if ($item['type'] === 'hysteria') { + $uri .= self::buildHysteria($user['uuid'], $item); + } + } + return base64_encode($uri); + } + + public static function buildShadowsocks($password, $server) + { + if ($server['cipher'] === '2022-blake3-aes-128-gcm') { + $serverKey = Helper::getServerKey($server['created_at'], 16); + $userKey = Helper::uuidToBase64($password, 16); + $password = "{$serverKey}:{$userKey}"; + } + if ($server['cipher'] === '2022-blake3-aes-256-gcm') { + $serverKey = Helper::getServerKey($server['created_at'], 32); + $userKey = Helper::uuidToBase64($password, 32); + $password = "{$serverKey}:{$userKey}"; + } + $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" => '0', + "net" => $server['network'], + "type" => "none", + "host" => "", + "path" => "", + "tls" => $server['tls'] ? "tls" : "", + ]; + if ($server['tls']) { + $config['fp'] = 'firefox'; + $config['scy'] = 'zero'; + if ($server['tlsSettings']) { + $tlsSettings = $server['tlsSettings']; + if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName'])) + $config['sni'] = $tlsSettings['serverName']; + } + } + if ((string)$server['network'] === 'tcp') { + $tcpSettings = $server['networkSettings']; + if (isset($tcpSettings['header']['type'])) $config['type'] = $tcpSettings['header']['type']; + if (isset($tcpSettings['header']['request']['path'][0])) $config['path'] = $tcpSettings['header']['request']['path'][0]; + if (isset($tcpSettings['header']['headers']['Host'][0])) $config['host'] = $tcpSettings['header']['headers']['Host'][0]; + } + if ((string)$server['network'] === 'ws') { + $wsSettings = $server['networkSettings']; + if (isset($wsSettings['path'])) $config['path'] = "${wsSettings['path']}?ed=4096"; + 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) + { + $remote = filter_var($server['host'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? '[' . $server['host'] . ']' : $server['host']; + $name = rawurlencode($server['name']); + $query = http_build_query([ + 'allowInsecure' => $server['allow_insecure'], + 'peer' => $server['server_name'], + 'sni' => $server['server_name'], + 'fp' => 'firefox' + ]); + $uri = "trojan://{$password}@{$remote}:{$server['port']}?{$query}#{$name}"; + $uri .= "\r\n"; + return $uri; + } + + public static function buildHysteria($password, $server) + { + $remote = filter_var($server['host'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? '[' . $server['host'] . ']' : $server['host']; + $name = rawurlencode($server['name']); + $query = http_build_query([ + 'protocol' => 'udp', + 'auth' => $password, + 'insecure' => $server['insecure'], + 'peer' => $server['server_name'], + 'upmbps' => $server['up_mbps'], + 'downmbps' => $server['up_mbps'] +// 'obfsParam' => $server['server_key'] + ]); + $uri = "hysteria://{$remote}:{$server['port']}?{$query}#{$name}"; + $uri .= "\r\n"; + return $uri; + } + public static function buildVless($uuid, $server) + { + $name = rawurlencode($server['name']); + $config = []; + $config['type'] = $server['network']; + $config['encryption'] = 'none'; + $config['security'] = $server['tls'] ? "tls" : "none"; + if ($server['tls']) { + $config['fp'] = 'firefox'; + if (is_array($server['tags']) && in_array("VLESS", $server['tags']) && in_array("XTLS", $server['tags'])) { + $config['flow'] = "xtls-rprx-vision-udp443"; + } + if ($server['tlsSettings']) { + $tlsSettings = $server['tlsSettings']; + if (isset($tlsSettings['serverName']) && !empty($tlsSettings['serverName'])) + $config['sni'] = $tlsSettings['serverName']; + } + } + if ((string)$server['network'] === 'tcp') { + $tcpSettings = $server['networkSettings']; + if (isset($tcpSettings['header']['type'])) $config['type'] = $tcpSettings['header']['type']; + if (isset($tcpSettings['header']['request']['path'][0])) $config['path'] = $tcpSettings['header']['request']['path'][0]; + if (isset($tcpSettings['header']['headers']['Host'][0])) $config['host'] = $tcpSettings['header']['headers']['Host'][0]; + } + if ((string)$server['network'] === 'ws') { + $wsSettings = $server['networkSettings']; + if (isset($wsSettings['path'])) $config['path'] = "${wsSettings['path']}?ed=4096"; + if (isset($wsSettings['headers']['Host'])) $config['host'] = $wsSettings['headers']['Host']; + } + if ((string)$server['network'] === 'grpc') { + $grpcSettings = $server['networkSettings']; + if (isset($grpcSettings['serviceName'])) $config['serviceName'] = $grpcSettings['serviceName']; + $config['mode'] = 'multi'; + } + $query = http_build_query($config); + $remote = filter_var($server['host'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? '[' . $server['host'] . ']' : $server['host']; + $uri = "vless://{$uuid}@{$remote}:{$server['port']}?{$query}#{$name}"; + $uri .= "\r\n"; + return $uri; + } +} diff --git a/app/Http/Controllers/Client/Protocols/QuantumultX.php b/app/Http/Controllers/Client/Protocols/QuantumultX.php new file mode 100644 index 0000000..61bab74 --- /dev/null +++ b/app/Http/Controllers/Client/Protocols/QuantumultX.php @@ -0,0 +1,116 @@ +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'] === 'vmess') { + $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; + } +} diff --git a/app/Http/Controllers/Client/Protocols/Shadowsocks.php b/app/Http/Controllers/Client/Protocols/Shadowsocks.php new file mode 100644 index 0000000..1b472e6 --- /dev/null +++ b/app/Http/Controllers/Client/Protocols/Shadowsocks.php @@ -0,0 +1,69 @@ +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) + { + $password = $user['uuid']; + if ($server['cipher'] === '2022-blake3-aes-128-gcm') { + $serverKey = Helper::getServerKey($server['created_at'], 16); + $userKey = Helper::uuidToBase64($password, 16); + $password = "{$serverKey}:{$userKey}"; + } + if ($server['cipher'] === '2022-blake3-aes-256-gcm') { + $serverKey = Helper::getServerKey($server['created_at'], 32); + $userKey = Helper::uuidToBase64($password, 32); + $password = "{$serverKey}:{$userKey}"; + } + $config = [ + "id" => $server['id'], + "remarks" => $server['name'], + "server" => $server['host'], + "server_port" => $server['port'], + "password" => $password, + "method" => $server['cipher'] + ]; + return $config; + } +} diff --git a/app/Http/Controllers/Client/Protocols/Stash.php b/app/Http/Controllers/Client/Protocols/Stash.php new file mode 100644 index 0000000..e6b46ba --- /dev/null +++ b/app/Http/Controllers/Client/Protocols/Stash.php @@ -0,0 +1,272 @@ +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'] === 'vmess') { + if (is_array($item['tags']) && in_array("VLESS", $item['tags'])) { + array_push($proxy, self::buildVless($user['uuid'], $item)); + array_push($proxies, $item['name']); + } else { + 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']); + } + if ($item['type'] === 'hysteria') { + array_push($proxy, self::buildHysteria($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); + } + $config['proxy-groups'] = array_filter($config['proxy-groups'], function($group) { + return $group['proxies']; + }); + $config['proxy-groups'] = array_values($config['proxy-groups']); + // Force the current subscription domain to be a direct rule + $subsDomain = $_SERVER['HTTP_HOST']; + if ($subsDomain) { + array_unshift($config['rules'], "DOMAIN,{$subsDomain},DIRECT"); + } + + $yaml = Yaml::dump($config, 2, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE); + $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::getServerKey($server['created_at'], 16); + $userKey = Helper::uuidToBase64($password, 16); + $password = "{$serverKey}:{$userKey}"; + } + if ($server['cipher'] === '2022-blake3-aes-256-gcm') { + $serverKey = Helper::getServerKey($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; + $array['cipher'] = 'zero'; + 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'] === 'tcp') { + $tcpSettings = $server['networkSettings']; + if (isset($tcpSettings['header']['type'])) $array['network'] = $tcpSettings['header']['type']; + if (isset($tcpSettings['header']['request']['path'][0])) $array['http-opts']['path'] = $tcpSettings['header']['request']['path'][0]; + } + 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']}?ed=4096"; + 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']}?ed=4096"; + if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host'])) + $array['ws-headers'] = ['Host' => $wsSettings['headers']['Host']]; + } + $array['max-early-data'] = 4096; + $array['early-data-header-name'] = 'Sec-WebSocket-Protocol'; + } + 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; + } + + public static function buildHysteria($password, $server) + { + $array = []; + $array['name'] = $server['name']; + $array['type'] = 'hysteria'; + $array['server'] = $server['host']; + $array['port'] = $server['port']; + $array['auth_str'] = $password; +// $array['obfs'] = $server['server_key']; + $array['protocol'] = 'udp'; + $array['up'] = $server['up_mbps']; + $array['down'] = $server['down_mbps']; + if (!empty($server['server_name'])) $array['sni'] = $server['server_name']; + $array['skip-cert-verify'] = !empty($server['insecure']) ? true : false; + return $array; + } + + public static function buildVless($uuid, $server) + { + $array = []; + $array['name'] = $server['name']; + $array['type'] = 'vless'; + $array['server'] = $server['host']; + $array['port'] = $server['port']; + $array['uuid'] = $uuid; + $array['udp'] = true; + + if ($server['tls']) { + $array['tls'] = true; + if (is_array($server['tags']) && in_array("VLESS", $server['tags']) && in_array("XTLS", $server['tags'])) { + $array['flow'] = "xtls-rprx-vision-udp443"; + } + 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'] === 'tcp') { + $tcpSettings = $server['networkSettings']; + if (isset($tcpSettings['header']['type'])) $array['network'] = $tcpSettings['header']['type']; + if (isset($tcpSettings['header']['request']['path'][0])) $array['http-opts']['path'] = $tcpSettings['header']['request']['path'][0]; + } + 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']}?ed=4096"; + 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']}?ed=4096"; + if (isset($wsSettings['headers']['Host']) && !empty($wsSettings['headers']['Host'])) + $array['ws-headers'] = ['Host' => $wsSettings['headers']['Host']]; + } + $array['max-early-data'] = 4096; + $array['early-data-header-name'] = 'Sec-WebSocket-Protocol'; + } + 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; + } + + private function isMatch($exp, $str) + { + return @preg_match($exp, $str); + } + + private function isRegex($exp) + { + return @preg_match($exp, null) !== false; + } +} diff --git a/app/Http/Controllers/Client/Protocols/Surfboard.php b/app/Http/Controllers/Client/Protocols/Surfboard.php new file mode 100644 index 0000000..cdaf7b8 --- /dev/null +++ b/app/Http/Controllers/Client/Protocols/Surfboard.php @@ -0,0 +1,161 @@ +user = $user; + $this->servers = $servers; + } + + public function handle() + { + $servers = $this->servers; + $user = $this->user; + + $appName = config('v2board.app_name', 'V2Board'); + header("content-disposition:attachment;filename*=UTF-8''".rawurlencode($appName).".conf"); + + $proxies = ''; + $proxyGroup = ''; + + foreach ($servers as $item) { + if ($item['type'] === 'shadowsocks' + && in_array($item['cipher'], [ + 'aes-128-gcm', + 'aes-192-gcm', + 'aes-256-gcm', + 'chacha20-ietf-poly1305' + ]) + ) { + // [Proxy] + $proxies .= self::buildShadowsocks($user['uuid'], $item); + // [Proxy Group] + $proxyGroup .= $item['name'] . ', '; + } + if ($item['type'] === 'vmess') { + // [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 = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}"); + $subsDomain = $_SERVER['HTTP_HOST']; + + $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); + + $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; + } + + + 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}", + "vmess-aead=true", + '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; + } +} diff --git a/app/Http/Controllers/Client/Protocols/Surge.php b/app/Http/Controllers/Client/Protocols/Surge.php new file mode 100644 index 0000000..ca7cd1b --- /dev/null +++ b/app/Http/Controllers/Client/Protocols/Surge.php @@ -0,0 +1,162 @@ +user = $user; + $this->servers = $servers; + } + + public function handle() + { + $servers = $this->servers; + $user = $this->user; + + $appName = config('v2board.app_name', 'V2Board'); + header("content-disposition:attachment;filename*=UTF-8''".rawurlencode($appName).".conf"); + + $proxies = ''; + $proxyGroup = ''; + + foreach ($servers as $item) { + if ($item['type'] === 'shadowsocks' + && in_array($item['cipher'], [ + 'aes-128-gcm', + 'aes-192-gcm', + 'aes-256-gcm', + 'chacha20-ietf-poly1305' + ]) + ) { + // [Proxy] + $proxies .= self::buildShadowsocks($user['uuid'], $item); + // [Proxy Group] + $proxyGroup .= $item['name'] . ', '; + } + if ($item['type'] === 'vmess') { + // [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 + $subsURL = Helper::getSubscribeUrl("/api/v1/client/subscribe?token={$user['token']}"); + $subsDomain = $_SERVER['HTTP_HOST']; + $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); + + $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; + } + + + 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}", + "vmess-aead=true", + '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; + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100755 index 0000000..af5cfb1 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,12 @@ + [ + 'tos_url' => config('v2board.tos_url'), + 'is_email_verify' => (int)config('v2board.email_verify', 0) ? 1 : 0, + 'is_invite_force' => (int)config('v2board.invite_force', 0) ? 1 : 0, + 'email_whitelist_suffix' => (int)config('v2board.email_whitelist_enable', 0) + ? $this->getEmailSuffix() + : 0, + 'is_recaptcha' => (int)config('v2board.recaptcha_enable', 0) ? 1 : 0, + 'recaptcha_site_key' => config('v2board.recaptcha_site_key'), + 'app_description' => config('v2board.app_description'), + 'app_url' => config('v2board.app_url'), + 'logo' => config('v2board.logo'), + ] + ]); + } + + private function getEmailSuffix() + { + $suffix = config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT); + if (!is_array($suffix)) { + return preg_split('/,/', $suffix); + } + return $suffix; + } +} diff --git a/app/Http/Controllers/Guest/PaymentController.php b/app/Http/Controllers/Guest/PaymentController.php new file mode 100644 index 0000000..271e39e --- /dev/null +++ b/app/Http/Controllers/Guest/PaymentController.php @@ -0,0 +1,49 @@ +notify($request->input()); + if (!$verify) abort(500, 'verify error'); + if (!$this->handle($verify['trade_no'], $verify['callback_no'])) { + abort(500, 'handle error'); + } + die(isset($verify['custom_result']) ? $verify['custom_result'] : 'success'); + } catch (\Exception $e) { + abort(500, 'fail'); + } + } + + private function handle($tradeNo, $callbackNo) + { + $order = Order::where('trade_no', $tradeNo)->first(); + if (!$order) { + abort(500, 'order is not found'); + } + if ($order->status !== 0) return true; + $orderService = new OrderService($order); + if (!$orderService->paid($callbackNo)) { + return false; + } + $telegramService = new TelegramService(); + $message = sprintf( + "💰成功收款%s元\n———————————————\n订单号:%s", + $order->total_amount / 100, + $order->trade_no + ); + $telegramService->sendMessageWithAdmin($message); + return true; + } +} diff --git a/app/Http/Controllers/Guest/PlanController.php b/app/Http/Controllers/Guest/PlanController.php new file mode 100755 index 0000000..cadc6f9 --- /dev/null +++ b/app/Http/Controllers/Guest/PlanController.php @@ -0,0 +1,18 @@ +get(); + return response([ + 'data' => $plan + ]); + } +} diff --git a/app/Http/Controllers/Guest/TelegramController.php b/app/Http/Controllers/Guest/TelegramController.php new file mode 100644 index 0000000..e0c6789 --- /dev/null +++ b/app/Http/Controllers/Guest/TelegramController.php @@ -0,0 +1,123 @@ +input('access_token') !== md5(config('v2board.telegram_bot_token'))) { + abort(401); + } + + $this->telegramService = new TelegramService(); + } + + public function webhook(Request $request) + { + $this->formatMessage($request->input()); + $this->formatChatJoinRequest($request->input()); + $this->handle(); + } + + public function handle() + { + if (!$this->msg) return; + $msg = $this->msg; + $commandName = explode('@', $msg->command); + + // To reduce request, only commands contains @ will get the bot name + if (count($commandName) == 2) { + $botName = $this->getBotName(); + if ($commandName[1] === $botName){ + $msg->command = $commandName[0]; + } + } + + try { + foreach (glob(base_path('app//Plugins//Telegram//Commands') . '/*.php') as $file) { + $command = basename($file, '.php'); + $class = '\\App\\Plugins\\Telegram\\Commands\\' . $command; + if (!class_exists($class)) continue; + $instance = new $class(); + if ($msg->message_type === 'message') { + if (!isset($instance->command)) continue; + if ($msg->command !== $instance->command) continue; + $instance->handle($msg); + return; + } + if ($msg->message_type === 'reply_message') { + if (!isset($instance->regex)) continue; + if (!preg_match($instance->regex, $msg->reply_text, $match)) continue; + $instance->handle($msg, $match); + return; + } + } + } catch (\Exception $e) { + $this->telegramService->sendMessage($msg->chat_id, $e->getMessage()); + } + } + + public function getBotName() + { + $response = $this->telegramService->getMe(); + return $response->result->username; + } + + private function formatMessage(array $data) + { + if (!isset($data['message'])) return; + if (!isset($data['message']['text'])) return; + $obj = new \StdClass(); + $text = explode(' ', $data['message']['text']); + $obj->command = $text[0]; + $obj->args = array_slice($text, 1); + $obj->chat_id = $data['message']['chat']['id']; + $obj->message_id = $data['message']['message_id']; + $obj->message_type = 'message'; + $obj->text = $data['message']['text']; + $obj->is_private = $data['message']['chat']['type'] === 'private'; + if (isset($data['message']['reply_to_message']['text'])) { + $obj->message_type = 'reply_message'; + $obj->reply_text = $data['message']['reply_to_message']['text']; + } + $this->msg = $obj; + } + + private function formatChatJoinRequest(array $data) + { + if (!isset($data['chat_join_request'])) return; + if (!isset($data['chat_join_request']['from']['id'])) return; + if (!isset($data['chat_join_request']['chat']['id'])) return; + $user = \App\Models\User::where('telegram_id', $data['chat_join_request']['from']['id']) + ->first(); + if (!$user) { + $this->telegramService->declineChatJoinRequest( + $data['chat_join_request']['chat']['id'], + $data['chat_join_request']['from']['id'] + ); + return; + } + $userService = new \App\Services\UserService(); + if (!$userService->isAvailable($user)) { + $this->telegramService->declineChatJoinRequest( + $data['chat_join_request']['chat']['id'], + $data['chat_join_request']['from']['id'] + ); + return; + } + $userService = new \App\Services\UserService(); + $this->telegramService->approveChatJoinRequest( + $data['chat_join_request']['chat']['id'], + $data['chat_join_request']['from']['id'] + ); + } +} diff --git a/app/Http/Controllers/Passport/AuthController.php b/app/Http/Controllers/Passport/AuthController.php new file mode 100644 index 0000000..1b47f1c --- /dev/null +++ b/app/Http/Controllers/Passport/AuthController.php @@ -0,0 +1,307 @@ +validate([ + 'email' => 'required|email:strict', + 'redirect' => 'nullable' + ]); + + if (Cache::get(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $params['email']))) { + abort(500, __('Sending frequently, please try again later')); + } + + $user = User::where('email', $params['email'])->first(); + if (!$user) { + return response([ + 'data' => true + ]); + } + + $code = Helper::guid(); + $key = CacheKey::get('TEMP_TOKEN', $code); + Cache::put($key, $user->id, 300); + Cache::put(CacheKey::get('LAST_SEND_LOGIN_WITH_MAIL_LINK_TIMESTAMP', $params['email']), time(), 60); + + + $redirect = '/#/login?verify=' . $code . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard'); + if (config('v2board.app_url')) { + $link = config('v2board.app_url') . $redirect; + } else { + $link = url($redirect); + } + + SendEmailJob::dispatch([ + 'email' => $user->email, + 'subject' => __('Login to :name', [ + 'name' => config('v2board.app_name', 'V2Board') + ]), + 'template_name' => 'login', + 'template_value' => [ + 'name' => config('v2board.app_name', 'V2Board'), + 'link' => $link, + 'url' => config('v2board.app_url') + ] + ]); + + return response([ + 'data' => $link + ]); + + } + + public function register(AuthRegister $request) + { + if ((int)config('v2board.register_limit_by_ip_enable', 0)) { + $registerCountByIP = Cache::get(CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip())) ?? 0; + if ((int)$registerCountByIP >= (int)config('v2board.register_limit_count', 3)) { + abort(500, __('Register frequently, please try again after :minute minute', [ + 'minute' => config('v2board.register_limit_expire', 60) + ])); + } + } + if ((int)config('v2board.recaptcha_enable', 0)) { + $recaptcha = new ReCaptcha(config('v2board.recaptcha_key')); + $recaptchaResp = $recaptcha->verify($request->input('recaptcha_data')); + if (!$recaptchaResp->isSuccess()) { + abort(500, __('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, __('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 alias is not supported')); + } + } + if ((int)config('v2board.stop_register', 0)) { + abort(500, __('Registration has closed')); + } + if ((int)config('v2board.invite_force', 0)) { + if (empty($request->input('invite_code'))) { + 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, __('Email verification code cannot be empty')); + } + if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) { + abort(500, __('Incorrect email verification code')); + } + } + $email = $request->input('email'); + $password = $request->input('password'); + $exist = User::where('email', $email)->first(); + if ($exist) { + abort(500, __('Email already exists')); + } + $user = new User(); + $user->email = $email; + $user->password = password_hash($password, PASSWORD_DEFAULT); + $user->uuid = Helper::guid(true); + $user->token = Helper::guid(); + if ($request->input('invite_code')) { + $inviteCode = InviteCode::where('code', $request->input('invite_code')) + ->where('status', 0) + ->first(); + if (!$inviteCode) { + if ((int)config('v2board.invite_force', 0)) { + abort(500, __('Invalid invitation code')); + } + } else { + $user->invite_user_id = $inviteCode->user_id ? $inviteCode->user_id : null; + if (!(int)config('v2board.invite_never_expire', 0)) { + $inviteCode->status = 1; + $inviteCode->save(); + } + } + } + + // try out + if ((int)config('v2board.try_out_plan_id', 0)) { + $plan = Plan::find(config('v2board.try_out_plan_id')); + if ($plan) { + $user->transfer_enable = $plan->transfer_enable * 1073741824; + $user->plan_id = $plan->id; + $user->group_id = $plan->group_id; + $user->expired_at = time() + (config('v2board.try_out_hour', 1) * 3600); + $user->speed_limit = $plan->speed_limit; + } + } + + if (!$user->save()) { + abort(500, __('Register failed')); + } + if ((int)config('v2board.email_verify', 0)) { + Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))); + } + + $user->last_login_at = time(); + $user->save(); + + if ((int)config('v2board.register_limit_by_ip_enable', 0)) { + Cache::put( + CacheKey::get('REGISTER_IP_RATE_LIMIT', $request->ip()), + (int)$registerCountByIP + 1, + (int)config('v2board.register_limit_expire', 60) * 60 + ); + } + + $authService = new AuthService($user); + + return response()->json([ + 'data' => $authService->generateAuthData($request) + ]); + } + + public function login(AuthLogin $request) + { + $email = $request->input('email'); + $password = $request->input('password'); + + if ((int)config('v2board.password_limit_enable', 1)) { + $passwordErrorCount = (int)Cache::get(CacheKey::get('PASSWORD_ERROR_LIMIT', $email), 0); + if ($passwordErrorCount >= (int)config('v2board.password_limit_count', 5)) { + abort(500, __('There are too many password errors, please try again after :minute minutes.', [ + 'minute' => config('v2board.password_limit_expire', 60) + ])); + } + } + + $user = User::where('email', $email)->first(); + if (!$user) { + abort(500, __('Incorrect email or password')); + } + if (!Helper::multiPasswordVerify( + $user->password_algo, + $user->password_salt, + $password, + $user->password) + ) { + if ((int)config('v2board.password_limit_enable')) { + Cache::put( + CacheKey::get('PASSWORD_ERROR_LIMIT', $email), + (int)$passwordErrorCount + 1, + 60 * (int)config('v2board.password_limit_expire', 60) + ); + } + abort(500, __('Incorrect email or password')); + } + + if ($user->banned) { + abort(500, __('Your account has been suspended')); + } + + $authService = new AuthService($user); + return response([ + 'data' => $authService->generateAuthData($request) + ]); + } + + public function token2Login(Request $request) + { + if ($request->input('token')) { + $redirect = '/#/login?verify=' . $request->input('token') . '&redirect=' . ($request->input('redirect') ? $request->input('redirect') : 'dashboard'); + if (config('v2board.app_url')) { + $location = config('v2board.app_url') . $redirect; + } else { + $location = url($redirect); + } + 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, __('Token error')); + } + $user = User::find($userId); + if (!$user) { + abort(500, __('The user does not ')); + } + if ($user->banned) { + abort(500, __('Your account has been suspended')); + } + Cache::forget($key); + $authService = new AuthService($user); + return response([ + 'data' => $authService->generateAuthData($request) + ]); + } + } + + public function getQuickLoginUrl(Request $request) + { + $authorization = $request->input('auth_data') ?? $request->header('authorization'); + if (!$authorization) abort(403, '未登录或登陆已过期'); + + $user = AuthService::decryptAuthData($authorization); + if (!$user) abort(403, '未登录或登陆已过期'); + + $code = Helper::guid(); + $key = CacheKey::get('TEMP_TOKEN', $code); + Cache::put($key, $user['id'], 60); + $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 forget(AuthForget $request) + { + if ((string)Cache::get(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))) !== (string)$request->input('email_code')) { + abort(500, __('Incorrect email verification code')); + } + $user = User::where('email', $request->input('email'))->first(); + if (!$user) { + 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, __('Reset failed')); + } + Cache::forget(CacheKey::get('EMAIL_VERIFY_CODE', $request->input('email'))); + return response([ + 'data' => true + ]); + } +} diff --git a/app/Http/Controllers/Passport/CommController.php b/app/Http/Controllers/Passport/CommController.php new file mode 100644 index 0000000..9376601 --- /dev/null +++ b/app/Http/Controllers/Passport/CommController.php @@ -0,0 +1,82 @@ + (int)config('v2board.email_verify', 0) ? 1 : 0 + ]); + } + + 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, __('Email verification code has been sent, please request again later')); + } + $code = rand(100000, 999999); + $subject = config('v2board.app_name', 'V2Board') . __('Email verification code'); + + SendEmailJob::dispatch([ + 'email' => $email, + 'subject' => $subject, + 'template_name' => 'verify', + 'template_value' => [ + 'name' => config('v2board.app_name', 'V2Board'), + 'code' => $code, + 'url' => config('v2board.app_url') + ] + ]); + + Cache::put(CacheKey::get('EMAIL_VERIFY_CODE', $email), $code, 300); + Cache::put(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email), time(), 60); + return response([ + 'data' => true + ]); + } + + public function pv(Request $request) + { + $inviteCode = InviteCode::where('code', $request->input('invite_code'))->first(); + if ($inviteCode) { + $inviteCode->pv = $inviteCode->pv + 1; + $inviteCode->save(); + } + + return response([ + 'data' => true + ]); + } + + private function getEmailSuffix() + { + $suffix = config('v2board.email_whitelist_suffix', Dict::EMAIL_WHITELIST_SUFFIX_DEFAULT); + if (!is_array($suffix)) { + return preg_split('/,/', $suffix); + } + return $suffix; + } +} diff --git a/app/Http/Controllers/Server/UniProxyController.php b/app/Http/Controllers/Server/UniProxyController.php new file mode 100644 index 0000000..c9d8446 --- /dev/null +++ b/app/Http/Controllers/Server/UniProxyController.php @@ -0,0 +1,139 @@ +input('token'); + if (empty($token)) { + abort(500, 'token is null'); + } + if ($token !== config('v2board.server_token')) { + abort(500, 'token is error'); + } + $this->nodeType = $request->input('node_type'); + if ($this->nodeType === 'v2ray') $this->nodeType = 'vmess'; + $this->nodeId = $request->input('node_id'); + $this->serverService = new ServerService(); + $this->nodeInfo = $this->serverService->getServer($this->nodeId, $this->nodeType); + if (!$this->nodeInfo) abort(500, 'server is not exist'); + } + + // 后端获取用户 + public function user(Request $request) + { + ini_set('memory_limit', -1); + Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_LAST_CHECK_AT', $this->nodeInfo->id), time(), 3600); + $users = $this->serverService->getAvailableUsers($this->nodeInfo->group_id); + $users = $users->toArray(); + + $response['users'] = $users; + + $eTag = sha1(json_encode($response)); + if (strpos($request->header('If-None-Match'), $eTag) !== false ) { + abort(304); + } + + return response($response)->header('ETag', "\"{$eTag}\""); + } + + // 后端提交数据 + public function push(Request $request) + { + $data = file_get_contents('php://input'); + $data = json_decode($data, true); + Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_ONLINE_USER', $this->nodeInfo->id), count($data), 3600); + Cache::put(CacheKey::get('SERVER_' . strtoupper($this->nodeType) . '_LAST_PUSH_AT', $this->nodeInfo->id), time(), 3600); + $userService = new UserService(); + $userService->trafficFetch($this->nodeInfo->toArray(), $this->nodeType, $data); + + return response([ + 'data' => true + ]); + } + + // 后端获取配置 + public function config(Request $request) + { + switch ($this->nodeType) { + case 'shadowsocks': + $response = [ + 'server_port' => $this->nodeInfo->server_port, + 'cipher' => $this->nodeInfo->cipher, + 'obfs' => $this->nodeInfo->obfs, + 'obfs_settings' => $this->nodeInfo->obfs_settings + ]; + + if ($this->nodeInfo->cipher === '2022-blake3-aes-128-gcm') { + $response['server_key'] = Helper::getServerKey($this->nodeInfo->created_at, 16); + } + if ($this->nodeInfo->cipher === '2022-blake3-aes-256-gcm') { + $response['server_key'] = Helper::getServerKey($this->nodeInfo->created_at, 32); + } + break; + case 'vmess': + $response = [ + 'server_port' => $this->nodeInfo->server_port, + 'network' => $this->nodeInfo->network, + 'networkSettings' => $this->nodeInfo->networkSettings, + 'tls' => $this->nodeInfo->tls + ]; + + if (is_array($this->nodeInfo->tags) && in_array("VLESS", $this->nodeInfo->tags)) { + $response['vless'] = true; + } else { + $response['vless'] = false; + } + break; + case 'trojan': + $response = [ + 'host' => $this->nodeInfo->host, + 'server_port' => $this->nodeInfo->server_port, + 'server_name' => $this->nodeInfo->server_name, + ]; + break; + case 'hysteria': + $response = [ + 'host' => $this->nodeInfo->host, + 'server_port' => $this->nodeInfo->server_port, + 'server_name' => $this->nodeInfo->server_name, + 'up_mbps' => $this->nodeInfo->up_mbps, + 'down_mbps' => $this->nodeInfo->down_mbps, + 'obfs' => Helper::getServerKey($this->nodeInfo->created_at, 16) + ]; + break; + } + $response['base_config'] = [ + 'push_interval' => (int)config('v2board.server_push_interval', 60), + 'pull_interval' => (int)config('v2board.server_pull_interval', 60) + ]; + if ($this->nodeInfo['route_id']) { + $response['routes'] = $this->serverService->getRoutes($this->nodeInfo['route_id']); + } + $eTag = sha1(json_encode($response)); + if (strpos($request->header('If-None-Match'), $eTag) !== false ) { + abort(304); + } + + return response($response)->header('ETag', "\"{$eTag}\""); + } +} diff --git a/app/Http/Controllers/Staff/NoticeController.php b/app/Http/Controllers/Staff/NoticeController.php new file mode 100644 index 0000000..e2e6d5e --- /dev/null +++ b/app/Http/Controllers/Staff/NoticeController.php @@ -0,0 +1,59 @@ + 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 + ]); + } +} diff --git a/app/Http/Controllers/Staff/PlanController.php b/app/Http/Controllers/Staff/PlanController.php new file mode 100755 index 0000000..7e48f1f --- /dev/null +++ b/app/Http/Controllers/Staff/PlanController.php @@ -0,0 +1,41 @@ +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 + ]); + } +} diff --git a/app/Http/Controllers/Staff/TicketController.php b/app/Http/Controllers/Staff/TicketController.php new file mode 100644 index 0000000..592c2a4 --- /dev/null +++ b/app/Http/Controllers/Staff/TicketController.php @@ -0,0 +1,85 @@ +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(); + 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->user['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 + ]); + } +} diff --git a/app/Http/Controllers/Staff/UserController.php b/app/Http/Controllers/Staff/UserController.php new file mode 100644 index 0000000..bf16f1d --- /dev/null +++ b/app/Http/Controllers/Staff/UserController.php @@ -0,0 +1,107 @@ +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 + ]); + } +} diff --git a/app/Http/Controllers/User/CommController.php b/app/Http/Controllers/User/CommController.php new file mode 100644 index 0000000..49a44ca --- /dev/null +++ b/app/Http/Controllers/User/CommController.php @@ -0,0 +1,41 @@ + [ + 'is_telegram' => (int)config('v2board.telegram_bot_enable', 0), + 'telegram_discuss_link' => config('v2board.telegram_discuss_link'), + 'stripe_pk' => config('v2board.stripe_pk_live'), + 'withdraw_methods' => config('v2board.commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT), + 'withdraw_close' => (int)config('v2board.withdraw_close_enable', 0), + 'currency' => config('v2board.currency', 'CNY'), + 'currency_symbol' => config('v2board.currency_symbol', '¥'), + 'commission_distribution_enable' => (int)config('v2board.commission_distribution_enable', 0), + 'commission_distribution_l1' => config('v2board.commission_distribution_l1'), + 'commission_distribution_l2' => config('v2board.commission_distribution_l2'), + 'commission_distribution_l3' => config('v2board.commission_distribution_l3') + ] + ]); + } + + public function getStripePublicKey(Request $request) + { + $payment = Payment::where('id', $request->input('id')) + ->where('payment', 'StripeCredit') + ->first(); + if (!$payment) abort(500, 'payment is not found'); + return response([ + 'data' => $payment->config['stripe_pk_live'] + ]); + } +} diff --git a/app/Http/Controllers/User/CouponController.php b/app/Http/Controllers/User/CouponController.php new file mode 100644 index 0000000..52e80ca --- /dev/null +++ b/app/Http/Controllers/User/CouponController.php @@ -0,0 +1,25 @@ +input('code'))) { + abort(500, __('Coupon cannot be empty')); + } + $couponService = new CouponService($request->input('code')); + $couponService->setPlanId($request->input('plan_id')); + $couponService->setUserId($request->user['id']); + $couponService->check(); + return response([ + 'data' => $couponService->getCoupon() + ]); + } +} diff --git a/app/Http/Controllers/User/InviteController.php b/app/Http/Controllers/User/InviteController.php new file mode 100644 index 0000000..9d5abc7 --- /dev/null +++ b/app/Http/Controllers/User/InviteController.php @@ -0,0 +1,88 @@ +user['id'])->where('status', 0)->count() >= config('v2board.invite_gen_limit', 5)) { + abort(500, __('The maximum number of creations has been reached')); + } + $inviteCode = new InviteCode(); + $inviteCode->user_id = $request->user['id']; + $inviteCode->code = Helper::randomChar(8); + return response([ + 'data' => $inviteCode->save() + ]); + } + + public function details(Request $request) + { + $current = $request->input('current') ? $request->input('current') : 1; + $pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10; + $builder = CommissionLog::where('invite_user_id', $request->user['id']) + ->where('get_amount', '>', 0) + ->select([ + 'id', + 'trade_no', + 'order_amount', + 'get_amount', + 'created_at' + ]) + ->orderBy('created_at', 'DESC'); + $total = $builder->count(); + $details = $builder->forPage($current, $pageSize) + ->get(); + return response([ + 'data' => $details, + 'total' => $total + ]); + } + + public function fetch(Request $request) + { + $codes = InviteCode::where('user_id', $request->user['id']) + ->where('status', 0) + ->get(); + $commission_rate = config('v2board.invite_commission', 10); + $user = User::find($request->user['id']); + if ($user->commission_rate) { + $commission_rate = $user->commission_rate; + } + $uncheck_commission_balance = (int)Order::where('status', 3) + ->where('commission_status', 0) + ->where('invite_user_id', $request->user['id']) + ->sum('commission_balance'); + if (config('v2board.commission_distribution_enable', 0)) { + $uncheck_commission_balance = $uncheck_commission_balance * (config('v2board.commission_distribution_l1') / 100); + } + $stat = [ + //已注册用户数 + (int)User::where('invite_user_id', $request->user['id'])->count(), + //有效的佣金 + (int)CommissionLog::where('invite_user_id', $request->user['id']) + ->sum('get_amount'), + //确认中的佣金 + $uncheck_commission_balance, + //佣金比例 + (int)$commission_rate, + //可用佣金 + (int)$user->commission_balance + ]; + return response([ + 'data' => [ + 'codes' => $codes, + 'stat' => $stat + ] + ]); + } +} diff --git a/app/Http/Controllers/User/KnowledgeController.php b/app/Http/Controllers/User/KnowledgeController.php new file mode 100644 index 0000000..51c7673 --- /dev/null +++ b/app/Http/Controllers/User/KnowledgeController.php @@ -0,0 +1,73 @@ +input('id')) { + $knowledge = Knowledge::where('id', $request->input('id')) + ->where('show', 1) + ->first() + ->toArray(); + if (!$knowledge) abort(500, __('Article does not exist')); + $user = User::find($request->user['id']); + $userService = new UserService(); + if (!$userService->isAvailable($user)) { + $this->formatAccessData($knowledge['body']); + } + $subscribeUrl = Helper::getSubscribeUrl("/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 + ]); + } + $builder = Knowledge::select(['id', 'category', 'title', 'updated_at']) + ->where('language', $request->input('language')) + ->where('show', 1) + ->orderBy('sort', 'ASC'); + $keyword = $request->input('keyword'); + if ($keyword) { + $builder = $builder->where(function ($query) use ($keyword) { + $query->where('title', 'LIKE', "%{$keyword}%") + ->orWhere('body', 'LIKE', "%{$keyword}%"); + }); + } + + $knowledges = $builder->get() + ->groupBy('category'); + return response([ + 'data' => $knowledges + ]); + } + + private function formatAccessData(&$body) + { + function getBetween($input, $start, $end){$substr = substr($input, strlen($start)+strpos($input, $start),(strlen($input) - strpos($input, $end))*(-1));return $start . $substr . $end;} + while (strpos($body, '') !== false) { + $accessData = getBetween($body, '', ''); + if ($accessData) { + $body = str_replace($accessData, '
2?n-2:0),i=2;i 1&&(a*=d(w),s*=d(w));var x=(i===o?-1:1)*d((a*a*(s*s)-a*a*(v*v)-s*s*(f*f))/(a*a*(v*v)+s*s*(f*f)))||0,_=x*a*v/s,E=x*-s*f/a,S=(e+n)/2+m(h)*_-p(h)*E,k=(t+r)/2+p(h)*_+m(h)*E,C=b([1,0],[(f-_)/a,(v-E)/s]),O=[(f-_)/a,(v-E)/s],T=[(-1*f-_)/a,(-1*v-E)/s],L=b(O,T);if(y(O,T)<=-1&&(L=g),y(O,T)>=1&&(L=0),L<0){var A=Math.round(L/g*1e6)/1e6;L=2*g+A%2*g}u.addData(l,S,k,a,s,C,L,h,o)}var x=/([mlvhzcqtsa])([^mlvhzcqtsa]*)/gi,_=/-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g;function E(e){var t=new o["a"];if(!e)return t;var n,r=0,i=0,a=r,s=i,c=o["a"].CMD,l=e.match(x);if(!l)return t;for(var u=0;u-1&&(s=o?s.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+s.split("\n").map(function(e){return" "+e}).join("\n"))):s=e.stylize("[Circular]","special")),S(a)){if(o&&i.match(/^\d+$/))return s;a=JSON.stringify(""+i),a.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(a=a.substr(1,a.length-2),a=e.stylize(a,"name")):(a=a.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),a=e.stylize(a,"string"))}return a+": "+s}function g(e,t,n){var r=e.reduce(function(e,t){return 0,t.indexOf("\n")>=0&&0,e+t.replace(/\u001b\[\d\d?m/g,"").length+1},0);return r>60?n[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+n[1]:n[0]+t+" "+e.join(", ")+" "+n[1]}function v(e){return Array.isArray(e)}function y(e){return"boolean"===typeof e}function b(e){return null===e}function w(e){return null==e}function x(e){return"number"===typeof e}function _(e){return"string"===typeof e}function E(e){return"symbol"===typeof e}function S(e){return void 0===e}function k(e){return C(e)&&"[object RegExp]"===P(e)}function C(e){return"object"===typeof e&&null!==e}function O(e){return C(e)&&"[object Date]"===P(e)}function T(e){return C(e)&&("[object Error]"===P(e)||e instanceof Error)}function L(e){return"function"===typeof e}function A(e){return null===e||"boolean"===typeof e||"number"===typeof e||"string"===typeof e||"symbol"===typeof e||"undefined"===typeof e}function P(e){return Object.prototype.toString.call(e)}function j(e){return e<10?"0"+e.toString(10):e.toString(10)}t.debuglog=function(n){if(S(o)&&(o=Object({NODE_ENV:"production"}).NODE_DEBUG||""),n=n.toUpperCase(),!a[n])if(new RegExp("\\b"+n+"\\b","i").test(o)){var r=e.pid;a[n]=function(){var e=t.format.apply(t,arguments);console.error("%s %d: %s",n,r,e)}}else a[n]=function(){};return a[n]},t.inspect=s,s.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},s.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"},t.isArray=v,t.isBoolean=y,t.isNull=b,t.isNullOrUndefined=w,t.isNumber=x,t.isString=_,t.isSymbol=E,t.isUndefined=S,t.isRegExp=k,t.isObject=C,t.isDate=O,t.isError=T,t.isFunction=L,t.isPrimitive=A,t.isBuffer=n("j/1Z");var M=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function R(){var e=new Date,t=[j(e.getHours()),j(e.getMinutes()),j(e.getSeconds())].join(":");return[e.getDate(),M[e.getMonth()],t].join(" ")}function N(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.log=function(){console.log("%s - %s",R(),t.format.apply(t,arguments))},t.inherits=n("FfBw"),t._extend=function(e,t){if(!t||!C(t))return e;var n=Object.keys(t),r=n.length;while(r--)e[n[r]]=t[n[r]];return e};var D="undefined"!==typeof Symbol?Symbol("util.promisify.custom"):void 0;function I(e,t){if(!e){var n=new Error("Promise was rejected with a falsy value");n.reason=e,e=n}return t(e)}function $(t){if("function"!==typeof t)throw new TypeError('The "original" argument must be of type Function');function n(){for(var n=[],r=0;r0)a=c,c=1+(c<<1),c<=0&&(c=s);c>s&&(c=s),a+=i,c+=i}else{s=i+1;while(cs&&(c=s);var l=a;a=i-c,c=i-l}a++;while(as&&(c=s);var l=a;a=i-c,c=i-l}else{s=r-i;while(c=0)a=c,c=1+(c<<1),c<=0&&(c=s);c>s&&(c=s),a+=i,c+=i}a++;while(a=0;h--)e[g+h]=e[m+h];if(0===r){w=!0;break}}if(e[p--]=c[d--],1===--s){w=!0;break}if(b=s-l(e[f],c,0,s,s-1,t),0!==b){for(p-=b,d-=b,s-=b,g=p+1,m=d+1,h=0;h=i||b>=i);if(w)break;v<0&&(v=0),v+=2}if(o=v,o<1&&(o=1),1===s){for(p-=r,f-=r,g=p+1,m=f+1,h=r-1;h>=0;h--)e[g+h]=e[m+h];e[p]=c[d]}else{if(0===s)throw new Error;for(m=p-(s-1),h=0;h=0;h--)e[g+h]=e[m+h];e[p]=c[d]}else for(m=p-(s-1),h=0;hf&&(d=f),c(e,n,n+d,n+l,t),l=d}u.pushRun(n,l),u.mergeRuns(),s-=l,n+=l}while(0!==s);u.forceMergeRuns()}}},BjZs:function(e,t,n){"use strict";function r(e){return s(e)||a(e)||o(e)||i()}function i(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function o(e,t){if(e){if("string"===typeof e)return c(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(n):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?c(e,t):void 0}}function a(e){if("undefined"!==typeof Symbol&&Symbol.iterator in Object(e))return Array.from(e)}function s(e){if(Array.isArray(e))return c(e)}function c(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n_((c-n)/w)&&S("overflow"),n+=(s-t)*w,t=s,a=0;ac&&S("overflow"),y==t){for(f=n,d=l;;d+=l){if(v=d<=o?u:d>=o+h?h:d-o,f{this.getData()},3e3)}render(){var e=this.props.system,t=e.queueStats,n=(e.getQueueStatsLoading,e.queueWorkload);e.getQueueWorkloadLoading;return s.a.createElement(c["a"],i()({},this.props,{title:"\u961f\u5217\u76d1\u63a7"}),s.a.createElement(u["a"],{loading:!t},s.a.createElement("div",{className:"block block-rounded "},s.a.createElement("div",{className:"block-header block-header-default"},s.a.createElement("h3",{className:"block-title"},"\u603b\u89c8")),s.a.createElement("div",{className:"block-content p-0"},s.a.createElement("div",{className:"row no-gutters"},s.a.createElement("div",{className:"col-lg-6 col-xl-3 border-right p-4 border-bottom"},s.a.createElement("div",null,s.a.createElement("div",null,"\u5f53\u524d\u4f5c\u4e1a\u91cf"),s.a.createElement("div",{className:"mt-4 font-size-h3"},(null===t||void 0===t?void 0:t.jobsPerMinute)||"0"))),s.a.createElement("div",{className:"col-lg-6 col-xl-3 border-right p-4 border-bottom"},s.a.createElement("div",null,s.a.createElement("div",null,"\u8fd1\u4e00\u5c0f\u65f6\u5904\u7406\u91cf"),s.a.createElement("div",{className:"mt-4 font-size-h3"},(null===t||void 0===t?void 0:t.recentJobs)||"0"))),s.a.createElement("div",{className:"col-lg-6 col-xl-3 border-right p-4 border-bottom"},s.a.createElement("div",null,s.a.createElement("div",null,"7\u65e5\u5185\u62a5\u9519\u6570\u91cf"),s.a.createElement("div",{className:"mt-4 font-size-h3"},(null===t||void 0===t?void 0:t.failedJobs)||"0"))),s.a.createElement("div",{className:"col-lg-6 col-xl-3 p-4 border-bottom overflow-hidden"},s.a.createElement("div",null,s.a.createElement("div",null,"\u72b6\u6001"),s.a.createElement("div",{className:"mt-4 font-size-h3"},t&&((null===t||void 0===t?void 0:t.status)?"\u8fd0\u884c\u4e2d":"\u672a\u542f\u52a8")),t&&((null===t||void 0===t?void 0:t.status)?s.a.createElement("i",{class:"si si-check text-success",style:{position:"absolute",fontSize:100,right:-20,bottom:-20}}):s.a.createElement("i",{class:"si si-close text-danger",style:{position:"absolute",fontSize:100,right:-20,bottom:-20}})))))))),s.a.createElement(u["a"],{loading:!n},s.a.createElement("div",{className:"block block-rounded "},s.a.createElement("div",{className:"block-header block-header-default"},s.a.createElement("h3",{className:"block-title"},"\u5f53\u524d\u4f5c\u4e1a\u8be6\u60c5")),s.a.createElement("div",{className:"block-content p-0"},s.a.createElement(o["a"],{columns:[{title:"\u961f\u5217\u540d\u79f0",dataIndex:"name",key:"name",render:e=>{var t={order_handle:"\u8ba2\u5355\u961f\u5217",send_email:"\u90ae\u4ef6\u961f\u5217",send_email_mass:"\u90ae\u4ef6\u7fa4\u53d1\u961f\u5217",send_telegram:"Telegram\u6d88\u606f\u961f\u5217",stat:"\u7edf\u8ba1\u961f\u5217",traffic_fetch:"\u6d41\u91cf\u6d88\u8d39\u961f\u5217"};return t[e]}},{title:"\u4f5c\u4e1a\u91cf",dataIndex:"processes",key:"processes"},{title:"\u4efb\u52a1\u91cf",dataIndex:"length",key:"length"},{title:"\u5360\u7528\u65f6\u95f4",dataIndex:"wait",key:"wait",align:"right",render:e=>e+"s"}],dataSource:n&&n.filter(e=>"default"!==e.name),pagination:!1})))))}}t["default"]=Object(l["c"])(e=>{var t=e.system;return{system:t}})(h)},Ji7U:function(e,t,n){"use strict";n.d(t,"a",function(){return i});var r=n("s4An");function i(e,t){if("function"!==typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&Object(r["a"])(e,t)}},KQm4:function(e,t,n){"use strict";var r=n("a3WO");function i(e){if(Array.isArray(e))return Object(r["a"])(e)}var o=n("25BE"),a=n("BsWD");function s(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function c(e){return i(e)||Object(o["a"])(e)||Object(a["a"])(e)||s()}n.d(t,"a",function(){return c})},KUxP:function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},KbcA:function(e,t,n){"use strict";var r=n("QbLZ"),i=n.n(r),o=n("iCc5"),a=n.n(o),s=n("FYw3"),c=n.n(s),l=n("mRg0"),u=n.n(l),h=n("q1tI"),f=n.n(h),d=n("17x9"),p=n.n(d),m=n("4IlW"),g=n("VCL8"),v=n("2zpS"),y=n("JDzL"),b=n("jBZG"),w=n("F4Vz"),x=n("wd/R"),_=n.n(x),E=function(e){function t(n){a()(this,t);var r=c()(this,e.call(this,n));return r.onKeyDown=function(e){var t=e.keyCode,n=e.ctrlKey||e.metaKey,i=r.state.value,o=r.props.disabledDate,a=i;switch(t){case m["a"].DOWN:a=i.clone(),a.add(3,"months");break;case m["a"].UP:a=i.clone(),a.add(-3,"months");break;case m["a"].LEFT:a=i.clone(),n?a.add(-1,"years"):a.add(-1,"months");break;case m["a"].RIGHT:a=i.clone(),n?a.add(1,"years"):a.add(1,"months");break;case m["a"].ENTER:return o&&o(i)||r.onSelect(i),e.preventDefault(),1;default:return}if(a!==i)return r.setValue(a),e.preventDefault(),1},r.handlePanelChange=function(e,t){"date"!==t&&r.setState({mode:t})},r.state={mode:"month",value:n.value||n.defaultValue||_()(),selectedValue:n.selectedValue||n.defaultSelectedValue},r}return u()(t,e),t.prototype.render=function(){var e=this.props,t=this.state,n=t.mode,r=t.value,i=f.a.createElement("div",{className:e.prefixCls+"-month-calendar-content"},f.a.createElement("div",{className:e.prefixCls+"-month-header-wrap"},f.a.createElement(v["a"],{prefixCls:e.prefixCls,mode:n,value:r,locale:e.locale,disabledMonth:e.disabledDate,monthCellRender:e.monthCellRender,monthCellContentRender:e.monthCellContentRender,onMonthSelect:this.onSelect,onValueChange:this.setValue,onPanelChange:this.handlePanelChange})),f.a.createElement(y["a"],{prefixCls:e.prefixCls,renderFooter:e.renderFooter}));return this.renderRoot({className:e.prefixCls+"-month-calendar",children:i})},t}(f.a.Component);E.propTypes=i()({},b["b"],w["c"],{monthCellRender:p.a.func,value:p.a.object,defaultValue:p.a.object,selectedValue:p.a.object,defaultSelectedValue:p.a.object,disabledDate:p.a.func}),E.defaultProps=i()({},w["b"],b["a"]),t["a"]=Object(g["polyfill"])(Object(b["c"])(Object(w["a"])(E)))},Kwbf:function(e,t,n){"use strict";var r={};function i(e,t){0}function o(e,t,n){t||r[n]||(e(!1,n),r[n]=!0)}function a(e,t){o(i,e,t)}t["a"]=a},KyW6:function(e,t,n){"use strict";n.r(t);n("Y/ft"),n("qIgq");var r=n("p0pE"),i=n.n(r),o=n("1l/V"),a=n.n(o),s=(n("0wlq"),n("dcFJ"),n("VxKu"),n("QsMh"),n("kgWH"),n("/gYn"),n("Q6cQ"),n("nwK/"),n("O42g"),n("XrRV"),n("jN/G"),n("PkQq"),n("er1Y"),n("/mWb"),n("jjMW"),n("OHgp"),n("EEQl"),n("HXXR"),n("kWR5"),n("Bz7s"),n("lZXM"),n("DBt0"),n("hIUm"),n("G7Hh"),n("DFAo"),n("0sxA"),n("rUcv"),n("3m+/"),n("9nSz"),n("IR7R"),n("UQt1"),n("u2w5"),n("zxrt"),n("Bus3"),n("OR3X"),n("o175"),n("XP1/"),n("w8uh"),n("HCMe"),n("QEzc"),n("QeHl"),n("SPFY"),n("7RDE"),n("fKm+"),n("N4uP"),n("zr8x"),n("zQzA"),n("wOl0"),n("RFCh"),n("q1tI")),c=n.n(s),l=n("i8i4"),u=n.n(l),h=n("sa7a"),f=n.n(h);function d(){d=function(){return e};var e={},t=Object.prototype,n=t.hasOwnProperty,r=Object.defineProperty||function(e,t,n){e[t]=n.value},i="function"==typeof Symbol?Symbol:{},o=i.iterator||"@@iterator",a=i.asyncIterator||"@@asyncIterator",s=i.toStringTag||"@@toStringTag";function c(e,t,n){return Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}),e[t]}try{c({},"")}catch(e){c=function(e,t,n){return e[t]=n}}function l(e,t,n,i){var o=t&&t.prototype instanceof f?t:f,a=Object.create(o.prototype),s=new C(i||[]);return r(a,"_invoke",{value:_(e,n,s)}),a}function u(e,t,n){try{return{type:"normal",arg:e.call(t,n)}}catch(e){return{type:"throw",arg:e}}}e.wrap=l;var h={};function f(){}function p(){}function m(){}var g={};c(g,o,function(){return this});var v=Object.getPrototypeOf,y=v&&v(v(O([])));y&&y!==t&&n.call(y,o)&&(g=y);var b=m.prototype=f.prototype=Object.create(g);function w(e){["next","throw","return"].forEach(function(t){c(e,t,function(e){return this._invoke(t,e)})})}function x(e,t){function i(r,o,a,s){var c=u(e[r],e,o);if("throw"!==c.type){var l=c.arg,h=l.value;return h&&"object"==typeof h&&n.call(h,"__await")?t.resolve(h.__await).then(function(e){i("next",e,a,s)},function(e){i("throw",e,a,s)}):t.resolve(h).then(function(e){l.value=e,a(l)},function(e){return i("throw",e,a,s)})}s(c.arg)}var o;r(this,"_invoke",{value:function(e,n){function r(){return new t(function(t,r){i(e,n,t,r)})}return o=o?o.then(r,r):r()}})}function _(e,t,n){var r="suspendedStart";return function(i,o){if("executing"===r)throw new Error("Generator is already running");if("completed"===r){if("throw"===i)throw o;return T()}for(n.method=i,n.arg=o;;){var a=n.delegate;if(a){var s=E(a,n);if(s){if(s===h)continue;return s}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if("suspendedStart"===r)throw r="completed",n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);r="executing";var c=u(e,t,n);if("normal"===c.type){if(r=n.done?"completed":"suspendedYield",c.arg===h)continue;return{value:c.arg,done:n.done}}"throw"===c.type&&(r="completed",n.method="throw",n.arg=c.arg)}}}function E(e,t){var n=t.method,r=e.iterator[n];if(void 0===r)return t.delegate=null,"throw"===n&&e.iterator.return&&(t.method="return",t.arg=void 0,E(e,t),"throw"===t.method)||"return"!==n&&(t.method="throw",t.arg=new TypeError("The iterator does not provide a '"+n+"' method")),h;var i=u(r,e.iterator,t.arg);if("throw"===i.type)return t.method="throw",t.arg=i.arg,t.delegate=null,h;var o=i.arg;return o?o.done?(t[e.resultName]=o.value,t.next=e.nextLoc,"return"!==t.method&&(t.method="next",t.arg=void 0),t.delegate=null,h):o:(t.method="throw",t.arg=new TypeError("iterator result is not an object"),t.delegate=null,h)}function S(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function k(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function C(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(S,this),this.reset(!0)}function O(e){if(e){var t=e[o];if(t)return t.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var r=-1,i=function t(){for(;++r=0&&x