first commit
This commit is contained in:
90
Xboard/app/Http/Controllers/V1/Client/AppController.php
Normal file
90
Xboard/app/Http/Controllers/V1/Client/AppController.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Client;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class AppController extends Controller
|
||||
{
|
||||
public function getConfig(Request $request)
|
||||
{
|
||||
$servers = [];
|
||||
$user = $request->user();
|
||||
$userService = new UserService();
|
||||
if ($userService->isAvailable($user)) {
|
||||
$servers = ServerService::getAvailableServers($user);
|
||||
}
|
||||
$defaultConfig = base_path() . '/resources/rules/app.clash.yaml';
|
||||
$customConfig = base_path() . '/resources/rules/custom.app.clash.yaml';
|
||||
if (File::exists($customConfig)) {
|
||||
$config = Yaml::parseFile($customConfig);
|
||||
} else {
|
||||
$config = Yaml::parseFile($defaultConfig);
|
||||
}
|
||||
$proxy = [];
|
||||
$proxies = [];
|
||||
|
||||
foreach ($servers as $item) {
|
||||
$protocol_settings = $item['protocol_settings'];
|
||||
if ($item['type'] === 'shadowsocks'
|
||||
&& in_array(data_get($protocol_settings, 'cipher'), [
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'chacha20-ietf-poly1305'
|
||||
])
|
||||
) {
|
||||
array_push($proxy, \App\Protocols\Clash::buildShadowsocks($user['uuid'], $item));
|
||||
array_push($proxies, $item['name']);
|
||||
}
|
||||
if ($item['type'] === 'vmess') {
|
||||
array_push($proxy, \App\Protocols\Clash::buildVmess($user['uuid'], $item));
|
||||
array_push($proxies, $item['name']);
|
||||
}
|
||||
if ($item['type'] === 'trojan') {
|
||||
array_push($proxy, \App\Protocols\Clash::buildTrojan($user['uuid'], $item));
|
||||
array_push($proxies, $item['name']);
|
||||
}
|
||||
}
|
||||
|
||||
$config['proxies'] = array_merge($config['proxies'] ? $config['proxies'] : [], $proxy);
|
||||
foreach ($config['proxy-groups'] as $k => $v) {
|
||||
$config['proxy-groups'][$k]['proxies'] = array_merge($config['proxy-groups'][$k]['proxies'], $proxies);
|
||||
}
|
||||
return(Yaml::dump($config));
|
||||
}
|
||||
|
||||
public function getVersion(Request $request)
|
||||
{
|
||||
if (strpos($request->header('user-agent'), 'tidalab/4.0.0') !== false
|
||||
|| strpos($request->header('user-agent'), 'tunnelab/4.0.0') !== false
|
||||
) {
|
||||
if (strpos($request->header('user-agent'), 'Win64') !== false) {
|
||||
$data = [
|
||||
'version' => admin_setting('windows_version'),
|
||||
'download_url' => admin_setting('windows_download_url')
|
||||
];
|
||||
} else {
|
||||
$data = [
|
||||
'version' => admin_setting('macos_version'),
|
||||
'download_url' => admin_setting('macos_download_url')
|
||||
];
|
||||
}
|
||||
}else{
|
||||
$data = [
|
||||
'windows_version' => admin_setting('windows_version'),
|
||||
'windows_download_url' => admin_setting('windows_download_url'),
|
||||
'macos_version' => admin_setting('macos_version'),
|
||||
'macos_download_url' => admin_setting('macos_download_url'),
|
||||
'android_version' => admin_setting('android_version'),
|
||||
'android_download_url' => admin_setting('android_download_url')
|
||||
];
|
||||
}
|
||||
return $this->success($data);
|
||||
}
|
||||
}
|
||||
247
Xboard/app/Http/Controllers/V1/Client/ClientController.php
Normal file
247
Xboard/app/Http/Controllers/V1/Client/ClientController.php
Normal file
@@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Client;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Server;
|
||||
use App\Protocols\General;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ClientController extends Controller
|
||||
{
|
||||
/**
|
||||
* Protocol prefix mapping for server names
|
||||
*/
|
||||
private const PROTOCOL_PREFIXES = [
|
||||
'hysteria' => [
|
||||
1 => '[Hy]',
|
||||
2 => '[Hy2]'
|
||||
],
|
||||
'vless' => '[vless]',
|
||||
'shadowsocks' => '[ss]',
|
||||
'vmess' => '[vmess]',
|
||||
'trojan' => '[trojan]',
|
||||
'tuic' => '[tuic]',
|
||||
'socks' => '[socks]',
|
||||
'anytls' => '[anytls]'
|
||||
];
|
||||
|
||||
|
||||
public function subscribe(Request $request)
|
||||
{
|
||||
HookManager::call('client.subscribe.before');
|
||||
$request->validate([
|
||||
'types' => ['nullable', 'string'],
|
||||
'filter' => ['nullable', 'string'],
|
||||
'flag' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$userService = new UserService();
|
||||
|
||||
if (!$userService->isAvailable($user)) {
|
||||
HookManager::call('client.subscribe.unavailable');
|
||||
return response('', 403, ['Content-Type' => 'text/plain']);
|
||||
}
|
||||
|
||||
return $this->doSubscribe($request, $user);
|
||||
}
|
||||
|
||||
public function doSubscribe(Request $request, $user, $servers = null)
|
||||
{
|
||||
if ($servers === null) {
|
||||
$servers = ServerService::getAvailableServers($user);
|
||||
$servers = HookManager::filter('client.subscribe.servers', $servers, $user, $request);
|
||||
}
|
||||
|
||||
$clientInfo = $this->getClientInfo($request);
|
||||
|
||||
$requestedTypes = $this->parseRequestedTypes($request->input('types'));
|
||||
$filterKeywords = $this->parseFilterKeywords($request->input('filter'));
|
||||
|
||||
$protocolClassName = app('protocols.manager')->matchProtocolClassName($clientInfo['flag'])
|
||||
?? General::class;
|
||||
|
||||
$serversFiltered = $this->filterServers(
|
||||
servers: $servers,
|
||||
allowedTypes: $requestedTypes,
|
||||
filterKeywords: $filterKeywords
|
||||
);
|
||||
|
||||
$this->setSubscribeInfoToServers($serversFiltered, $user, count($servers) - count($serversFiltered));
|
||||
$serversFiltered = $this->addPrefixToServerName($serversFiltered);
|
||||
|
||||
// Instantiate the protocol class with filtered servers and client info
|
||||
$protocolInstance = app()->make($protocolClassName, [
|
||||
'user' => $user,
|
||||
'servers' => $serversFiltered,
|
||||
'clientName' => $clientInfo['name'] ?? null,
|
||||
'clientVersion' => $clientInfo['version'] ?? null,
|
||||
'userAgent' => $clientInfo['flag'] ?? null
|
||||
]);
|
||||
|
||||
return $protocolInstance->handle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the input string for requested server types.
|
||||
*/
|
||||
private function parseRequestedTypes(?string $typeInputString): array
|
||||
{
|
||||
if (blank($typeInputString) || $typeInputString === 'all') {
|
||||
return Server::VALID_TYPES;
|
||||
}
|
||||
|
||||
$requested = collect(preg_split('/[|,|]+/', $typeInputString))
|
||||
->map(fn($type) => trim($type))
|
||||
->filter() // Remove empty strings that might result from multiple delimiters
|
||||
->all();
|
||||
|
||||
return array_values(array_intersect($requested, Server::VALID_TYPES));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the input string for filter keywords.
|
||||
*/
|
||||
private function parseFilterKeywords(?string $filterInputString): ?array
|
||||
{
|
||||
if (blank($filterInputString) || mb_strlen($filterInputString) > 20) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return collect(preg_split('/[|,|]+/', $filterInputString))
|
||||
->map(fn($keyword) => trim($keyword))
|
||||
->filter() // Remove empty strings
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters servers based on allowed types and keywords.
|
||||
*/
|
||||
private function filterServers(array $servers, array $allowedTypes, ?array $filterKeywords): array
|
||||
{
|
||||
return collect($servers)->filter(function ($server) use ($allowedTypes, $filterKeywords) {
|
||||
// Condition 1: Server type must be in the list of allowed types
|
||||
if ($allowedTypes && !in_array($server['type'], $allowedTypes)) {
|
||||
return false; // Filter out (don't keep)
|
||||
}
|
||||
|
||||
// Condition 2: If filterKeywords are provided, at least one keyword must match
|
||||
if (!empty($filterKeywords)) { // Check if $filterKeywords is not empty
|
||||
$keywordMatch = collect($filterKeywords)->contains(function ($keyword) use ($server) {
|
||||
return stripos($server['name'], $keyword) !== false
|
||||
|| in_array($keyword, $server['tags'] ?? []);
|
||||
});
|
||||
if (!$keywordMatch) {
|
||||
return false; // Filter out if no keywords match
|
||||
}
|
||||
}
|
||||
// Keep the server if its type is allowed AND (no filter keywords OR at least one keyword matched)
|
||||
return true;
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
private function getClientInfo(Request $request): array
|
||||
{
|
||||
$flag = strtolower($request->input('flag') ?? $request->header('User-Agent', ''));
|
||||
|
||||
$clientName = null;
|
||||
$clientVersion = null;
|
||||
|
||||
if (preg_match('/([a-zA-Z0-9\-_]+)[\/\s]+(v?[0-9]+(?:\.[0-9]+){0,2})/', $flag, $matches)) {
|
||||
$potentialName = strtolower($matches[1]);
|
||||
$clientVersion = preg_replace('/^v/', '', $matches[2]);
|
||||
|
||||
if (in_array($potentialName, app('protocols.flags'))) {
|
||||
$clientName = $potentialName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$clientName) {
|
||||
$flags = collect(app('protocols.flags'))->sortByDesc(fn($f) => strlen($f))->values()->all();
|
||||
foreach ($flags as $name) {
|
||||
if (stripos($flag, $name) !== false) {
|
||||
$clientName = $name;
|
||||
if (!$clientVersion) {
|
||||
$pattern = '/' . preg_quote($name, '/') . '[\/\s]+(v?[0-9]+(?:\.[0-9]+){0,2})/i';
|
||||
if (preg_match($pattern, $flag, $vMatches)) {
|
||||
$clientVersion = preg_replace('/^v/', '', $vMatches[1]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$clientVersion) {
|
||||
if (preg_match('/\/v?(\d+(?:\.\d+){0,2})/', $flag, $matches)) {
|
||||
$clientVersion = $matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'flag' => $flag,
|
||||
'name' => $clientName,
|
||||
'version' => $clientVersion
|
||||
];
|
||||
}
|
||||
|
||||
private function setSubscribeInfoToServers(&$servers, $user, $rejectServerCount = 0)
|
||||
{
|
||||
if (!isset($servers[0]))
|
||||
return;
|
||||
if ($rejectServerCount > 0) {
|
||||
array_unshift($servers, array_merge($servers[0], [
|
||||
'name' => "过滤掉{$rejectServerCount}条线路",
|
||||
]));
|
||||
}
|
||||
if (!(int) admin_setting('show_info_to_server_enable', 0))
|
||||
return;
|
||||
$useTraffic = $user['u'] + $user['d'];
|
||||
$totalTraffic = $user['transfer_enable'];
|
||||
$remainingTraffic = Helper::trafficConvert($totalTraffic - $useTraffic);
|
||||
$expiredDate = $user['expired_at'] ? date('Y-m-d', $user['expired_at']) : __('长期有效');
|
||||
$userService = new UserService();
|
||||
$resetDay = $userService->getResetDay($user);
|
||||
array_unshift($servers, array_merge($servers[0], [
|
||||
'name' => "套餐到期:{$expiredDate}",
|
||||
]));
|
||||
if ($resetDay) {
|
||||
array_unshift($servers, array_merge($servers[0], [
|
||||
'name' => "距离下次重置剩余:{$resetDay} 天",
|
||||
]));
|
||||
}
|
||||
array_unshift($servers, array_merge($servers[0], [
|
||||
'name' => "剩余流量:{$remainingTraffic}",
|
||||
]));
|
||||
}
|
||||
|
||||
private function addPrefixToServerName(array $servers): array
|
||||
{
|
||||
if (!admin_setting('show_protocol_to_server_enable', false)) {
|
||||
return $servers;
|
||||
}
|
||||
return collect($servers)
|
||||
->map(function (array $server): array {
|
||||
$server['name'] = $this->getPrefixedServerName($server);
|
||||
return $server;
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
private function getPrefixedServerName(array $server): string
|
||||
{
|
||||
$type = $server['type'] ?? '';
|
||||
if (!isset(self::PROTOCOL_PREFIXES[$type])) {
|
||||
return $server['name'] ?? '';
|
||||
}
|
||||
$prefix = is_array(self::PROTOCOL_PREFIXES[$type])
|
||||
? self::PROTOCOL_PREFIXES[$type][$server['protocol_settings']['version'] ?? 1] ?? ''
|
||||
: self::PROTOCOL_PREFIXES[$type];
|
||||
return $prefix . ($server['name'] ?? '');
|
||||
}
|
||||
}
|
||||
39
Xboard/app/Http/Controllers/V1/Guest/CommController.php
Normal file
39
Xboard/app/Http/Controllers/V1/Guest/CommController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Guest;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Utils\Dict;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CommController extends Controller
|
||||
{
|
||||
public function config()
|
||||
{
|
||||
$data = [
|
||||
'tos_url' => admin_setting('tos_url'),
|
||||
'is_email_verify' => (int) admin_setting('email_verify', 0) ? 1 : 0,
|
||||
'is_invite_force' => (int) admin_setting('invite_force', 0) ? 1 : 0,
|
||||
'email_whitelist_suffix' => (int) admin_setting('email_whitelist_enable', 0)
|
||||
? Helper::getEmailSuffix()
|
||||
: 0,
|
||||
'is_captcha' => (int) admin_setting('captcha_enable', 0) ? 1 : 0,
|
||||
'captcha_type' => admin_setting('captcha_type', 'recaptcha'),
|
||||
'recaptcha_site_key' => admin_setting('recaptcha_site_key'),
|
||||
'recaptcha_v3_site_key' => admin_setting('recaptcha_v3_site_key'),
|
||||
'recaptcha_v3_score_threshold' => admin_setting('recaptcha_v3_score_threshold', 0.5),
|
||||
'turnstile_site_key' => admin_setting('turnstile_site_key'),
|
||||
'app_description' => admin_setting('app_description'),
|
||||
'app_url' => admin_setting('app_url'),
|
||||
'logo' => admin_setting('logo'),
|
||||
// 保持向后兼容
|
||||
'is_recaptcha' => (int) admin_setting('captcha_enable', 0) ? 1 : 0,
|
||||
];
|
||||
|
||||
$data = HookManager::filter('guest_comm_config', $data);
|
||||
|
||||
return $this->success($data);
|
||||
}
|
||||
}
|
||||
52
Xboard/app/Http/Controllers/V1/Guest/PaymentController.php
Normal file
52
Xboard/app/Http/Controllers/V1/Guest/PaymentController.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Guest;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use App\Services\OrderService;
|
||||
use App\Services\PaymentService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Services\Plugin\HookManager;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
public function notify($method, $uuid, Request $request)
|
||||
{
|
||||
HookManager::call('payment.notify.before', [$method, $uuid, $request]);
|
||||
try {
|
||||
$paymentService = new PaymentService($method, null, $uuid);
|
||||
$verify = $paymentService->notify($request->input());
|
||||
if (!$verify) {
|
||||
HookManager::call('payment.notify.failed', [$method, $uuid, $request]);
|
||||
return $this->fail([422, 'verify error']);
|
||||
}
|
||||
HookManager::call('payment.notify.verified', $verify);
|
||||
if (!$this->handle($verify['trade_no'], $verify['callback_no'])) {
|
||||
return $this->fail([400, 'handle error']);
|
||||
}
|
||||
return (isset($verify['custom_result']) ? $verify['custom_result'] : 'success');
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
return $this->fail([500, 'fail']);
|
||||
}
|
||||
}
|
||||
|
||||
private function handle($tradeNo, $callbackNo)
|
||||
{
|
||||
$order = Order::where('trade_no', $tradeNo)->first();
|
||||
if (!$order) {
|
||||
return $this->fail([400202, 'order is not found']);
|
||||
}
|
||||
if ($order->status !== Order::STATUS_PENDING)
|
||||
return true;
|
||||
$orderService = new OrderService($order);
|
||||
if (!$orderService->paid($callbackNo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HookManager::call('payment.notify.success', $order);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
25
Xboard/app/Http/Controllers/V1/Guest/PlanController.php
Normal file
25
Xboard/app/Http/Controllers/V1/Guest/PlanController.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Guest;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\PlanResource;
|
||||
use App\Models\Plan;
|
||||
use App\Services\PlanService;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PlanController extends Controller
|
||||
{
|
||||
|
||||
protected $planService;
|
||||
public function __construct(PlanService $planService)
|
||||
{
|
||||
$this->planService = $planService;
|
||||
}
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$plan = $this->planService->getAvailablePlans();
|
||||
return $this->success(PlanResource::collection($plan));
|
||||
}
|
||||
}
|
||||
126
Xboard/app/Http/Controllers/V1/Guest/TelegramController.php
Normal file
126
Xboard/app/Http/Controllers/V1/Guest/TelegramController.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Guest;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\TelegramService;
|
||||
use App\Services\UserService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TelegramController extends Controller
|
||||
{
|
||||
protected ?object $msg = null;
|
||||
protected TelegramService $telegramService;
|
||||
protected UserService $userService;
|
||||
|
||||
public function __construct(TelegramService $telegramService, UserService $userService)
|
||||
{
|
||||
$this->telegramService = $telegramService;
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
public function webhook(Request $request): void
|
||||
{
|
||||
$expectedToken = md5(admin_setting('telegram_bot_token'));
|
||||
if ($request->input('access_token') !== $expectedToken) {
|
||||
throw new ApiException('access_token is error', 401);
|
||||
}
|
||||
|
||||
$data = $request->json()->all();
|
||||
|
||||
$this->formatMessage($data);
|
||||
$this->formatChatJoinRequest($data);
|
||||
$this->handle();
|
||||
}
|
||||
|
||||
private function handle(): void
|
||||
{
|
||||
if (!$this->msg)
|
||||
return;
|
||||
$msg = $this->msg;
|
||||
$this->processBotName($msg);
|
||||
try {
|
||||
HookManager::call('telegram.message.before', [$msg]);
|
||||
$handled = HookManager::filter('telegram.message.handle', false, [$msg]);
|
||||
if (!$handled) {
|
||||
HookManager::call('telegram.message.unhandled', [$msg]);
|
||||
}
|
||||
HookManager::call('telegram.message.after', [$msg]);
|
||||
} catch (\Exception $e) {
|
||||
HookManager::call('telegram.message.error', [$msg, $e]);
|
||||
$this->telegramService->sendMessage($msg->chat_id, $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function processBotName(object $msg): void
|
||||
{
|
||||
$commandParts = explode('@', $msg->command);
|
||||
|
||||
if (count($commandParts) === 2) {
|
||||
$botName = $this->getBotName();
|
||||
if ($commandParts[1] === $botName) {
|
||||
$msg->command = $commandParts[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getBotName(): string
|
||||
{
|
||||
$response = $this->telegramService->getMe();
|
||||
return $response->result->username;
|
||||
}
|
||||
|
||||
private function formatMessage(array $data): void
|
||||
{
|
||||
if (!isset($data['message']['text']))
|
||||
return;
|
||||
|
||||
$message = $data['message'];
|
||||
$text = explode(' ', $message['text']);
|
||||
|
||||
$this->msg = (object) [
|
||||
'command' => $text[0],
|
||||
'args' => array_slice($text, 1),
|
||||
'chat_id' => $message['chat']['id'],
|
||||
'message_id' => $message['message_id'],
|
||||
'message_type' => 'message',
|
||||
'text' => $message['text'],
|
||||
'is_private' => $message['chat']['type'] === 'private',
|
||||
];
|
||||
|
||||
if (isset($message['reply_to_message']['text'])) {
|
||||
$this->msg->message_type = 'reply_message';
|
||||
$this->msg->reply_text = $message['reply_to_message']['text'];
|
||||
}
|
||||
}
|
||||
|
||||
private function formatChatJoinRequest(array $data): void
|
||||
{
|
||||
$joinRequest = $data['chat_join_request'] ?? null;
|
||||
if (!$joinRequest)
|
||||
return;
|
||||
|
||||
$chatId = $joinRequest['chat']['id'] ?? null;
|
||||
$userId = $joinRequest['from']['id'] ?? null;
|
||||
|
||||
if (!$chatId || !$userId)
|
||||
return;
|
||||
|
||||
$user = User::where('telegram_id', $userId)->first();
|
||||
|
||||
if (!$user) {
|
||||
$this->telegramService->declineChatJoinRequest($chatId, $userId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->userService->isAvailable($user)) {
|
||||
$this->telegramService->declineChatJoinRequest($chatId, $userId);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->telegramService->approveChatJoinRequest($chatId, $userId);
|
||||
}
|
||||
}
|
||||
175
Xboard/app/Http/Controllers/V1/Passport/AuthController.php
Normal file
175
Xboard/app/Http/Controllers/V1/Passport/AuthController.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Passport;
|
||||
|
||||
use App\Helpers\ResponseEnum;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Passport\AuthForget;
|
||||
use App\Http\Requests\Passport\AuthLogin;
|
||||
use App\Http\Requests\Passport\AuthRegister;
|
||||
use App\Services\Auth\LoginService;
|
||||
use App\Services\Auth\MailLinkService;
|
||||
use App\Services\Auth\RegisterService;
|
||||
use App\Services\AuthService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
protected MailLinkService $mailLinkService;
|
||||
protected RegisterService $registerService;
|
||||
protected LoginService $loginService;
|
||||
|
||||
public function __construct(
|
||||
MailLinkService $mailLinkService,
|
||||
RegisterService $registerService,
|
||||
LoginService $loginService
|
||||
) {
|
||||
$this->mailLinkService = $mailLinkService;
|
||||
$this->registerService = $registerService;
|
||||
$this->loginService = $loginService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过邮件链接登录
|
||||
*/
|
||||
public function loginWithMailLink(Request $request)
|
||||
{
|
||||
$params = $request->validate([
|
||||
'email' => 'required|email:strict',
|
||||
'redirect' => 'nullable'
|
||||
]);
|
||||
|
||||
[$success, $result] = $this->mailLinkService->handleMailLink(
|
||||
$params['email'],
|
||||
$request->input('redirect')
|
||||
);
|
||||
|
||||
if (!$success) {
|
||||
return $this->fail($result);
|
||||
}
|
||||
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
public function register(AuthRegister $request)
|
||||
{
|
||||
[$success, $result] = $this->registerService->register($request);
|
||||
|
||||
if (!$success) {
|
||||
return $this->fail($result);
|
||||
}
|
||||
|
||||
$authService = new AuthService($result);
|
||||
return $this->success($authService->generateAuthData());
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
public function login(AuthLogin $request)
|
||||
{
|
||||
$email = $request->input('email');
|
||||
$password = $request->input('password');
|
||||
|
||||
[$success, $result] = $this->loginService->login($email, $password);
|
||||
|
||||
if (!$success) {
|
||||
return $this->fail($result);
|
||||
}
|
||||
|
||||
$authService = new AuthService($result);
|
||||
return $this->success($authService->generateAuthData());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过token登录
|
||||
*/
|
||||
public function token2Login(Request $request)
|
||||
{
|
||||
// 处理直接通过token重定向
|
||||
if ($token = $request->input('token')) {
|
||||
$redirect = '/#/login?verify=' . $token . '&redirect=' . ($request->input('redirect', 'dashboard'));
|
||||
|
||||
return redirect()->to(
|
||||
admin_setting('app_url')
|
||||
? admin_setting('app_url') . $redirect
|
||||
: url($redirect)
|
||||
);
|
||||
}
|
||||
|
||||
// 处理通过验证码登录
|
||||
if ($verify = $request->input('verify')) {
|
||||
$userId = $this->mailLinkService->handleTokenLogin($verify);
|
||||
|
||||
if (!$userId) {
|
||||
return response()->json([
|
||||
'message' => __('Token error')
|
||||
], 400);
|
||||
}
|
||||
|
||||
$user = \App\Models\User::find($userId);
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'message' => __('User not found')
|
||||
], 400);
|
||||
}
|
||||
|
||||
$authService = new AuthService($user);
|
||||
|
||||
return response()->json([
|
||||
'data' => $authService->generateAuthData()
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => __('Invalid request')
|
||||
], 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取快速登录URL
|
||||
*/
|
||||
public function getQuickLoginUrl(Request $request)
|
||||
{
|
||||
$authorization = $request->input('auth_data') ?? $request->header('authorization');
|
||||
|
||||
if (!$authorization) {
|
||||
return response()->json([
|
||||
'message' => ResponseEnum::CLIENT_HTTP_UNAUTHORIZED
|
||||
], 401);
|
||||
}
|
||||
|
||||
$user = AuthService::findUserByBearerToken($authorization);
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'message' => ResponseEnum::CLIENT_HTTP_UNAUTHORIZED_EXPIRED
|
||||
], 401);
|
||||
}
|
||||
|
||||
$url = $this->loginService->generateQuickLoginUrl($user, $request->input('redirect'));
|
||||
return $this->success($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码处理
|
||||
*/
|
||||
public function forget(AuthForget $request)
|
||||
{
|
||||
[$success, $result] = $this->loginService->resetPassword(
|
||||
$request->input('email'),
|
||||
$request->input('email_code'),
|
||||
$request->input('password')
|
||||
);
|
||||
|
||||
if (!$success) {
|
||||
return $this->fail($result);
|
||||
}
|
||||
|
||||
return $this->success(true);
|
||||
}
|
||||
}
|
||||
76
Xboard/app/Http/Controllers/V1/Passport/CommController.php
Normal file
76
Xboard/app/Http/Controllers/V1/Passport/CommController.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Passport;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Passport\CommSendEmailVerify;
|
||||
use App\Jobs\SendEmailJob;
|
||||
use App\Models\InviteCode;
|
||||
use App\Models\User;
|
||||
use App\Services\CaptchaService;
|
||||
use App\Utils\CacheKey;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class CommController extends Controller
|
||||
{
|
||||
|
||||
public function sendEmailVerify(CommSendEmailVerify $request)
|
||||
{
|
||||
// 验证人机验证码
|
||||
$captchaService = app(CaptchaService::class);
|
||||
[$captchaValid, $captchaError] = $captchaService->verify($request);
|
||||
if (!$captchaValid) {
|
||||
return $this->fail($captchaError);
|
||||
}
|
||||
|
||||
$email = $request->input('email');
|
||||
|
||||
// 检查白名单后缀限制
|
||||
if ((int) admin_setting('email_whitelist_enable', 0)) {
|
||||
$isRegisteredEmail = User::byEmail($email)->exists();
|
||||
if (!$isRegisteredEmail) {
|
||||
$allowedSuffixes = Helper::getEmailSuffix();
|
||||
$emailSuffix = substr(strrchr($email, '@'), 1);
|
||||
|
||||
if (!in_array($emailSuffix, $allowedSuffixes)) {
|
||||
return $this->fail([400, __('Email suffix is not in whitelist')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Cache::get(CacheKey::get('LAST_SEND_EMAIL_VERIFY_TIMESTAMP', $email))) {
|
||||
return $this->fail([400, __('Email verification code has been sent, please request again later')]);
|
||||
}
|
||||
$code = rand(100000, 999999);
|
||||
$subject = admin_setting('app_name', 'XBoard') . __('Email verification code');
|
||||
|
||||
SendEmailJob::dispatch([
|
||||
'email' => $email,
|
||||
'subject' => $subject,
|
||||
'template_name' => 'verify',
|
||||
'template_value' => [
|
||||
'name' => admin_setting('app_name', 'XBoard'),
|
||||
'code' => $code,
|
||||
'url' => admin_setting('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 $this->success(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 $this->success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Server;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ServerShadowsocks;
|
||||
use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\CacheKey;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/*
|
||||
* Tidal Lab Shadowsocks
|
||||
* Github: https://github.com/tokumeikoi/tidalab-ss
|
||||
*/
|
||||
class ShadowsocksTidalabController extends Controller
|
||||
{
|
||||
// 后端获取用户
|
||||
public function user(Request $request)
|
||||
{
|
||||
ini_set('memory_limit', -1);
|
||||
$server = $request->attributes->get('node_info');
|
||||
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600);
|
||||
$users = ServerService::getAvailableUsers($server);
|
||||
$result = [];
|
||||
foreach ($users as $user) {
|
||||
array_push($result, [
|
||||
'id' => $user->id,
|
||||
'port' => $server->server_port,
|
||||
'cipher' => $server->cipher,
|
||||
'secret' => $user->uuid
|
||||
]);
|
||||
}
|
||||
$eTag = sha1(json_encode($result));
|
||||
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
|
||||
return response(null,304);
|
||||
}
|
||||
return response([
|
||||
'data' => $result
|
||||
])->header('ETag', "\"{$eTag}\"");
|
||||
}
|
||||
|
||||
// 后端提交数据
|
||||
public function submit(Request $request)
|
||||
{
|
||||
$server = $request->attributes->get('node_info');
|
||||
$data = json_decode(request()->getContent(), true);
|
||||
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_ONLINE_USER', $server->id), count($data), 3600);
|
||||
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_PUSH_AT', $server->id), time(), 3600);
|
||||
$userService = new UserService();
|
||||
$formatData = [];
|
||||
|
||||
foreach ($data as $item) {
|
||||
$formatData[$item['user_id']] = [$item['u'], $item['d']];
|
||||
}
|
||||
$userService->trafficFetch($server, 'shadowsocks', $formatData);
|
||||
|
||||
return response([
|
||||
'ret' => 1,
|
||||
'msg' => 'ok'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Server;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ServerTrojan;
|
||||
use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\CacheKey;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/*
|
||||
* Tidal Lab Trojan
|
||||
* Github: https://github.com/tokumeikoi/tidalab-trojan
|
||||
*/
|
||||
class TrojanTidalabController extends Controller
|
||||
{
|
||||
const TROJAN_CONFIG = '{"run_type":"server","local_addr":"0.0.0.0","local_port":443,"remote_addr":"www.taobao.com","remote_port":80,"password":[],"ssl":{"cert":"server.crt","key":"server.key","sni":"domain.com"},"api":{"enabled":true,"api_addr":"127.0.0.1","api_port":10000}}';
|
||||
|
||||
// 后端获取用户
|
||||
public function user(Request $request)
|
||||
{
|
||||
ini_set('memory_limit', -1);
|
||||
$server = $request->attributes->get('node_info');
|
||||
if ($server->type !== 'trojan') {
|
||||
return $this->fail([400, '节点不存在']);
|
||||
}
|
||||
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600);
|
||||
$users = ServerService::getAvailableUsers($server);
|
||||
$result = [];
|
||||
foreach ($users as $user) {
|
||||
$user->trojan_user = [
|
||||
"password" => $user->uuid,
|
||||
];
|
||||
unset($user->uuid);
|
||||
array_push($result, $user);
|
||||
}
|
||||
$eTag = sha1(json_encode($result));
|
||||
if (strpos($request->header('If-None-Match'), $eTag) !== false) {
|
||||
return response(null, 304);
|
||||
}
|
||||
return response([
|
||||
'msg' => 'ok',
|
||||
'data' => $result,
|
||||
])->header('ETag', "\"{$eTag}\"");
|
||||
}
|
||||
|
||||
// 后端提交数据
|
||||
public function submit(Request $request)
|
||||
{
|
||||
$server = $request->attributes->get('node_info');
|
||||
if ($server->type !== 'trojan') {
|
||||
return $this->fail([400, '节点不存在']);
|
||||
}
|
||||
$data = json_decode(request()->getContent(), true);
|
||||
Cache::put(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server->id), count($data), 3600);
|
||||
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_PUSH_AT', $server->id), time(), 3600);
|
||||
$userService = new UserService();
|
||||
$formatData = [];
|
||||
foreach ($data as $item) {
|
||||
$formatData[$item['user_id']] = [$item['u'], $item['d']];
|
||||
}
|
||||
$userService->trafficFetch($server, 'trojan', $formatData);
|
||||
|
||||
return response([
|
||||
'ret' => 1,
|
||||
'msg' => 'ok'
|
||||
]);
|
||||
}
|
||||
|
||||
// 后端获取配置
|
||||
public function config(Request $request)
|
||||
{
|
||||
$server = $request->attributes->get('node_info');
|
||||
if ($server->type !== 'trojan') {
|
||||
return $this->fail([400, '节点不存在']);
|
||||
}
|
||||
$request->validate([
|
||||
'node_id' => 'required',
|
||||
'local_port' => 'required'
|
||||
], [
|
||||
'node_id.required' => '节点ID不能为空',
|
||||
'local_port.required' => '本地端口不能为空'
|
||||
]);
|
||||
try {
|
||||
$json = $this->getTrojanConfig($server, $request->input('local_port'));
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
return $this->fail([500, '配置获取失败']);
|
||||
}
|
||||
|
||||
return (json_encode($json, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
private function getTrojanConfig($server, int $localPort)
|
||||
{
|
||||
$protocolSettings = $server->protocol_settings;
|
||||
$json = json_decode(self::TROJAN_CONFIG);
|
||||
$json->local_port = $server->server_port;
|
||||
$json->ssl->sni = data_get($protocolSettings, 'server_name', $server->host);
|
||||
$json->ssl->cert = "/root/.cert/server.crt";
|
||||
$json->ssl->key = "/root/.cert/server.key";
|
||||
$json->api->api_port = $localPort;
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
178
Xboard/app/Http/Controllers/V1/Server/UniProxyController.php
Normal file
178
Xboard/app/Http/Controllers/V1/Server/UniProxyController.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Server;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\DeviceStateService;
|
||||
use App\Services\NodeSyncService;
|
||||
use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\CacheKey;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class UniProxyController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DeviceStateService $deviceStateService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求的节点信息
|
||||
*/
|
||||
private function getNodeInfo(Request $request)
|
||||
{
|
||||
return $request->attributes->get('node_info');
|
||||
}
|
||||
|
||||
// 后端获取用户
|
||||
public function user(Request $request)
|
||||
{
|
||||
ini_set('memory_limit', -1);
|
||||
$node = $this->getNodeInfo($request);
|
||||
$nodeType = $node->type;
|
||||
$nodeId = $node->id;
|
||||
Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_CHECK_AT', $nodeId), time(), 3600);
|
||||
$users = ServerService::getAvailableUsers($node);
|
||||
|
||||
$response['users'] = $users;
|
||||
|
||||
$eTag = sha1(json_encode($response));
|
||||
if (strpos($request->header('If-None-Match', ''), $eTag) !== false) {
|
||||
return response(null, 304);
|
||||
}
|
||||
|
||||
return response($response)->header('ETag', "\"{$eTag}\"");
|
||||
}
|
||||
|
||||
// 后端提交数据
|
||||
public function push(Request $request)
|
||||
{
|
||||
$res = json_decode(request()->getContent(), true);
|
||||
if (!is_array($res)) {
|
||||
return $this->fail([422, 'Invalid data format']);
|
||||
}
|
||||
$data = array_filter($res, function ($item) {
|
||||
return is_array($item)
|
||||
&& count($item) === 2
|
||||
&& is_numeric($item[0])
|
||||
&& is_numeric($item[1]);
|
||||
});
|
||||
if (empty($data)) {
|
||||
return $this->success(true);
|
||||
}
|
||||
$node = $this->getNodeInfo($request);
|
||||
$nodeType = $node->type;
|
||||
$nodeId = $node->id;
|
||||
|
||||
Cache::put(
|
||||
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId),
|
||||
count($data),
|
||||
3600
|
||||
);
|
||||
Cache::put(
|
||||
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_PUSH_AT', $nodeId),
|
||||
time(),
|
||||
3600
|
||||
);
|
||||
|
||||
$userService = new UserService();
|
||||
$userService->trafficFetch($node, $nodeType, $data);
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
// 后端获取配置
|
||||
public function config(Request $request)
|
||||
{
|
||||
$node = $this->getNodeInfo($request);
|
||||
$response = ServerService::buildNodeConfig($node);
|
||||
|
||||
$response['base_config'] = [
|
||||
'push_interval' => (int) admin_setting('server_push_interval', 60),
|
||||
'pull_interval' => (int) admin_setting('server_pull_interval', 60)
|
||||
];
|
||||
|
||||
$eTag = sha1(json_encode($response));
|
||||
if (strpos($request->header('If-None-Match', ''), $eTag) !== false) {
|
||||
return response(null, 304);
|
||||
}
|
||||
return response($response)->header('ETag', "\"{$eTag}\"");
|
||||
}
|
||||
|
||||
// 获取在线用户数据
|
||||
public function alivelist(Request $request): JsonResponse
|
||||
{
|
||||
$node = $this->getNodeInfo($request);
|
||||
$deviceLimitUsers = ServerService::getAvailableUsers($node)
|
||||
->where('device_limit', '>', 0);
|
||||
|
||||
$alive = $this->deviceStateService->getAliveList(collect($deviceLimitUsers));
|
||||
|
||||
return response()->json(['alive' => (object) $alive]);
|
||||
}
|
||||
|
||||
// 后端提交在线数据
|
||||
public function alive(Request $request): JsonResponse
|
||||
{
|
||||
$node = $this->getNodeInfo($request);
|
||||
$data = json_decode(request()->getContent(), true);
|
||||
if ($data === null) {
|
||||
return response()->json([
|
||||
'error' => 'Invalid online data'
|
||||
], 400);
|
||||
}
|
||||
|
||||
foreach ($data as $uid => $ips) {
|
||||
$this->deviceStateService->setDevices((int) $uid, $node->id, $ips);
|
||||
}
|
||||
|
||||
return response()->json(['data' => true]);
|
||||
}
|
||||
|
||||
// 提交节点负载状态
|
||||
public function status(Request $request): JsonResponse
|
||||
{
|
||||
$node = $this->getNodeInfo($request);
|
||||
|
||||
$data = $request->validate([
|
||||
'cpu' => 'required|numeric|min:0|max:100',
|
||||
'mem.total' => 'required|integer|min:0',
|
||||
'mem.used' => 'required|integer|min:0',
|
||||
'swap.total' => 'required|integer|min:0',
|
||||
'swap.used' => 'required|integer|min:0',
|
||||
'disk.total' => 'required|integer|min:0',
|
||||
'disk.used' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
$nodeType = $node->type;
|
||||
$nodeId = $node->id;
|
||||
|
||||
$statusData = [
|
||||
'cpu' => (float) $data['cpu'],
|
||||
'mem' => [
|
||||
'total' => (int) $data['mem']['total'],
|
||||
'used' => (int) $data['mem']['used'],
|
||||
],
|
||||
'swap' => [
|
||||
'total' => (int) $data['swap']['total'],
|
||||
'used' => (int) $data['swap']['used'],
|
||||
],
|
||||
'disk' => [
|
||||
'total' => (int) $data['disk']['total'],
|
||||
'used' => (int) $data['disk']['used'],
|
||||
],
|
||||
'updated_at' => now()->timestamp,
|
||||
];
|
||||
|
||||
$cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3);
|
||||
cache([
|
||||
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LOAD_STATUS', $nodeId) => $statusData,
|
||||
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_LOAD_AT', $nodeId) => now()->timestamp,
|
||||
], $cacheTime);
|
||||
|
||||
return response()->json(['data' => true, "code" => 0, "message" => "success"]);
|
||||
}
|
||||
}
|
||||
39
Xboard/app/Http/Controllers/V1/User/CommController.php
Normal file
39
Xboard/app/Http/Controllers/V1/User/CommController.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Payment;
|
||||
use App\Utils\Dict;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CommController extends Controller
|
||||
{
|
||||
public function config()
|
||||
{
|
||||
$data = [
|
||||
'is_telegram' => (int)admin_setting('telegram_bot_enable', 0),
|
||||
'telegram_discuss_link' => admin_setting('telegram_discuss_link'),
|
||||
'stripe_pk' => admin_setting('stripe_pk_live'),
|
||||
'withdraw_methods' => admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT),
|
||||
'withdraw_close' => (int)admin_setting('withdraw_close_enable', 0),
|
||||
'currency' => admin_setting('currency', 'CNY'),
|
||||
'currency_symbol' => admin_setting('currency_symbol', '¥'),
|
||||
'commission_distribution_enable' => (int)admin_setting('commission_distribution_enable', 0),
|
||||
'commission_distribution_l1' => admin_setting('commission_distribution_l1'),
|
||||
'commission_distribution_l2' => admin_setting('commission_distribution_l2'),
|
||||
'commission_distribution_l3' => admin_setting('commission_distribution_l3')
|
||||
];
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
public function getStripePublicKey(Request $request)
|
||||
{
|
||||
$payment = Payment::where('id', $request->input('id'))
|
||||
->where('payment', 'StripeCredit')
|
||||
->first();
|
||||
if (!$payment) throw new ApiException('payment is not found');
|
||||
return $this->success($payment->config['stripe_pk_live']);
|
||||
}
|
||||
}
|
||||
25
Xboard/app/Http/Controllers/V1/User/CouponController.php
Normal file
25
Xboard/app/Http/Controllers/V1/User/CouponController.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\CouponResource;
|
||||
use App\Services\CouponService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CouponController extends Controller
|
||||
{
|
||||
public function check(Request $request)
|
||||
{
|
||||
if (empty($request->input('code'))) {
|
||||
return $this->fail([422, __('Coupon cannot be empty')]);
|
||||
}
|
||||
$couponService = new CouponService($request->input('code'));
|
||||
$couponService->setPlanId($request->input('plan_id'));
|
||||
$couponService->setUserId($request->user()->id);
|
||||
$couponService->setPeriod($request->input('period'));
|
||||
$couponService->check();
|
||||
return $this->success(CouponResource::make($couponService->getCoupon()));
|
||||
}
|
||||
}
|
||||
193
Xboard/app/Http/Controllers/V1/User/GiftCardController.php
Normal file
193
Xboard/app/Http/Controllers/V1/User/GiftCardController.php
Normal file
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\User\GiftCardCheckRequest;
|
||||
use App\Http\Requests\User\GiftCardRedeemRequest;
|
||||
use App\Models\GiftCardUsage;
|
||||
use App\Services\GiftCardService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GiftCardController extends Controller
|
||||
{
|
||||
/**
|
||||
* 查询兑换码信息
|
||||
*/
|
||||
public function check(GiftCardCheckRequest $request)
|
||||
{
|
||||
try {
|
||||
$giftCardService = new GiftCardService($request->input('code'));
|
||||
$giftCardService->setUser($request->user());
|
||||
|
||||
// 1. 验证礼品卡本身是否有效 (如不存在、已过期、已禁用)
|
||||
$giftCardService->validateIsActive();
|
||||
|
||||
// 2. 检查用户是否满足使用条件,但不在此处抛出异常
|
||||
$eligibility = $giftCardService->checkUserEligibility();
|
||||
|
||||
// 3. 获取卡片信息和奖励预览
|
||||
$codeInfo = $giftCardService->getCodeInfo();
|
||||
$rewardPreview = $giftCardService->previewRewards();
|
||||
|
||||
return $this->success([
|
||||
'code_info' => $codeInfo, // 这里面已经包含 plan_info
|
||||
'reward_preview' => $rewardPreview,
|
||||
'can_redeem' => $eligibility['can_redeem'],
|
||||
'reason' => $eligibility['reason'],
|
||||
]);
|
||||
|
||||
} catch (ApiException $e) {
|
||||
// 这里只捕获 validateIsActive 抛出的异常
|
||||
return $this->fail([400, $e->getMessage()]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('礼品卡查询失败', [
|
||||
'code' => $request->input('code'),
|
||||
'user_id' => $request->user()->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
return $this->fail([500, '查询失败,请稍后重试']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用兑换码
|
||||
*/
|
||||
public function redeem(GiftCardRedeemRequest $request)
|
||||
{
|
||||
try {
|
||||
$giftCardService = new GiftCardService($request->input('code'));
|
||||
$giftCardService->setUser($request->user());
|
||||
$giftCardService->validate();
|
||||
|
||||
// 使用礼品卡
|
||||
$result = $giftCardService->redeem([
|
||||
// 'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
]);
|
||||
|
||||
Log::info('礼品卡使用成功', [
|
||||
'code' => $request->input('code'),
|
||||
'user_id' => $request->user()->id,
|
||||
'rewards' => $result['rewards'],
|
||||
]);
|
||||
|
||||
return $this->success([
|
||||
'message' => '兑换成功!',
|
||||
'rewards' => $result['rewards'],
|
||||
'invite_rewards' => $result['invite_rewards'],
|
||||
'template_name' => $result['template_name'],
|
||||
]);
|
||||
|
||||
} catch (ApiException $e) {
|
||||
return $this->fail([400, $e->getMessage()]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('礼品卡使用失败', [
|
||||
'code' => $request->input('code'),
|
||||
'user_id' => $request->user()->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
return $this->fail([500, '兑换失败,请稍后重试']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户兑换记录
|
||||
*/
|
||||
public function history(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'page' => 'integer|min:1',
|
||||
'per_page' => 'integer|min:1|max:100',
|
||||
]);
|
||||
|
||||
$perPage = $request->input('per_page', 15);
|
||||
|
||||
$usages = GiftCardUsage::with(['template', 'code'])
|
||||
->where('user_id', $request->user()->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate($perPage);
|
||||
|
||||
$data = $usages->getCollection()->map(function (GiftCardUsage $usage) {
|
||||
return [
|
||||
'id' => $usage->id,
|
||||
'code' => ($usage->code instanceof \App\Models\GiftCardCode && $usage->code->code)
|
||||
? (substr($usage->code->code, 0, 8) . '****')
|
||||
: '',
|
||||
'template_name' => $usage->template->name ?? '',
|
||||
'template_type' => $usage->template->type ?? '',
|
||||
'template_type_name' => $usage->template->type_name ?? '',
|
||||
'rewards_given' => $usage->rewards_given,
|
||||
'invite_rewards' => $usage->invite_rewards,
|
||||
'multiplier_applied' => $usage->multiplier_applied,
|
||||
'created_at' => $usage->created_at,
|
||||
];
|
||||
})->values();
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
'pagination' => [
|
||||
'current_page' => $usages->currentPage(),
|
||||
'last_page' => $usages->lastPage(),
|
||||
'per_page' => $usages->perPage(),
|
||||
'total' => $usages->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兑换记录详情
|
||||
*/
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'required|integer|exists:v2_gift_card_usage,id',
|
||||
]);
|
||||
|
||||
$usage = GiftCardUsage::with(['template', 'code', 'inviteUser'])
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('id', $request->input('id'))
|
||||
->first();
|
||||
|
||||
if (!$usage) {
|
||||
return $this->fail([404, '记录不存在']);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'id' => $usage->id,
|
||||
'code' => $usage->code->code ?? '',
|
||||
'template' => [
|
||||
'name' => $usage->template->name ?? '',
|
||||
'description' => $usage->template->description ?? '',
|
||||
'type' => $usage->template->type ?? '',
|
||||
'type_name' => $usage->template->type_name ?? '',
|
||||
'icon' => $usage->template->icon ?? '',
|
||||
'theme_color' => $usage->template->theme_color ?? '',
|
||||
],
|
||||
'rewards_given' => $usage->rewards_given,
|
||||
'invite_rewards' => $usage->invite_rewards,
|
||||
'invite_user' => $usage->inviteUser ? [
|
||||
'id' => $usage->inviteUser->id ?? '',
|
||||
'email' => isset($usage->inviteUser->email) ? (substr($usage->inviteUser->email, 0, 3) . '***@***') : '',
|
||||
] : null,
|
||||
'user_level_at_use' => $usage->user_level_at_use,
|
||||
'plan_id_at_use' => $usage->plan_id_at_use,
|
||||
'multiplier_applied' => $usage->multiplier_applied,
|
||||
// 'ip_address' => $usage->ip_address,
|
||||
'notes' => $usage->notes,
|
||||
'created_at' => $usage->created_at,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的礼品卡类型
|
||||
*/
|
||||
public function types(Request $request)
|
||||
{
|
||||
return $this->success([
|
||||
'types' => \App\Models\GiftCardTemplate::getTypeMap(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
79
Xboard/app/Http/Controllers/V1/User/InviteController.php
Normal file
79
Xboard/app/Http/Controllers/V1/User/InviteController.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ComissionLogResource;
|
||||
use App\Http\Resources\InviteCodeResource;
|
||||
use App\Models\CommissionLog;
|
||||
use App\Models\InviteCode;
|
||||
use App\Models\Order;
|
||||
use App\Models\User;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InviteController extends Controller
|
||||
{
|
||||
public function save(Request $request)
|
||||
{
|
||||
if (InviteCode::where('user_id', $request->user()->id)->where('status', 0)->count() >= admin_setting('invite_gen_limit', 5)) {
|
||||
return $this->fail([400,__('The maximum number of creations has been reached')]);
|
||||
}
|
||||
$inviteCode = new InviteCode();
|
||||
$inviteCode->user_id = $request->user()->id;
|
||||
$inviteCode->code = Helper::randomChar(8);
|
||||
return $this->success($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)
|
||||
->orderBy('created_at', 'DESC');
|
||||
$total = $builder->count();
|
||||
$details = $builder->forPage($current, $pageSize)
|
||||
->get();
|
||||
return response([
|
||||
'data' => ComissionLogResource::collection($details),
|
||||
'total' => $total
|
||||
]);
|
||||
}
|
||||
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$commission_rate = admin_setting('invite_commission', 10);
|
||||
$user = User::find($request->user()->id)
|
||||
->load(['codes' => fn($query) => $query->where('status', 0)]);
|
||||
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', $user->id)
|
||||
->sum('commission_balance');
|
||||
if (admin_setting('commission_distribution_enable', 0)) {
|
||||
$uncheck_commission_balance = $uncheck_commission_balance * (admin_setting('commission_distribution_l1') / 100);
|
||||
}
|
||||
$stat = [
|
||||
//已注册用户数
|
||||
(int)User::where('invite_user_id', $user->id)->count(),
|
||||
//有效的佣金
|
||||
(int)CommissionLog::where('invite_user_id', $user->id)
|
||||
->sum('get_amount'),
|
||||
//确认中的佣金
|
||||
$uncheck_commission_balance,
|
||||
//佣金比例
|
||||
(int)$commission_rate,
|
||||
//可用佣金
|
||||
(int)$user->commission_balance
|
||||
];
|
||||
$data = [
|
||||
'codes' => InviteCodeResource::collection($user->codes),
|
||||
'stat' => $stat
|
||||
];
|
||||
return $this->success($data);
|
||||
}
|
||||
}
|
||||
150
Xboard/app/Http/Controllers/V1/User/KnowledgeController.php
Normal file
150
Xboard/app/Http/Controllers/V1/User/KnowledgeController.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\KnowledgeResource;
|
||||
use App\Models\Knowledge;
|
||||
use App\Models\User;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class KnowledgeController extends Controller
|
||||
{
|
||||
private UserService $userService;
|
||||
|
||||
public function __construct(UserService $userService)
|
||||
{
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'id' => 'nullable|sometimes|integer|min:1',
|
||||
'language' => 'nullable|sometimes|string|max:10',
|
||||
'keyword' => 'nullable|sometimes|string|max:255',
|
||||
]);
|
||||
|
||||
return $request->input('id')
|
||||
? $this->fetchSingle($request)
|
||||
: $this->fetchList($request);
|
||||
}
|
||||
|
||||
private function fetchSingle(Request $request)
|
||||
{
|
||||
$knowledge = $this->buildKnowledgeQuery()
|
||||
->where('id', $request->input('id'))
|
||||
->first();
|
||||
|
||||
if (!$knowledge) {
|
||||
return $this->fail([500, __('Article does not exist')]);
|
||||
}
|
||||
|
||||
$knowledge = $knowledge->toArray();
|
||||
$knowledge = $this->processKnowledgeContent($knowledge, $request->user());
|
||||
|
||||
return $this->success(KnowledgeResource::make($knowledge));
|
||||
}
|
||||
|
||||
private function fetchList(Request $request)
|
||||
{
|
||||
$builder = $this->buildKnowledgeQuery(['id', 'category', 'title', 'updated_at', 'body'])
|
||||
->where('language', $request->input('language'))
|
||||
->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()
|
||||
->map(function ($knowledge) use ($request) {
|
||||
$knowledge = $knowledge->toArray();
|
||||
$knowledge = $this->processKnowledgeContent($knowledge, $request->user());
|
||||
return KnowledgeResource::make($knowledge);
|
||||
})
|
||||
->groupBy('category');
|
||||
|
||||
return $this->success($knowledges);
|
||||
}
|
||||
|
||||
private function buildKnowledgeQuery(array $select = ['*'])
|
||||
{
|
||||
return Knowledge::select($select)->where('show', 1);
|
||||
}
|
||||
|
||||
private function processKnowledgeContent(array $knowledge, User $user): array
|
||||
{
|
||||
if (!isset($knowledge['body'])) {
|
||||
return $knowledge;
|
||||
}
|
||||
|
||||
if (!$this->userService->isAvailable($user)) {
|
||||
$this->formatAccessData($knowledge['body']);
|
||||
}
|
||||
$subscribeUrl = Helper::getSubscribeUrl($user['token']);
|
||||
$knowledge['body'] = $this->replacePlaceholders($knowledge['body'], $subscribeUrl);
|
||||
|
||||
return $knowledge;
|
||||
}
|
||||
|
||||
private function formatAccessData(&$body): void
|
||||
{
|
||||
$rules = [
|
||||
[
|
||||
'type' => 'regex',
|
||||
'pattern' => '/<!--access start-->(.*?)<!--access end-->/s',
|
||||
'replacement' => '<div class="v2board-no-access">' . __('You must have a valid subscription to view content in this area') . '</div>'
|
||||
]
|
||||
];
|
||||
|
||||
$this->applyReplacementRules($body, $rules);
|
||||
}
|
||||
|
||||
private function replacePlaceholders(string $body, string $subscribeUrl): string
|
||||
{
|
||||
$rules = [
|
||||
[
|
||||
'type' => 'string',
|
||||
'search' => '{{siteName}}',
|
||||
'replacement' => admin_setting('app_name', 'XBoard')
|
||||
],
|
||||
[
|
||||
'type' => 'string',
|
||||
'search' => '{{subscribeUrl}}',
|
||||
'replacement' => $subscribeUrl
|
||||
],
|
||||
[
|
||||
'type' => 'string',
|
||||
'search' => '{{urlEncodeSubscribeUrl}}',
|
||||
'replacement' => urlencode($subscribeUrl)
|
||||
],
|
||||
[
|
||||
'type' => 'string',
|
||||
'search' => '{{safeBase64SubscribeUrl}}',
|
||||
'replacement' => str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($subscribeUrl))
|
||||
]
|
||||
];
|
||||
|
||||
$this->applyReplacementRules($body, $rules);
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function applyReplacementRules(string &$body, array $rules): void
|
||||
{
|
||||
foreach ($rules as $rule) {
|
||||
if ($rule['type'] === 'regex') {
|
||||
$body = preg_replace($rule['pattern'], $rule['replacement'], $body);
|
||||
} else {
|
||||
$body = str_replace($rule['search'], $rule['replacement'], $body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Xboard/app/Http/Controllers/V1/User/NoticeController.php
Normal file
26
Xboard/app/Http/Controllers/V1/User/NoticeController.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Notice;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NoticeController extends Controller
|
||||
{
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$current = $request->input('current') ? $request->input('current') : 1;
|
||||
$pageSize = 5;
|
||||
$model = Notice::orderBy('sort', 'ASC')
|
||||
->orderBy('id', 'DESC')
|
||||
->where('show', true);
|
||||
$total = $model->count();
|
||||
$res = $model->forPage($current, $pageSize)
|
||||
->get();
|
||||
return response([
|
||||
'data' => $res,
|
||||
'total' => $total
|
||||
]);
|
||||
}
|
||||
}
|
||||
212
Xboard/app/Http/Controllers/V1/User/OrderController.php
Normal file
212
Xboard/app/Http/Controllers/V1/User/OrderController.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\User\OrderSave;
|
||||
use App\Http\Resources\OrderResource;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use App\Services\CouponService;
|
||||
use App\Services\OrderService;
|
||||
use App\Services\PaymentService;
|
||||
use App\Services\PlanService;
|
||||
use App\Services\UserService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class OrderController extends Controller
|
||||
{
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'status' => 'nullable|integer|in:0,1,2,3',
|
||||
]);
|
||||
$orders = Order::with('plan')
|
||||
->where('user_id', $request->user()->id)
|
||||
->when($request->input('status') !== null, function ($query) use ($request) {
|
||||
$query->where('status', $request->input('status'));
|
||||
})
|
||||
->orderBy('created_at', 'DESC')
|
||||
->get();
|
||||
|
||||
return $this->success(OrderResource::collection($orders));
|
||||
}
|
||||
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'trade_no' => 'required|string',
|
||||
]);
|
||||
$order = Order::with(['payment', 'plan'])
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('trade_no', $request->input('trade_no'))
|
||||
->first();
|
||||
if (!$order) {
|
||||
return $this->fail([400, __('Order does not exist or has been paid')]);
|
||||
}
|
||||
$order['try_out_plan_id'] = (int) admin_setting('try_out_plan_id');
|
||||
if (!$order->plan) {
|
||||
return $this->fail([400, __('Subscription plan does not exist')]);
|
||||
}
|
||||
if ($order->surplus_order_ids) {
|
||||
$order['surplus_orders'] = Order::whereIn('id', $order->surplus_order_ids)->get();
|
||||
}
|
||||
return $this->success(OrderResource::make($order));
|
||||
}
|
||||
|
||||
public function save(OrderSave $request)
|
||||
{
|
||||
$request->validate([
|
||||
'plan_id' => 'required|exists:App\Models\Plan,id',
|
||||
'period' => 'required|string'
|
||||
]);
|
||||
|
||||
$user = User::findOrFail($request->user()->id);
|
||||
$userService = app(UserService::class);
|
||||
|
||||
if ($userService->isNotCompleteOrderByUserId($user->id)) {
|
||||
throw new ApiException(__('You have an unpaid or pending order, please try again later or cancel it'));
|
||||
}
|
||||
|
||||
$plan = Plan::findOrFail($request->input('plan_id'));
|
||||
$planService = new PlanService($plan);
|
||||
|
||||
$planService->validatePurchase($user, $request->input('period'));
|
||||
|
||||
$order = OrderService::createFromRequest(
|
||||
$user,
|
||||
$plan,
|
||||
$request->input('period'),
|
||||
$request->input('coupon_code')
|
||||
);
|
||||
|
||||
return $this->success($order->trade_no);
|
||||
}
|
||||
|
||||
protected function applyCoupon(Order $order, string $couponCode): void
|
||||
{
|
||||
$couponService = new CouponService($couponCode);
|
||||
if (!$couponService->use($order)) {
|
||||
throw new ApiException(__('Coupon failed'));
|
||||
}
|
||||
$order->coupon_id = $couponService->getId();
|
||||
}
|
||||
|
||||
protected function handleUserBalance(Order $order, User $user, UserService $userService): void
|
||||
{
|
||||
$remainingBalance = $user->balance - $order->total_amount;
|
||||
|
||||
if ($remainingBalance > 0) {
|
||||
if (!$userService->addBalance($order->user_id, -$order->total_amount)) {
|
||||
throw new ApiException(__('Insufficient balance'));
|
||||
}
|
||||
$order->balance_amount = $order->total_amount;
|
||||
$order->total_amount = 0;
|
||||
} else {
|
||||
if (!$userService->addBalance($order->user_id, -$user->balance)) {
|
||||
throw new ApiException(__('Insufficient balance'));
|
||||
}
|
||||
$order->balance_amount = $user->balance;
|
||||
$order->total_amount = $order->total_amount - $user->balance;
|
||||
}
|
||||
}
|
||||
|
||||
public function checkout(Request $request)
|
||||
{
|
||||
$tradeNo = $request->input('trade_no');
|
||||
$method = $request->input('method');
|
||||
$order = Order::where('trade_no', $tradeNo)
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('status', 0)
|
||||
->first();
|
||||
if (!$order) {
|
||||
return $this->fail([400, __('Order does not exist or has been paid')]);
|
||||
}
|
||||
// free process
|
||||
if ($order->total_amount <= 0) {
|
||||
$orderService = new OrderService($order);
|
||||
if (!$orderService->paid($order->trade_no))
|
||||
return $this->fail([400, '支付失败']);
|
||||
return response([
|
||||
'type' => -1,
|
||||
'data' => true
|
||||
]);
|
||||
}
|
||||
$payment = Payment::find($method);
|
||||
if (!$payment || !$payment->enable) {
|
||||
return $this->fail([400, __('Payment method is not available')]);
|
||||
}
|
||||
$paymentService = new PaymentService($payment->payment, $payment->id);
|
||||
$order->handling_amount = NULL;
|
||||
if ($payment->handling_fee_fixed || $payment->handling_fee_percent) {
|
||||
$order->handling_amount = (int) round(($order->total_amount * ($payment->handling_fee_percent / 100)) + $payment->handling_fee_fixed);
|
||||
}
|
||||
$order->payment_id = $method;
|
||||
if (!$order->save())
|
||||
return $this->fail([400, __('Request failed, please try again later')]);
|
||||
$result = $paymentService->pay([
|
||||
'trade_no' => $tradeNo,
|
||||
'total_amount' => isset($order->handling_amount) ? ($order->total_amount + $order->handling_amount) : $order->total_amount,
|
||||
'user_id' => $order->user_id,
|
||||
'stripe_token' => $request->input('token')
|
||||
]);
|
||||
return response([
|
||||
'type' => $result['type'],
|
||||
'data' => $result['data']
|
||||
]);
|
||||
}
|
||||
|
||||
public function check(Request $request)
|
||||
{
|
||||
$tradeNo = $request->input('trade_no');
|
||||
$order = Order::where('trade_no', $tradeNo)
|
||||
->where('user_id', $request->user()->id)
|
||||
->first();
|
||||
if (!$order) {
|
||||
return $this->fail([400, __('Order does not exist')]);
|
||||
}
|
||||
return $this->success($order->status);
|
||||
}
|
||||
|
||||
public function getPaymentMethod()
|
||||
{
|
||||
$methods = Payment::select([
|
||||
'id',
|
||||
'name',
|
||||
'payment',
|
||||
'icon',
|
||||
'handling_fee_fixed',
|
||||
'handling_fee_percent'
|
||||
])
|
||||
->where('enable', 1)
|
||||
->orderBy('sort', 'ASC')
|
||||
->get();
|
||||
|
||||
return $this->success($methods);
|
||||
}
|
||||
|
||||
public function cancel(Request $request)
|
||||
{
|
||||
if (empty($request->input('trade_no'))) {
|
||||
return $this->fail([422, __('Invalid parameter')]);
|
||||
}
|
||||
$order = Order::where('trade_no', $request->input('trade_no'))
|
||||
->where('user_id', $request->user()->id)
|
||||
->first();
|
||||
if (!$order) {
|
||||
return $this->fail([400, __('Order does not exist')]);
|
||||
}
|
||||
if ($order->status !== 0) {
|
||||
return $this->fail([400, __('You can only cancel pending orders')]);
|
||||
}
|
||||
$orderService = new OrderService($order);
|
||||
if (!$orderService->cancel()) {
|
||||
return $this->fail([400, __('Cancel failed')]);
|
||||
}
|
||||
return $this->success(true);
|
||||
}
|
||||
}
|
||||
38
Xboard/app/Http/Controllers/V1/User/PlanController.php
Normal file
38
Xboard/app/Http/Controllers/V1/User/PlanController.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\PlanResource;
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use App\Services\PlanService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PlanController extends Controller
|
||||
{
|
||||
protected PlanService $planService;
|
||||
|
||||
public function __construct(PlanService $planService)
|
||||
{
|
||||
$this->planService = $planService;
|
||||
}
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$user = User::find($request->user()->id);
|
||||
if ($request->input('id')) {
|
||||
$plan = Plan::where('id', $request->input('id'))->first();
|
||||
if (!$plan) {
|
||||
return $this->fail([400, __('Subscription plan does not exist')]);
|
||||
}
|
||||
if (!$this->planService->isPlanAvailableForUser($plan, $user)) {
|
||||
return $this->fail([400, __('Subscription plan does not exist')]);
|
||||
}
|
||||
return $this->success(PlanResource::make($plan));
|
||||
}
|
||||
|
||||
$plans = $this->planService->getAvailablePlans();
|
||||
return $this->success(PlanResource::collection($plans));
|
||||
}
|
||||
}
|
||||
31
Xboard/app/Http/Controllers/V1/User/ServerController.php
Normal file
31
Xboard/app/Http/Controllers/V1/User/ServerController.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\NodeResource;
|
||||
use App\Models\User;
|
||||
use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ServerController extends Controller
|
||||
{
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$user = User::find($request->user()->id);
|
||||
$servers = [];
|
||||
$userService = new UserService();
|
||||
if ($userService->isAvailable($user)) {
|
||||
$servers = ServerService::getAvailableServers($user);
|
||||
}
|
||||
$eTag = sha1(json_encode(array_column($servers, 'cache_key')));
|
||||
if (strpos($request->header('If-None-Match', ''), $eTag) !== false ) {
|
||||
return response(null,304);
|
||||
}
|
||||
$data = NodeResource::collection($servers);
|
||||
return response([
|
||||
'data' => $data
|
||||
])->header('ETag', "\"{$eTag}\"");
|
||||
}
|
||||
}
|
||||
26
Xboard/app/Http/Controllers/V1/User/StatController.php
Normal file
26
Xboard/app/Http/Controllers/V1/User/StatController.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\TrafficLogResource;
|
||||
use App\Models\StatUser;
|
||||
use App\Services\StatisticalService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StatController extends Controller
|
||||
{
|
||||
public function getTrafficLog(Request $request)
|
||||
{
|
||||
$startDate = now()->startOfMonth()->timestamp;
|
||||
$records = StatUser::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('record_at', '>=', $startDate)
|
||||
->orderBy('record_at', 'DESC')
|
||||
->get();
|
||||
|
||||
$data = TrafficLogResource::collection(collect($records));
|
||||
return $this->success($data);
|
||||
}
|
||||
}
|
||||
26
Xboard/app/Http/Controllers/V1/User/TelegramController.php
Normal file
26
Xboard/app/Http/Controllers/V1/User/TelegramController.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Services\TelegramService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TelegramController extends Controller
|
||||
{
|
||||
public function getBotInfo()
|
||||
{
|
||||
$telegramService = new TelegramService();
|
||||
$response = $telegramService->getMe();
|
||||
$data = [
|
||||
'username' => $response->result->username
|
||||
];
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
public function unbind(Request $request)
|
||||
{
|
||||
$user = User::where('user_id', $request->user()->id)->first();
|
||||
}
|
||||
}
|
||||
154
Xboard/app/Http/Controllers/V1/User/TicketController.php
Normal file
154
Xboard/app/Http/Controllers/V1/User/TicketController.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\User\TicketSave;
|
||||
use App\Http\Requests\User\TicketWithdraw;
|
||||
use App\Http\Resources\TicketResource;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\TicketMessage;
|
||||
use App\Models\User;
|
||||
use App\Services\TicketService;
|
||||
use App\Utils\Dict;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TicketController extends Controller
|
||||
{
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
if ($request->input('id')) {
|
||||
$ticket = Ticket::where('id', $request->input('id'))
|
||||
->where('user_id', $request->user()->id)
|
||||
->first()
|
||||
->load('message');
|
||||
if (!$ticket) {
|
||||
return $this->fail([400, __('Ticket does not exist')]);
|
||||
}
|
||||
$ticket['message'] = TicketMessage::where('ticket_id', $ticket->id)->get();
|
||||
$ticket['message']->each(function ($message) use ($ticket) {
|
||||
$message['is_me'] = ($message['user_id'] == $ticket->user_id);
|
||||
});
|
||||
return $this->success(TicketResource::make($ticket)->additional(['message' => true]));
|
||||
}
|
||||
$ticket = Ticket::where('user_id', $request->user()->id)
|
||||
->orderBy('created_at', 'DESC')
|
||||
->get();
|
||||
return $this->success(TicketResource::collection($ticket));
|
||||
}
|
||||
|
||||
public function save(TicketSave $request)
|
||||
{
|
||||
$ticketService = new TicketService();
|
||||
$ticket = $ticketService->createTicket(
|
||||
$request->user()->id,
|
||||
$request->input('subject'),
|
||||
$request->input('level'),
|
||||
$request->input('message')
|
||||
);
|
||||
HookManager::call('ticket.create.after', $ticket);
|
||||
return $this->success(true);
|
||||
|
||||
}
|
||||
|
||||
public function reply(Request $request)
|
||||
{
|
||||
if (empty($request->input('id'))) {
|
||||
return $this->fail([400, __('Invalid parameter')]);
|
||||
}
|
||||
if (empty($request->input('message'))) {
|
||||
return $this->fail([400, __('Message cannot be empty')]);
|
||||
}
|
||||
$ticket = Ticket::where('id', $request->input('id'))
|
||||
->where('user_id', $request->user()->id)
|
||||
->first();
|
||||
if (!$ticket) {
|
||||
return $this->fail([400, __('Ticket does not exist')]);
|
||||
}
|
||||
if ($ticket->status) {
|
||||
return $this->fail([400, __('The ticket is closed and cannot be replied')]);
|
||||
}
|
||||
if ((int) admin_setting('ticket_must_wait_reply', 0) && $request->user()->id == $this->getLastMessage($ticket->id)->user_id) {
|
||||
return $this->fail(codeResponse: [400, __('Please wait for the technical enginneer to reply')]);
|
||||
}
|
||||
$ticketService = new TicketService();
|
||||
if (
|
||||
!$ticketService->reply(
|
||||
$ticket,
|
||||
$request->input('message'),
|
||||
$request->user()->id
|
||||
)
|
||||
) {
|
||||
return $this->fail([400, __('Ticket reply failed')]);
|
||||
}
|
||||
HookManager::call('ticket.reply.user.after', $ticket);
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
|
||||
public function close(Request $request)
|
||||
{
|
||||
if (empty($request->input('id'))) {
|
||||
return $this->fail([422, __('Invalid parameter')]);
|
||||
}
|
||||
$ticket = Ticket::where('id', $request->input('id'))
|
||||
->where('user_id', $request->user()->id)
|
||||
->first();
|
||||
if (!$ticket) {
|
||||
return $this->fail([400, __('Ticket does not exist')]);
|
||||
}
|
||||
$ticket->status = Ticket::STATUS_CLOSED;
|
||||
if (!$ticket->save()) {
|
||||
return $this->fail([500, __('Close failed')]);
|
||||
}
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
private function getLastMessage($ticketId)
|
||||
{
|
||||
return TicketMessage::where('ticket_id', $ticketId)
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
}
|
||||
|
||||
public function withdraw(TicketWithdraw $request)
|
||||
{
|
||||
if ((int) admin_setting('withdraw_close_enable', 0)) {
|
||||
return $this->fail([400, 'Unsupported withdraw']);
|
||||
}
|
||||
if (
|
||||
!in_array(
|
||||
$request->input('withdraw_method'),
|
||||
admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT)
|
||||
)
|
||||
) {
|
||||
return $this->fail([422, __('Unsupported withdrawal method')]);
|
||||
}
|
||||
$user = User::find($request->user()->id);
|
||||
$limit = admin_setting('commission_withdraw_limit', 100);
|
||||
if ($limit > ($user->commission_balance / 100)) {
|
||||
return $this->fail([422, __('The current required minimum withdrawal commission is :limit', ['limit' => $limit])]);
|
||||
}
|
||||
try {
|
||||
$ticketService = new TicketService();
|
||||
$subject = __('[Commission Withdrawal Request] This ticket is opened by the system');
|
||||
$message = sprintf(
|
||||
"%s\r\n%s",
|
||||
__('Withdrawal method') . ":" . $request->input('withdraw_method'),
|
||||
__('Withdrawal account') . ":" . $request->input('withdraw_account')
|
||||
);
|
||||
$ticket = $ticketService->createTicket(
|
||||
$request->user()->id,
|
||||
$subject,
|
||||
2,
|
||||
$message
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
HookManager::call('ticket.create.after', $ticket);
|
||||
return $this->success(true);
|
||||
}
|
||||
}
|
||||
223
Xboard/app/Http/Controllers/V1/User/UserController.php
Normal file
223
Xboard/app/Http/Controllers/V1/User/UserController.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\User;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\User\UserChangePassword;
|
||||
use App\Http\Requests\User\UserTransfer;
|
||||
use App\Http\Requests\User\UserUpdate;
|
||||
use App\Models\Order;
|
||||
use App\Models\Plan;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\LoginService;
|
||||
use App\Services\AuthService;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\CacheKey;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
protected $loginService;
|
||||
|
||||
public function __construct(
|
||||
LoginService $loginService
|
||||
) {
|
||||
$this->loginService = $loginService;
|
||||
}
|
||||
|
||||
public function getActiveSession(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
$authService = new AuthService($user);
|
||||
return $this->success($authService->getSessions());
|
||||
}
|
||||
|
||||
public function removeActiveSession(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
$authService = new AuthService($user);
|
||||
return $this->success($authService->removeSession($request->input('session_id')));
|
||||
}
|
||||
|
||||
public function checkLogin(Request $request)
|
||||
{
|
||||
$data = [
|
||||
'is_login' => $request->user()?->id ? true : false
|
||||
];
|
||||
if ($request->user()?->is_admin) {
|
||||
$data['is_admin'] = true;
|
||||
}
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
public function changePassword(UserChangePassword $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
if (
|
||||
!Helper::multiPasswordVerify(
|
||||
$user->password_algo,
|
||||
$user->password_salt,
|
||||
$request->input('old_password'),
|
||||
$user->password
|
||||
)
|
||||
) {
|
||||
return $this->fail([400, __('The old password is wrong')]);
|
||||
}
|
||||
$user->password = password_hash($request->input('new_password'), PASSWORD_DEFAULT);
|
||||
$user->password_algo = NULL;
|
||||
$user->password_salt = NULL;
|
||||
if (!$user->save()) {
|
||||
return $this->fail([400, __('Save failed')]);
|
||||
}
|
||||
|
||||
$currentToken = $user->currentAccessToken();
|
||||
if ($currentToken) {
|
||||
$user->tokens()->where('id', '!=', $currentToken->id)->delete();
|
||||
} else {
|
||||
$user->tokens()->delete();
|
||||
}
|
||||
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function info(Request $request)
|
||||
{
|
||||
$user = User::where('id', $request->user()->id)
|
||||
->select([
|
||||
'email',
|
||||
'transfer_enable',
|
||||
'last_login_at',
|
||||
'created_at',
|
||||
'banned',
|
||||
'remind_expire',
|
||||
'remind_traffic',
|
||||
'expired_at',
|
||||
'balance',
|
||||
'commission_balance',
|
||||
'plan_id',
|
||||
'discount',
|
||||
'commission_rate',
|
||||
'telegram_id',
|
||||
'uuid'
|
||||
])
|
||||
->first();
|
||||
if (!$user) {
|
||||
return $this->fail([400, __('The user does not exist')]);
|
||||
}
|
||||
$user['avatar_url'] = 'https://cdn.v2ex.com/gravatar/' . md5($user->email) . '?s=64&d=identicon';
|
||||
return $this->success($user);
|
||||
}
|
||||
|
||||
public function getStat(Request $request)
|
||||
{
|
||||
$stat = [
|
||||
Order::where('status', 0)
|
||||
->where('user_id', $request->user()->id)
|
||||
->count(),
|
||||
Ticket::where('status', 0)
|
||||
->where('user_id', $request->user()->id)
|
||||
->count(),
|
||||
User::where('invite_user_id', $request->user()->id)
|
||||
->count()
|
||||
];
|
||||
return $this->success($stat);
|
||||
}
|
||||
|
||||
public function getSubscribe(Request $request)
|
||||
{
|
||||
$user = User::where('id', $request->user()->id)
|
||||
->select([
|
||||
'plan_id',
|
||||
'token',
|
||||
'expired_at',
|
||||
'u',
|
||||
'd',
|
||||
'transfer_enable',
|
||||
'email',
|
||||
'uuid',
|
||||
'device_limit',
|
||||
'speed_limit',
|
||||
'next_reset_at'
|
||||
])
|
||||
->first();
|
||||
if (!$user) {
|
||||
return $this->fail([400, __('The user does not exist')]);
|
||||
}
|
||||
if ($user->plan_id) {
|
||||
$user['plan'] = Plan::find($user->plan_id);
|
||||
if (!$user['plan']) {
|
||||
return $this->fail([400, __('Subscription plan does not exist')]);
|
||||
}
|
||||
}
|
||||
$user['subscribe_url'] = Helper::getSubscribeUrl($user['token']);
|
||||
$userService = new UserService();
|
||||
$user['reset_day'] = $userService->getResetDay($user);
|
||||
$user = HookManager::filter('user.subscribe.response', $user);
|
||||
return $this->success($user);
|
||||
}
|
||||
|
||||
public function resetSecurity(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
$user->uuid = Helper::guid(true);
|
||||
$user->token = Helper::guid();
|
||||
if (!$user->save()) {
|
||||
return $this->fail([400, __('Reset failed')]);
|
||||
}
|
||||
return $this->success(Helper::getSubscribeUrl($user->token));
|
||||
}
|
||||
|
||||
public function update(UserUpdate $request)
|
||||
{
|
||||
$updateData = $request->only([
|
||||
'remind_expire',
|
||||
'remind_traffic'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
try {
|
||||
$user->update($updateData);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail([400, __('Save failed')]);
|
||||
}
|
||||
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function transfer(UserTransfer $request)
|
||||
{
|
||||
$amount = $request->input('transfer_amount');
|
||||
try {
|
||||
DB::transaction(function () use ($request, $amount) {
|
||||
$user = User::lockForUpdate()->find($request->user()->id);
|
||||
if (!$user) {
|
||||
throw new \Exception(__('The user does not exist'));
|
||||
}
|
||||
if ($amount > $user->commission_balance) {
|
||||
throw new \Exception(__('Insufficient commission balance'));
|
||||
}
|
||||
$user->commission_balance -= $amount;
|
||||
$user->balance += $amount;
|
||||
if (!$user->save()) {
|
||||
throw new \Exception(__('Transfer failed'));
|
||||
}
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail([400, $e->getMessage()]);
|
||||
}
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function getQuickLoginUrl(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$url = $this->loginService->generateQuickLoginUrl($user, $request->input('redirect'));
|
||||
return $this->success($url);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user