This commit is contained in:
iVampireSP.com 2023-05-14 15:42:18 +08:00
parent cd9d716892
commit 7f5bd004a2
No known key found for this signature in database
GPG Key ID: 2F7B001CA27A8132
53 changed files with 2721 additions and 210 deletions

View File

@ -0,0 +1,48 @@
<?php
namespace App\Console\Commands;
use App\Models\Admin;
use Illuminate\Console\Command;
class ChangePassword extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'pwd';
/**
* The console command description.
*
* @var string
*/
protected $description = '修改指定管理员用户的密码';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$email = $this->ask('请输入邮箱');
$admin = Admin::where('email', $email)->first();
if (! $admin) {
$this->error('用户不存在');
return 1;
}
$password = $this->secret('请输入新密码');
$admin->password = bcrypt($password);
$admin->save();
$this->info('密码修改成功');
return 0;
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Console\Commands;
use App\Models\Admin;
use Illuminate\Console\Command;
class CreateAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'admin:create';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a new admin user';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
// if is local
if (env('APP_ENV') == 'local') {
$this->info('由于是 local 环境,将会自动创建 Admin 用户。');
Admin::create([
'name' => 'Test',
'email' => 'im@ivampiresp.com',
'password' => bcrypt('123456'),
]);
$this->info('邮箱: im@ivampiresp.com, 密码: 123456');
return 0;
}
// ask for the name of the admin to create
$name = $this->ask('请输入用户名');
// ask for the email of the admin to create
$email = $this->ask('请输入邮箱');
// enter password
$password = $this->secret('请输入密码(密码不会显示在终端)');
// create the admin
Admin::create([
'name' => $name,
'email' => $email,
'password' => bcrypt($password),
]);
$this->info('管理员创建成功!');
return 0;
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class MakeAdmin extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'make-admin {email}';
/**
* The console command description.
*
* @var string
*/
protected $description = '将一个用户升级为管理员,需要提供用户的邮箱。';
/**
* Execute the console command.
*/
public function handle(): void
{
$email = $this->argument('email');
$user = (new User)->where('email', $email)->first();
if ($user) {
if (!$user->is_admin) {
$user->is_admin = true;
$user->save();
$this->info('用户已经升级为管理员。需要取消管理员身份请使用再次执行此命令。');
return;
}
$cancel = $this->confirm('用户已经升级为管理员,要取消管理员身份吗?', true);
if ($cancel) {
$user->is_admin = false;
$user->save();
$this->info('用户已经取消管理员身份。');
} else {
$this->info('用户仍然是管理员。');
}
} else {
$this->error('用户不存在。');
}
}
}

View File

@ -3,6 +3,7 @@
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use App\Http\Controllers\Admin\ServerController;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
@ -12,7 +13,15 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
$schedule->call(function () {
(new ServerController())->checkServer();
})->everyMinute()->name('FrpServer')->withoutOverlapping()->onOneServer();
// $schedule->job(new Cost())->hourly()->name('FrpServerCost')->withoutOverlapping()->onOneServer();
// every three days
// $schedule->job(new ReviewWebsiteJob())->daily()->name('reviewWebsiteJob')->withoutOverlapping()->onOneServer();
}
/**

58
app/Helpers.php Normal file
View File

@ -0,0 +1,58 @@
<?php
if (! function_exists('success')) {
function success($data = null, $to = false)
{
if ($to) {
return redirect()->to($to)->with('status', $data ?? 'Success!');
}
return redirect()->back()->with('status', $data ?? 'Success!');
}
}
if (! function_exists('failed')) {
function failed($data = null)
{
return redirect()->back()->with('error', $data ?? 'Success!');
}
}
if (! function_exists('isUser')) {
function isUser($user_id)
{
return auth()->id() === $user_id ? true : false;
}
}
if (! function_exists('unitConversion')) {
function unitConversion($num)
{
$p = 0;
$format = 'Bytes';
if ($num > 0 && $num < 1024) {
$p = 0;
return number_format($num).' '.$format;
}
if ($num >= 1024 && $num < pow(1024, 2)) {
$p = 1;
$format = 'KB';
}
if ($num >= pow(1024, 2) && $num < pow(1024, 3)) {
$p = 2;
$format = 'MB';
}
if ($num >= pow(1024, 3) && $num < pow(1024, 4)) {
$p = 3;
$format = 'GB';
}
if ($num >= pow(1024, 4) && $num < pow(1024, 5)) {
$p = 3;
$format = 'TB';
}
$num /= pow(1024, $p);
return number_format($num, 3).' '.$format;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use App\Http\Controllers\Controller;
class IndexController extends Controller
{
public function index(Request $request)
{
// if not login, redirect to login
if (! Auth::guard('admin')->check()) {
return view('admin.login');
} else {
$servers = Server::where('status', '!=', 'up')->get();
return view('admin.index', compact('servers'));
}
}
public function login(Request $request)
{
// attempt to login
if (Auth::guard('admin')->attempt($request->only(['email', 'password']), $request->has('remember'))) {
// if success, redirect to home
return redirect()->intended('/');
} else {
// if fail, redirect to login with error message
return redirect()->back()->withErrors(['message' => '用户名或密码错误'])->withInput();
}
}
public function logout()
{
// logout
Auth::guard('admin')->logout();
return redirect()->route('admin.login');
}
}

View File

@ -0,0 +1,247 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Support\Frp;
use App\Models\Server;
use Illuminate\View\View;
use App\Jobs\ServerCheckJob;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Log;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Cache;
use GuzzleHttp\Exception\RequestException;
class ServerController extends Controller
{
/**
* Display a listing of the resource.
*
* @return View
*/
public function index()
{
//
$servers = Server::get();
// $servers = Server::simplePaginate(10);
return view('admin.servers.index', compact('servers'));
}
/**
* Show the form for creating a new resource.
*
* @return View
*/
public function create()
{
return view('admin.servers.create');
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$request->validate($this->rules());
$request_data = $request->toArray();
// $request_data['user_id'] = auth()->id();
$request_data['allow_http'] = $request->allow_http ?? 0;
$request_data['allow_https'] = $request->allow_https ?? 0;
$request_data['allow_tcp'] = $request->allow_tcp ?? 0;
$request_data['allow_udp'] = $request->allow_udp ?? 0;
$request_data['allow_stcp'] = $request->allow_stcp ?? 0;
$request_data['allow_xtcp'] = $request->allow_xtcp ?? 0;
$request_data['allow_sudp'] = $request->allow_sudp ?? 0;
$request_data['is_china_mainland'] = $request->is_china_mainland ?? 0;
$server = Server::create($request_data);
return redirect()->route('admin.servers.edit', $server);
}
/**
* Display the specified resource.
*
* @param Server $server
* @return RedirectResponse|View
*/
public function show(Server $server)
{
try {
$serverInfo = (object) (new Frp($server))->serverInfo();
} catch (RequestException $e) {
Log::error($e->getMessage());
return redirect()->route('admin.servers.index')->with('error', '服务器连接失败。');
}
return view('admin.servers.show', compact('server'));
}
/**
* Show the form for editing the specified resource.
*
* @param Server $server
* @return View
*/
public function edit(Server $server)
{
$serverInfo = (object) (new Frp($server))->serverInfo();
return view('admin.servers.edit', compact('server', 'serverInfo'));
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param Server $server
* @return RedirectResponse
*/
public function update(Request $request, Server $server)
{
if (! $request->has('status')) {
$request->merge(['allow_http' => $request->has('allow_http') ? true : false]);
$request->merge(['allow_https' => $request->has('allow_https') ? true : false]);
$request->merge(['allow_tcp' => $request->has('allow_tcp') ? true : false]);
$request->merge(['allow_udp' => $request->has('allow_udp') ? true : false]);
$request->merge(['allow_stcp' => $request->has('allow_stcp') ? true : false]);
$request->merge(['allow_xtcp' => $request->has('allow_xtcp') ? true : false]);
$request->merge(['allow_sudp' => $request->has('allow_sudp') ? true : false]);
$request->merge(['is_china_mainland' => $request->has('is_china_mainland') ? true : false]);
}
$data = $request->all();
$server->update($data);
return redirect()->route('admin.servers.index')->with('success', '服务器成功更新。');
}
/**
* Remove the specified resource from storage.
*
* @param Server $server
* @return RedirectResponse
*/
public function destroy(Server $server)
{
$server->delete();
return redirect()->route('admin.servers.index')->with('success', '服务器成功删除。');
}
public function rules($id = null)
{
return [
'name' => 'required|max:20',
'server_address' => [
'required',
Rule::unique('servers')->ignore($id),
],
'server_port' => 'required|integer|max:65535|min:1',
'token' => 'required|max:50',
'dashboard_port' => 'required|integer|max:65535|min:1',
'dashboard_user' => 'required|max:20',
'dashboard_password' => 'required|max:32',
'allow_http' => 'boolean',
'allow_https' => 'boolean',
'allow_tcp' => 'boolean',
'allow_udp' => 'boolean',
'allow_stcp' => 'boolean',
'allow_xtcp' => 'boolean',
'allow_sudp' => 'boolean',
'min_port' => 'required|integer|max:65535|min:1',
'max_port' => 'required|integer|max:65535|min:1',
'max_tunnels' => 'required|integer|max:65535|min:1',
];
}
public function checkServer($id = null)
{
if (is_null($id)) {
// refresh all
Server::chunk(100, function ($servers) {
foreach ($servers as $server) {
dispatch(new ServerCheckJob($server->id));
}
});
} else {
if (Server::where('id', $id)->exists()) {
dispatch(new ServerCheckJob($id));
return true;
} else {
return false;
}
}
}
public function scanTunnel($server_id)
{
$server = Server::find($server_id);
if (is_null($server)) {
return false;
}
$frp = new Frp($server);
if ($server->allow_http) {
$proxies = $frp->httpTunnels()['proxies'] ?? ['proxies' => []];
$this->cacheProxies($proxies);
}
if ($server->allow_https) {
$proxies = $frp->httpsTunnels()['proxies'] ?? ['proxies' => []];
$this->cacheProxies($proxies);
}
if ($server->allow_tcp) {
$proxies = $frp->tcpTunnels()['proxies'] ?? ['proxies' => []];
$this->cacheProxies($proxies);
}
if ($server->allow_udp) {
$proxies = $frp->udpTunnels()['proxies'] ?? ['proxies' => []];
$this->cacheProxies($proxies);
}
if ($server->allow_stcp) {
$proxies = $frp->stcpTunnels()['proxies'] ?? ['proxies' => []];
$this->cacheProxies($proxies);
}
if ($server->allow_xtcp) {
$proxies = $frp->xtcpTunnels()['proxies'] ?? ['proxies' => []];
$this->cacheProxies($proxies);
}
}
private function cacheProxies($proxies)
{
foreach ($proxies as $proxy) {
if (! isset($proxy['name'])) {
continue;
}
$cache_key = 'frpTunnel_data_'.$proxy['name'];
Cache::put($cache_key, $proxy, 86400);
}
}
public function getTunnel($name)
{
$cache_key = 'frpTunnel_data_'.$name;
return Cache::get($cache_key);
}
}

View File

@ -0,0 +1,146 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\Tunnel;
use Illuminate\View\View;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
class TunnelController extends Controller
{
/**
* Display a listing of the resource.
*
* @return RedirectResponse|View
*/
public function index(Request $request)
{
$hosts = Tunnel::with('user');
// if has has_free_traffic
if ($request->has_free_traffic == 1) {
$hosts = $hosts->where('free_traffic', '>', 0);
}
foreach ($request->except(['has_free_traffic', 'page']) as $key => $value) {
if (empty($value)) {
continue;
}
if ($request->{$key}) {
$hosts = $hosts->where($key, 'LIKE', '%'.$value.'%');
}
}
$count = $hosts->count();
$hosts = $hosts->simplePaginate(100);
return view('admin.tunnels.index', ['hosts' => $hosts, 'count' => $count]);
}
/**
* Display the specified resource.
*
* @param Tunnel $tunnel
* @return View
*/
public function show(Tunnel $tunnel)
{
$tunnel->load('server');
return view('admin.tunnels.show', compact('tunnel'));
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param Tunnel $tunnel
* @return RedirectResponse
*/
public function update(Request $request, Tunnel $tunnel)
{
$request->validate([
'locked_reason' => 'nullable|string'
]);
$tunnel->update($request->all());
return back()->with('success', '完成。');
}
/**
* Remove the specified resource from storage.
*
* @param Tunnel $host
* @return RedirectResponse
*/
public function destroy(Tunnel $host)
{
$host->delete();
return back()->with('success', '已开始销毁。');
}
public function generateConfig(Tunnel $tunnel)
{
$tunnel->load('server');
// 配置文件
$config = [];
$config['server'] = <<<EOF
[common]
server_addr = {$tunnel->server->server_address}
server_port = {$tunnel->server->server_port}
token = {$tunnel->server->token}
EOF;
$local_addr = explode(':', $tunnel->local_address);
$config['client'] = <<<EOF
[{$tunnel->client_token}]
type = {$tunnel->protocol}
local_ip = {$local_addr[0]}
local_port = {$local_addr[1]}
EOF;
if ($tunnel->protocol == 'tcp' || $tunnel->protocol == 'udp') {
$config['client'] .= PHP_EOL . 'remote_port = ' . $tunnel->remote_port;
} elseif ($tunnel->protocol == 'http' || $tunnel->protocol == 'https') {
$config['client'] .= PHP_EOL . 'custom_domains = ' . $tunnel->custom_domain . PHP_EOL;
} elseif ($tunnel->server->allow_stcp || $tunnel->server->allow_xtcp || $tunnel->server->allow_sudp) {
$uuid = Str::uuid();
$config['client'] .= <<<EOF
sk = {$tunnel->sk}
# 以下的是对端配置文件,请不要复制或者使用!
# 如果你想让别人通过 XTCP|STCP|SUDP 连接到你的主机,请将以下配置文件发给你信任的人。如果你不信任他人, 请勿发送, 这样会导致不信任的人也能通过 XTCP 连接到你的主机。
# XTCP 连接不能保证稳定性, 并且也不会100%成功。
#------ 对端复制开始 --------
[common]
server_addr = {$tunnel->server->server_address}
server_port = {$tunnel->server->server_port}
user = visitor_{$uuid}
token = {$tunnel->server->token}
[lae_visitor_{$uuid}]
type = xtcp
role = visitor
server_name = lae_visitor_{$uuid}
sk = {$tunnel->sk}
bind_addr = {$local_addr[0]}
bind_port = {$local_addr[1]}
#------ 对端复制结束 --------
# 非常感谢您的支持。如果您觉得这个项目不错,请将我们的网站分享给您的朋友。谢谢。
EOF;
}
return $config;
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Models\User;
use Illuminate\View\View;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*
* @return View
*/
public function index(Request $request)
{
$users = new User();
$count = User::count();
foreach ($request->except(['page']) as $key => $value) {
if (empty($value)) {
continue;
}
if ($request->{$key}) {
$users = $users->where($key, 'LIKE', '%'.$value.'%');
}
}
$count = $users->count();
$users = $users->simplePaginate(100);
return view('admin.users.index', ['users' => $users, 'count' => $count]);
}
public function update(Request $request, User $user)
{
$user->update($request->all());
// if not ajax
if ($request->ajax()) {
return response()->json([
'status' => 'success',
'message' => '更新成功',
]);
} else {
return redirect()->route('users.index');
}
}
/**
* Remove the specified resource from storage.
*
* @param User $user
* @return RedirectResponse
*/
public function destroy(User $user)
{
$user->delete();
return back()->with('success', '删除成功');
}
}

View File

@ -40,15 +40,19 @@ public function handler(Request $request, $key)
return $this->failed('找不到隧道。');
}
switch ($host->status) {
case 'stopped':
return $this->failed('隧道已停止。');
case 'error':
return $this->failed('隧道出错。');
case 'suspended':
return $this->failed('隧道已暂停。');
if ($host->locked_reason) {
return $this->failed('隧道被锁定,原因是' . $host->locked_reason . '。');
}
// switch ($host->status) {
// case 'stopped':
// return $this->failed('隧道已停止。');
// case 'error':
// return $this->failed('隧道出错。');
// case 'suspended':
// return $this->failed('隧道已暂停。');
// }
if ($request->input('content')['proxy_type'] !== $host->protocol) {
return $this->failed('不允许的隧道协议。');
}
@ -69,14 +73,11 @@ public function handler(Request $request, $key)
}
// cache
// $cache_key = 'frp_user_' . $request->content['proxy_name'];
// Cache::put($cache_key, $host->user_id);
$cache_key = 'frpTunnel_data_' . $host->client_token;
Cache::put($cache_key, ['status' => 'online']);
// $host->run_id = $request->input('content')['user']['run_id'];
// $host->saveQuietly();
$host->run_id = $request->input('content')['user']['run_id'];
$host->saveQuietly();
// $data = [
// 'message' => '隧道 ' . $host->name . ' 已启动。',

View File

@ -147,12 +147,19 @@ public function store(Request $request)
/**
* Display the specified resource.
*/
public function show(TunnelRequest $request, Tunnel $tunnel)
public function show(TunnelRequest $tunnelRequest, Tunnel $tunnel)
{
unset($request);
unset($tunnelRequest);
$tunnel['config'] = $tunnel->getConfig();
return $this->success($tunnel);
}
public function close(TunnelRequest $tunnelRequest, Tunnel $tunnel) {
unset($tunnelRequest);
$tunnel->close();
return $this->noContent();
}
/**
* Update the specified resource in storage.
*/

View File

@ -1,29 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class Admin
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure(Request): (Response) $next
*
* @return Response
*/
public function handle(Request $request, Closure $next): Response
{
if (!$request->user('sanctum')?->isAdmin()) {
return response()->json([
'message' => 'You are not authorized to access this resource.'
], 403);
}
return $next($request);
}
}

35
app/Jobs/CheckServer.php Normal file
View File

@ -0,0 +1,35 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CheckServer implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
//
}
}

151
app/Jobs/Cost.php Normal file
View File

@ -0,0 +1,151 @@
<?php
namespace App\Jobs;
use App\Models\Host;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class Cost implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private $http;
/**
* Execute the job.
*
* @return void
*/
public function handle_old()
{
$this->http = Http::remote('remote')->asForm();
Server::with('hosts')->where('status', 'up')->whereNot('price_per_gb', 0)->chunk(100, function ($servers) {
foreach ($servers as $server) {
// $ServerCheckJob = new ServerCheckJob($server->id);
// $ServerCheckJob->handle();
foreach ($server->hosts as $host) {
$host->load('user');
Log::debug('------------');
Log::debug('主机: ' . $host->name);
Log::debug('属于用户: ' . $host->user->name);
$cache_key = 'frpTunnel_data_' . $host->client_token;
// $tunnel = 'frp_user_' . $host->client_token;
// $tunnel_user_id = Cache::get($tunnel);
$tunnel_data = Cache::get($cache_key, null);
if (!is_null($tunnel_data)) {
$traffic = ($tunnel_data['today_traffic_in'] ?? 0) + ($tunnel_data['today_traffic_out'] ?? 0);
// $traffic = 1073741824 * 10;
Log::debug('本次使用的流量: ' . round($traffic / 1024 / 1024 / 1024, 2) ?? 0);
$day = date('d');
$traffic_key = 'traffic_day_' . $day . '_used_' . $host->id;
$used_traffic = Cache::get($traffic_key, 0);
if ($used_traffic !== $traffic) {
// 保存 2 天
Cache::put($traffic_key, $traffic, 86400);
$used_traffic_gb = round($used_traffic / 1024 / 1024 / 1024, 2);
// Log::debug('上次使用的流量: ' . $used_traffic);
Log::debug('上次使用的流量 GB: ' . $used_traffic_gb);
$used_traffic = $traffic - $used_traffic;
Log::debug('流量差值: ' . round($used_traffic / 1024 / 1024 / 1024, 2) . ' GB');
}
$left_traffic = 0;
if ($host->user->free_traffic > 0) {
Log::debug('开始扣除免费流量时的 used_traffic: ' . round($used_traffic / 1024 / 1024 / 1024, 2));
$user_free_traffic = round($host->user->free_traffic * 1024 * 1024 * 1024, 2);
Log::debug('用户免费流量: ' . round($user_free_traffic / 1024 / 1024 / 1024, 2));
// $used_traffic -= $user_free_traffic;
// $used_traffic = abs($used_traffic);
Log::debug('扣除免费流量时的 used_traffic: ' . $used_traffic / 1024 / 1024 / 1024);
// 获取剩余
$left_traffic = $user_free_traffic - $used_traffic;
Log::debug('计算后剩余的免费流量: ' . $left_traffic / 1024 / 1024 / 1024);
// 保存
if ($left_traffic < 0) {
$left_traffic = 0;
}
// 保留两位小数
$left_traffic = round($left_traffic / 1024 / 1024 / 1024, 2);
$host->user->free_traffic = $left_traffic;
$host->user->save();
}
$used_traffic = abs($used_traffic);
Log::debug('实际用量:' . $used_traffic / 1024 / 1024 / 1024);
// $used_traffic -= $server->free_traffic * 1024 * 1024 * 1024;
// // $used_traffic = abs($used_traffic);
// Log::debug('服务器免费流量: ' . $server->free_traffic * 1024 * 1024 * 1024);
// Log::debug('使用的流量(减去服务器免费流量): ' . $used_traffic);
if ($used_traffic > 0 && $left_traffic == 0) {
Log::debug('此时 used_traffic: ' . $used_traffic);
// 要计费的流量
$traffic = round($used_traffic / (1024 * 1024 * 1024), 2) ?? 0;
$traffic = abs($traffic);
$gb = round($traffic, 2);
// 计算价格
$cost = $traffic * $host->server->price_per_gb;
$cost = abs($cost);
// 记录到日志
// if local
// if (config('app.env') == 'local') {
Log::debug('计费:' . $host->server->name . ' ' . $host->name . ' ' . $gb . 'GB ' . $cost . ' 的 CNY 消耗');
// }
// 如果计费金额大于 0则扣费
if ($cost > 0) {
// 发送扣费请求
$this->http->post('hosts/' . $host->host_id . '/cost', [
'amount' => $cost,
'description' => $host->name . ' 的 ' . $gb . ' GB 流量费用。',
]);
}
}
}
}
}
});
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Jobs;
use App\Http\Controllers\Admin\ServerController;
use App\Models\Server;
use App\Support\Frp;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
class ServerCheckJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $id;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($id)
{
$this->id = $id;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$frpServer = Server::find($this->id);
if (!is_null($frpServer)) {
// $frp = new FrpController($this->id);
$s = new ServerController();
$s->scanTunnel($frpServer->id);
$frpController = new Frp($frpServer);
$meta = $frpController->serverInfo();
if (!$meta) {
$meta = [
'status' => 'failed',
];
echo '服务器不可用: ' . $frpServer->name . ' failed' . PHP_EOL;
} else {
echo 'ServerCheckJob: ' . $frpServer->name . PHP_EOL;
}
$data = $frpServer->toArray();
$data['meta'] = $meta;
Cache::put('serverinfo_' . $frpServer->id, $data, 300);
}
}
}

40
app/Jobs/StatusJob.php Normal file
View File

@ -0,0 +1,40 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
class StatusJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $host_id;
public array $requests;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($host_id, $requests)
{
$this->host_id = $host_id;
$this->requests = $requests;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
Http::remote()->asForm()->patch('hosts/'.$this->host_id, $this->requests);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Jobs;
use App\Models\Host;
use App\Models\Tunnel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class StopAllHostJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $user_id;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(int $user_id)
{
//
$this->user_id = $user_id;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$hosts = Tunnel::where('user_id', $this->user_id);
$hosts->chunk(100, function () use ($hosts) {
foreach ($hosts as $host) {
$host->status = 'stopped';
$host->save();
}
});
}
}

View File

@ -1,9 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ActiveTunnel extends Model
{
}

View File

@ -2,7 +2,6 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

View File

@ -7,13 +7,16 @@
class Server extends Model
{
public $hidden = [
protected $hidden = [
'dashboard_password',
'dashboard_user',
'dashboard_port'
'dashboard_port',
'key'
];
protected $fillable = [
'name',
'key',
'server_address',
'server_port',
'token',
@ -31,6 +34,7 @@ class Server extends Model
'max_port',
'max_tunnels',
'is_china_mainland',
'status'
];
// tunnels

View File

@ -2,7 +2,12 @@
namespace App\Models;
use App\Support\Frp;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Cache;
use Illuminate\Database\Eloquent\Model;
use App\Http\Controllers\Admin\ServerController;
use App\Http\Controllers\Admin\TunnelController;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Tunnel extends Model
@ -18,6 +23,8 @@ class Tunnel extends Model
'status',
'server_id',
'user_id',
'locked_reason',
'run_id'
];
protected $with = [
@ -27,6 +34,55 @@ class Tunnel extends Model
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function getConfig()
{
return (new TunnelController)->generateConfig($this);
}
public function close()
{
if ($this->run_id) {
$frp = new Frp($this->server);
$closed = $frp->close($this->run_id);
if ($closed) {
$cache_key = 'frpTunnel_data_' . $this->client_token;
Cache::forget($cache_key);
$this->run_id = null;
$this->saveQuietly();
}
return true;
}
return false;
}
protected static function boot()
{
parent::boot();
static::creating(function (self $tunnel) {
$tunnel->client_token = Str::random(18);
});
static::updated(function (self $tunnel) {
if ($tunnel->locked_reason) {
$tunnel->close();
}
});
static::deleted(function (self $tunnel) {
$tunnel->close();
});
}
}

View File

@ -11,7 +11,7 @@ class AppServiceProvider extends ServiceProvider
*/
public function register(): void
{
//
require_once app_path() . '/Helpers.php';
}
/**

View File

@ -31,8 +31,14 @@ public function boot(): void
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware(['web', 'auth:admin'])
->as('admin.')
->prefix('admin')
->group(base_path('routes/admin.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}

View File

@ -11,12 +11,12 @@ class Frp
{
public string|int $id;
protected Server $frpServer;
protected Server $server;
public function __construct($id)
public function __construct(Server $server)
{
$this->frpServer = (new Server)->find($id);
$this->id = $id;
$this->server = $server;
$this->id = $server->id;
}
public function serverInfo()
@ -48,22 +48,22 @@ protected function cache($key, $path = null)
protected function get($url)
{
$addr = 'http://' . $this->frpServer->server_address . ':' . $this->frpServer->dashboard_port . '/api' . $url;
$addr = 'http://' . $this->server->server_address . ':' . $this->server->dashboard_port . '/api' . $url;
try {
$resp = Http::timeout(3)->withBasicAuth($this->frpServer->dashboard_user, $this->frpServer->dashboard_password)->get($addr)->json() ?? [];
$resp = Http::timeout(3)->withBasicAuth($this->server->dashboard_user, $this->server->dashboard_password)->get($addr)->json() ?? [];
// if under maintenance
if ($this->frpServer->status !== 'maintenance') {
if ($this->frpServer->status !== 'up') {
$this->frpServer->status = 'up';
if ($this->server->status !== 'maintenance') {
if ($this->server->status !== 'up') {
$this->server->status = 'up';
}
}
} catch (Exception) {
$this->frpServer->status = 'down';
$this->server->status = 'down';
$resp = false;
} finally {
$this->frpServer->save();
$this->server->save();
}
return $resp;

View File

@ -0,0 +1,18 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class AppLayout extends Component
{
/**
* Get the view / contents that represents the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('layouts.app');
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class GuestLayout extends Component
{
/**
* Get the view / contents that represents the component.
*
* @return \Illuminate\View\View
*/
public function render()
{
return view('layouts.guest');
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class Menu extends Component
{
/**
* Create a new component instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view('components.menu');
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class ServerView extends Component
{
public $server;
public $url;
/**
* Create a new component instance.
*
* @return void
*/
public function __construct($server, $url)
{
$this->server = $server;
$this->url = $url;
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\Contracts\View\View|\Closure|string
*/
public function render()
{
return view('components.server-view');
}
}

View File

@ -40,6 +40,11 @@
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
],
/*
@ -65,6 +70,11 @@
'model' => App\Models\User::class,
],
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',

View File

@ -17,7 +17,6 @@ public function up(): void
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->boolean('is_admin')->default(false)->index();
$table->rememberToken();
$table->timestamps();
});

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('admins', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('admins');
}
};

View File

@ -0,0 +1,38 @@
<?php
use App\Models\Server;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('servers', function (Blueprint $table) {
$table->string('key')->nullable()->index()->after('id');
});
Server::all()->each(function (Server $server) {
$server->key = $server->id;
$server->save();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('key');
});
}
};

View File

@ -16,6 +16,8 @@ public function up(): void
$table->string('name')->index();
$table->string('client_token')->index();
$table->char('protocol', 5)->index()->default('tcp');
$table->string('custom_domain')->nullable()->index();
@ -38,6 +40,9 @@ public function up(): void
// use_compression
$table->boolean('use_compression')->default(false)->index();
$table->string('run_id')->nullable()->index();
$table->string('locked_reason')->nullable();
$table->timestamps();
});

View File

@ -14,6 +14,8 @@
"@vitejs/plugin-vue": "^4.0.0",
"bootstrap": "5.3.0-alpha1",
"bootstrap-icons": "^1.10.3",
"echarts": "^5.4.2",
"humanize-plus": "^1.8.2",
"vue": "^3.2.36",
"vue-axios": "^3.5.2",
"vue-router": "^4.0.13"

View File

@ -8,9 +8,6 @@
</template>
<script setup>
</script>
<style scoped>
.mb-8 {

View File

@ -25,10 +25,10 @@ const items = ref([
name: "隧道",
route: "tunnels",
},
{
name: "服务器列表",
route: "servers",
},
// {
// name: "",
// route: "servers",
// },
{
name: "创建隧道",
route: "tunnels.create",
@ -36,14 +36,14 @@ const items = ref([
]);
//
if (window.Base.User.is_admin) {
// items.value.push({
// name: "",
// route: "servers",
// });
items.value.push({
name: "创建服务器",
route: "servers.create",
});
}
// if (window.Base.User.is_admin) {
// // items.value.push({
// // name: "",
// // route: "servers",
// // });
// items.value.push({
// name: "",
// route: "servers.create",
// });
// }
</script>

View File

@ -3,74 +3,90 @@
<table class="table table-hover">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">名称</th>
<th scope="col">协议</th>
<th scope="col">本地地址</th>
<th scope="col">远程端口/域名</th>
<th scope="col">连接数</th>
<th scope="col">下载流量</th>
<th scope="col">上载流量</th>
<th scope="col">服务器</th>
<th scope="col">状态</th>
</tr>
<tr>
<th scope="col">ID</th>
<th scope="col">名称</th>
<th scope="col">协议</th>
<th scope="col">本地地址</th>
<th scope="col">远程端口/域名</th>
<th scope="col">连接数</th>
<th scope="col">下载流量</th>
<th scope="col">上载流量</th>
<th scope="col">服务器</th>
<th scope="col">状态</th>
</tr>
</thead>
<tbody>
<tr v-for="tunnel in tunnels">
<th>1</th>
<td>
<router-link :to="{name: 'tunnels.show', params: {id: tunnel.id}}">
{{ tunnel.name }}
</router-link>
<tr v-for="tunnel in tunnels">
<th>1</th>
<td>
<router-link
:to="{
name: 'tunnels.show',
params: { id: tunnel.id },
}"
>
{{ tunnel.name }}
</router-link>
</td>
<td>
{{ tunnel.protocol.toString().toUpperCase() }}
</td>
<td>
{{ tunnel.local_address }}
</td>
</td>
<td>
{{ tunnel.protocol.toString().toUpperCase() }}
</td>
<td>
{{ tunnel.local_address }}
</td>
<td>
<span
v-if="
tunnel.protocol === 'http' ||
tunnel.protocol === 'https'
"
>
{{ tunnel.custom_domain }}
</span>
<span v-else>
{{ tunnel.server.server_address }}:{{
tunnel.remote_port
}}
</span>
</td>
<td>
{{ tunnel.server.server_address }}{{ tunnel.remote_port }}
</td>
<td>0</td>
<td>0.000 Bytes</td>
<td>0.000 Bytes</td>
<td>0</td>
<td>0.000 Bytes</td>
<td>0.000 Bytes</td>
<td>{{ tunnel.server.name }}</td>
<td>{{ tunnel.server.name }}</td>
<td>
<span class="text-danger">离线</span>
</td>
</tr>
<td>
<span class="text-success" v-if="tunnel.run_id">在线</span>
<span class="text-danger" v-else="tunnel.run_id">离线</span>
</td>
</tr>
</tbody>
</table>
</template>
<script setup>
import {ref} from "vue";
import { ref } from "vue";
import http from "../../plugins/http";
const tunnels = ref([
{
id: '0',
protocol: '',
id: "0",
protocol: "",
server: {
server_address: '',
server_port: '',
name: '',
}
}
])
server_address: "",
server_port: "",
name: "",
},
run_id: ""
},
]);
http.get('tunnels').then((res) => {
tunnels.value = res.data
console.log(tunnels.value)
})
http.get("tunnels").then((res) => {
tunnels.value = res.data;
console.log(tunnels.value);
});
</script>

View File

@ -2,38 +2,243 @@
<div>
<h2>隧道: {{ tunnel.name }}</h2>
</div>
<div>
<div v-show="chart" id="chart" style="height: 400px"></div>
</div>
<div>
<h2>配置文件</h2>
<pre
>{{ tunnel.config.server }}
{{ tunnel.config.client }}
</pre>
</div>
<div v-if="tunnel.run_id" class="mb-3">
<h2>强制下线</h2>
<button class="btn btn-primary" @click="kickTunnel()">强制下线</button>
</div>
<div>
<h2>删除</h2>
<button class="btn btn-primary" @click="deleteTunnel()">
删除隧道
</button>
</div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import { onMounted, onUnmounted, ref } from "vue";
import http from "../../plugins/http";
import router from "../../plugins/router";
import * as echarts from "echarts";
const showChart = ref(false);
let chart = undefined;
let tunnel_id = router.currentRoute.value.params.id
let tunnel_id = router.currentRoute.value.params.id;
const tunnel = ref({
name: '',
protocol: '',
local_address: '',
remote_port: '',
})
name: "",
protocol: "",
local_address: "",
remote_port: "",
config: {
server: "",
client: "",
},
run_id: "",
});
function getTunnel() {
http.get(`/tunnels/${tunnel_id}`).then(res => {
tunnel.value = res.data
})
http.get(`/tunnels/${tunnel_id}`).then((res) => {
tunnel.value = res.data;
});
}
function updateTunnel() {
http.put(`/tunnels/${tunnel_id}`, tunnel.value).then(res => {
tunnel.value = res.data
})
http.put(`/tunnels/${tunnel_id}`, tunnel.value).then((res) => {
tunnel.value = res.data;
});
}
onMounted(getTunnel)
let chartOptions = {
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
formatter: function (data) {
let html = "";
if (data.length > 0) {
html += data[0].name + "<br/>";
}
for (let v of data) {
let colorEl =
'<span style="display:inline-block;margin-right:5px;' +
"border-radius:10px;width:9px;height:9px;background-color:" +
v.color +
'"></span>';
html += `${colorEl + v.seriesName}: ${Humanize.fileSize(
v.value
)}<br/>`;
}
return html;
},
},
legend: {
data: ["入站流量", "出站流量"],
},
grid: {
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true,
},
xAxis: [
{
type: "category",
data: [],
},
],
yAxis: [
{
type: "value",
axisLabel: {
formatter: function (value) {
return Humanize.fileSize(value);
},
},
},
],
series: [
{
name: "入站流量",
type: "bar",
data: [],
},
{
name: "出站流量",
type: "bar",
data: [],
},
],
};
function initChart() {
let chartDom = document.getElementById("chart");
chart = echarts.init(chartDom, {
backgroundColor: "transparent",
renderer: "svg",
});
chartOptions && chart.setOption(chartOptions);
}
function deleteTunnel() {
if (confirm("确定删除隧道吗?")) {
http.delete(`/tunnels/${tunnel_id}`).then(() => {
alert("删除成功");
router.push({ name: "tunnels" });
});
}
}
function kickTunnel() {
http.post(`/tunnels/${tunnel_id}/close`);
}
function refresh() {
http.get("/tunnels/" + tunnel_id).then((res) => {
tunnel.value = res.data;
// console.log(res.data)
// tunnel.value.tunnel = res.data.tunnel
// console.log(tunnel.value.tunnel.conf)
// console.log(tunnel,tunnel.value,res.data);
// console.log(res.data);
if (res.data.traffic) {
if (!showChart.value) {
// initChart()
showChart.value = true;
}
let now = new Date();
now = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate() - 6
);
let dates = [];
for (let i = 0; i < 7; i++) {
dates.push(
now.getFullYear() +
"-" +
(now.getMonth() + 1) +
"-" +
now.getDate()
);
now = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate() + 1
);
}
chartOptions.xAxis[0].data = dates;
let trafficInArr = res.data.traffic.traffic_in;
let trafficOutArr = res.data.traffic.traffic_out;
if (!trafficInArr || !trafficOutArr) {
return;
}
trafficInArr = trafficInArr.reverse();
trafficOutArr = trafficOutArr.reverse();
if (chart) {
chartOptions.series[0].data = trafficInArr;
chartOptions.series[1].data = trafficOutArr;
chart.setOption(chartOptions);
}
}
});
}
refresh();
let resizeInterval = setInterval(() => {
chart && chart.resize();
});
onMounted(() => {
getTunnel();
window.addEventListener("resize", () => {
chart && chart.resize();
});
});
const timer = setInterval(refresh, 10000);
onUnmounted(() => {
clearInterval(timer);
if (resizeInterval) {
clearInterval(resizeInterval);
}
// remove listener
window.removeEventListener("resize", () => {
resizeInterval = setInterval(() => {
chart && chart.resize();
}, 1000);
initChart();
chart && chart.resize();
});
});
</script>

View File

@ -0,0 +1,10 @@
<x-app-layout>
@if (count($servers) > 0)
<h1>不在线或维护中的服务器</h1>
@foreach ($servers as $server)
<x-Server-View :server="$server" :url="route('admin.servers.edit', $server->id)" />
@endforeach
@endif
</x-app-layout>

View File

@ -0,0 +1,19 @@
<x-app-layout>
<div>
<h1>登录</h1>
<form action="{{ route('admin.login') }}" method="POST">
@csrf
{{-- email --}}
<input type="text" name="email" placeholder="邮箱">
{{-- password --}}
<input type="password" name="password" placeholder="密码">
{{-- remember --}}
<input type="checkbox" name="remember" id="remember">
<label for="remember">记住我</label>
{{-- submit --}}
<button type="submit">登录</button>
</form>
</div>
</x-app-layout>

View File

@ -0,0 +1,134 @@
<x-app-layout>
<form action="{{ route('admin.servers.store') }}" method="post">
@csrf
<div class="row">
<div class="col">
<h3>服务器</h3>
<div class="mb-3">
<label for="serverName" class="form-label">服务器名称</label>
<input type="text" required class="form-control" id="serverName" placeholder="输入服务器名称,它将会被搜索到"
name="name">
</div>
<h3>Frps 信息</h3>
<div class="mb-3">
<label for="serverAddr" class="form-label">Frps 地址</label>
<input type="text" required class="form-control" id="serverAddr" name="server_address">
</div>
<div class="mb-3">
<label for="serverPort" class="form-label">Frps 端口</label>
<input type="text" required class="form-control" id="serverPort" name="server_port">
</div>
<div class="mb-3">
<label for="serverToken" class="form-label">Frps 令牌</label>
<input type="text" required class="form-control" id="serverToken" name="token">
</div>
</div>
<div class="col">
<h3>Frps Dashboard 配置</h3>
<div class="mb-3">
<label for="dashboardPort" class="form-label">端口</label>
<input type="text" required class="form-control" id="dashboardPort" name="dashboard_port"
value="7500">
</div>
<div class="mb-3">
<label for="dashboardUser" class="form-label">登录用户名</label>
<input type="text" required class="form-control" id="dashboardUser" name="dashboard_user"
value="admin">
</div>
<div class="mb-3">
<label for="dashboardPwd" class="form-label">密码</label>
<input type="text" required class="form-control" id="dashboardPwd" name="dashboard_password"
value="admin">
</div>
<h3>端口范围限制</h3>
<div class="input-group input-group-sm mb-3">
<input type="text" required class="form-control" placeholder="最小端口,比如: 10000" name="min_port"
value="10000">
<input type="text" required class="form-control" placeholder="最大端口,比如: 65535" name="max_port"
value="65535">
</div>
<h3>最多隧道数量</h3>
<div class="input-group input-group-sm mb-3">
<input type="text" required class="form-control" placeholder="最多隧道数量,比如:1024个隧道"
name="max_tunnels">
</div>
<h3>隧道协议限制</h3>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_http" value="1" id="allow_http">
<label class="form-check-label" for="allow_http">
允许 HTTP
</label>
<br />
超文本传输协议
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_https" value="1" id="allow_https">
<label class="form-check-label" for="allow_https">
允许 HTTPS
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_tcp" value="1" id="allow_tcp">
<label class="form-check-label" for="allow_tcp">
允许 TCP
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_udp" value="1" id="allow_udp">
<label class="form-check-label" for="allow_udp">
允许 UDP
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_stcp" value="1"
id="allow_stcp">
<label class="form-check-label" for="allow_stcp">
允许 STCP
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_xtcp" value="1"
id="allow_xtcp">
<label class="form-check-label" for="allow_xtcp">
允许 XTCP
</label>
</div>
<p>服务器是否位于中国大陆</p>
<div class="row">
<div class="col-auto">
<div class="input-group input-group-sm mb-3">
{{-- checkbox --}}
<input type="checkbox" name="is_china_mainland" value="1" id="is_china_mainland">
</div>
</div>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary mb-3">新建服务器</button>
</div>
</div>
</div>
</form>
</x-app-layout>

View File

@ -0,0 +1,336 @@
<x-app-layout>
{{-- <nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<button class="nav-link active" aria-selected="true" data-bs-toggle="tab" data-bs-target="#nav-info"
type="button" role="tab">基础信息</button>
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-settings" type="button"
role="tab">服务器设置</button>
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#nav-frps" type="button" role="tab">Frps
配置文件</button>
</div>
</nav> --}}
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade" id="nav-settings" role="tabpanel">
<div class="row mt-3">
<div class="col">
<form action="{{ route('admin.servers.update', $server->id) }}" method="post">
@csrf
@method('PUT')
<h3>服务器</h3>
<div class="mb-3">
<label for="serverName" class="form-label">服务器名称</label>
<input type="text" required value="{{ $server->name }}" class="form-control"
id="serverName" placeholder="输入服务器名称,它将会被搜索到" name="name">
</div>
<div class="mb-3">
<label for="serverKey" class="form-label">鉴权 Key</label>
<input type="text" required value="{{ $server->key }}" class="form-control"
id="serverKey" name="key">
</div>
<h3>Frps 信息</h3>
<div class="mb-3">
<label for="serverAddr" class="form-label">Frps 地址</label>
<input type="text" required value="{{ $server->server_address }}" class="form-control"
id="serverAddr" name="server_address">
</div>
<div class="mb-3">
<label for="serverPort" class="form-label">Frps 端口</label>
<input type="text" required value="{{ $server->server_port }}" class="form-control"
id="serverPort" name="server_port">
</div>
<div class="mb-3">
<label for="serverToken" class="form-label">Frps 令牌</label>
<input type="text" required value="{{ $server->token }}" class="form-control"
id="serverToken" name="token">
</div>
</div>
<div class="col">
<h3>Frps Dashboard 配置</h3>
<div class="mb-3">
<label for="dashboardPort" class="form-label">端口</label>
<input type="text" required value="{{ $server->dashboard_port }}" class="form-control"
id="dashboardPort" name="dashboard_port">
</div>
<div class="mb-3">
<label for="dashboardUser" class="form-label">登录用户名</label>
<input type="text" required value="{{ $server->dashboard_user }}" class="form-control"
id="dashboardUser" name="dashboard_user">
</div>
<div class="mb-3">
<label for="dashboardPwd" class="form-label">密码</label>
<input type="text" required value="{{ $server->dashboard_password }}" class="form-control"
id="dashboardPwd" name="dashboard_password">
</div>
<h3>端口范围限制</h3>
<div class="input-group input-group-sm mb-3">
<input type="text" value="{{ $server->min_port }}" required class="form-control"
placeholder="最小端口,比如:1024" name="min_port">
<input type="text" value="{{ $server->max_port }}" required class="form-control"
placeholder="最大端口,比如:65535" name="max_port">
</div>
<h3>最多隧道数量</h3>
<div class="input-group input-group-sm mb-3">
<input type="text" value="{{ $server->max_tunnels }}" required class="form-control"
placeholder="最多隧道数量,比如:1024个隧道" name="max_tunnels">
</div>
<h3>隧道协议限制</h3>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_http" value="1" id="allow_http"
@if ($server->allow_http) checked @endif>
<label class="form-check-label" for="allow_http">
允许 HTTP
</label>
<br />
超文本传输协议
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_https" value="1"
id="allow_https" @if ($server->allow_https) checked @endif>
<label class="form-check-label" for="allow_https">
允许 HTTPS
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_tcp" value="1"
id="allow_tcp" @if ($server->allow_tcp) checked @endif>
<label class="form-check-label" for="allow_tcp">
允许 TCP
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_udp" value="1"
id="allow_udp" @if ($server->allow_udp) checked @endif>
<label class="form-check-label" for="allow_udp">
允许 UDP
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_stcp" value="1"
id="allow_stcp" @if ($server->allow_stcp) checked @endif>
<label class="form-check-label" for="allow_stcp">
允许 STCP
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_xtcp" value="1"
id="allow_xtcp" @if ($server->allow_xtcp) checked @endif>
<label class="form-check-label" for="allow_xtcp">
允许 XTCP
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_sudp" value="1"
id="allow_sudp" @if ($server->allow_sudp) checked @endif>
<label class="form-check-label" for="allow_sudp">
允许 SUDP
</label>
</div>
<div class="row">
<div class="col-auto">
<div class="input-group input-group-sm mb-3">
{{-- checkbox --}}
<input type="checkbox" value="1" name="is_china_mainland"
id="is_china_mainland" @if ($server->is_china_mainland) checked @endif>
<span>服务器是否位于中国大陆</span>
</div>
</div>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary mb-3">保存更改</button>
</form>
<form class="d-inline" action="{{ route('admin.servers.destroy', $server->id) }}" method="post">
@method('DELETE')
@csrf
<button type="submit" class="btn btn-danger mb-3"
onclick="confirm('确定删除这个服务器吗?删除后将无法恢复,与此关联的隧道将一并删除。') ? true : event.preventDefault()">删除服务器</button>
</form>
</div>
</div>
</div>
</div>
<div class="tab-pane fade show active" id="nav-info" role="tabpanel">
<table class="table">
<tbody>
<tr>
<td>Frps 版本</td>
<td>{{ $serverInfo->version ?? 0 }}</td>
</tr>
<tr>
<td>绑定端口</td>
<td>{{ $serverInfo->bind_port ?? 0 }}</td>
</tr>
@if ($serverInfo->bind_udp_port ?? 0)
<tr>
<td>UDP 端口</td>
<td>{{ $serverInfo->bind_udp_port ?? 0 }}</td>
</tr>
@endif
<tr>
<td>HTTP 端口</td>
<td>{{ $serverInfo->vhost_http_port ?? 0 }}</td>
</tr>
<tr>
<td>HTTPS 端口</td>
<td>{{ $serverInfo->vhost_https_port ?? 0 }}</td>
</tr>
<tr>
<td>KCP 端口</td>
<td>{{ $serverInfo->kcp_bind_port ?? 0 }}</td>
</tr>
@if (!empty($serverInfo->subdomain_host))
<tr>
<td>子域名</td>
<td>{{ $serverInfo->subdomain_host ?? 0 }}</td>
</tr>
@endif
<tr>
<td>Max PoolCount</td>
<td>{{ $serverInfo->max_pool_count ?? 0 }}</td>
</tr>
<tr>
<td>Max Ports Peer Client</td>
<td>{{ $serverInfo->max_ports_per_client ?? 0 }}</td>
</tr>
<tr>
<td>Heartbeat timeout</td>
<td>{{ $serverInfo->heart_beat_timeout ?? 0 }}</td>
</tr>
<tr>
<td>自启动以来总入流量</td>
<td>{{ unitConversion($serverInfo->total_traffic_in ?? 0) }}</td>
</tr>
<tr>
<td>自启动以来总出流量</td>
<td>{{ unitConversion($serverInfo->total_traffic_out ?? 0) }}</td>
</tr>
<tr>
<td>客户端数量</td>
<td>{{ $serverInfo->client_counts ?? 0 }}</td>
</tr>
<tr>
<td>当前连接数量</td>
<td>{{ $serverInfo->cur_conns ?? 0 }}</td>
</tr>
</tbody>
</table>
</div>
@if ($server->status == 'down')
<span style="color: red">无法连接到服务器。</span>
<form action="{{ route('admin.servers.update', $server->id) }}" method="POST">
@csrf
@method('PUT')
<input type="hidden" name="status" value="up" />
<button type="submit">强制标记为在线</button>
</form>
@else
<span style="color: green">正常</span>
<form action="{{ route('admin.servers.update', $server->id) }}" method="POST">
@csrf
@method('PUT')
<input type="hidden" name="status" value="down" />
<button type="submit">标记为离线</button>
</form>
@endif
@if ($server->status == 'maintenance')
<span style="color: red">维护中</span>
<form action="{{ route('admin.servers.update', $server->id) }}" method="POST">
@csrf
@method('PUT')
<input type="hidden" name="status" value="down" />
<button type="submit">取消维护</button>
</form>
@else
<form action="{{ route('admin.servers.update', $server->id) }}" method="POST">
@csrf
@method('PUT')
<input type="hidden" name="status" value="maintenance" />
<button type="submit">开始维护</button>
</form>
@endif
<form action="{{ route('admin.servers.destroy', $server->id) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit">删除</button>
</form>
<div class="tab-pane fade" id="nav-frps" role="tabpanel">
<textarea readonly class="form-control" rows="20" cols="80">[common]
bind_port = {{ $server->server_port }}
bind_udp_port = {{ $server->server_port }}
@if ($server->server_port + 1 > 65535)
kcp_bind_port = {{ $server->server_port - 1 }}
@else
kcp_bind_port = {{ $server->server_port + 1 }}
@endif
token = {{ $server->token }}
@if ($server->allow_http)
vhost_http_port = 80
@endif
@if ($server->allow_https)
vhost_https_port = 443
@endif
dashboard_port = {{ $server->dashboard_port }}
dashboard_user = {{ $server->dashboard_user }}
dashboard_pwd = {{ $server->dashboard_password }}
[plugin.port-manager]
addr = {{ route('api.tunnel.handler', '') }}/
path = {{ $server->key }}
ops = NewProxy
</textarea>
将这些文件放入: frps.ini
</div>
</div>
</x-app-layout>

View File

@ -0,0 +1,10 @@
<x-app-layout>
<a href="{{ route('admin.servers.create') }}">添加 Frps 服务器</a>
<div class="list-group mt-3">
@foreach ($servers as $server)
<x-Server-View :server="$server" :url="route('admin.servers.edit', $server->id)" />
@endforeach
</div>
</x-app-layout>

View File

@ -0,0 +1,100 @@
<x-app-layout>
@php($user = auth()->user())
<h3>{{ $server->name }}</h3>
<a href="{{ route('servers.edit', $server->id) }}">编辑服务器</a>
<p>
服务器地址: {{ $server->server_address }} <br />
允许的协议列表: <br />
{{ $server->allow_http ? 'HTTP' : ' ' }}
{{ $server->allow_https ? 'HTTPS' : ' ' }}
{{ $server->allow_tcp ? 'TCP' : ' ' }}
{{ $server->allow_udp ? 'UDP' : ' ' }}
{{ $server->allow_stcp ? 'STCP' : ' ' }}
{{ $server->allow_xtcp ? 'XTCP' : ' ' }}
</p>
<p>端口号范围: {{ $server->min_port }} ~ {{ $server->max_port }} </p>
<p>隧道数量: {{ $server->tunnels }} / {{ $server->max_tunnels }} </p>
<p>客户端数量:{{ $serverInfo->client_counts ?? 0 }},连接数:{{ $serverInfo->cur_conns ?? 0 }},进站流量:{{ unitConversion($serverInfo->total_traffic_in ?? 0) }},出站流量:{{ unitConversion($serverInfo->total_traffic_out ?? 0) }},
{{ $serverInfo->version ?? '离线' }}
</p>
{{--
<h3>使用这个服务器创建隧道</h3>
<form action="{{ route('tunnels.store') }}" method="POST">
@csrf
<input type="hidden" name="server_id" value="{{ $server->id }}" />
<div class="form-floating mb-3">
<input type="text" class="form-control" name="name">
<label>隧道名称</label>
</div>
<select class="form-select" name="protocol" id="protocol">
<option selected>选择协议</option>
<option value="http" @if (!$user->verified_at) disabled @endif>HTTP</option>
<option value="https" @if (!$user->verified_at) disabled @endif>HTTPS</option>
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
<option value="stcp">STCP</option>
</select>
<div class="form-floating mb-3 mt-3">
<input type="text" class="form-control" name="local_address">
<label>本地地址</label>
</div>
<div class="form-floating mb-3 hidden" id="remote">
<input type="text" class="form-control" name="remote_port">
<label>远程端口</label>
</div>
<div class="form-floating mb-3 hidden" id="domain">
<input type="text" class="form-control" name="custom_domain">
<label>域名</label>
</div>
<div class="form-floating mb-3 hidden" id="sk">
<input type="text" class="form-control" name="sk">
<label>STCP 密钥</label>
</div>
<button type="submit" class="btn btn-primary">创建</button>
</form> --}}
{{--
<script>
const protocol = document.getElementById('protocol');
protocol.addEventListener('change', () => {
let val = protocol.value;
function hide(id) {
document.getElementById(id).style.display = 'none';
}
function show(id) {
document.getElementById(id).style.display = 'block';
}
if (val == 'http' || val == 'https') {
hide('sk')
hide('remote')
show('domain')
} else if (val == 'tcp' || val == 'udp') {
hide('sk')
hide('domain')
show('remote')
} else if (val == 'stcp') {
hide('sk')
hide('domain')
show('sk')
}
})
</script> --}}
</x-app-layout>

View File

@ -0,0 +1,103 @@
<x-app-layout>
<h1>隧道</h1>
<form name="filter">
Host ID: <input type="text" name="host_id" value="{{ Request::get('host_id') }}" />
名称: <input type="text" name="name" value="{{ Request::get('name') }}" />
<select name="protocol">
<option value="">协议</option>
<option value="http" @if (Request::get('protocol') == 'http') selected @endif>HTTP</option>
<option value="https" @if (Request::get('protocol') == 'https') selected @endif>HTTPS</option>
<option value="tcp" @if (Request::get('protocol') == 'tcp') selected @endif>TCP</option>
<option value="udp" @if (Request::get('protocol') == 'udp') selected @endif>UDP</option>
<option value="xtcp" @if (Request::get('protocol') == 'xtcp') selected @endif>XTCP</option>
<option value="stcp" @if (Request::get('protocol') == 'stcp') selected @endif>STCP</option>
<option value="sudp" @if (Request::get('protocol') == 'sudp') selected @endif>SUDP</option>
</select>
<button type="submit">筛选</button>
</form>
<p>总计: {{ $count }}</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>客户</th>
<th>状态</th>
<th scope="col">协议</th>
<th scope="col">本地地址</th>
<th scope="col">远程端口/域名</th>
<th scope="col">连接数</th>
<th scope="col">下载流量</th>
<th scope="col">上载流量</th>
<th scope="col">服务器</th>
<th scope="col">隧道状态</th>
<th>创建时间</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach ($hosts as $host)
<tr>
<td>{{ $host->host_id }}</td>
<td>{{ $host->name }}</td>
<td>{{ $host->user->name }}</td>
<td>{{ $host->status }}</td>
@php($cache = Cache::get('frpTunnel_data_' . $host->client_token, ['status' => 'offline']))
<td>{{ strtoupper($host->protocol) }}</td>
<td>{{ $host->local_address }}</td>
@if ($host->protocol == 'http' || $host->protocol == 'https')
<td>{{ $host->custom_domain }}</td>
@else
<td>{{ $host->server->server_address . ':' . $host->remote_port }}</td>
@endif
<td>{{ $cache['cur_conns'] ?? 0 }}</td>
<td>{{ unitConversion($cache['today_traffic_in'] ?? 0) }}</td>
<td>{{ unitConversion($cache['today_traffic_out'] ?? 0) }}</td>
<td><a href="{{ route('admin.servers.show', $host->server->id) }}">{{ $host->server->name }}</a></td>
<td>
@if($host->locked_reason)
<span class="text-danger">被锁定,因为 {{ $host->locked_reason }}</span>
<br />
@endif
@if ($cache['status'] === 'online')
<span class="text-success">在线</span>
@else
<span class="text-danger">离线</span>
@endif
</td>
<td>{{ $host->created_at }}</td>
<td>{{ $host->updated_at }}</td>
<td>
<a href="{{ route('admin.tunnels.show', ['tunnel' => $host]) }}">编辑</a>
<form action="{{ route('admin.tunnels.destroy', ['tunnel' => $host]) }}" method="POST"
onsubmit="return confirm('真的要删除吗?')">
@csrf
@method('DELETE')
<button type="submit">删除</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $hosts->links() }}
</x-app-layout>

View File

@ -0,0 +1,88 @@
<x-app-layout>
@php($cache = Cache::get('frpTunnel_data_' . $tunnel->client_token, []))
<p> 隧道状态:
@if ($cache['status'] ?? false === 'online')
<span style="color: green">在线</span>
@else
<span style="color: red">离线</span>
@endif
</p>
<p> 连接数:{{ $cache['cur_conns'] ?? 0 }}</p>
<p> 下载流量:{{ unitConversion($cache['today_traffic_in'] ?? 0) }}</p>
<p> 上载流量:{{ unitConversion($cache['today_traffic_out'] ?? 0) }}</p>
<hr />
<p>如果填写锁定原因,隧道将会立即下线,并且客户端无法登录。</p>
<form action="{{ route('admin.tunnels.update', $tunnel) }}" method="POST">
@csrf
@method('PATCH')
<input type="text" name="locked_reason" @if($tunnel->locked_reason) value="{{$tunnel->locked_reason}}" @endif placeholder="留空解除" />
<button type="submit">确定</button>
</form>
{{-- @if ($host->protocol == 'http' || $host->protocol == 'https')
<h3>网页截图</h3>
<img src="" />
@endif --}}
<h3>配置文件</h3>
<textarea id="config" cols="80" rows="20" readonly="readonly"></textarea>
<script>
let tunnel_config = {!! $tunnel !!}
// let put_config()
function put_config() {
let local_addr = tunnel_config.local_address.split(':')
let config = `[common]
server_addr = ${tunnel_config.server.server_address}
server_port = ${tunnel_config.server.server_port}
token = ${tunnel_config.server.token}
# ${tunnel_config.name} 于服务器 ${tunnel_config.server.name}
[${tunnel_config.client_token}]
type = ${tunnel_config.protocol}
local_ip = ${local_addr[0]}
local_port = ${local_addr[1]}
`;
if (tunnel_config.protocol == 'tcp' || tunnel_config.protocol == 'udp') {
config += `remote_port = ${tunnel_config.remote_port}
`;
} else if (tunnel_config.protocol == 'http' || tunnel_config.protocol == 'https') {
config += `custom_domains = ${tunnel_config.custom_domain}
`;
} else if (tunnel_config.protocol == 'stcp') {
let random = Math.floor(Math.random() * 50);
config += `sk = ${tunnel_config.sk}
#------ Visitor config file --------
[common]
server_addr = ${tunnel_config.server.server_address}
server_port = ${tunnel_config.server.server_port}
user = client
token = ${tunnel_config.server.token}
[client_visitor_${random}]
type = stcp
role = visitor
server_name = ${tunnel_config.client_token}
sk = ${tunnel_config.sk}
bind_addr = 127.0.0.1
bind_port = ${local_addr[1]}
#------ Visitor config file --------
`
}
document.getElementById('config').value = config;
};
put_config();
</script>
</x-app-layout>

View File

@ -0,0 +1,88 @@
<x-app-layout>
<h1>已经发现的客户</h1>
<p>总计: {{ $count }}</p>
<form name="filter">
用户 ID: <input type="text" name="id" value="{{ Request::get('id') }}" />
名称: <input type="text" name="name" value="{{ Request::get('name') }}" />
邮箱: <input type="text" name="email" value="{{ Request::get('email') }}" />
<button type="submit">筛选</button>
</form>
<table>
{{-- 表头 --}}
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>邮箱</th>
<th>剩余流量</th>
{{-- <th>发现时间</th>
<th>更新时间</th> --}}
{{-- <th>操作</th> --}}
</tr>
</thead>
{{-- 表内容 --}}
<tbody>
@foreach ($users as $user)
<tr>
<td>{{ $user->id }}</td>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
<td><input type="text" value="{{ $user->traffic ?? 0 }}"
onchange="updateTraffic({{ $user->id }}, this)" /> GB</td>
{{-- <td>{{ $user->created_at }}</td>
<td>{{ $user->updated_at }}</td> --}}
{{-- <td>
<a href="{{ route('user.show', $user) }}">查看</a>
<a href="{{ route('user.edit', $user) }}">编辑</a>
<form action="{{ route('user.destroy', $user) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit">删除</button>
</form>
</td> --}}
</tr>
@endforeach
</tbody>
</table>
{{ $users->links() }}
<script>
function updateTraffic(userId, input) {
const url = '/users/' + userId
// xml http request
const xhr = new XMLHttpRequest();
xhr.open('PATCH', url);
xhr.setRequestHeader('Content-Type', 'application/json');
// csrf
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
xhr.setRequestHeader('X-CSRF-TOKEN', csrfToken);
// not follow redirect
xhr.responseType = 'json';
// add ajax header
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.send(JSON.stringify({
traffic: input.value
}));
xhr.onload = function() {
if (xhr.status != 200) {
alert(`Error ${xhr.status}: ${xhr.statusText}`);
}
};
}
</script>
</x-app-layout>

View File

@ -0,0 +1,67 @@
<div>
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
<div class="container-fluid">
<a class="navbar-brand" href="/">
Edge.st {{ config('app.name') }}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMenu"
aria-controls="navMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
@php($verified = auth()->user()->verified_at ?? null)
<div class="collapse navbar-collapse" id="navMenu">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ route('servers.index') }}">服务器列表</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ route('tunnels.index') }}">隧道列表</a>
</li>
@if (!$verified)
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ route('verify') }}">实名认证</a>
</li>
@endif
</ul>
<ul class="navbar-nav ml-auto">
@auth
@if (auth()->user()->is_admin)
<li class="nav-item">
<a class="nav-link" aria-current="page"
href="{{ route('servers.create') }}">创建服务器</a>
</li>
@endif
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ route('tunnels.create') }}">创建隧道</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
{{ auth()->user()->name }}
</a>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="{{ route('servers.index') }}">服务器列表</a>
<a class="dropdown-item" href="{{ route('tunnels.index') }}">隧道列表</a>
<a class="dropdown-item" href="#"
onclick="axios.post(route('login.logout')).then(() => {window.location.reload()})">
注销&nbsp;<span x-html="config.app_html"></span>
</a>
</div>
</li>
@else
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ route('login.redirect') }}">登录</a>
</li>
@endauth
</ul>
</div>
</div>
</nav>
</div>

View File

@ -0,0 +1,29 @@
<hr />
<div>
<a href="{{ $url }}" class="list-group-item list-group-item-action">
{{-- <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1 text-success">{{ $server->name }}</h5>
<small class="text-muted">{{ $server->updated_at->diffForHumans() }}</small>
</div> --}}
{{-- <p class="mb-1"></p> --}}
{{ $server->name }}
{{ $server->updated_at->diffForHumans() }}
</a>
<small class="text-muted">
<p>状态: {{ $server->status }}</p>
服务器地址: {{ $server->server_address }}, 支持的协议:
{{ $server->allow_http ? 'HTTP' : ' ' }}
{{ $server->allow_https ? 'HTTPS' : ' ' }}
{{ $server->allow_tcp ? 'TCP' : ' ' }}
{{ $server->allow_udp ? 'UDP' : ' ' }}
{{ $server->allow_STCP ? 'STCP' : ' ' }}
服务器位于@if ($server->is_china_mainland)
<span style="color: green">中国大陆</span>
@else
<span style="color: red">境外</span>
@endif
<p> GB 需要消耗的 CNY: {{ $server->price_per_gb }}</p>
</small>
</div>
<hr />

View File

@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
<title>{{ config('app.name', 'LAE') }}</title>
</head>
<body>
<div id="top">
@auth
<h3 style="text-align: center">{{ config('app.display_name') }} 后台</h3>
{{-- 顶部横向菜单 --}}
<div class="top-menu">
<ul>
<li><a href="/">首页</a></li>
<li><a href="{{ route('admin.users.index') }}">用户</a></li>
<li><a href="{{ route('admin.tunnels.index') }}">隧道</a></li>
<li><a href="{{ route('admin.servers.index') }}">服务器</a></li>
<li><a href="{{ route('logout') }}">退出</a></li>
</ul>
</div>
@endauth
</div>
{{-- display error --}}
{{-- if has success --}}
@if (session('success'))
<p style="color: green">
{{ session('success') }}
</p>
@endif
@if (session('error'))
<p style="color: red">
{{ session('error') }}
</p>
@endif
@if ($errors->any())
<div>
<ul>
@foreach ($errors->all() as $error)
<li style="color: red;">{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<button onclick="location.reload()">重新加载</button>
<button onclick="location.reload()">重新加载</button>
<button onclick="location.reload()">重新加载</button>
<button onclick="location.reload()">重新加载</button>
<hr />
<div class="min-h-screen bg-gray-100">
{{-- 摆烂 --}}
<!-- Page Content -->
<main>
{{ $slot }}
</main>
</div>
</body>
</html>

27
routes/admin.php Normal file
View File

@ -0,0 +1,27 @@
<?php
use App\Http\Controllers\Admin\TunnelController;
use App\Http\Controllers\Admin\IndexController;
use App\Http\Controllers\Admin\ReplyController;
use App\Http\Controllers\Admin\ReviewController;
use App\Http\Controllers\Admin\ServerController;
use App\Http\Controllers\Admin\UserController;
use App\Http\Controllers\Admin\WorkOrderController;
use Illuminate\Support\Facades\Route;
Route::withoutMiddleware('auth:admin')->group(function() {
Route::get('/login', [IndexController::class, 'index'])->name('login');
Route::post('/login', [IndexController::class, 'login']);
});
// Auth group
Route::group(['middleware' => 'auth'], function () {
Route::get('/', [IndexController::class, 'index'])->name('index');
Route::resource('users', UserController::class);
Route::resource('servers', ServerController::class);
Route::resource('tunnels', TunnelController::class);
Route::get('/logout', [IndexController::class, 'logout'])->name('logout');
});

View File

@ -5,11 +5,17 @@
use App\Http\Controllers\Api\UserController;
use App\Http\Controllers\Api\ServerController;
use App\Http\Controllers\Api\TunnelController;
use App\Http\Controllers\Api\PortManagerController;
Route::prefix('tunnel')->name('api.tunnel.')->group(function () {
Route::post('/handler/{key}', [PortManagerController::class, 'handler'])->name('handler');
});
Route::middleware('auth:sanctum')->group(function () {
Route::get('user', UserController::class);
Route::apiResource('tunnels', TunnelController::class);
Route::post('tunnels/{tunnel}/close', [TunnelController::class, 'close']);
Route::apiResource('servers', ServerController::class);
});

View File

@ -263,6 +263,14 @@ delayed-stream@~1.0.0:
resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
echarts@^5.4.2:
version "5.4.2"
resolved "https://registry.npmmirror.com/echarts/-/echarts-5.4.2.tgz#9f38781c9c6ae323e896956178f6956952c77a48"
integrity sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==
dependencies:
tslib "2.3.0"
zrender "5.4.3"
esbuild@^0.16.14:
version "0.16.17"
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259"
@ -327,6 +335,11 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
humanize-plus@^1.8.2:
version "1.8.2"
resolved "https://registry.npmmirror.com/humanize-plus/-/humanize-plus-1.8.2.tgz#a65b34459ad6367adbb3707a82a3c9f916167030"
integrity sha512-jaLeQyyzjjINGv7O9JJegjsaUcWjSj/1dcXvLEgU3pGdqCdP1PiC/uwr+saJXhTNBHZtmKnmpXyazgh+eceRxA==
is-core-module@^2.9.0:
version "2.11.0"
resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
@ -431,6 +444,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tslib@2.3.0:
version "2.3.0"
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
vite-plugin-full-reload@^1.0.5:
version "1.0.5"
resolved "https://registry.npmmirror.com/vite-plugin-full-reload/-/vite-plugin-full-reload-1.0.5.tgz#6cddfa94e51909843bc7156ab728dbac972b8560"
@ -473,3 +491,10 @@ vue@^3.2.36:
"@vue/runtime-dom" "3.2.36"
"@vue/server-renderer" "3.2.36"
"@vue/shared" "3.2.36"
zrender@5.4.3:
version "5.4.3"
resolved "https://registry.npmmirror.com/zrender/-/zrender-5.4.3.tgz#41ffaf835f3a3210224abd9d6964b48ff01e79f5"
integrity sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==
dependencies:
tslib "2.3.0"