mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 10:17:17 +08:00
特性(网站管理):新增免费SSL证书申请
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
46
composer.lock
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
return [
|
||||
'name' => '耗子Linux面板',
|
||||
'version' => '20221205',
|
||||
'version' => '20221208',
|
||||
'plugin_dir' => '/www/panel/plugins',
|
||||
];
|
||||
401
public/panel/modules/file.js
Normal file
401
public/panel/modules/file.js
Normal 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"></i>'
|
||||
,next: '<i class="layui-icon"></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);
|
||||
});
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user