vk-ssl-auto-deploy 0.8.3 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -4
- package/package.json +1 -1
- package/public/stylesheets/style.css +8 -8
- package/routes/admin.js +243 -0
- package/views/admin.ejs +225 -6
- package/views/error.ejs +3 -3
- package/views/index.ejs +10 -10
package/README.md
CHANGED
|
@@ -1,18 +1,48 @@
|
|
|
1
1
|
# vk-ssl-auto-deploy
|
|
2
2
|
|
|
3
|
-
SSL
|
|
3
|
+
无忧SSL证书平台自动部署工具 - 提供HTTP API接口,支持证书文件自动上传和部署
|
|
4
|
+
|
|
5
|
+
### 支持系统
|
|
6
|
+
|
|
7
|
+
| 系统 | 最低版本 |
|
|
8
|
+
|------|----------|
|
|
9
|
+
| Alibaba Cloud Linux | 3+ |
|
|
10
|
+
| CentOS | 7+ |
|
|
11
|
+
| Ubuntu | 20+ |
|
|
12
|
+
| Debian | 12+ |
|
|
13
|
+
| Fedora | 40+ |
|
|
14
|
+
| OpenSUSE | 15+ |
|
|
15
|
+
| Rocky Linux | 8+ |
|
|
16
|
+
| CentOS Stream | 9+ |
|
|
17
|
+
| AlmaLinux | 8+ |
|
|
18
|
+
| Anolis OS | 不支持 |
|
|
19
|
+
| Windows | 10 / 11 / Server 2016+ |
|
|
4
20
|
|
|
5
21
|
## 一键安装(推荐)
|
|
6
22
|
|
|
23
|
+
### Linux 系统
|
|
24
|
+
|
|
7
25
|
Linux 服务器执行以下命令,自动完成安装、部署和开机自启配置:
|
|
8
26
|
|
|
9
27
|
```bash
|
|
10
28
|
curl -fsSL https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.sh | sudo bash
|
|
11
29
|
```
|
|
12
30
|
|
|
13
|
-
|
|
31
|
+
### Windows 系统
|
|
32
|
+
|
|
33
|
+
**PowerShell (推荐):**
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
iwr -useb https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.ps1 | iex
|
|
37
|
+
```
|
|
14
38
|
|
|
15
|
-
|
|
39
|
+
**CMD (命令提示符):**
|
|
40
|
+
|
|
41
|
+
```cmd
|
|
42
|
+
powershell -c "iwr -useb https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.ps1 | iex"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
安装完成后,访问 `http://服务器IP:6001/admin`,默认密码:`admin@123456`
|
|
16
46
|
|
|
17
47
|
---
|
|
18
48
|
|
|
@@ -30,7 +60,7 @@ curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash - && sudo yum insta
|
|
|
30
60
|
|
|
31
61
|
#### Windows系统
|
|
32
62
|
|
|
33
|
-
```
|
|
63
|
+
```powershell
|
|
34
64
|
winget install --id OpenJS.NodeJS.LTS -e
|
|
35
65
|
```
|
|
36
66
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
body {
|
|
2
|
-
padding: 50px;
|
|
3
|
-
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
a {
|
|
7
|
-
color: #00B7FF;
|
|
8
|
-
}
|
|
1
|
+
body {
|
|
2
|
+
padding: 50px;
|
|
3
|
+
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
a {
|
|
7
|
+
color: #00B7FF;
|
|
8
|
+
}
|
package/routes/admin.js
CHANGED
|
@@ -3,6 +3,11 @@ const router = express.Router();
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
+
const { exec } = require('child_process');
|
|
7
|
+
const { promisify } = require('util');
|
|
8
|
+
const AdmZip = require('adm-zip');
|
|
9
|
+
|
|
10
|
+
const execAsync = promisify(exec);
|
|
6
11
|
|
|
7
12
|
const CONFIG_FILE = path.join(__dirname, '..', 'config.json');
|
|
8
13
|
|
|
@@ -266,4 +271,242 @@ router.get('/api/certs/local', verifyPassword, (req, res) => {
|
|
|
266
271
|
}
|
|
267
272
|
});
|
|
268
273
|
|
|
274
|
+
/**
|
|
275
|
+
* POST /api/test-nginx
|
|
276
|
+
* 测试 Nginx 路径是否正确
|
|
277
|
+
*/
|
|
278
|
+
router.post('/api/test-nginx', verifyPassword, async (req, res) => {
|
|
279
|
+
try {
|
|
280
|
+
const { nginxPath } = req.body;
|
|
281
|
+
|
|
282
|
+
if (!nginxPath) {
|
|
283
|
+
return res.json({
|
|
284
|
+
code: 400,
|
|
285
|
+
msg: 'Nginx 路径不能为空',
|
|
286
|
+
data: null
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 检查文件是否存在
|
|
291
|
+
if (!fs.existsSync(nginxPath)) {
|
|
292
|
+
return res.json({
|
|
293
|
+
code: 404,
|
|
294
|
+
msg: 'Nginx 文件不存在',
|
|
295
|
+
data: null
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 执行 nginx -t 命令测试配置
|
|
300
|
+
try {
|
|
301
|
+
// 获取 nginx 所在目录作为工作目录
|
|
302
|
+
const nginxDir = path.dirname(nginxPath);
|
|
303
|
+
|
|
304
|
+
const { stdout, stderr } = await execAsync(`"${nginxPath}" -t`, {
|
|
305
|
+
timeout: 10000, // 10秒超时
|
|
306
|
+
cwd: nginxDir // 设置工作目录为 nginx 所在目录
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// nginx -t 的输出通常在 stderr 中
|
|
310
|
+
const output = stderr || stdout;
|
|
311
|
+
|
|
312
|
+
// 检查是否包含 "syntax is ok" 和 "test is successful"
|
|
313
|
+
if (output.includes('syntax is ok') || output.includes('test is successful')) {
|
|
314
|
+
return res.json({
|
|
315
|
+
code: 0,
|
|
316
|
+
msg: 'Nginx 配置测试通过\n' + output,
|
|
317
|
+
data: { output }
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
return res.json({
|
|
321
|
+
code: 500,
|
|
322
|
+
msg: 'Nginx 配置测试失败\n' + output,
|
|
323
|
+
data: { output }
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
} catch (execError) {
|
|
327
|
+
// 执行命令失败
|
|
328
|
+
return res.json({
|
|
329
|
+
code: 500,
|
|
330
|
+
msg: 'Nginx 配置测试失败: ' + (execError.stderr || execError.message),
|
|
331
|
+
data: null
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
res.json({
|
|
336
|
+
code: 500,
|
|
337
|
+
msg: '测试失败: ' + error.message,
|
|
338
|
+
data: null
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* GET /api/cert/download/:fileName
|
|
345
|
+
* 下载证书(包含 cert.pem 和 cert.key 的 zip 压缩包)
|
|
346
|
+
*/
|
|
347
|
+
router.get('/api/cert/download/:fileName', verifyPassword, (req, res) => {
|
|
348
|
+
try {
|
|
349
|
+
const { fileName } = req.params;
|
|
350
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
351
|
+
const certDir = path.isAbsolute(config.certSaveDir) ?
|
|
352
|
+
config.certSaveDir :
|
|
353
|
+
path.join(__dirname, '..', config.certSaveDir);
|
|
354
|
+
|
|
355
|
+
// 检查目录是否存在
|
|
356
|
+
if (!fs.existsSync(certDir)) {
|
|
357
|
+
return res.status(404).json({
|
|
358
|
+
code: 404,
|
|
359
|
+
msg: '证书目录不存在',
|
|
360
|
+
data: null
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 构建证书文件路径
|
|
365
|
+
const crtFile = path.join(certDir, `${fileName}.crt`);
|
|
366
|
+
const keyFile = path.join(certDir, `${fileName}.key`);
|
|
367
|
+
|
|
368
|
+
// 检查文件是否存在
|
|
369
|
+
if (!fs.existsSync(crtFile)) {
|
|
370
|
+
return res.status(404).json({
|
|
371
|
+
code: 404,
|
|
372
|
+
msg: '证书文件不存在',
|
|
373
|
+
data: null
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!fs.existsSync(keyFile)) {
|
|
378
|
+
return res.status(404).json({
|
|
379
|
+
code: 404,
|
|
380
|
+
msg: '私钥文件不存在',
|
|
381
|
+
data: null
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 创建 ZIP 压缩包
|
|
386
|
+
const zip = new AdmZip();
|
|
387
|
+
|
|
388
|
+
// 添加证书文件(重命名为 cert.pem)
|
|
389
|
+
zip.addLocalFile(crtFile, '', 'cert.pem');
|
|
390
|
+
|
|
391
|
+
// 添加私钥文件(重命名为 cert.key)
|
|
392
|
+
zip.addLocalFile(keyFile, '', 'cert.key');
|
|
393
|
+
|
|
394
|
+
// 生成 ZIP 文件
|
|
395
|
+
const zipBuffer = zip.toBuffer();
|
|
396
|
+
|
|
397
|
+
// 设置响应头
|
|
398
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
399
|
+
res.setHeader('Content-Disposition', `attachment; filename="${fileName}.zip"`);
|
|
400
|
+
res.setHeader('Content-Length', zipBuffer.length);
|
|
401
|
+
|
|
402
|
+
// 发送文件
|
|
403
|
+
res.send(zipBuffer);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
res.status(500).json({
|
|
406
|
+
code: 500,
|
|
407
|
+
msg: '下载失败: ' + error.message,
|
|
408
|
+
data: null
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// 检查更新
|
|
414
|
+
router.post('/check-update', verifyPassword, async (req, res) => {
|
|
415
|
+
try {
|
|
416
|
+
const https = require('https');
|
|
417
|
+
const currentVersion = require('../package.json').version;
|
|
418
|
+
|
|
419
|
+
// 获取远程 package.json
|
|
420
|
+
const remoteUrl = 'https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/package.json';
|
|
421
|
+
|
|
422
|
+
https.get(remoteUrl, (response) => {
|
|
423
|
+
let data = '';
|
|
424
|
+
|
|
425
|
+
response.on('data', (chunk) => {
|
|
426
|
+
data += chunk;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
response.on('end', () => {
|
|
430
|
+
try {
|
|
431
|
+
const remotePackage = JSON.parse(data);
|
|
432
|
+
const remoteVersion = remotePackage.version;
|
|
433
|
+
|
|
434
|
+
const hasUpdate = currentVersion !== remoteVersion;
|
|
435
|
+
|
|
436
|
+
res.json({
|
|
437
|
+
code: 0,
|
|
438
|
+
msg: hasUpdate ? '发现新版本' : '已是最新版本',
|
|
439
|
+
data: {
|
|
440
|
+
currentVersion,
|
|
441
|
+
remoteVersion,
|
|
442
|
+
hasUpdate
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
} catch (error) {
|
|
446
|
+
res.status(500).json({
|
|
447
|
+
code: 500,
|
|
448
|
+
msg: '解析远程版本信息失败: ' + error.message,
|
|
449
|
+
data: null
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}).on('error', (error) => {
|
|
454
|
+
res.status(500).json({
|
|
455
|
+
code: 500,
|
|
456
|
+
msg: '获取远程版本信息失败: ' + error.message,
|
|
457
|
+
data: null
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
} catch (error) {
|
|
461
|
+
res.status(500).json({
|
|
462
|
+
code: 500,
|
|
463
|
+
msg: '检查更新失败: ' + error.message,
|
|
464
|
+
data: null
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// 执行更新
|
|
470
|
+
router.post('/execute-update', verifyPassword, async (req, res) => {
|
|
471
|
+
try {
|
|
472
|
+
const platform = process.platform;
|
|
473
|
+
let command;
|
|
474
|
+
|
|
475
|
+
if (platform === 'win32') {
|
|
476
|
+
// Windows 系统
|
|
477
|
+
command = 'powershell -c "iwr -useb https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.ps1 | iex"';
|
|
478
|
+
} else {
|
|
479
|
+
// Linux/Unix 系统
|
|
480
|
+
command = 'curl -fsSL https://gitee.com/vk1688/vk-ssl-auto-deploy/raw/master/install.sh | sudo bash';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 异步执行更新命令
|
|
484
|
+
exec(command, (error, stdout, stderr) => {
|
|
485
|
+
if (error) {
|
|
486
|
+
console.error('更新失败:', error);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
console.log('更新输出:', stdout);
|
|
490
|
+
if (stderr) {
|
|
491
|
+
console.error('更新错误:', stderr);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
res.json({
|
|
496
|
+
code: 0,
|
|
497
|
+
msg: '更新命令已执行,程序将在更新完成后自动重启',
|
|
498
|
+
data: {
|
|
499
|
+
platform,
|
|
500
|
+
command
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
} catch (error) {
|
|
504
|
+
res.status(500).json({
|
|
505
|
+
code: 500,
|
|
506
|
+
msg: '执行更新失败: ' + error.message,
|
|
507
|
+
data: null
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
269
512
|
module.exports = router;
|
package/views/admin.ejs
CHANGED
|
@@ -101,8 +101,8 @@
|
|
|
101
101
|
border-radius: 8px;
|
|
102
102
|
color: #e0e0e0;
|
|
103
103
|
font-size: 0.95em;
|
|
104
|
-
transition: all 0.3s;
|
|
105
104
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
105
|
+
will-change: border-color, box-shadow;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
input[type="text"]:focus,
|
|
@@ -158,8 +158,9 @@
|
|
|
158
158
|
font-size: 1em;
|
|
159
159
|
font-weight: 600;
|
|
160
160
|
cursor: pointer;
|
|
161
|
-
transition:
|
|
161
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
162
162
|
box-shadow: 0 4px 15px rgba(0, 255, 136, 0.3);
|
|
163
|
+
will-change: transform, box-shadow;
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
button:hover {
|
|
@@ -362,7 +363,7 @@
|
|
|
362
363
|
/* 操作面板按钮组 */
|
|
363
364
|
.operation-buttons {
|
|
364
365
|
display: grid;
|
|
365
|
-
grid-template-columns: 1fr 1fr;
|
|
366
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
366
367
|
gap: 15px;
|
|
367
368
|
margin-top: 20px;
|
|
368
369
|
}
|
|
@@ -371,6 +372,31 @@
|
|
|
371
372
|
width: 100%;
|
|
372
373
|
height: 60px;
|
|
373
374
|
font-size: 1em;
|
|
375
|
+
position: relative;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* 红点提示 */
|
|
379
|
+
.update-badge {
|
|
380
|
+
position: absolute;
|
|
381
|
+
top: 8px;
|
|
382
|
+
right: 8px;
|
|
383
|
+
width: 10px;
|
|
384
|
+
height: 10px;
|
|
385
|
+
background: #f44336;
|
|
386
|
+
border-radius: 50%;
|
|
387
|
+
box-shadow: 0 0 0 2px #ff0000, 0 0 10px rgba(244, 67, 54, 0.6);
|
|
388
|
+
animation: pulse-badge 2s infinite;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@keyframes pulse-badge {
|
|
392
|
+
0%, 100% {
|
|
393
|
+
transform: scale(1);
|
|
394
|
+
opacity: 1;
|
|
395
|
+
}
|
|
396
|
+
50% {
|
|
397
|
+
transform: scale(1.2);
|
|
398
|
+
opacity: 0.8;
|
|
399
|
+
}
|
|
374
400
|
}
|
|
375
401
|
|
|
376
402
|
/* 弹窗样式 */
|
|
@@ -448,7 +474,8 @@
|
|
|
448
474
|
align-items: center;
|
|
449
475
|
justify-content: center;
|
|
450
476
|
border-radius: 8px;
|
|
451
|
-
transition:
|
|
477
|
+
transition: background-color 0.2s, color 0.2s;
|
|
478
|
+
will-change: background-color, color;
|
|
452
479
|
}
|
|
453
480
|
|
|
454
481
|
.close-btn:hover {
|
|
@@ -470,6 +497,8 @@
|
|
|
470
497
|
border-radius: 8px;
|
|
471
498
|
color: #e0e0e0;
|
|
472
499
|
font-size: 0.95em;
|
|
500
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
501
|
+
will-change: border-color, box-shadow;
|
|
473
502
|
}
|
|
474
503
|
|
|
475
504
|
.search-box input:focus {
|
|
@@ -618,6 +647,9 @@
|
|
|
618
647
|
<button onclick="executeUpdate()">
|
|
619
648
|
更新证书
|
|
620
649
|
</button>
|
|
650
|
+
<button id="updateBtn" onclick="checkForUpdates()">
|
|
651
|
+
软件更新
|
|
652
|
+
</button>
|
|
621
653
|
</div>
|
|
622
654
|
|
|
623
655
|
<div style="margin-top: 30px; padding-top: 30px; border-top: 1px solid rgba(255, 255, 255, 0.05);">
|
|
@@ -660,7 +692,7 @@
|
|
|
660
692
|
|
|
661
693
|
<!-- 本地证书列表 -->
|
|
662
694
|
<div class="card full-width">
|
|
663
|
-
<h2 class="card-title"
|
|
695
|
+
<h2 class="card-title">本地证书列表 <span id="certCount" style="color: #888; font-size: 0.8em; font-weight: normal;">(0)</span></h2>
|
|
664
696
|
<div class="search-box">
|
|
665
697
|
<input type="text" id="certSearch" placeholder="🔍 搜索证书域名...">
|
|
666
698
|
</div>
|
|
@@ -742,7 +774,10 @@
|
|
|
742
774
|
</div>
|
|
743
775
|
<div class="form-group">
|
|
744
776
|
<label for="nginxDir">Nginx路径</label>
|
|
745
|
-
<
|
|
777
|
+
<div style="display: flex; gap: 10px; align-items: flex-start;">
|
|
778
|
+
<input type="text" id="nginxDir" name="nginxDir" placeholder="请输入Nginx路径" style="flex: 1;">
|
|
779
|
+
<button type="button" onclick="testNginxPath()" style="flex: none; padding: 12px 20px; min-width: auto;">测试</button>
|
|
780
|
+
</div>
|
|
746
781
|
<div class="hint">Nginx可执行文件路径(留空则不重载)</div>
|
|
747
782
|
</div>
|
|
748
783
|
<div class="form-group">
|
|
@@ -883,6 +918,7 @@
|
|
|
883
918
|
connectWebSocket();
|
|
884
919
|
startScheduleInfoSync();
|
|
885
920
|
updateCallbackApiUrl();
|
|
921
|
+
checkForUpdates(true); // 静默检查更新
|
|
886
922
|
|
|
887
923
|
// 搜索框事件
|
|
888
924
|
document.getElementById('certSearch').addEventListener('input', filterCerts);
|
|
@@ -943,6 +979,39 @@
|
|
|
943
979
|
document.getElementById('configModal').classList.remove('show');
|
|
944
980
|
}
|
|
945
981
|
|
|
982
|
+
// 测试Nginx路径
|
|
983
|
+
async function testNginxPath() {
|
|
984
|
+
const nginxPath = document.getElementById('nginxDir').value.trim();
|
|
985
|
+
if (!nginxPath) {
|
|
986
|
+
showAlert('提示', '请先输入Nginx路径', 'warning');
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
addLog('→ 开始测试 Nginx 路径...', 'info');
|
|
991
|
+
|
|
992
|
+
try {
|
|
993
|
+
const response = await authenticatedFetch('/admin/api/test-nginx', {
|
|
994
|
+
method: 'POST',
|
|
995
|
+
headers: { 'Content-Type': 'application/json' },
|
|
996
|
+
body: JSON.stringify({ nginxPath })
|
|
997
|
+
});
|
|
998
|
+
const result = await response.json();
|
|
999
|
+
|
|
1000
|
+
if (result.code === 0) {
|
|
1001
|
+
addLog('✓ Nginx 路径测试成功', 'success');
|
|
1002
|
+
showAlert('测试成功', result.msg || 'Nginx 配置测试通过', 'success');
|
|
1003
|
+
} else {
|
|
1004
|
+
addLog('✗ Nginx 路径测试失败: ' + result.msg, 'error');
|
|
1005
|
+
showAlert('测试失败', result.msg, 'error');
|
|
1006
|
+
}
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
if (error.message !== '未设置密码' && error.message !== '密码验证失败') {
|
|
1009
|
+
addLog('✗ Nginx 路径测试失败: ' + error.message, 'error');
|
|
1010
|
+
showAlert('测试失败', error.message, 'error');
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
946
1015
|
// 加载配置
|
|
947
1016
|
async function loadConfig() {
|
|
948
1017
|
try {
|
|
@@ -1028,6 +1097,9 @@
|
|
|
1028
1097
|
function renderCerts(certs) {
|
|
1029
1098
|
const container = document.getElementById('certListContainer');
|
|
1030
1099
|
|
|
1100
|
+
// 更新证书数量显示
|
|
1101
|
+
document.getElementById('certCount').textContent = `(${certs.length})`;
|
|
1102
|
+
|
|
1031
1103
|
if (certs.length === 0) {
|
|
1032
1104
|
container.innerHTML = '<div class="empty-state">暂无证书文件</div>';
|
|
1033
1105
|
return;
|
|
@@ -1041,6 +1113,7 @@
|
|
|
1041
1113
|
html += '<th>过期时间</th>';
|
|
1042
1114
|
html += '<th>剩余天数</th>';
|
|
1043
1115
|
html += '<th>状态</th>';
|
|
1116
|
+
html += '<th style="width: 100px;">操作</th>';
|
|
1044
1117
|
html += '</tr></thead><tbody>';
|
|
1045
1118
|
|
|
1046
1119
|
sortedCerts.forEach(cert => {
|
|
@@ -1056,6 +1129,7 @@
|
|
|
1056
1129
|
html += `<td>${formatDate(cert.notAfter)}</td>`;
|
|
1057
1130
|
html += `<td>${cert.remainingDays} 天</td>`;
|
|
1058
1131
|
html += `<td><span class="status-badge ${statusClass}">${statusText}</span></td>`;
|
|
1132
|
+
html += `<td><button onclick="downloadCert('${escapeHtml(cert.fileName)}')" style="padding: 6px 12px; font-size: 0.85em;">下载</button></td>`;
|
|
1059
1133
|
html += '</tr>';
|
|
1060
1134
|
});
|
|
1061
1135
|
|
|
@@ -1063,6 +1137,59 @@
|
|
|
1063
1137
|
container.innerHTML = html;
|
|
1064
1138
|
}
|
|
1065
1139
|
|
|
1140
|
+
// 下载证书
|
|
1141
|
+
async function downloadCert(fileName) {
|
|
1142
|
+
try {
|
|
1143
|
+
addLog(`→ 开始下载证书: ${fileName}`, 'info');
|
|
1144
|
+
|
|
1145
|
+
const password = getStoredPassword();
|
|
1146
|
+
const response = await fetch(`/admin/api/cert/download/${encodeURIComponent(fileName)}`, {
|
|
1147
|
+
method: 'GET',
|
|
1148
|
+
headers: {
|
|
1149
|
+
'X-Password': password
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
if (response.status === 401) {
|
|
1154
|
+
clearPassword();
|
|
1155
|
+
showPasswordModal();
|
|
1156
|
+
addLog('✗ 口令验证失败,请重新输入', 'error');
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (!response.ok) {
|
|
1161
|
+
const result = await response.json();
|
|
1162
|
+
throw new Error(result.msg || '下载失败');
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// 获取文件名
|
|
1166
|
+
const contentDisposition = response.headers.get('Content-Disposition');
|
|
1167
|
+
let downloadFileName = `${fileName}.zip`;
|
|
1168
|
+
if (contentDisposition) {
|
|
1169
|
+
const match = contentDisposition.match(/filename[*]?=['"]?([^'";\n]+)['"]?/);
|
|
1170
|
+
if (match && match[1]) {
|
|
1171
|
+
downloadFileName = match[1];
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// 下载文件
|
|
1176
|
+
const blob = await response.blob();
|
|
1177
|
+
const url = window.URL.createObjectURL(blob);
|
|
1178
|
+
const a = document.createElement('a');
|
|
1179
|
+
a.href = url;
|
|
1180
|
+
a.download = downloadFileName;
|
|
1181
|
+
document.body.appendChild(a);
|
|
1182
|
+
a.click();
|
|
1183
|
+
window.URL.revokeObjectURL(url);
|
|
1184
|
+
document.body.removeChild(a);
|
|
1185
|
+
|
|
1186
|
+
addLog(`✓ 证书下载成功: ${fileName}`, 'success');
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
addLog(`✗ 证书下载失败: ${error.message}`, 'error');
|
|
1189
|
+
showAlert('下载失败', error.message, 'error');
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1066
1193
|
// 过滤证书
|
|
1067
1194
|
function filterCerts() {
|
|
1068
1195
|
const keyword = document.getElementById('certSearch').value.toLowerCase();
|
|
@@ -1108,6 +1235,98 @@
|
|
|
1108
1235
|
}
|
|
1109
1236
|
}
|
|
1110
1237
|
|
|
1238
|
+
// 检查程序更新
|
|
1239
|
+
async function checkForUpdates(silent = false) {
|
|
1240
|
+
if (!silent) {
|
|
1241
|
+
addLog('→ 正在检查更新...', 'info');
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
try {
|
|
1245
|
+
const response = await authenticatedFetch('/admin/check-update', {
|
|
1246
|
+
method: 'POST'
|
|
1247
|
+
});
|
|
1248
|
+
const result = await response.json();
|
|
1249
|
+
|
|
1250
|
+
if (result.code === 0) {
|
|
1251
|
+
const { currentVersion, remoteVersion, hasUpdate } = result.data;
|
|
1252
|
+
|
|
1253
|
+
if (hasUpdate) {
|
|
1254
|
+
// 显示红点提示
|
|
1255
|
+
const updateBtn = document.getElementById('updateBtn');
|
|
1256
|
+
if (updateBtn && !updateBtn.querySelector('.update-badge')) {
|
|
1257
|
+
const badge = document.createElement('span');
|
|
1258
|
+
badge.className = 'update-badge';
|
|
1259
|
+
updateBtn.appendChild(badge);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (!silent) {
|
|
1263
|
+
addLog(`✓ 发现新版本: ${remoteVersion} (当前版本: ${currentVersion})`, 'success');
|
|
1264
|
+
const confirmed = await showConfirm(
|
|
1265
|
+
`发现新版本 ${remoteVersion}`,
|
|
1266
|
+
`当前版本: ${currentVersion}\n新版本: ${remoteVersion}\n\n确定要立即更新吗?更新完成后程序将自动重启。`
|
|
1267
|
+
);
|
|
1268
|
+
|
|
1269
|
+
if (confirmed) {
|
|
1270
|
+
await performUpdate();
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
} else {
|
|
1274
|
+
// 移除红点(如果有)
|
|
1275
|
+
const updateBtn = document.getElementById('updateBtn');
|
|
1276
|
+
if (updateBtn) {
|
|
1277
|
+
const badge = updateBtn.querySelector('.update-badge');
|
|
1278
|
+
if (badge) {
|
|
1279
|
+
badge.remove();
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (!silent) {
|
|
1284
|
+
addLog(`✓ 已是最新版本: ${currentVersion}`, 'success');
|
|
1285
|
+
showAlert('检查更新', `当前已是最新版本 ${currentVersion}`, 'success');
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
if (!silent) {
|
|
1290
|
+
addLog('✗ 检查更新失败: ' + result.msg, 'error');
|
|
1291
|
+
showAlert('检查更新失败', result.msg, 'error');
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
if (error.message !== '未设置密码' && error.message !== '密码验证失败') {
|
|
1296
|
+
if (!silent) {
|
|
1297
|
+
addLog('✗ 检查更新失败: ' + error.message, 'error');
|
|
1298
|
+
showAlert('检查更新失败', error.message, 'error');
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// 执行更新
|
|
1305
|
+
async function performUpdate() {
|
|
1306
|
+
addLog('→ 开始执行更新...', 'info');
|
|
1307
|
+
|
|
1308
|
+
try {
|
|
1309
|
+
const response = await authenticatedFetch('/admin/execute-update', {
|
|
1310
|
+
method: 'POST'
|
|
1311
|
+
});
|
|
1312
|
+
const result = await response.json();
|
|
1313
|
+
|
|
1314
|
+
if (result.code === 0) {
|
|
1315
|
+
addLog('✓ 更新命令已执行,程序将在更新完成后自动重启', 'success');
|
|
1316
|
+
addLog('→ 请稍等片刻,更新完成后刷新页面即可', 'info');
|
|
1317
|
+
showAlert('更新中', '更新命令已执行,程序将在更新完成后自动重启。请稍等片刻后刷新页面。', 'success');
|
|
1318
|
+
} else {
|
|
1319
|
+
addLog('✗ 执行更新失败: ' + result.msg, 'error');
|
|
1320
|
+
showAlert('执行更新失败', result.msg, 'error');
|
|
1321
|
+
}
|
|
1322
|
+
} catch (error) {
|
|
1323
|
+
if (error.message !== '未设置密码' && error.message !== '密码验证失败') {
|
|
1324
|
+
addLog('✗ 执行更新失败: ' + error.message, 'error');
|
|
1325
|
+
showAlert('执行更新失败', error.message, 'error');
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1111
1330
|
// 同步定时任务信息
|
|
1112
1331
|
function startScheduleInfoSync() {
|
|
1113
1332
|
// 立即同步一次
|
package/views/error.ejs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
<h1><%= message %></h1>
|
|
2
|
-
<h2><%= error.status %></h2>
|
|
3
|
-
<pre><%= error.stack %></pre>
|
|
1
|
+
<h1><%= message %></h1>
|
|
2
|
+
<h2><%= error.status %></h2>
|
|
3
|
+
<pre><%= error.stack %></pre>
|
package/views/index.ejs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<title><%= title %></title>
|
|
5
|
-
<link rel='stylesheet' href='/stylesheets/style.css' />
|
|
6
|
-
</head>
|
|
7
|
-
<body>
|
|
8
|
-
<h1><%= title %></h1>
|
|
9
|
-
</body>
|
|
10
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title><%= title %></title>
|
|
5
|
+
<link rel='stylesheet' href='/stylesheets/style.css' />
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<h1><%= title %></h1>
|
|
9
|
+
</body>
|
|
10
|
+
</html>
|