2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 10:17:17 +08:00

特性(网站管理):新增免费SSL证书申请

This commit is contained in:
耗子
2022-12-08 00:10:28 +08:00
parent ab85294dd6
commit 66957ca86d
9 changed files with 668 additions and 9 deletions

View File

@@ -57,6 +57,7 @@ class SettingsController extends Controller
$data = [
'name' => $settingArr['name'],
'username' => $request->user()->username,
'email' => $request->user()->email,
'password' => '',
'port' => $matches[1],
'api' => $api,
@@ -80,19 +81,22 @@ class SettingsController extends Controller
$settings = $request->all();
// 将数据入库
foreach ($settings as $key => $value) {
if ($key == 'access_token' || $key == 'username' || $key == 'password' || $key == 'api_token' || $key == 'api' || $key == 'port') {
if ($key == 'access_token' || $key == 'username' || $key == 'email' || $key == 'password' || $key == 'api_token' || $key == 'api' || $key == 'port') {
continue;
}
// 创建或更新
Setting::query()->updateOrCreate(['name' => $key], ['value' => $value]);
}
// 单独处理用户名密码
// 单独处理用户名密码、邮箱
if ($request->input('username') != $request->user()->username) {
$request->user()->update(['username' => $request->input('username')]);
}
if ($request->input('password') != '') {
$request->user()->update(['password' => Hash::make($request->input('password'))]);
}
if ($request->input('email') != $request->user()->email) {
$request->user()->update(['email' => $request->input('email')]);
}
// 处理面板端口
$port = $request->input('port');
$nginxConf = file_get_contents('/www/server/nginx/conf/nginx.conf');

View File

@@ -9,9 +9,12 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Website;
use App\Models\Setting;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use skoerfgen\ACMECert\ACME_Exception;
use skoerfgen\ACMECert\ACMECert;
class WebsitesController extends Controller
{
@@ -372,6 +375,13 @@ EOF;
$website['ssl_certificate_key'] = file_get_contents($matches5[1][0]);
$website['http_redirect'] = str_contains($nginx_config, '# http重定向标记位');
$website['hsts'] = str_contains($nginx_config, '# hsts标记位');
try {
$sslDate = (new ACMECert())->getRemainingDays($website['ssl_certificate']);
$sslDate = round($sslDate, 2);
} catch (Exception $e) {
$sslDate = '未知';
}
$website['ssl_date'] = $sslDate;
} else {
$website['ssl_certificate'] = @file_get_contents('/www/server/vhost/ssl/'.$name.'.pem');
$website['ssl_certificate_key'] = @file_get_contents('/www/server/vhost/ssl/'.$name.'.key');
@@ -1027,6 +1037,152 @@ EOF;
return response()->json($res);
}
/**
* 签发SSL证书
* @param Request $request
* @return JsonResponse
* @throws ACME_Exception|Exception
*/
public function issueSsl(Request $request): JsonResponse
{
try {
$input = $this->validate($request, [
'type' => 'required|in:lets,buypass,google,sslcom,zerossl',
'name' => 'required',
]);
} catch (ValidationException $e) {
return response()->json([
'code' => 1,
'msg' => '参数错误:'.$e->getMessage(),
'errors' => $e->errors()
], 200);
}
$user = $request->user();
// 检查网站是否存在
$website = Website::query()->where('name', $input['name'])->first();
if (!$website) {
return response()->json([
'code' => 1,
'msg' => '网站不存在',
], 200);
}
// 从配置文件中获取网站域名
$nginxConfig = file_get_contents('/www/server/vhost/'.$website['name'].'.conf');
$domainConfig = $this->cut('# server_name标记位开始', '# server_name标记位结束', $nginxConfig);
preg_match_all('/server_name\s+(.+);/', $domainConfig, $matches1);
$domains = explode(" ", $matches1[1][0]);
// 从配置文件中获取网站目录
$pathConfig = $this->cut('# root标记位开始', '# root标记位结束', $nginxConfig);
preg_match_all('/root\s+(.+);/', $pathConfig, $matches2);
$path = $matches2[1][0];
/**
* 对域名需要进行一下处理,如果域名是泛域名,返回暂不支持泛域名
*/
foreach ($domains as $domain) {
if (str_contains($domain, '*')) {
return response()->json([
'code' => 1,
'msg' => '暂不支持泛域名',
], 200);
}
}
switch ($input['type']) {
case 'lets':
$ac = new ACMECert('https://acme-v02.api.letsencrypt.org/directory');
break;
case 'buypass':
$ac = new ACMECert('https://api.buypass.com/acme/directory');
break;
case 'google':
$ac = new ACMECert('https://dv.acme-v02.api.pki.goog/directory');
break;
case 'sslcom':
$ac = new ACMECert('https://acme.ssl.com/sslcom-dv-rsa');
break;
case 'zerossl':
$ac = new ACMECert('https://acme.zerossl.com/v2/DV90');
break;
default:
$res = [
'code' => 1,
'msg' => '参数错误type',
];
return response()->json($res);
break;
}
try {
$accountKey = $ac->generateECKey('P-384');
$certKey = $ac->generateECKey('P-384');
} catch (Exception $e) {
return response()->json([
'code' => 1,
'msg' => '生成密钥失败:'.$e->getMessage(),
], 200);
}
try {
$ac->loadAccountKey($accountKey);
} catch (Exception $e) {
return response()->json([
'code' => 1,
'msg' => '加载密钥失败:'.$e->getMessage(),
], 200);
}
try {
$ac->register(true, $user->email);
} catch (Exception $e) {
return response()->json([
'code' => 1,
'msg' => '注册CA账户失败'.$e->getMessage(),
], 200);
}
// 初始化域名数组
$domainConfig = [];
foreach ($domains as $domain) {
$domainConfig[$domain] = [
'challenge' => 'http-01',
'docroot' => $path
];
}
$handler = function ($opts) {
$fn = $opts['config']['docroot'].$opts['key'];
@mkdir(dirname($fn), 0777, true);
file_put_contents($fn, $opts['value']);
return function ($opts) {
unlink($opts['config']['docroot'].$opts['key']);
};
};
// 申请证书
try {
$fullchain = $ac->getCertificateChain($certKey, $domainConfig, $handler);
} catch (ACME_Exception $e) {
return response()->json([
'code' => 1,
'msg' => '申请证书失败:'.$e->getMessage(),
], 200);
}
// 写入证书
$sslDir = '/www/server/vhost/ssl/';
file_put_contents($sslDir.$website['name'].'.key', $certKey);
file_put_contents($sslDir.$website['name'].'.pem', $fullchain);
// 返回
$res = [
'code' => 0,
'msg' => 'success',
];
return response()->json($res);
}
/**
* 裁剪字符串
* @param $begin

View File

@@ -10,7 +10,8 @@
"laravel/framework": "^9.19",
"laravel/sanctum": "^3.0",
"laravel/tinker": "^2.7",
"overtrue/laravel-lang": "^6.0"
"overtrue/laravel-lang": "^6.0",
"skoerfgen/acmecert": "^3.2"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",

46
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "efb63260d6e5ee652c84bfee6f458ada",
"content-hash": "199471d49176a915109df8e3774d6e7e",
"packages": [
{
"name": "brick/math",
@@ -2980,6 +2980,50 @@
],
"time": "2022-09-16T03:22:46+00:00"
},
{
"name": "skoerfgen/acmecert",
"version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/skoerfgen/ACMECert.git",
"reference": "5cf724cebf4bcab13ec609308d6a0cbd136ccdfd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/skoerfgen/ACMECert/zipball/5cf724cebf4bcab13ec609308d6a0cbd136ccdfd",
"reference": "5cf724cebf4bcab13ec609308d6a0cbd136ccdfd",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"php": ">=5.6.0"
},
"suggest": {
"ext-curl": "Optional for better http performance"
},
"type": "library",
"autoload": {
"psr-4": {
"skoerfgen\\ACMECert\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Stefan Körfgen",
"homepage": "https://github.com/skoerfgen"
}
],
"description": "PHP client library for Let's Encrypt and other ACME v2 - RFC 8555 compatible Certificate Authorities",
"support": {
"issues": "https://github.com/skoerfgen/ACMECert/issues",
"source": "https://github.com/skoerfgen/ACMECert"
},
"time": "2022-08-03T21:36:49+00:00"
},
{
"name": "symfony/console",
"version": "v6.1.7",

View File

@@ -1,6 +1,6 @@
<?php
return [
'name' => '耗子Linux面板',
'version' => '20221205',
'version' => '20221208',
'plugin_dir' => '/www/panel/plugins',
];

View File

@@ -0,0 +1,401 @@
layui.define(['jquery', 'layer', 'laypage'], function(exports) { //提示模块也可以依赖其它模块layui.define('layer', callback);
var $ = layui.jquery,
layer = layui.layer,
laypage = layui.laypage;
//外部接口
var fm = {
config:{'test':'test','thumb':{'nopic':'',width:100,height:100},icon_url:'ico/',btn_upload:true,btn_create:true}
,cache: {} //数据缓存
,index: layui.fm ? (layui.fm.index + 10000) : 0
//设置全局项
,set: function(options){
var that = this;
that.config = $.extend({}, that.config, options);
return that;
}
//事件监听
,on: function(events, callback){
return layui.onevent.call(this, 'file', events, callback);
}
,dirRoot:[{'path':'','name': '根目录'}]
,v:'1.0.1.2019.12.26'
}
//操作当前实例
, thisFm = function() {
var that = this,
options = that.config,
id = options.id || options.index;
// console.log(id)
if (id) {
thisFm.that[id] = that; //记录当前实例对象
thisFm.config[id] = options; //记录当前实例配置项
}
return {
config: options,
reload: function(options) {
that.reload.call(that, options);
}
}
}
//获取当前实例配置项
,getThisFmConfig = function(id){
var config = thisFm.config[id];
if(!config) hint.error('The ID option was not found in the fm instance');
return config || null;
}
//构造器
,Class = function(options){
var that = this;
that.config = $.extend({}, that.config, fm.config, options);
//记录所有实例
thisFm.that = {}; //记录所有实例对象
thisFm.config = {}; //记录所有实例配置项
// console.log(that.config)
that.render();
};
//渲染
Class.prototype.render = function(){
var that = this
,options = that.config;
options.elem = $(options.elem);
options.where = options.where || {};
options.id = options.id || options.elem.attr('id') || that.index;
//请求参数的自定义格式
options.request = $.extend({
pageName: 'page'
,limitName: 'limit'
}, options.request)
//响应数据的自定义格式
options.response = $.extend({
statusName: 'code'
,statusCode: 0
,msgName: 'msg'
,dataName: 'data'
,countName: 'count'
}, options.response);
//如果 page 传入 laypage 对象
if(typeof options.page === 'object'){
options.limit = options.page.limit || options.limit;
options.limits = options.page.limits || options.limits;
that.page = options.page.curr = options.page.curr || 1;
delete options.page.elem;
delete options.page.jump;
}
if(!options.elem[0]) return that;
//渲染主体
var _btn = ''
if(options.btn_create){
_btn +='<button type="button" class="layui-btn layui-btn-primary layui-btn-sm" id="new_dir">建文件夹</button>';
}
if(options.btn_upload){
_btn +='<button type="button" class="layui-btn layui-btn-primary layui-btn-sm" id="uploadfile">上传文件</button>';
}
var _html = '<div class="layui-card" >' +
'<div class="layui-card-body">' +
'<div class="layui-btn-group tool_bar">' +
_btn+
'<button type="button" class="layui-btn layui-btn-primary layui-btn-sm" id="back"><i class="layui-icon layui-icon-left line"></i></button>' +
'</div>' +
'<div class="layui-inline path_bar" id="">' +
'<a ><i class="layui-icon layui-icon-more-vertical line" ></i>根目录</a>' +
'</div>' +
'</div><hr><div class="layui-card-body">' +
'<div class="file-body layui-form" style="">' +
'<ul class="file layui-row fm_body layui-col-space10" >' +
'</ul>' +
'</div>' +
'<hr><div ><div class="layui_page_'+options.id+'" id="layui_page_'+options.id+'"></div></div></div>';
options.elem.html(_html);
options.index = that.index;
that.key = options.id || options.index;
//各级容器
that.layPage = options.elem.find('.layui_page_'+options.id);
that.layBody = options.elem.find('.fm_body');
that.layPathBar = options.elem.find('.path_bar');
that.layToolBar = options.elem.find('.tool_bar');
that.pullData(that.page); //请求数据
that.events(); //事件
}
//页码
Class.prototype.page = 1;
//获得数据
Class.prototype.pullData = function(curr) {
var that = this,
options = that.config,
request = options.request,
response = options.response,
_status = false;
that.startTime = new Date().getTime(); //渲染开始时间
if (options.url) { //Ajax请求
var params = {};
params[request.pageName] = curr;
params[request.limitName] = options.limit;
//参数
var data = $.extend(params, options.where);
if (options.contentType && options.contentType.indexOf("application/json") == 0) { //提交 json 格式
data = JSON.stringify(data);
}
that.loading();
$.ajax({
type: options.method || 'get',
url: options.url,
contentType: options.contentType,
data: data,
async: false,
dataType: 'json',
headers: options.headers || {},
success: function(res) {
//如果有数据解析的回调,则获得其返回的数据
if (typeof options.parseData === 'function') {
res = options.parseData(res) || res;
}
//检查数据格式是否符合规范
if (res[response.statusName] != response.statusCode) {
that.errorView(
res[response.msgName] ||
('返回的数据不符合规范,正确的成功状态码应为:"' + response.statusName + '": ' + response.statusCode)
);
} else {
// console.log(res, curr, res[response.countName]);
that.renderData(res, curr, res[response.countName]);
options.time = (new Date().getTime() - that.startTime) + ' ms'; //耗时(接口请求+视图渲染)
}
typeof options.done === 'function' && options.done(res, curr, res[response.countName]);
_status = true;
},
error: function(e, m) {
that.errorView('数据接口请求异常:' + m);
}
});
}
return _status;
};
//数据渲染
Class.prototype.renderData = function(res, curr, count){
var that = this
,options = that.config
,data = res[options.response.dataName] || []
//渲染数据
var _content = ''
layui.each(data,function(i,v){
let _img,_type;
_type = v.type;
switch (v.type) {
case 'directory':
_img = '<div style="width:'+options.thumb['width']+'px;height:'+options.thumb['height']+'px;line-height:'+options.thumb['height']+'px"><img src="ico/dir.png" style="vertical-align:middle;"></div>';
_type = 'DIR';
break;
default:
if (v.type == 'png' || v.type == 'gif' || v.type == 'jpg' || v.type == 'image') {
_img = '<img src="' + v.thumb + '" width="'+options.thumb['width']+'" height="'+options.thumb['height']+'" onerror=\'this.src="'+options.thumb['nopic']+'"\' />';
} else {
_img = '<div style="width:'+options.thumb['width']+'px;height:'+options.thumb['height']+'px;line-height:'+options.thumb['height']+'px"><img src="' + options.icon_url + v.type + '.png" onerror=\'this.src="'+options.thumb['nopic']+'"\' /></div>';
}
break;
}
_content+='<li style="display:inline-block" data-type="'+_type+'" data-index="'+i+'">' +
'<div class="content" align="center">'+
_img +
'<p class="layui-elip" title="' + v.name + '">' + v.name + ' </p>' +
'</div>' +
'</li>';
});
options.elem.find('.file').html(_content);
fm.cache[options.id] = data; //记录数据
//显示隐藏分页栏
// console.log(that.layPage)
that.layPage[(count == 0 || (data.length === 0 && curr == 1)) ? 'addClass' : 'removeClass']('layui-hide');
if(data.length === 0){
return that.errorView('空目录');
} else {
//that.layFixed.removeClass('layui-hide');
}
//同步分页状态
if(options.page){
// console.log(options,'layui_page_' + options.id)
options.page = $.extend({
elem: 'layui_page_' + options.id
,count: count
,limit: options.limit
,limits: options.limits || [10,20,30,40,50,60,70,80,90]
,groups: 3
,layout: ['prev', 'page', 'next', 'skip', 'count', 'limit']
,prev: '<i class="layui-icon">&#xe603;</i>'
,next: '<i class="layui-icon">&#xe602;</i>'
,jump: function(obj, first){
if(!first){
//分页本身并非需要做以下更新,下面参数的同步,主要是因为其它处理统一用到了它们
//而并非用的是 options.page 中的参数(以确保分页未开启的情况仍能正常使用)
that.page = obj.curr; //更新页码
options.limit = obj.limit; //更新每页条数
that.pullData(obj.curr);
}
}
}, options.page);
options.page.count = count; //更新总条数
laypage.render(options.page);
}
};
//更新路径工具条
Class.prototype.updatePathBar = function(){
// console.log('updatePathBar',fm.dirRoot);
var that = this
,options = that.config;
//请求数据
let dir_cur = fm.dirRoot[fm.dirRoot.length -1];
options.where = {'path':dir_cur['path']}
let _rs = that.pullData(1);
// console.log(_rs)
if(false == _rs) return;
that.layPathBar.html('');
fm.dirRoot.map(function(item,index,arr){
let icon = index==0 ?'layui-icon-more-vertical':'layui-icon-right';
let html = '<i class="layui-icon '+icon+'"></i>'+
'<a data-path="' + item.path + '" data-name="' + item.name + '" >' + item.name + '</a>'
that.layPathBar.append(html);
})
}
//事件处理
Class.prototype.events = function(){
var that = this
,options = that.config
,_BODY = $('body')
,dict = {}
,filter = options.elem.attr('lay-filter');
//文件事件
that.layBody.on('click', 'li', function(){ //单击行
setPicEvent.call(this, 'pic');
});
//文件夹事件
that.layBody.on('click', 'li[data-type=DIR]', function(){ //单击行
var othis = $(this);
var data = fm.cache[options.id];
var index = othis.data('index');
data = data[index] || {};
//导航图标
fm.dirRoot.push({'path':data.path,'name': data.name});
that.updatePathBar();
});
//返回上一级目录
that.layToolBar.on('click', '#back', function(){
var othis = $(this);
if(fm.dirRoot.length == 1) return layer.msg('已经是根目录');
fm.dirRoot.length >1 && fm.dirRoot.pop()
that.updatePathBar();
// console.log('back');
});
//上传文件
that.layToolBar.on('click', '#uploadfile', function(){
var othis = $(this);
let eventType = 'uploadfile';
layui.event.call(this,
'file', eventType + '('+ filter +')'
,{obj:othis,path:fm.dirRoot[fm.dirRoot.length -1]['path']}
);
// console.log('uploadfile');
});
//新建文件夹
that.layToolBar.on('click', '#new_dir', function(){
var othis = $(this);
let eventType = 'new_dir';
layer.prompt({ title: '请输入新文件夹名字', formType: 0 }, function(name, index) {
layer.close(index);
//新建文件夹
layui.event.call(this,
'file', eventType + '('+ filter +')'
,{obj:othis,folder:name,path:fm.dirRoot[fm.dirRoot.length -1]['path']}
);
});
});
//创建点击文件事件监听
var setPicEvent = function(eventType) {
var othis = $(this);
var data = fm.cache[options.id];
var index = othis.data('index');
if (othis.data('type')=='DIR') return; //不触发事件
data = data[index] || {};
layui.event.call(this,
'file', eventType + '('+ filter +')'
,{obj:othis,data:data}
);
};
};
//请求loading
Class.prototype.loading = function(hide){
var that = this
,options = that.config;
if(options.loading){
if(hide){
that.layInit && that.layInit.remove();
delete that.layInit;
that.layBox.find(ELEM_INIT).remove();
} else {
that.layInit = $(['<div class="layui-table-init">'
,'<i class="layui-icon layui-icon-loading layui-anim layui-anim-rotate layui-anim-loop"></i>'
,'</div>'].join(''));
that.layBox.append(that.layInit);
}
}
};
//异常提示
Class.prototype.errorView = function(html){
var that = this
layer.msg(html);
};
//重载
Class.prototype.reload = function(options){
var that = this;
options = options || {};
delete that.haveInit;
if(options.data && options.data.constructor === Array) delete that.config.data;
that.config = $.extend(true, {}, that.config, options);
that.render();
};
//重载
fm.reload = function(id, options){
var config = getThisFmConfig(id); //获取当前实例配置项
if(!config) return;
var that = thisFm.that[id];
that.reload(options);
return thisFm.call(that);
};
//核心入口
fm.render = function(options){
var inst = new Class(options);
return thisFm.call(inst);
};
exports('file', fm);
});

View File

@@ -1,7 +1,7 @@
<!--
Name: 面板设置模版
Author: 耗子
Date: 2022-12-01
Date: 2022-12-08
-->
<title>面板设置</title>
<div class="layui-fluid">
@@ -31,7 +31,9 @@ Date: 2022-12-01
<div class="layui-input-inline">
<input type="checkbox" name="multi_login" lay-skin="switch" lay-text="ON|OFF"/>
</div>
<div class="layui-form-mid layui-word-aux">开启后将允许多设备同时登录面板,可能具有一定安全隐患</div>
<div class="layui-form-mid layui-word-aux">
开启后将允许多设备同时登录面板,可能具有一定安全隐患
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">面板名称</label>
@@ -54,6 +56,13 @@ Date: 2022-12-01
</div>
<div class="layui-form-mid layui-word-aux">修改面板的登录密码(留空不修改)</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">面板邮箱</label>
<div class="layui-input-inline">
<input type="text" name="email" value="获取中ing..." class="layui-input" disabled/>
</div>
<div class="layui-form-mid layui-word-aux">修改面板账号的邮箱目前用于签发免费SSL证书</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">面板端口</label>
<div class="layui-input-inline">

View File

@@ -1,7 +1,7 @@
<!--
Name: 网站 - 编辑
Author: 耗子
Date: 2022-12-01
Date: 2022-12-08
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<div class="layui-tab" lay-filter="website-edit-tab">
@@ -151,10 +151,19 @@ Date: 2022-12-01
@{{ d.params.config.hsts== 1 ? 'checked' : '' }} />
</div>
</div>
<div class="layui-inline">
<div class="layui-input-inline">
<button id="issue-ssl" class="layui-btn layui-btn-sm">签发免费SSL证书</button>
</div>
</div>
</div>
<div class="layui-form-item layui-form-text">
@{{# if(d.params.config.ssl == 1){ }}
<label class="layui-form-label">证书 <span style="color: red; float: right;">剩余有效期:@{{ d.params.config.ssl_date }}</span></label>
@{{# }else{ }}
<label class="layui-form-label">证书</label>
@{{# } }}
<div class="layui-input-block">
<textarea name="ssl_certificate" placeholder="请输入pem证书文件的内容"
class="layui-textarea">@{{ d.params.config.ssl_certificate }}</textarea>
@@ -344,7 +353,40 @@ Date: 2022-12-01
}
});
});
})
});
// 监听签发证书按钮
$('#issue-ssl').click(function () {
layer.confirm('确定要申请签发免费SSL证书吗', function (index) {
index = layer.msg('正在签发证书,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
});
admin.req({
url: '/api/panel/website/issueSsl'
, type: 'post'
, data: {
name: params.config.name
, type: 'lets'
}
, success: function (res) {
layer.close(index);
if (res.code === 0) {
layer.msg('签发成功', {icon: 1});
setTimeout(function () {
admin.render();
}, 1000);
} else {
layer.alert(res.msg, {icon: 2});
}
}
, error: function (xhr, status, error) {
layer.closeAll('loading');
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
});
});
};
</script>

View File

@@ -84,6 +84,8 @@ Route::prefix('panel')->group(function () {
Route::post('deleteBackup', [WebsitesController::class, 'deleteBackup']);
// 重置网站配置
Route::post('resetSiteConfig', [WebsitesController::class, 'resetSiteConfig']);
// 签发SSL证书
Route::post('issueSsl', [WebsitesController::class, 'issueSsl']);
});
// 监控
Route::middleware('auth:sanctum')->prefix('monitor')->group(function () {