vk-ssl-auto-deploy 0.6.7 → 0.7.0
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/app.js +18 -7
- package/config.json +7 -7
- package/package.json +4 -4
- package/routes/admin.js +266 -0
- package/routes/cert.js +134 -17
- package/utils/websocket.js +116 -0
- package/views/admin.ejs +1319 -0
package/app.js
CHANGED
|
@@ -3,13 +3,13 @@ const express = require('express');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const cookieParser = require('cookie-parser');
|
|
5
5
|
const logger = require('morgan');
|
|
6
|
+
const http = require('http');
|
|
6
7
|
|
|
7
8
|
const indexRouter = require('./routes/index');
|
|
8
9
|
const testRouter = require('./routes/test');
|
|
9
10
|
const certRouter = require('./routes/cert');
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
11
|
+
const adminRouter = require('./routes/admin');
|
|
12
|
+
const { setupWebSocket, broadcastLog } = require('./utils/websocket');
|
|
13
13
|
|
|
14
14
|
const app = express();
|
|
15
15
|
|
|
@@ -26,6 +26,7 @@ app.use(express.static(path.join(__dirname, 'public')));
|
|
|
26
26
|
app.use('/', indexRouter);
|
|
27
27
|
app.use('/test', testRouter);
|
|
28
28
|
app.use(`/api/cert`, certRouter);
|
|
29
|
+
app.use('/admin', adminRouter);
|
|
29
30
|
|
|
30
31
|
// catch 404 and forward to error handler
|
|
31
32
|
app.use((req, res, next) => {
|
|
@@ -42,11 +43,21 @@ app.use((err, req, res, next) => {
|
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
// 启动服务器
|
|
45
|
-
const port = process.env.PORT ||
|
|
46
|
+
const port = process.env.PORT || 6001;
|
|
46
47
|
const host = process.env.HOST || '0.0.0.0';
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
// 创建 HTTP 服务器
|
|
50
|
+
const server = http.createServer(app);
|
|
51
|
+
|
|
52
|
+
// 设置 WebSocket 服务器
|
|
53
|
+
setupWebSocket(server);
|
|
54
|
+
|
|
55
|
+
// 启动服务器
|
|
56
|
+
server.listen(port, host, () => {
|
|
57
|
+
console.log('listenPort:', port);
|
|
58
|
+
console.log('管理后台地址: http://' + (host === '0.0.0.0' ? 'localhost' : host) + ':' + port + '/admin');
|
|
50
59
|
});
|
|
51
60
|
|
|
52
|
-
|
|
61
|
+
// 导出应用和广播函数
|
|
62
|
+
module.exports = app;
|
|
63
|
+
module.exports.broadcastLog = broadcastLog;
|
package/config.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
{
|
|
2
|
-
"key": "
|
|
3
|
-
"certSaveDir": "/vk-cert",
|
|
4
|
-
"nginxDir": "/www/server/nginx/sbin/nginx",
|
|
5
|
-
"domains": [],
|
|
6
|
-
"callbackCommand": [],
|
|
7
|
-
"
|
|
1
|
+
{
|
|
2
|
+
"key": "",
|
|
3
|
+
"certSaveDir": "/vk-cert",
|
|
4
|
+
"nginxDir": "/www/server/nginx/sbin/nginx",
|
|
5
|
+
"domains": [],
|
|
6
|
+
"callbackCommand": [],
|
|
7
|
+
"password": "admin@123456"
|
|
8
8
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vk-ssl-auto-deploy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "SSL证书自动部署工具 - 提供HTTP API接口,支持证书文件自动上传和部署",
|
|
5
5
|
"main": "app.js",
|
|
6
6
|
"bin": {
|
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://gitee.com/vk1688/vk-ssl-auto-deploy",
|
|
37
37
|
"files": [
|
|
38
|
-
"bin/",
|
|
39
38
|
"routes/",
|
|
40
39
|
"utils/",
|
|
41
40
|
"views/",
|
|
@@ -58,9 +57,10 @@
|
|
|
58
57
|
"http-errors": "~1.6.3",
|
|
59
58
|
"morgan": "~1.9.1",
|
|
60
59
|
"multer": "^2.0.2",
|
|
61
|
-
"node-forge": "^1.3.1"
|
|
60
|
+
"node-forge": "^1.3.1",
|
|
61
|
+
"ws": "^8.18.3"
|
|
62
62
|
},
|
|
63
63
|
"optionalDependencies": {
|
|
64
64
|
"pm2": "^5.3.0"
|
|
65
65
|
}
|
|
66
|
-
}
|
|
66
|
+
}
|
package/routes/admin.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILE = path.join(__dirname, '..', 'config.json');
|
|
8
|
+
|
|
9
|
+
// 密码验证中间件
|
|
10
|
+
function verifyPassword(req, res, next) {
|
|
11
|
+
try {
|
|
12
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
13
|
+
const configPassword = config.password;
|
|
14
|
+
const requestPassword = req.headers['x-password'];
|
|
15
|
+
|
|
16
|
+
// 如果配置中没有设置密码,直接通过
|
|
17
|
+
if (!configPassword) {
|
|
18
|
+
return next();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 验证密码
|
|
22
|
+
if (requestPassword === configPassword) {
|
|
23
|
+
return next();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 密码错误
|
|
27
|
+
return res.status(401).json({
|
|
28
|
+
code: 401,
|
|
29
|
+
msg: '口令错误',
|
|
30
|
+
data: null
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return res.status(500).json({
|
|
34
|
+
code: 500,
|
|
35
|
+
msg: '验证失败: ' + error.message,
|
|
36
|
+
data: null
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 读取证书信息的工具函数(从 cert.js 复制)
|
|
42
|
+
function parseCertificate(certContent) {
|
|
43
|
+
try {
|
|
44
|
+
// 优先使用 Node.js 内置的 crypto 模块
|
|
45
|
+
if (crypto.X509Certificate) {
|
|
46
|
+
const cert = new crypto.X509Certificate(certContent);
|
|
47
|
+
const parseCN = (dn) => {
|
|
48
|
+
const cnMatch = dn.match(/CN=([^,]+)/);
|
|
49
|
+
return cnMatch ? cnMatch[1] : '';
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
notBefore: new Date(cert.validFrom),
|
|
53
|
+
notAfter: new Date(cert.validTo),
|
|
54
|
+
subject: parseCN(cert.subject),
|
|
55
|
+
issuer: parseCN(cert.issuer)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// 降级方案:使用 node-forge
|
|
59
|
+
const forge = require('node-forge');
|
|
60
|
+
const cert = forge.pki.certificateFromPem(certContent);
|
|
61
|
+
return {
|
|
62
|
+
notBefore: cert.validity.notBefore,
|
|
63
|
+
notAfter: cert.validity.notAfter,
|
|
64
|
+
subject: cert.subject.attributes.find(attr => attr.name === 'commonName')?.value || '',
|
|
65
|
+
issuer: cert.issuer.attributes.find(attr => attr.name === 'commonName')?.value || ''
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* GET /admin
|
|
74
|
+
* 管理页面(不需要密码验证)
|
|
75
|
+
*/
|
|
76
|
+
router.get('/', (req, res) => {
|
|
77
|
+
res.render('admin', { title: 'SSL证书管理后台' });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* POST /api/verify-password
|
|
82
|
+
* 验证密码(用于前端验证)
|
|
83
|
+
*/
|
|
84
|
+
router.post('/api/verify-password', (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
87
|
+
const configPassword = config.password;
|
|
88
|
+
const requestPassword = req.headers['x-password'] || req.body.password;
|
|
89
|
+
|
|
90
|
+
// 如果配置中没有设置密码,直接通过
|
|
91
|
+
if (!configPassword) {
|
|
92
|
+
return res.json({
|
|
93
|
+
code: 0,
|
|
94
|
+
msg: 'success',
|
|
95
|
+
data: { valid: true }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 验证密码
|
|
100
|
+
if (requestPassword === configPassword) {
|
|
101
|
+
return res.json({
|
|
102
|
+
code: 0,
|
|
103
|
+
msg: 'success',
|
|
104
|
+
data: { valid: true }
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 密码错误
|
|
109
|
+
return res.status(401).json({
|
|
110
|
+
code: 401,
|
|
111
|
+
msg: '口令错误',
|
|
112
|
+
data: { valid: false }
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return res.status(500).json({
|
|
116
|
+
code: 500,
|
|
117
|
+
msg: '验证失败: ' + error.message,
|
|
118
|
+
data: null
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* GET /api/config
|
|
125
|
+
* 获取配置信息
|
|
126
|
+
*/
|
|
127
|
+
router.get('/api/config', verifyPassword, (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
130
|
+
res.json({
|
|
131
|
+
code: 0,
|
|
132
|
+
msg: 'success',
|
|
133
|
+
data: config
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
res.json({
|
|
137
|
+
code: 500,
|
|
138
|
+
msg: '读取配置失败: ' + error.message,
|
|
139
|
+
data: null
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* POST /api/config
|
|
146
|
+
* 保存配置信息
|
|
147
|
+
*/
|
|
148
|
+
router.post('/api/config', verifyPassword, (req, res) => {
|
|
149
|
+
try {
|
|
150
|
+
const newConfig = req.body;
|
|
151
|
+
|
|
152
|
+
// 验证必填字段
|
|
153
|
+
if (!newConfig.key) {
|
|
154
|
+
return res.json({
|
|
155
|
+
code: 400,
|
|
156
|
+
msg: 'API Key 不能为空',
|
|
157
|
+
data: null
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!newConfig.certSaveDir) {
|
|
162
|
+
return res.json({
|
|
163
|
+
code: 400,
|
|
164
|
+
msg: '证书保存目录不能为空',
|
|
165
|
+
data: null
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 确保 domains 和 callbackCommand 是数组
|
|
170
|
+
if (!Array.isArray(newConfig.domains)) {
|
|
171
|
+
newConfig.domains = [];
|
|
172
|
+
}
|
|
173
|
+
if (!Array.isArray(newConfig.callbackCommand)) {
|
|
174
|
+
newConfig.callbackCommand = [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 保存配置文件
|
|
178
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, '\t'), 'utf8');
|
|
179
|
+
|
|
180
|
+
// 重新加载配置(清除 require 缓存)
|
|
181
|
+
delete require.cache[require.resolve('../config.json')];
|
|
182
|
+
|
|
183
|
+
res.json({
|
|
184
|
+
code: 0,
|
|
185
|
+
msg: '配置保存成功',
|
|
186
|
+
data: newConfig
|
|
187
|
+
});
|
|
188
|
+
} catch (error) {
|
|
189
|
+
res.json({
|
|
190
|
+
code: 500,
|
|
191
|
+
msg: '保存配置失败: ' + error.message,
|
|
192
|
+
data: null
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* GET /api/certs/local
|
|
199
|
+
* 获取本地证书列表及有效期信息
|
|
200
|
+
*/
|
|
201
|
+
router.get('/api/certs/local', verifyPassword, (req, res) => {
|
|
202
|
+
try {
|
|
203
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
204
|
+
const certDir = path.isAbsolute(config.certSaveDir) ?
|
|
205
|
+
config.certSaveDir :
|
|
206
|
+
path.join(__dirname, '..', config.certSaveDir);
|
|
207
|
+
|
|
208
|
+
// 检查目录是否存在
|
|
209
|
+
if (!fs.existsSync(certDir)) {
|
|
210
|
+
return res.json({
|
|
211
|
+
code: 0,
|
|
212
|
+
msg: 'success',
|
|
213
|
+
data: []
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 读取目录中的所有 .crt 文件
|
|
218
|
+
const files = fs.readdirSync(certDir);
|
|
219
|
+
const crtFiles = files.filter(f => f.endsWith('.crt'));
|
|
220
|
+
|
|
221
|
+
const certList = [];
|
|
222
|
+
|
|
223
|
+
for (const fileName of crtFiles) {
|
|
224
|
+
const filePath = path.join(certDir, fileName);
|
|
225
|
+
const certContent = fs.readFileSync(filePath, 'utf8');
|
|
226
|
+
const certInfo = parseCertificate(certContent);
|
|
227
|
+
|
|
228
|
+
if (certInfo) {
|
|
229
|
+
const now = new Date();
|
|
230
|
+
const expiryDate = new Date(certInfo.notAfter);
|
|
231
|
+
const remainingMs = expiryDate - now;
|
|
232
|
+
const remainingDays = Math.floor(remainingMs / (1000 * 60 * 60 * 24));
|
|
233
|
+
|
|
234
|
+
// 提取域名(从文件名)
|
|
235
|
+
const baseName = fileName.replace('.crt', '');
|
|
236
|
+
|
|
237
|
+
certList.push({
|
|
238
|
+
fileName: baseName,
|
|
239
|
+
domain: certInfo.subject || baseName,
|
|
240
|
+
issuer: certInfo.issuer,
|
|
241
|
+
notBefore: certInfo.notBefore.toISOString(),
|
|
242
|
+
notAfter: certInfo.notAfter.toISOString(),
|
|
243
|
+
remainingDays: remainingDays,
|
|
244
|
+
status: remainingDays < 0 ? 'expired' : (remainingDays < 14 ? 'warning' : 'valid')
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 按剩余天数排序(过期的在前,然后是快过期的)
|
|
250
|
+
certList.sort((a, b) => a.remainingDays - b.remainingDays);
|
|
251
|
+
|
|
252
|
+
res.json({
|
|
253
|
+
code: 0,
|
|
254
|
+
msg: 'success',
|
|
255
|
+
data: certList
|
|
256
|
+
});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
res.json({
|
|
259
|
+
code: 500,
|
|
260
|
+
msg: '获取证书列表失败: ' + error.message,
|
|
261
|
+
data: null
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
module.exports = router;
|
package/routes/cert.js
CHANGED
|
@@ -8,10 +8,75 @@ const crypto = require('crypto');
|
|
|
8
8
|
const axios = require('axios');
|
|
9
9
|
const forge = require('node-forge');
|
|
10
10
|
const chalk = require('chalk');
|
|
11
|
-
const config = require('../config.json');
|
|
12
11
|
|
|
13
12
|
const execAsync = promisify(exec);
|
|
14
13
|
|
|
14
|
+
// 获取广播函数
|
|
15
|
+
let broadcastLog = null;
|
|
16
|
+
try {
|
|
17
|
+
const app = require('../app');
|
|
18
|
+
broadcastLog = app.broadcastLog;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
// 如果获取失败,使用空函数
|
|
21
|
+
broadcastLog = () => {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 配置文件路径
|
|
25
|
+
const CONFIG_FILE = path.join(__dirname, '..', 'config.json');
|
|
26
|
+
|
|
27
|
+
// 密码验证中间件
|
|
28
|
+
function verifyPassword(req, res, next) {
|
|
29
|
+
try {
|
|
30
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
31
|
+
const configPassword = config.password;
|
|
32
|
+
const requestPassword = req.headers['x-password'];
|
|
33
|
+
|
|
34
|
+
// 如果配置中没有设置密码,直接通过
|
|
35
|
+
if (!configPassword) {
|
|
36
|
+
return next();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 验证密码
|
|
40
|
+
if (requestPassword === configPassword) {
|
|
41
|
+
return next();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 密码错误
|
|
45
|
+
return res.status(401).json({
|
|
46
|
+
code: 401,
|
|
47
|
+
msg: '口令错误',
|
|
48
|
+
data: null
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return res.status(500).json({
|
|
52
|
+
code: 500,
|
|
53
|
+
msg: '验证失败: ' + error.message,
|
|
54
|
+
data: null
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 动态获取配置(每次调用都重新读取文件)
|
|
61
|
+
* @returns {Object} 配置对象
|
|
62
|
+
*/
|
|
63
|
+
function getConfig() {
|
|
64
|
+
try {
|
|
65
|
+
const configContent = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
66
|
+
return JSON.parse(configContent);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.error('[配置读取错误]', error.message);
|
|
69
|
+
// 返回默认配置
|
|
70
|
+
return {
|
|
71
|
+
key: '',
|
|
72
|
+
certSaveDir: '/vk-cert',
|
|
73
|
+
nginxDir: '',
|
|
74
|
+
domains: [],
|
|
75
|
+
callbackCommand: []
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
15
80
|
// 日志工具 - 带颜色输出(仅使用5种颜色:白色、灰色、黄色、绿色、红色)
|
|
16
81
|
const logger = {
|
|
17
82
|
// 错误 - 红色
|
|
@@ -34,7 +99,6 @@ const logger = {
|
|
|
34
99
|
|
|
35
100
|
// 无忧SSL API配置
|
|
36
101
|
const WUYOU_API_BASE = 'https://openapi-ssl.vk168.top/http'; // 请勿修改
|
|
37
|
-
const API_KEY = config.key;
|
|
38
102
|
|
|
39
103
|
// QPS限制:每次API调用后等待时间(毫秒)
|
|
40
104
|
const API_CALL_DELAY = 1000; // 确保QPS不超过1
|
|
@@ -118,13 +182,14 @@ function releaseLock() {
|
|
|
118
182
|
*/
|
|
119
183
|
async function getCertList(pageIndex = 1, pageSize = 10000) {
|
|
120
184
|
try {
|
|
185
|
+
const config = getConfig(); // 动态获取配置
|
|
121
186
|
logger.info(`[API调用] 获取证书列表 - 页码: ${pageIndex}, 每页: ${pageSize}`);
|
|
122
187
|
|
|
123
188
|
const params = {
|
|
124
189
|
pageIndex,
|
|
125
190
|
pageSize,
|
|
126
191
|
status: 3,
|
|
127
|
-
key:
|
|
192
|
+
key: config.key
|
|
128
193
|
};
|
|
129
194
|
|
|
130
195
|
// 添加domains过滤条件
|
|
@@ -137,12 +202,13 @@ async function getCertList(pageIndex = 1, pageSize = 10000) {
|
|
|
137
202
|
timeout: 60000
|
|
138
203
|
}
|
|
139
204
|
);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
205
|
+
if (response.data?.code === 0) {
|
|
206
|
+
logger.success(`[API响应] 获取证书列表成功 - 共 ${response.data?.data?.total || 0} 条记录`);
|
|
207
|
+
} else {
|
|
208
|
+
logger.error('[API错误] 获取证书列表失败:', response.data?.msg);
|
|
209
|
+
}
|
|
143
210
|
// QPS限制:调用后等待
|
|
144
211
|
await sleep(API_CALL_DELAY);
|
|
145
|
-
|
|
146
212
|
return response.data;
|
|
147
213
|
} catch (error) {
|
|
148
214
|
logger.error('[API错误] 获取证书列表失败:', error.message);
|
|
@@ -157,11 +223,12 @@ async function getCertList(pageIndex = 1, pageSize = 10000) {
|
|
|
157
223
|
*/
|
|
158
224
|
async function downloadCerts(certIds) {
|
|
159
225
|
try {
|
|
226
|
+
const config = getConfig(); // 动态获取配置
|
|
160
227
|
logger.info(`[API调用] 批量下载证书 - 数量: ${certIds.length}`);
|
|
161
228
|
const params = {
|
|
162
229
|
certIds,
|
|
163
230
|
type: "nginx",
|
|
164
|
-
key:
|
|
231
|
+
key: config.key
|
|
165
232
|
};
|
|
166
233
|
|
|
167
234
|
const response = await axios.post(
|
|
@@ -295,6 +362,7 @@ function generateFileName(domains) {
|
|
|
295
362
|
* @param {string} keyContent - 私钥内容
|
|
296
363
|
*/
|
|
297
364
|
function saveCertFiles(fileName, certContent, keyContent) {
|
|
365
|
+
const config = getConfig(); // 动态获取配置
|
|
298
366
|
const certDir = path.isAbsolute(config.certSaveDir) ?
|
|
299
367
|
config.certSaveDir :
|
|
300
368
|
path.join(__dirname, '..', config.certSaveDir);
|
|
@@ -321,6 +389,7 @@ function saveCertFiles(fileName, certContent, keyContent) {
|
|
|
321
389
|
* 重载Nginx配置
|
|
322
390
|
*/
|
|
323
391
|
async function reloadNginx() {
|
|
392
|
+
const config = getConfig(); // 动态获取配置
|
|
324
393
|
if (!config.nginxDir || !config.nginxDir.trim()) {
|
|
325
394
|
logger.dim('[Nginx重载] 未配置nginxDir,跳过重载');
|
|
326
395
|
return { success: true, message: '未配置nginxDir' };
|
|
@@ -349,6 +418,8 @@ async function reloadNginx() {
|
|
|
349
418
|
* 执行证书自动部署任务
|
|
350
419
|
*/
|
|
351
420
|
async function executeDeployTask() {
|
|
421
|
+
const config = getConfig(); // 动态获取配置
|
|
422
|
+
|
|
352
423
|
if (isTaskRunning) {
|
|
353
424
|
logger.warn('[任务执行] 已有任务正在执行,跳过本次执行');
|
|
354
425
|
return { success: false, message: '任务正在执行中' };
|
|
@@ -387,7 +458,7 @@ async function executeDeployTask() {
|
|
|
387
458
|
const certListResponse = await getCertList(1, 10000);
|
|
388
459
|
|
|
389
460
|
if (!certListResponse || certListResponse.code !== 0) {
|
|
390
|
-
throw new Error(`获取证书列表失败: ${certListResponse?.message || '未知错误'}`);
|
|
461
|
+
throw new Error(`获取证书列表失败: ${certListResponse?.msg || certListResponse?.message || '未知错误'}`);
|
|
391
462
|
}
|
|
392
463
|
|
|
393
464
|
const certList = certListResponse.data?.rows || [];
|
|
@@ -490,7 +561,7 @@ async function executeDeployTask() {
|
|
|
490
561
|
const downloadResponse = await downloadCerts(certIds);
|
|
491
562
|
|
|
492
563
|
if (!downloadResponse || downloadResponse.code !== 0) {
|
|
493
|
-
throw new Error(`下载失败: ${downloadResponse?.message || '未知错误'}`);
|
|
564
|
+
throw new Error(`下载失败: ${downloadResponse?.msg || downloadResponse?.message || '未知错误'}`);
|
|
494
565
|
}
|
|
495
566
|
|
|
496
567
|
const downloadedCerts = downloadResponse.data?.certs || [];
|
|
@@ -577,6 +648,13 @@ async function executeDeployTask() {
|
|
|
577
648
|
logger.dim(`[证书保存目录] ${config.certSaveDir}`);
|
|
578
649
|
logger.divider();
|
|
579
650
|
console.log('');
|
|
651
|
+
|
|
652
|
+
// 任务完成后,通过WebSocket通知前端刷新证书列表
|
|
653
|
+
if (broadcastLog && result.stats.downloadedCerts > 0) {
|
|
654
|
+
setTimeout(() => {
|
|
655
|
+
broadcastLog('CERT_UPDATE_COMPLETE', 'cert_update');
|
|
656
|
+
}, 1000);
|
|
657
|
+
}
|
|
580
658
|
}
|
|
581
659
|
|
|
582
660
|
return result;
|
|
@@ -604,8 +682,8 @@ function initScheduledTask() {
|
|
|
604
682
|
};
|
|
605
683
|
|
|
606
684
|
// scheduledExecutionTime = {
|
|
607
|
-
// hour:
|
|
608
|
-
// minute:
|
|
685
|
+
// hour: 17,
|
|
686
|
+
// minute: 9
|
|
609
687
|
// }
|
|
610
688
|
|
|
611
689
|
logger.success(
|
|
@@ -674,9 +752,7 @@ function initScheduledTask() {
|
|
|
674
752
|
if (lastExecutionDate !== currentDate) {
|
|
675
753
|
// 换行后输出触发信息
|
|
676
754
|
console.log('');
|
|
677
|
-
logger.success(
|
|
678
|
-
`[定时任务] 触发执行 - 当前时间: ${currentHour}:${currentMinute.toString().padStart(2, '0')}`
|
|
679
|
-
);
|
|
755
|
+
logger.success(`[定时任务] 触发执行`);
|
|
680
756
|
lastExecutionDate = currentDate;
|
|
681
757
|
|
|
682
758
|
// 异步执行任务,使用try-catch确保异常不会影响定时器
|
|
@@ -685,6 +761,7 @@ function initScheduledTask() {
|
|
|
685
761
|
}).finally(() => {
|
|
686
762
|
needWrite = true;
|
|
687
763
|
lastCountdownText = "";
|
|
764
|
+
logger.log(`[定时任务] 运行结束`);
|
|
688
765
|
})
|
|
689
766
|
}
|
|
690
767
|
}
|
|
@@ -705,7 +782,8 @@ function initScheduledTask() {
|
|
|
705
782
|
* GET /api/cert/status
|
|
706
783
|
* 获取任务状态
|
|
707
784
|
*/
|
|
708
|
-
router.get('/status', (req, res) => {
|
|
785
|
+
router.get('/status', verifyPassword, (req, res) => {
|
|
786
|
+
const config = getConfig(); // 动态获取配置
|
|
709
787
|
res.json({
|
|
710
788
|
code: 0,
|
|
711
789
|
msg: 'success',
|
|
@@ -724,11 +802,50 @@ router.get('/status', (req, res) => {
|
|
|
724
802
|
});
|
|
725
803
|
});
|
|
726
804
|
|
|
805
|
+
/**
|
|
806
|
+
* GET /api/cert/schedule-info
|
|
807
|
+
* 获取定时任务信息和服务器时间
|
|
808
|
+
*/
|
|
809
|
+
router.get('/schedule-info', verifyPassword, (req, res) => {
|
|
810
|
+
const now = new Date();
|
|
811
|
+
const serverTime = now.toISOString();
|
|
812
|
+
|
|
813
|
+
// 计算下次执行时间
|
|
814
|
+
let nextExecutionTime = null;
|
|
815
|
+
if (scheduledExecutionTime) {
|
|
816
|
+
const targetTime = new Date(now);
|
|
817
|
+
targetTime.setHours(scheduledExecutionTime.hour, scheduledExecutionTime.minute, 0, 0);
|
|
818
|
+
|
|
819
|
+
// 如果今天的执行时间已过,则计算明天的执行时间
|
|
820
|
+
if (now >= targetTime) {
|
|
821
|
+
targetTime.setDate(targetTime.getDate() + 1);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
nextExecutionTime = targetTime.toISOString();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
res.json({
|
|
828
|
+
code: 0,
|
|
829
|
+
msg: 'success',
|
|
830
|
+
data: {
|
|
831
|
+
serverTime,
|
|
832
|
+
scheduledExecutionTime: scheduledExecutionTime ? {
|
|
833
|
+
hour: scheduledExecutionTime.hour,
|
|
834
|
+
minute: scheduledExecutionTime.minute,
|
|
835
|
+
formatted: `${scheduledExecutionTime.hour}:${scheduledExecutionTime.minute.toString().padStart(2, '0')}`
|
|
836
|
+
} : null,
|
|
837
|
+
nextExecutionTime,
|
|
838
|
+
isTaskRunning,
|
|
839
|
+
lastExecutionDate
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
727
844
|
/**
|
|
728
845
|
* GET /api/cert/execute
|
|
729
846
|
* 手动触发任务执行
|
|
730
847
|
*/
|
|
731
|
-
router.get('/execute', async (req, res) => {
|
|
848
|
+
router.get('/execute', verifyPassword, async (req, res) => {
|
|
732
849
|
logger.info('[手动执行] 收到手动执行请求');
|
|
733
850
|
|
|
734
851
|
if (isTaskRunning) {
|