初始化项目

from lae-modules-frp
This commit is contained in:
iVampireSP.com 2023-03-15 21:45:41 +08:00
parent ccf9eb7f73
commit 399b523893
No known key found for this signature in database
GPG Key ID: 2F7B001CA27A8132
22 changed files with 1672 additions and 0 deletions

View File

@ -0,0 +1,56 @@
<?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('用户不存在。');
}
}
}

139
app/Helpers/ApiResponse.php Normal file
View File

@ -0,0 +1,139 @@
<?php
namespace App\Helpers;
use Illuminate\Http\JsonResponse;
trait ApiResponse
{
public function notFound($message = 'Not found'): JsonResponse
{
return $this->error($message, 404);
}
// success
public function error($message = '', $code = 400): JsonResponse
{
return $this->apiResponse(['message' => $message], $code);
}
// error
public function apiResponse($data, $status = 200): JsonResponse
{
if (is_string($data)) {
$data = ['message' => $data];
}
return response()->json($data, $status);
}
// not found
public function forbidden($message = 'Forbidden'): JsonResponse
{
return $this->error($message, 403);
}
// forbidden
public function unauthorized($message = 'Unauthorized'): JsonResponse
{
return $this->error($message, 401);
}
// unauthorized
public function badRequest($message = 'Bad request'): JsonResponse
{
return $this->error($message);
}
// bad request
public function created($message = 'Created'): JsonResponse
{
return $this->success($message, 201);
}
// created
public function success($data = []): JsonResponse
{
return $this->apiResponse($data);
}
// accepted
public function accepted($message = 'Accepted'): JsonResponse
{
return $this->success($message, 202);
}
// no content
public function noContent($message = 'No content'): JsonResponse
{
return $this->success($message, 204);
}
// updated
public function updated($message = 'Updated'): JsonResponse
{
return $this->success($message, 200);
}
// deleted
public function deleted($message = 'Deleted'): JsonResponse
{
return $this->success($message, 200);
}
// not allowed
public function notAllowed($message = 'Not allowed'): JsonResponse
{
return $this->error($message, 405);
}
// conflict
public function conflict($message = 'Conflict'): JsonResponse
{
return $this->error($message, 409);
}
// too many requests
public function tooManyRequests($message = 'Too many requests'): JsonResponse
{
return $this->error($message, 429);
}
// server error
public function serverError($message = 'Server error'): JsonResponse
{
return $this->error($message, 500);
}
// service unavailable
public function serviceUnavailable($message = 'Service unavailable'): JsonResponse
{
return $this->error($message, 503);
}
// method not allowed
public function methodNotAllowed($message = 'Method not allowed'): JsonResponse
{
return $this->error($message, 405);
}
// not acceptable
public function notAcceptable($message = 'Not acceptable'): JsonResponse
{
return $this->error($message, 406);
}
// precondition failed
public function preconditionFailed($message = 'Precondition failed'): JsonResponse
{
return $this->error($message, 412);
}
}

View File

@ -0,0 +1,109 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Server;
use App\Models\Tunnel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class PortManagerController extends Controller
{
public function handler(Request $request, $key)
{
if ($request->input('op') != 'NewProxy') {
return $this->failed('登录失败,请检查配置文件。');
}
if (!isset($request->input('content')['user']['run_id'])) {
return $this->failed('此客户端不安全,我们不能让您登录。');
}
// if (!is_null($request->content['user']['user'])) {
// return $this->failed('用户不被允许。');
// }
$server = (new Server)->where('key', $key)->first();
if (is_null($server)) {
return $this->failed('服务器不存在。');
}
if ($server->status != 'up') {
return $this->failed('此服务器暂时不接受新的连接。');
}
// Search tunnel
$host = Tunnel::where('client_token', $request->input('content')['proxy_name'])->where('server_id', $server->id)->first();
if (is_null($host)) {
return $this->failed('找不到隧道。');
}
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('不允许的隧道协议。');
}
$test_protocol = 'allow_' . $request->input('content')['proxy_type'];
if (!$server->$test_protocol) {
return $this->failed('服务器不支持这个隧道协议。');
}
if ($request->input('content')['proxy_type'] == 'tcp' || $request->input('content')['proxy_type'] == 'udp') {
if ($request->input('content')['remote_port'] !== $host->remote_port || $host->remote_port < $server->min_port || $host->remote_port > $server->max_port) {
return $this->failed('拒绝启动隧道,因为端口不在允许范围内。');
}
} else if ($request->input('content')['proxy_type'] == 'http' || $request->input('content')['proxy_type'] == 'https') {
if ($request->input('content')['custom_domains'][0] != $host->custom_domain) {
return $this->failed('隧道配置文件有误。');
}
}
// 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();
// $data = [
// 'message' => '隧道 ' . $host->name . ' 已启动。',
// 'event' => 'notifications',
// ];
return $this->frpSuccess();
}
// override
private function failed($reason = null)
{
return response()->json([
'reject' => true,
'reject_reason' => $reason ?? '隧道验证失败,请检查配置文件或前往这个网址重新配置隧道:' . config('app.url'),
'unchange' => true,
]);
}
private function frpSuccess()
{
$response = [
'reject' => false,
'unchange' => true,
];
return response()->json($response);
}
}

View File

@ -0,0 +1,123 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Server;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class ServerController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$servers = Server::all();
return $this->success($servers);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$this->abortIfNotAdmin();
$request->validate($this->rules());
$server = (new Server)->create([
'name' => $request->input('name'),
'server_address' => $request->input('server_address'),
'server_port' => $request->input('server_port'),
'token' => $request->input('token'),
'dashboard_port' => $request->input('dashboard_port'),
'dashboard_user' => $request->input('dashboard_user'),
'dashboard_password' => $request->input('dashboard_password'),
'allow_http' => $request->boolean('allow_http'),
'allow_https' => $request->boolean('allow_https'),
'allow_tcp' => $request->boolean('allow_tcp'),
'allow_udp' => $request->boolean('allow_udp'),
'allow_stcp' => $request->boolean('allow_stcp'),
'allow_sudp' => $request->boolean('allow_sudp'),
'allow_xtcp' => $request->boolean('allow_xtcp'),
'min_port' => $request->input('min_port'),
'max_port' => $request->input('max_port'),
'max_tunnels' => $request->input('max_tunnels'),
'is_china_mainland' => $request->boolean('is_china_mainland'),
]);
return $this->created($server);
}
public function abortIfNotAdmin()
{
if (!auth('sanctum')->user()?->isAdmin()) {
abort(403, 'You are not allowed to access this resource.');
}
}
public function rules($id = null)
{
return [
'name' => 'sometimes|max:20',
'server_address' => [
'sometimes',
Rule::unique('servers')->ignore($id),
],
'server_port' => 'sometimes|integer|max:65535|min:1',
'token' => 'sometimes|max:50',
'dashboard_port' => 'sometimes|integer|max:65535|min:1',
'dashboard_user' => 'sometimes|max:20',
'dashboard_password' => 'sometimes|max:30',
'allow_http' => 'nullable|boolean',
'allow_https' => 'nullable|boolean',
'allow_tcp' => 'nullable|boolean',
'allow_udp' => 'nullable|boolean',
'allow_stcp' => 'nullable|boolean',
'allow_sudp' => 'nullable|boolean',
'allow_xtcp' => 'nullable|boolean',
'min_port' => 'sometimes|integer|max:65535|min:1',
'max_port' => 'sometimes|integer|max:65535|min:1',
'max_tunnels' => 'sometimes|integer|max:65535|min:1',
];
}
/**
* Display the specified resource.
*/
public function show(Request $request, Server $server)
{
if ($request->user('sanctum')->isAdmin()) {
$server->makeVisible($server->hidden);
}
return $this->success($server);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Server $server)
{
$this->abortIfNotAdmin();
$request->validate($this->rules($server->id));
$server->update($request->all());
return $this->updated();
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Server $server)
{
$this->abortIfNotAdmin();
$server->delete();
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class TunnelController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request)
{
return $this->success(
$request->user()->tunnels()
);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(string $id)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, string $id)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(string $id)
{
//
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function __invoke(Request $request)
{
return $this->success(auth('sanctum')->user());
}
}

View File

@ -0,0 +1,29 @@
<?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);
}
}

View File

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

43
app/Models/Admin.php Normal file
View File

@ -0,0 +1,43 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class Admin extends Authenticatable
{
use HasApiTokens, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}

51
app/Models/Server.php Normal file
View File

@ -0,0 +1,51 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Server extends Model
{
public $hidden = [
'dashboard_password',
'dashboard_user',
'dashboard_port'
];
protected $fillable = [
'name',
'server_address',
'server_port',
'token',
'dashboard_port',
'dashboard_user',
'dashboard_password',
'allow_http',
'allow_https',
'allow_tcp',
'allow_udp',
'allow_stcp',
'allow_sudp',
'allow_xtcp',
'min_port',
'max_port',
'max_tunnels',
'is_china_mainland',
];
// tunnels
protected static function booted()
{
static::creating(function ($server) {
// $server->key = Str::random(32);
});
}
// on create
public function tunnels(): HasMany
{
return $this->hasMany(Tunnel::class);
}
}

27
app/Models/Tunnel.php Normal file
View File

@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tunnel extends Model
{
protected $fillable = [
'name',
'protocol',
'custom_domain',
'local_address',
'remote_port',
'client_token',
'sk',
'status',
'server_id',
'user_id',
];
public function server()
{
return $this->belongsTo(Server::class);
}
}

111
app/Support/Frp.php Normal file
View File

@ -0,0 +1,111 @@
<?php
namespace App\Support;
use App\Models\Server;
use Exception;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class Frp
{
public string|int $id;
protected Server $frpServer;
public function __construct($id)
{
$this->frpServer = (new Server)->find($id);
$this->id = $id;
}
public function serverInfo()
{
return $this->cache('serverinfo', '/serverinfo');
}
protected function cache($key, $path = null)
{
$cache_key = 'frpTunnel_' . $this->id . '_' . $key;
if (Cache::has($cache_key)) {
return Cache::get($cache_key);
} else {
if ($path == null) {
return null;
} else {
$data = $this->get($path);
if (!$data) {
// request failed
Cache::put($cache_key, [], 10);
} else {
Cache::put($cache_key, $data, 60);
}
return $data;
}
}
}
protected function get($url)
{
$addr = 'http://' . $this->frpServer->server_address . ':' . $this->frpServer->dashboard_port . '/api' . $url;
try {
$resp = Http::timeout(3)->withBasicAuth($this->frpServer->dashboard_user, $this->frpServer->dashboard_password)->get($addr)->json() ?? [];
// if under maintenance
if ($this->frpServer->status !== 'maintenance') {
if ($this->frpServer->status !== 'up') {
$this->frpServer->status = 'up';
}
}
} catch (Exception) {
$this->frpServer->status = 'down';
$resp = false;
} finally {
$this->frpServer->save();
}
return $resp;
}
public function tcpTunnels()
{
return $this->cache('tcpTunnels', '/proxy/tcp');
}
public function udpTunnels()
{
return $this->cache('udpTunnels', '/proxy/udp');
}
public function httpTunnels()
{
return $this->cache('httpTunnels', '/proxy/http');
}
public function httpsTunnels()
{
return $this->cache('httpsTunnels', '/proxy/https');
}
public function stcpTunnels()
{
return $this->cache('stcpTunnels', '/proxy/stcp');
}
public function xtcpTunnels()
{
return $this->cache('stcpTunnels', '/proxy/xtcp');
}
public function traffic($name)
{
return $this->cache('traffic_' . $name, '/traffic/' . $name);
}
public function close($run_id)
{
return $this->get('/client/close/' . $run_id);
}
}

View File

@ -0,0 +1,63 @@
<?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('servers', function (Blueprint $table) {
$table->id();
$table->string('name')->index();
$table->string('server_address')->index();
$table->string('server_port')->index();
$table->unsignedSmallInteger('dashboard_port');
$table->string('dashboard_user')->nullable();
$table->string('dashboard_password')->nullable();
$table->string('token')->nullable()->index();
$table->boolean('allow_http')->index()->default(true);
$table->boolean('allow_https')->index()->default(true);
$table->boolean('allow_tcp')->index()->default(true);
$table->boolean('allow_udp')->index()->default(true);
$table->boolean('allow_stcp')->index()->default(true);
$table->boolean('allow_sudp')->index()->default(true);
$table->boolean('allow_xtcp')->index()->default(true);
// bandwidth_limit
$table->unsignedBigInteger('bandwidth_limit')->default(0)->index();
$table->unsignedSmallInteger('min_port')->default(10000)->index();
$table->unsignedSmallInteger('max_port')->default(60000)->index();
$table->unsignedInteger('max_tunnels')->default(100)->index();
$table->unsignedInteger('tunnels')->default(0)->index();
$table->string('status')->default('maintenance');
// is_china_mainland
$table->boolean('is_china_mainland')->default(false)->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('servers');
}
};

View File

@ -0,0 +1,55 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('tunnels', function (Blueprint $table) {
$table->id();
$table->string('name')->index();
$table->char('protocol', 5)->index()->default('tcp');
$table->string('custom_domain')->nullable()->index();
$table->string('local_address')->index();
$table->unsignedSmallInteger('remote_port')->index()->nullable();
$table->string('client_token')->index()->unique();
$table->string('sk')->index()->nullable();
$table->unsignedBigInteger('user_id')->index();
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
$table->unsignedBigInteger('server_id')->index();
$table->foreign('server_id')->references('id')->on('servers')->cascadeOnDelete();
// use_encryption
$table->boolean('use_encryption')->default(false)->index();
// use_compression
$table->boolean('use_compression')->default(false)->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('tunnels');
}
};

View File

@ -0,0 +1,52 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('active_tunnels', function (Blueprint $table) {
$table->id();
// 协议
$table->char('protocol', 5)->index()->default('tcp');
// 流量(全部 MB)
$table->unsignedBigInteger('traffic')->default(0)->index();
// 隧道名称
$table->string('name')->index();
// 记录正在运行的隧道
$table->unsignedBigInteger('tunnel_id')->index()->nullable();
$table->foreign('tunnel_id')->references('id')->on('tunnels')->cascadeOnDelete();
// 用户 ID
$table->unsignedBigInteger('user_id')->index()->nullable();
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
// run id
$table->string('run_id')->index()->nullable();
// 上次心跳
$table->timestamp('last_heartbeat_at')->nullable()->index();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('active_tunnels');
}
};

View File

View File

@ -0,0 +1,299 @@
<template>
<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"
v-model="data.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"
v-model="data.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"
v-model="data.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"
v-model="data.token"
/>
</div>
<h3>服务器位置</h3>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
name="is_china_mainland"
value="1"
id="is_china_mainland"
v-model="data.is_china_mainland"
/>
<label class="form-check-label" for="is_china_mainland">
在中国大陆
</label>
</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"
v-model="data.dashboard_port"
/>
</div>
<div class="mb-3">
<label for="dashboardUser" class="form-label">登录用户名</label>
<input
type="text"
required
class="form-control"
id="dashboardUser"
name="dashboard_user"
v-model="data.dashboard_user"
/>
</div>
<div class="mb-3">
<label for="dashboardPwd" class="form-label">密码</label>
<input
type="text"
required
class="form-control"
id="dashboardPwd"
name="dashboard_password"
v-model="data.dashboard_password"
/>
</div>
<h3>端口范围限制</h3>
<div class="input-group input-group-sm mb-3">
<input
type="text"
required
class="form-control"
placeholder="最小端口,比如:1024"
name="min_port"
v-model="data.min_port"
/>
<input
type="text"
required
class="form-control"
placeholder="最大端口,比如:65535"
name="max_port"
v-model="data.max_port"
/>
</div>
<h3>最多隧道数量</h3>
<div class="input-group input-group-sm mb-3">
<input
type="text"
required
class="form-control"
placeholder="最多隧道数量,比如:1024个隧道"
name="max_tunnels"
v-model="data.max_tunnels"
/>
</div>
<h3>隧道协议限制</h3>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
name="allow_http"
value="1"
id="allow_http"
v-model="data.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"
v-model="data.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"
v-model="data.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"
v-model="data.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"
v-model="data.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_sudp"
value="1"
id="allow_sudp"
v-model="data.allow_sudp"
/>
<label class="form-check-label" for="allow_sudp">
允许 SUDP
</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
name="allow_xtcp"
value="1"
id="allow_xtcp"
v-model="data.allow_xtcp"
/>
<label class="form-check-label" for="allow_xtcp">
允许 XTCP
</label>
</div>
<div class="col-auto">
<button
type="submit"
class="btn btn-primary mb-3"
@click.prevent="submit"
>
新建服务器
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import http from "../../plugins/http";
const data = ref({
name: "",
server_address: "",
token: "",
dashboard_port: "",
dashboard_user: "",
dashboard_password: "",
min_port: "",
max_port: "",
max_tunnels: "",
allow_http: "",
allow_https: "",
allow_tcp: "",
allow_udp: "",
allow_stcp: "",
allow_sudp: "",
allow_xtcp: "",
is_china_mainland: "",
});
const submit = () => {
http.post("/servers", data.value).then((res) => {
if (res.status === 200 || res.status === 201) {
alert("服务器创建成功。");
}
});
};
//
const fillForm = () => {
const urlParams = new URLSearchParams(window.location.search);
for (const [key, value] of urlParams.entries()) {
// push to data
data.value[key] = value;
}
};
fillForm();
</script>

View File

@ -0,0 +1,311 @@
<template>
<div class="row">
<div class="col">
<div class="mb-3">
<label for="serverName" class="form-label">服务器名称</label>
<input
type="text"
required
class="form-control"
id="serverName"
placeholder="输入服务器名称,它将会被搜索到"
name="name"
v-model="data.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"
v-model="data.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"
v-model="data.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"
v-model="data.token"
/>
</div>
<h3>服务器位置</h3>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
name="is_china_mainland"
value="1"
id="is_china_mainland"
v-model="data.is_china_mainland"
/>
<label class="form-check-label" for="is_china_mainland">
在中国大陆
</label>
</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"
v-model="data.dashboard_port"
/>
</div>
<div class="mb-3">
<label for="dashboardUser" class="form-label">登录用户名</label>
<input
type="text"
required
class="form-control"
id="dashboardUser"
name="dashboard_user"
v-model="data.dashboard_user"
/>
</div>
<div class="mb-3">
<label for="dashboardPwd" class="form-label">密码</label>
<input
type="text"
required
class="form-control"
id="dashboardPwd"
name="dashboard_password"
v-model="data.dashboard_password"
/>
</div>
<h3>端口范围限制</h3>
<div class="input-group input-group-sm mb-3">
<input
type="text"
required
class="form-control"
placeholder="最小端口,比如:1024"
name="min_port"
v-model="data.min_port"
/>
<input
type="text"
required
class="form-control"
placeholder="最大端口,比如:65535"
name="max_port"
v-model="data.max_port"
/>
</div>
<h3>最多隧道数量</h3>
<div class="input-group input-group-sm mb-3">
<input
type="text"
required
class="form-control"
placeholder="最多隧道数量,比如:1024个隧道"
name="max_tunnels"
v-model="data.max_tunnels"
/>
</div>
<h3>隧道协议限制</h3>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
name="allow_http"
value="1"
id="allow_http"
v-model="data.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"
v-model="data.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"
v-model="data.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"
v-model="data.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"
v-model="data.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_sudp"
value="1"
id="allow_sudp"
v-model="data.allow_sudp"
/>
<label class="form-check-label" for="allow_sudp">
允许 SUDP
</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
name="allow_xtcp"
value="1"
id="allow_xtcp"
v-model="data.allow_xtcp"
/>
<label class="form-check-label" for="allow_xtcp">
允许 XTCP
</label>
</div>
<div class="col-auto mt-3">
<button
type="submit"
class="btn btn-primary mb-3"
@click.prevent="submit"
>
修改服务器配置
</button>
<button
type="submit"
class="btn btn-danger mb-3"
style="margin-left: 5px "
@click.prevent="destroy"
>
删除服务器
</button>
</div>
</div>
</div>
</template>
<script setup>
import {ref} from 'vue'
import http from '../../plugins/http'
import route from '../../plugins/router'
const data = ref({})
let origin = {}
http.get('servers/' + route.currentRoute.value.params.id).then((res) => {
data.value = res.data
origin = JSON.parse(JSON.stringify(res.data))
})
const submit = () => {
// get changes
let changes = {}
for (let key in data.value) {
if (data.value[key] !== origin[key]) {
changes[key] = data.value[key]
}
}
http.patch('servers/' + route.currentRoute.value.params.id, changes).then((res) => {
console.log(res)
})
}
const destroy = () => {
if (!confirm("确定删除服务器吗?")) {
return false;
}
http.delete('servers/' + route.currentRoute.value.params.id).then(res => {
if (res.status === 200 || res.status === 204) {
alert("删除成功。")
route.push({
name: 'servers'
})
}
})
}
</script>

View File

@ -0,0 +1,69 @@
<template>
<div>
<h3>服务器列表</h3>
<div class="list-group">
<template v-for="server in servers">
<router-link :to="{
name: 'servers.edit',
params: {
id: server.id
}
}" class="list-group-item list-group-item-action" aria-current="true">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ server.name }}</h5>
<small>{{ server.status }}</small>
</div>
<p class="mb-1">
<span class="badge bg-primary" v-show="server.allow_http">HTTP</span>
<span class="badge bg-primary" v-show="server.allow_https">HTTPS</span>
<span class="badge bg-primary" v-show="server.allow_tcp">TCP</span>
<span class="badge bg-primary" v-show="server.allow_udp">UDP</span>
<span class="badge bg-primary" v-show="server.allow_stcp">STCP</span>
<span class="badge bg-primary" v-show="server.allow_sudp">SUDP</span>
<span class="badge bg-primary" v-show="server.allow_xtcp">XTCP</span>
</p>
<small>{{ server.server_address }}:{{ server.server_port }}</small>
</router-link>
</template>
</div>
</div>
</template>
<script setup>
import {ref} from 'vue'
import http from '../../plugins/http'
const servers = ref([{
"id": "1",
"name": "",
"server_address": "",
"server_port": "",
"token": "",
"allow_http": 1,
"allow_https": 0,
"allow_tcp": 0,
"allow_udp": 0,
"allow_stcp": 0,
"allow_sudp": 0,
"allow_xtcp": 0,
"bandwidth_limit": 0,
"min_port": 0,
"max_port": 1024,
"max_tunnels": 100,
"tunnels": 0,
"status": "maintenance",
"is_china_mainland": 0,
"created_at": "2023-03-15T11:57:47.000000Z",
"updated_at": "2023-03-15T11:57:47.000000Z"
}])
http.get('servers').then((res) => {
servers.value = res.data
})
</script>

View File

@ -0,0 +1,3 @@
<template>
<h1>您名下的隧道</h1>
</template>

View File

@ -0,0 +1,3 @@
<template>
<h3>创建隧道</h3>
</template>

View File

@ -0,0 +1,55 @@
<template>
<h3>隧道列表</h3>
<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>
</thead>
<tbody>
<tr>
<th>1</th>
<td><a href="http://portio.test/tunnels/1/edit">stcp</a></td>
<td>STCP</td>
<td>127.0.0.1:80</td>
<td>127.0.01:</td>
<td>0</td>
<td>0.000 Bytes</td>
<td>0.000 Bytes</td>
<td><a href="http://portio.test/servers/1">Test</a></td>
<td>
<span class="text-danger">离线</span>
</td>
</tr>
</tbody>
</table>
</template>
<script setup>
import { ref } from "vue";
import http from "../../plugins/http";
const tunnels = ref([])
http.get('tunnels').then((res) => {
tunnels.value = res.data
console.log(tunnels.value)
})
</script>