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

特性(网站):新增备份功能及问题修复

This commit is contained in:
耗子
2022-12-02 23:55:45 +08:00
parent b4b43cb4c4
commit 642d3339fc
18 changed files with 802 additions and 92 deletions

46
CHANGELOG.md Normal file
View File

@@ -0,0 +1,46 @@
# 更新日志
所有重要的更改都将在此文件中记录。
## [20221203] - 常规更新
- 修复禁用Ping不生效的问题
- 修复网站设置PHP版本未赋值的问题
- 新增网站目录备份功能
- 新增面板多用户登录功能
- 实装网站运行状态设置功能
- 实装网站配置文件重置功能
- 前端多处优化
## [20221202] - 常规更新
- 修复网站添加数据库不能为空的问题
- 校验网站目录必须以/开头
- 修复网站名可以编辑的问题
- 新增面板API开关
- 新增计划任务模块
- 安装脚本优化,提高在不同地区下的安装成功率
## [20221201] - 首版发布
- 修复了二测时发现的一些问题
- 首发于Hostloc感谢各位mjj大佬的支持。
## [20221130] - 内部二测
- 内部二测
- 修复了一测发现的问题
- 新增了一些插件,基本功能已经完善
## [20221121] - 内部一测
- 内部一测
## 2022-10 - 2022-11
- 重构版本开始开发
## 2022-07 - 2022-08
- 项目启动、早期开发

View File

@@ -13,6 +13,8 @@
| RockyLinux | 8 |
| AlmaLinux | 9 |
| AlmaLinux | 8 |
| 其他RHEL发行版 | 8 |
| 其他RHEL发行版 | 9 |
广告: [`WeAvatar` —统一头像服务](https://weavatar.com)

View File

@@ -286,7 +286,10 @@ class PluginsController extends Controller
return response()->json($data);
}
Plugin::query()->where('slug', $slug)->update(['show' => $show]);
Plugin::query()->where('slug', $slug)->updateOrInsert(
['slug' => $slug],
['show' => $show]
);
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = '设置成功';

View File

@@ -34,7 +34,13 @@ class SettingsController extends Controller
// 从nginx配置文件中获取面板端口
$nginxConf = file_get_contents('/www/server/nginx/conf/nginx.conf');
preg_match('/listen\s+(\d+)/', $nginxConf, $matches);
if (!isset($matches[1])) {
$res['code'] = 1;
$res['msg'] = '获取面板端口失败请检查nginx主配置文件';
return response()->json($res);
}
// API
$api = 0;
$apiToken = '';
if (isset($settingArr['api']) && $settingArr['api'] == 1) {
@@ -42,11 +48,12 @@ class SettingsController extends Controller
$apiToken = $settingArr['api_token'] ?? '';
}
if (!isset($matches[1])) {
$res['code'] = 1;
$res['msg'] = '获取面板端口失败请检查nginx主配置文件';
return response()->json($res);
// 多设备登录
$multiLogin = 0;
if (isset($settingArr['multi_login']) && $settingArr['multi_login'] == 1) {
$multiLogin = 1;
}
$data = [
'name' => $settingArr['name'],
'username' => $request->user()->username,
@@ -54,6 +61,7 @@ class SettingsController extends Controller
'port' => $matches[1],
'api' => $api,
'api_token' => $apiToken,
'multi_login' => $multiLogin,
];
$res['code'] = 0;
$res['msg'] = 'success';
@@ -72,10 +80,11 @@ 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') {
if ($key == 'access_token' || $key == 'username' || $key == 'password' || $key == 'api_token' || $key == 'api' || $key == 'port') {
continue;
}
Setting::query()->where('name', $key)->update(['value' => $value]);
// 创建或更新
Setting::query()->updateOrCreate(['name' => $key], ['value' => $value]);
}
// 单独处理用户名和密码
if ($request->input('username') != $request->user()->username) {

View File

@@ -3,9 +3,11 @@
* 耗子Linux面板 - 用户控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
@@ -34,15 +36,21 @@ class UsersController extends Controller
'errors' => $e->errors()
], 422);
}
if (auth()->attempt(['username' => $credentials['username'], 'password' => $credentials['password']], $credentials['remember'])) {
if (auth()->attempt(['username' => $credentials['username'], 'password' => $credentials['password']],
$credentials['remember'])) {
$user = auth()->user();
$user->tokens()->delete();
// 多设备登录
$multiLogin = Setting::query()->where('name', 'multi_login')->value('value');
if ($multiLogin != 1) {
$user->tokens()->delete();
}
$token = $user->createToken('token')->plainTextToken;
return response()->json(['code' => 0, 'msg' => '登录成功', 'data' => ['access_token' => $token]]);
} else {
return response()->json(['code' => 1, 'msg' => '登录失败,用户名或密码错误']);
}
}
public function getInfo(Request $request): JsonResponse
{
$user = $request->user();

View File

@@ -30,6 +30,12 @@ class WebsitesController extends Controller
'data' => []
]);
}
foreach ($websiteList as $website) {
// 如果PHP是0将其设置为字符串的00
if ($website->php == '0') {
$website->php = '00';
}
}
return response()->json([
'code' => 0,
'msg' => '获取成功',
@@ -335,10 +341,10 @@ EOF;
$website['domain'] .= PHP_EOL.$v;
}
}
// 从nginx配置中root标记位提取全部根目录
// 从nginx配置中root标记位提取运行目录
$root_raw = $this->cut('# root标记位开始', '# root标记位结束', $nginx_config);
preg_match_all('/root\s+(.+);/', $root_raw, $matches2);
$website['path'] = $matches2[1][0];
$website['root'] = $matches2[1][0];
// 从nginx配置中index标记位提取全部默认文件
$index_raw = $this->cut('# index标记位开始', '# index标记位结束', $nginx_config);
preg_match_all('/index\s+(.+);/', $index_raw, $matches3);
@@ -396,6 +402,11 @@ EOF;
// 读取访问日志
$website['log'] = shell_exec('tail -n 100 /www/wwwlogs/'.$name.'.log');
// 如果PHP是0将其设置为字符串的00
if ($website['php'] == '0') {
$website['php'] = '00';
}
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = $website;
@@ -413,6 +424,20 @@ EOF;
$name = $request->input('name');
$config = $request->input('config');
$website = Website::query()->where('name', $name)->first();
if (!$website) {
return response()->json([
'code' => 1,
'msg' => '网站不存在',
], 200);
}
if ($website->status != 1) {
return response()->json([
'code' => 1,
'msg' => '网站已停用,请先启用',
], 200);
}
$res['code'] = 0;
$res['msg'] = 'success';
@@ -467,12 +492,12 @@ EOF;
$configRaw = str_replace($port_config_old, PHP_EOL.$port.PHP_EOL.' ', $configRaw);
}
// 网站目录
// 运行目录
$pathConfig = $this->cut('# root标记位开始', '# root标记位结束', $configRaw);
preg_match_all('/root\s+(.+);/', $pathConfig, $matches1);
$pathConfigOld = $matches1[1][0];
if (!empty(trim($pathConfigOld)) && $pathConfigOld != PHP_EOL) {
$pathConfigNew = str_replace($pathConfigOld, $config['path'], $pathConfig);
$pathConfigNew = str_replace($pathConfigOld, $config['root'], $pathConfig);
$configRaw = str_replace($pathConfig, $pathConfigNew, $configRaw);
}
@@ -497,7 +522,7 @@ EOF;
if (is_dir($config['path'])) {
file_put_contents($config['path'].'/.user.ini', $open_basedir);
// 为.user.ini文件添加i权限
shell_exec('chattr +i '.$config['path'].'/.user.ini');
// shell_exec('chattr +i '.$config['path'].'/.user.ini');
}
} else {
// 移除.user.ini文件的i权限
@@ -600,8 +625,10 @@ EOL;
}
// 将数据入库
Website::query()->where('name', $name)->update(['php' => $config['php']]);
Website::query()->where('name', $name)->update(['ssl' => $config['ssl']]);
$website->php = $config['php'];
$website->ssl = $config['ssl'];
$website->path = $config['path'];
$website->save();
file_put_contents('/www/server/vhost/'.$name.'.conf', $configRaw);
file_put_contents('/www/server/vhost/rewrite/'.$name.'.conf', $config['rewrite']);
shell_exec('systemctl reload nginx');
@@ -637,7 +664,376 @@ EOL;
return response()->json($res);
}
// 裁剪字符串
/**
* 获取备份列表
*/
public function getBackupList(): JsonResponse
{
$backupPath = '/www/backup/website';
// 判断备份目录是否存在
if (!is_dir($backupPath)) {
mkdir($backupPath, 0644, true);
}
$backupFiles = scandir($backupPath);
$backupFiles = array_diff($backupFiles, ['.', '..']);
$backupFiles = array_values($backupFiles);
$backupFiles = array_map(function ($backupFile) {
return [
'backup' => $backupFile,
'size' => formatBytes(filesize('/www/backup/website/'.$backupFile)),
];
}, $backupFiles);
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = $backupFiles;
return response()->json($res);
}
/**
* 创建备份
*/
public function createBackup(Request $request): JsonResponse
{
// 消毒数据
try {
$credentials = $this->validate($request, [
'name' => 'required|max:255',
]);
} catch (ValidationException $e) {
return response()->json([
'code' => 1,
'msg' => '参数错误:'.$e->getMessage(),
'errors' => $e->errors()
], 200);
}
$backupPath = '/www/backup/website';
// 判断备份目录是否存在
if (!is_dir($backupPath)) {
mkdir($backupPath, 0644, true);
}
// 从数据库中获取网站目录
$sitePath = Website::query()->where('name', $credentials['name'])->value('path');
$backupFile = $backupPath.'/'.$credentials['name'].'_'.date('YmdHis').'.zip';
shell_exec('zip -r '.$backupFile.' '.$sitePath.' 2>&1');
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 上传备份
*/
public function uploadBackup(Request $request): JsonResponse
{
// 消毒数据
try {
$credentials = $this->validate($request, [
'file' => 'required|file',
]);
} catch (ValidationException $e) {
return response()->json([
'code' => 1,
'msg' => '参数错误:'.$e->getMessage(),
'errors' => $e->errors()
], 200);
}
$file = $request->file('file');
$backupPath = '/www/backup/website';
// 判断备份目录是否存在
if (!is_dir($backupPath)) {
mkdir($backupPath, 0644, true);
}
$backupFile = $backupPath.'/'.$file->getClientOriginalName();
$file->move($backupPath, $file->getClientOriginalName());
// 返回文件名
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = $file->getClientOriginalName();
return response()->json($res);
}
/**
* 恢复备份
*/
public function restoreBackup(Request $request): JsonResponse
{
// 消毒数据
try {
$credentials = $this->validate($request, [
'name' => 'required|max:255',
'backup' => 'required|max:255',
]);
} catch (ValidationException $e) {
return response()->json([
'code' => 1,
'msg' => '参数错误:'.$e->getMessage(),
'errors' => $e->errors()
], 200);
}
$backupPath = '/www/backup/website';
// 判断备份目录是否存在
if (!is_dir($backupPath)) {
mkdir($backupPath, 0644, true);
}
$backupFile = $backupPath.'/'.$credentials['backup'];
// 判断备份文件是否存在
if (!is_file($backupFile)) {
return response()->json([
'code' => 1,
'msg' => '备份文件不存在',
], 200);
}
shell_exec('rm -rf /www/wwwroot/'.$credentials['name'].'/*');
shell_exec('unzip -o '.$backupFile.' -d /www/wwwroot/'.$credentials['name'].' 2>&1');
// 设置权限
shell_exec('chown -R www:www /www/wwwroot/'.$credentials['name']);
shell_exec('chmod -R 755 /www/wwwroot/'.$credentials['name']);
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 删除备份
*/
public function deleteBackup(Request $request): JsonResponse
{
// 消毒数据
try {
$credentials = $this->validate($request, [
'backup' => 'required|max:255',
]);
} catch (ValidationException $e) {
return response()->json([
'code' => 1,
'msg' => '参数错误:'.$e->getMessage(),
'errors' => $e->errors()
], 200);
}
$backupPath = '/www/backup/website';
// 判断备份目录是否存在
if (!is_dir($backupPath)) {
mkdir($backupPath, 0644, true);
}
$backupFile = $backupPath.'/'.$credentials['backup'];
// 判断备份文件是否存在
if (!is_file($backupFile)) {
return response()->json([
'code' => 1,
'msg' => '备份文件不存在',
], 200);
}
unlink($backupFile);
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 重置网站配置文件
*/
public function resetSiteConfig(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'name' => 'required|max:255',
]);
} catch (ValidationException $e) {
return response()->json([
'code' => 1,
'msg' => '参数错误:'.$e->getMessage(),
'errors' => $e->errors()
], 200);
}
$website = Website::query()->where('name', $credentials['name'])->first();
if (!$website) {
return response()->json([
'code' => 1,
'msg' => '网站不存在',
], 200);
}
// 如果PHP是0将其设置为字符串的00
if ($website['php'] == '0') {
$website['php'] = '00';
}
// 更新网站状态为运行
$website->status = 1;
// 更新网站ssl状态为关闭
$website->ssl = 0;
$website->save();
$nginxConfig = <<<EOF
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
server
{
# port标记位开始
listen 80;
# port标记位结束
# server_name标记位开始
server_name localhost;
# server_name标记位结束
# index标记位开始
index index.php index.html;
# index标记位结束
# root标记位开始
root $website[path];
# root标记位结束
# ssl标记位开始
# ssl标记位结束
# php标记位开始
include enable-php-$website[php].conf;
# php标记位结束
# waf标记位开始
waf on;
waf_rule_path /www/server/nginx/ngx_waf/assets/rules/;
waf_mode DYNAMIC;
waf_cc_deny rate=1000r/m duration=60m;
waf_cache capacity=50;
# waf标记位结束
# 错误页配置,可自行设置
#error_page 404 /404.html;
#error_page 502 /502.html;
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
include /www/server/vhost/rewrite/$website[name].conf;
# 面板默认禁止访问部分敏感目录,可自行修改
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
{
return 404;
}
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存可自行修改
location ~ .*\.(js|css)$
{
expires 1h;
error_log /dev/null;
access_log /dev/null;
}
access_log /www/wwwlogs/$website[name].log;
error_log /www/wwwlogs/$website[name].log;
}
EOF;
file_put_contents('/www/server/vhost/'.$website['name'].'.conf', $nginxConfig);
// 重置伪静态规则
shell_exec('echo "" > /www/server/vhost/rewrite/'.$website['name'].'.conf');
// 重载nginx
shell_exec('systemctl reload nginx');
// 返回
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 设置网站运行状态
*/
public function setSiteStatus(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'name' => 'required|max:255',
'status' => 'required|in:0,1',
]);
} catch (ValidationException $e) {
return response()->json([
'code' => 1,
'msg' => '参数错误:'.$e->getMessage(),
'errors' => $e->errors()
], 200);
}
$website = Website::query()->where('name', $credentials['name'])->first();
if (!$website) {
return response()->json([
'code' => 1,
'msg' => '网站不存在',
], 200);
}
$nginxConfig = file_get_contents('/www/server/vhost/'.$website['name'].'.conf');
// 运行目录
$pathConfig = $this->cut('# root标记位开始', '# root标记位结束', $nginxConfig);
preg_match_all('/root\s+(.+);/', $pathConfig, $matches1);
$pathConfigOld = $matches1[1][0];
if (!empty(trim($pathConfigOld)) && $pathConfigOld != PHP_EOL) {
if ($credentials['status'] == 0) {
$pathConfigNew = str_replace($pathConfigOld, '/www/server/nginx/html', $pathConfig);
// 将旧配置追加到新配置中
$pathConfigNew .= '# '.$pathConfigOld.PHP_EOL.' ';
} else {
// 匹配旧配置
preg_match_all('/# (.+)/', $pathConfig, $matches2);
// 还原旧配置
$pathConfigNew = str_replace($pathConfigOld, $matches2[1][0], $pathConfig);
// 删除旧配置
$pathConfigNew = str_replace(PHP_EOL.' # '.$matches2[1][0], '', $pathConfigNew);
}
$nginxConfig = str_replace($pathConfig, $pathConfigNew, $nginxConfig);
}
// 默认文件
$indexConfig = $this->cut('# index标记位开始', '# index标记位结束', $nginxConfig);
preg_match_all('/index\s+(.+);/', $indexConfig, $matches2);
$indexConfigOld = $matches2[1][0];
if (!empty(trim($indexConfigOld)) && $indexConfigOld != PHP_EOL) {
if ($credentials['status'] == 0) {
$indexConfigNew = str_replace($indexConfigOld, 'stop.html', $indexConfig);
// 将旧配置追加到新配置中
$indexConfigNew .= '# '.$indexConfigOld.PHP_EOL.' ';
} else {
// 匹配旧配置
preg_match_all('/# (.+)/', $indexConfig, $matches2);
// 还原旧配置
$indexConfigNew = str_replace($indexConfigOld, $matches2[1][0], $indexConfig);
// 删除旧配置
$indexConfigNew = str_replace(PHP_EOL.' # '.$matches2[1][0], '', $indexConfigNew);
}
$nginxConfig = str_replace($indexConfig, $indexConfigNew, $nginxConfig);
}
// 写入配置文件
file_put_contents('/www/server/vhost/'.$website['name'].'.conf', $nginxConfig);
$website->status = $credentials['status'];
$website->save();
// 重载nginx
shell_exec('systemctl reload nginx');
// 返回
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 裁剪字符串
* @param $begin
* @param $end
* @param $str
* @return string
*/
private function cut($begin, $end, $str): string
{
$b = mb_strpos($str, $begin) + mb_strlen($begin);

View File

@@ -88,14 +88,14 @@
, url: '/api/panel/cron/getList'
, cols: [[
{field: 'id', hide: true, title: 'ID', sort: true}
, {field: 'name', width: '15%', title: '任务名'}
, {field: 'type', width: '10%', title: '任务类型'}
, {field: 'name', width: 150, title: '任务名'}
, {field: 'type', width: 150, title: '任务类型'}
, {field: 'status', title: '启用', width: 100, templet: '#cron-table-status', unresize: true}
, {field: 'time', width: '25%', title: '任务周期cron表达式'}
, {field: 'time', width: 200, title: '任务周期cron表达式'}
, {field: 'updated_at', title: '上次运行时间'}
, {
field: 'edit',
width: '15%',
width: 160,
title: '操作',
templet: '#cron-table-edit',
fixed: 'right',

View File

@@ -272,8 +272,5 @@ Date: 2022-11-30
});
});
});
/**
* TODO: 为主页安排一个应用展示
*/
</script>

View File

@@ -2,42 +2,29 @@
<div class="layui-fluid">
<div class="layui-card">
<!--<div class="layui-form layui-card-header layuiadmin-card-header-auto" lay-filter="plugin-form">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">搜索</label>
<div class="layui-input-block">
<input type="text" name="plugin_search" placeholder="请输入关键词如php" autocomplete="off"
class="layui-input">
</div>
</div>
<div class="layui-inline">
<button class="layui-btn layuiadmin-btn-order" lay-submit lay-filter="plugin-search-submit">
<i class="layui-icon layui-icon-search layuiadmin-button-btn"></i>
</button>
</div>
</div>
</div>-->
<div class="layui-card-header">
按钮点击一次即可,请勿重复点击以免重复操作,任务中心在右上角!
</div>
<div class="layui-card-body">
<table id="panel-plugin" lay-filter="panel-plugin"></table>
<!-- 操作按钮模板 -->
<script type="text/html" id="panel-plugin-control">
@{{# if(d.control.installed == true && d.control.allow_uninstall == true){ }}
@{{# if(d.control.update == true){ }}
@{{# if(d.control.installed == true && d.control.allow_uninstall == true){ }}
@{{# if(d.control.update == true){ }}
<a class="layui-btn layui-btn-xs" lay-event="update">更新</a>
@{{# } }}
@{{# } }}
<a class="layui-btn layui-btn-xs" lay-event="open">管理</a>
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="uninstall">卸载</a>
@{{# } else{ }}
@{{# if(d.control.installed == true && d.control.allow_uninstall == false){ }}
@{{# if(d.control.update == true){ }}
@{{# }else{ }}
@{{# if(d.control.installed == true && d.control.allow_uninstall == false){ }}
@{{# if(d.control.update == true){ }}
<a class="layui-btn layui-btn-xs" lay-event="update">更新</a>
@{{# } }}
@{{# } }}
<a class="layui-btn layui-btn-xs" lay-event="open">管理</a>
@{{# } else{ }}
@{{# }else{ }}
<a class="layui-btn layui-btn-xs" lay-event="install">安装</a>
@{{# } }}
@{{# } }}
@{{# } }}
@{{# } }}
</script>
<!-- 首页显示开关 -->
<script type="text/html" id="plugin-show">
@@ -63,12 +50,19 @@
, url: '/api/panel/plugin/getList'
, cols: [[
{field: 'slug', hide: true, title: 'Slug', sort: true}
, {field: 'name', width: '13%', title: '插件名'}
, {field: 'describe', width: '42%', title: '描述'}
, {field: 'install_version', width: '12%', title: '已装版本'}
, {field: 'version', width: '12%', title: '最新版本'}
, {field: 'name', width: 150, title: '插件名'}
, {field: 'describe', title: '描述'}
, {field: 'install_version', width: 140, title: '已装版本'}
, {field: 'version', width: 140, title: '最新版本'}
, {field: 'show', title: '首页显示', width: 90, templet: '#plugin-show', unresize: true}
, {field: 'control', title: '操作', templet: '#panel-plugin-control', fixed: 'right', align: 'left'}
, {
field: 'control',
width: 160,
title: '操作',
templet: '#panel-plugin-control',
fixed: 'right',
align: 'left'
}
]]
, page: false
, text: '耗子Linux面板数据加载出现异常'

View File

@@ -207,17 +207,12 @@
table.render({
elem: '#safe-port'
, url: '/api/panel/safe/getFirewallRules'
//, toolbar: '#website-list-bar'
, title: '网站列表'
, title: '防火墙'
, cols: [[
{field: 'port', title: '端口', width: 200}
{field: 'port', title: '端口', width: 100}
, {field: 'protocol', title: '协议'}
, {fixed: 'right', title: '操作', toolbar: '#safe-port-setting', width: 150}
]]
/**
* TODO: 分页
*/
//, page: true
});
table.on('tool(safe-port)', function (obj) {
let data = obj.data;
@@ -253,7 +248,7 @@
layer.msg('端口号不合法', {icon: 2});
return false;
}
var index = layer.load(2);
var index = layer.load();
admin.req({
url: '/api/panel/safe/setSshPort'
, type: 'post'
@@ -281,7 +276,7 @@
layer.msg('端口号不合法', {icon: 2});
return false;
}
var index = layer.load(2);
var index = layer.load();
admin.req({
url: '/api/panel/safe/addFirewallRule'
, type: 'post'

View File

@@ -26,6 +26,13 @@ Date: 2022-12-01
</div>
<div class="layui-form-mid layui-word-aux">API Token用于携带访问面板接口</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">多设备登录</label>
<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>
<div class="layui-form-item">
<label class="layui-form-label">面板名称</label>
<div class="layui-input-inline">
@@ -105,11 +112,18 @@ Date: 2022-12-01
// 面板设置
form.on('submit(panel_setting_submit)', function (obj) {
// 面板API
if (obj.field.api === "on") {
obj.field.api = 1;
} else {
obj.field.api = 0;
}
// 多设备登录
if (obj.field.multi_login === "on") {
obj.field.multi_login = 1;
} else {
obj.field.multi_login = 0;
}
// 提交修改
admin.req({
url: "/api/panel/setting/save"
@@ -121,8 +135,8 @@ Date: 2022-12-01
layer.msg('面板设置保存失败,请刷新重试!')
return false;
}
layer.msg('面板设置保存成功!');
admin.render();
layer.msg('面板设置保存成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error);

View File

@@ -26,7 +26,7 @@ Date: 2022-12-01
<div class="layui-input-block">
<select name="php" lay-filter="add-website-php">
@{{# layui.each(d.params.php_version, function(index, item){ }}
@{{# if(index == "00"){ }}
@{{# if(item == "00"){ }}
<option value="@{{ item }}" selected="">@{{ item }}</option>
@{{# }else{ }}
<option value="@{{ item }}">@{{ item }}</option>

View File

@@ -0,0 +1,159 @@
<!--
Name: 网站 - 备份
Author: 耗子
Date: 2022-12-02
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<div class="layui-row">
<div class="layui-col-xs12 layui-col-sm12 layui-col-md12">
<table class="layui-hide" id="website-backup-list" lay-filter="website-backup-list"></table>
</div>
</div>
</script>
<!-- 备份顶部工具栏 -->
<script type="text/html" id="website-backup-bar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="backup_website">备份网站</button>
<button class="layui-btn layui-btn-sm" id="upload_website_backup">上传备份</button>
</div>
</script>
<!-- 备份右侧管理 -->
<script type="text/html" id="website-backup-control">
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="restore">恢复</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
<script>
layui.data.sendParams = function (params) {
console.log(params);
layui.use(['admin', 'form', 'laydate', 'code'], function () {
var $ = layui.$
, admin = layui.admin
, layer = layui.layer
, table = layui.table
, upload = layui.upload;
console.log(params);
// 渲染表格
table.render({
elem: '#website-backup-list'
, url: '/api/panel/website/getBackupList'
, toolbar: '#website-backup-bar'
, title: '备份列表'
, cols: [[
{field: 'backup', title: '备份名称', width: 500}
, {field: 'size', title: '文件大小'}
, {field: 'right', title: '操作', width: 150, toolbar: '#website-backup-control'}
]]
, text: {
none: '无备份数据'
}
, done: function (res, curr, count) {
upload.render({
elem: '#upload_website_backup'
, url: '/api/panel/website/uploadBackup'
, accept: 'file'
, exts: 'zip'
, before: function (obj) {
index = layer.msg('正在上传备份文件,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
});
}
, done: function (res) {
layer.close(index);
layer.msg('上传成功!', {icon: 1});
table.reload('website-backup-list');
}
, error: function (res) {
layer.msg('上传失败:' + res.msg, {icon: 2});
}
});
}
});
// 头工具栏事件
table.on('toolbar(website-backup-list)', function (obj) {
if (obj.event === 'backup_website') {
index = layer.msg('正在备份网站,请稍等...', {
icon: 16
, time: 0
});
admin.req({
url: '/api/panel/website/createBackup'
, type: 'post'
, data: {
name: params.data.name
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板网站失败接口返回' + result);
layer.alert('备份失败!');
return false;
}
table.reload('website-backup-list');
layer.msg('备份成功!', {icon: 1});
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
}
});
// 行工具事件
table.on('tool(website-backup-list)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('确定要删除网站备份 <b style="color: red;">' + data.backup + '</b> 吗?', function (index) {
index = layer.msg('正在删除网站备份,请稍等...', {
icon: 16
, time: 0
});
admin.req({
url: "/api/panel/website/deleteBackup"
, method: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板网站备份删除失败接口返回' + result);
layer.msg('网站备份删除失败,请刷新重试!')
return false;
}
obj.del();
layer.alert('网站备份' + data.backup + '删除成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
} else if (obj.event === 'restore') {
layer.confirm('高风险操作,确定要恢复网站备份 <b style="color: red;">' + data.backup + '</b> 吗?', function (index) {
index = layer.msg('正在恢复网站备份,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
});
data.name = params.data.name;
admin.req({
url: "/api/panel/website/restoreBackup"
, method: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板网站备份恢复失败接口返回' + result);
layer.msg('网站备份恢复失败,请刷新重试!')
return false;
}
layer.alert('网站备份' + data.backup + '恢复成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
}
});
});
};
</script>

View File

@@ -1,9 +1,10 @@
<!--
Name: 网站 - 全局设置
Author: 耗子
Date: 2022-11-30
Date: 2022-12-02
-->
<script type="text/html" template lay-url="/api/panel/website/getDefaultSettings" lay-done="layui.data.sendParams(d.params)">
<script type="text/html" template lay-url="/api/panel/website/getDefaultSettings"
lay-done="layui.data.sendParams(d.params)">
<div class="layui-tab">
<ul class="layui-tab-title">
<li class="layui-this">默认页</li>
@@ -20,7 +21,7 @@ Date: 2022-11-30
<div class="layui-tab-item">
<!-- 停止页 -->
<blockquote class="layui-elem-quote layui-quote-nm">
设置站点停止时的提示页面。
设置站点停止时的提示页面,设置后需重新开关网站方可生效
</blockquote>
<div id="stop-editor" style="height: 400px;">@{{ d.data.stop }}</div>
</div>
@@ -33,7 +34,7 @@ Date: 2022-11-30
<script>
let indexEditor = '';
let stopEditor = '';
layui.data.sendParams = function(params) {
layui.data.sendParams = function (params) {
layui.use(['admin', 'form', 'laydate', 'code'], function () {
var $ = layui.$
, admin = layui.admin
@@ -51,7 +52,7 @@ Date: 2022-11-30
});
$('#save-website-default-settings').click(function () {
layer.load(2);
layer.load();
admin.req({
url: '/api/panel/website/saveDefaultSettings'
, type: 'post'

View File

@@ -4,7 +4,7 @@ Author: 耗子
Date: 2022-12-01
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<div class="layui-tab">
<div class="layui-tab" lay-filter="website-edit-tab">
<ul class="layui-tab-title">
<li class="layui-this">域名端口</li>
<li>基本设置</li>
@@ -26,6 +26,7 @@ Date: 2022-12-01
class="layui-textarea">@{{ d.params.config.domain }}</textarea>
</div>
</div>
<hr>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">端口</label>
<div class="layui-input-block">
@@ -46,6 +47,13 @@ Date: 2022-12-01
class="layui-input" value="@{{ d.params.config.path }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">运行目录</label>
<div class="layui-input-block">
<input type="text" name="root" autocomplete="off" placeholder="请输入网站运行目录Laravel等程序需要"
class="layui-input" value="@{{ d.params.config.root }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">默认文档</label>
<div class="layui-input-block">
@@ -58,7 +66,7 @@ Date: 2022-12-01
<div class="layui-input-block">
<select name="php" lay-filter="website-php">
@{{# layui.each(d.params.php_version, function(index, item){ }}
@{{# if(index == "00"){ }}
@{{# if(item == d.params.config.php){ }}
<option value="@{{ item }}" selected="">@{{ item }}</option>
@{{# }else{ }}
<option value="@{{ item }}">@{{ item }}</option>
@@ -199,8 +207,18 @@ Date: 2022-12-01
, admin = layui.admin
, layer = layui.layer
, code = layui.code
, form = layui.form;
, form = layui.form
, element = layui.element;
form.render();
element.render();
element.on('tab(website-edit-tab)', function (data) {
if (data.index === 6) {
// 隐藏保存按钮
$('.layui-footer').hide();
} else {
$('.layui-footer').show();
}
});
rewriteEditor = ace.edit("rewrite-editor", {
mode: "ace/mode/nginx",
selectionStyle: "text"
@@ -219,7 +237,7 @@ Date: 2022-12-01
$("#clean-site-log").click(function () {
layer.confirm('确定要清空日志吗?', function (index) {
layer.close(index);
layer.load(2);
layer.load();
admin.req({
url: '/api/panel/website/clearSiteLog'
, type: 'post'
@@ -244,7 +262,7 @@ Date: 2022-12-01
});
$('#save-site-config').click(function () {
layer.load(2);
layer.load();
var port = $('textarea[name="port"]').val();
var reg = new RegExp(/\n443.*\n?/);
// 如果开启了https就自动添加443端口
@@ -270,6 +288,7 @@ Date: 2022-12-01
ssl_certificate: $('textarea[name="ssl_certificate"]').val(),
ssl_certificate_key: $('textarea[name="ssl_certificate_key"]').val(),
path: $('input[name="path"]').val(),
root: $('input[name="root"]').val(),
index: $('input[name="index"]').val(),
php: $('select[name="php"]').val(),
open_basedir: $('input[name="open_basedir"]').prop('checked') ? 1 : 0,
@@ -298,9 +317,33 @@ Date: 2022-12-01
});
});
// 重配置
$('#site-config-restore').click(function (){
layer.msg('待开发功能!', {icon: 2});
// 重配置
$('#site-config-restore').click(function () {
layer.confirm('高风险操作,网站配置重置后所有配置均需重新设置,确定要重置配置吗?', function (index) {
index = layer.msg('重置网站配置', {
icon: 16
, time: 0
});
admin.req({
url: '/api/panel/website/resetSiteConfig'
, type: 'post'
, data: {name: params.config.name}
, success: function (res) {
layer.close(index);
if (res.code === 0) {
layer.alert('重置成功,你需要重新添加域名/端口绑定,设置各配置参数!', function (index) {
admin.render();
layer.close(index);
});
} else {
layer.msg(res.msg, {icon: 2});
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
})
});
};

View File

@@ -16,11 +16,13 @@ Date: 2022-11-28
<script type="text/html" id="website-list-bar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="website_add">添加网站</button>
<button class="layui-btn layui-btn-sm" lay-event="website_default_settings">全局设置</button>
<button class="layui-btn layui-btn-sm" lay-event="website_default_settings">全局设置
</button>
</div>
</script>
<!-- 右侧网站设置和删除网站 -->
<script type="text/html" id="website-setting">
<script type="text/html" id="website-control">
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="backup">备份</a>
<a class="layui-btn layui-btn-xs" lay-event="edit">设置</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
@@ -77,13 +79,13 @@ Date: 2022-11-28
, toolbar: '#website-list-bar'
, title: '网站列表'
, cols: [[
{field: 'name', title: '网站名', width: 200, fixed: 'left', unresize: true, sort: true}
{field: 'name', title: '网站名', width: 150, unresize: true, sort: true}
, {field: 'run', title: '运行', width: 100, templet: '#website-run', unresize: true}
, {field: 'path', title: '目录', width: 250}
, {field: 'php', title: 'PHP', width: 60}
, {field: 'ssl', title: 'SSL', width: 110, templet: '#website-ssl'}
, {field: 'note', title: '备注', edit: 'textarea'}
, {fixed: 'right', title: '操作', toolbar: '#website-setting', width: 150}
, {fixed: 'right', title: '操作', toolbar: '#website-control', width: 160}
]]
/**
* TODO: 分页
@@ -96,7 +98,7 @@ Date: 2022-11-28
if (obj.event === 'website_add') {
admin.popup({
title: '添加网站'
, area: ['70%', '60%']
, area: ['80%', '80%']
, id: 'LAY-popup-website-add'
, success: function (layer, index) {
view(this.id).render('website/add', {
@@ -110,11 +112,10 @@ Date: 2022-11-28
} else if (obj.event === 'website_default_settings') {
admin.popup({
title: '全局设置'
, area: ['70%', '60%']
, area: ['80%', '80%']
, id: 'LAY-popup-website-add'
, success: function (layer, index) {
view(this.id).render('website/default_settings', {
}).done(function () {
view(this.id).render('website/default_settings', {}).done(function () {
form.render(null, 'LAY-popup-website-default-settings');
});
}
@@ -166,7 +167,7 @@ Date: 2022-11-28
// 打开编辑网站页面
admin.popup({
title: '编辑网站 - ' + data.name
, area: ['70%', '80%']
, area: ['80%', '80%']
, id: 'LAY-popup-website-edit'
, success: function (layero, index) {
view(this.id).render('website/edit', {
@@ -184,12 +185,26 @@ Date: 2022-11-28
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
} else if (obj.event === 'backup') {
// 打开备份页面
admin.popup({
title: '备份管理 - ' + data.name
, area: ['70%', '80%']
, id: 'LAY-popup-website-backup'
, success: function (layero, index) {
view(this.id).render('website/backup', {
data: data
}).done(function () {
form.render(null, 'LAY-popup-website-backup');
});
}
});
}
});
// 网站备注编辑
table.on('edit(website-list)', function (obj) {
var value = obj.value // 得到修改后的值
let value = obj.value // 得到修改后的值
, data = obj.data; // 得到行数据
admin.req({
url: "/api/panel/website/updateSiteNote"
@@ -216,10 +231,27 @@ Date: 2022-11-28
form.on('switch(website-run-checkbox)', function (obj) {
let $ = layui.$;
let website_name = $(this).data('website-name');
let run = obj.elem.checked ? 1 : 0;
let status = obj.elem.checked ? 1 : 0;
//console.log(website_name); //当前行数据
layer.msg('待开发功能!', {icon: 2});
admin.req({
url: "/api/panel/website/setSiteStatus"
, method: 'post'
, data: {
name: website_name,
status: status
}
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板网站运行状态设置失败接口返回' + result);
layer.msg('网站运行状态设置失败,请刷新重试!')
return false;
}
layer.alert('网站 ' + website_name + ' 运行状态设置成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板ajax请求出错错误' + error);
}
});
});
});

View File

@@ -75,6 +75,15 @@ Route::prefix('panel')->group(function () {
Route::post('saveSiteSettings', [WebsitesController::class, 'saveSiteSettings']);
Route::post('clearSiteLog', [WebsitesController::class, 'clearSiteLog']);
Route::post('updateSiteNote', [WebsitesController::class, 'updateSiteNote']);
Route::post('setSiteStatus', [WebsitesController::class, 'setSiteStatus']);
// 获取备份列表
Route::get('getBackupList', [WebsitesController::class, 'getBackupList']);
Route::post('createBackup', [WebsitesController::class, 'createBackup']);
Route::post('uploadBackup', [WebsitesController::class, 'uploadBackup']);
Route::post('restoreBackup', [WebsitesController::class, 'restoreBackup']);
Route::post('deleteBackup', [WebsitesController::class, 'deleteBackup']);
// 重置网站配置
Route::post('resetSiteConfig', [WebsitesController::class, 'resetSiteConfig']);
});
// 监控
Route::middleware('auth:sanctum')->prefix('monitor')->group(function () {

View File

@@ -36,6 +36,8 @@ Route::prefix('panel/views')->group(function () {
Route::view('add', 'website.add');
// 编辑
Route::view('edit', 'website.edit');
// 备份
Route::view('backup', 'website.backup');
});
// 监控
Route::view('monitor', 'monitor');