easy-devops 0.1.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/LICENSE +21 -0
- package/README.md +325 -0
- package/cli/index.js +91 -0
- package/cli/managers/domain-manager.js +451 -0
- package/cli/managers/nginx-manager.js +329 -0
- package/cli/managers/node-manager.js +275 -0
- package/cli/managers/ssl-manager.js +397 -0
- package/cli/menus/.gitkeep +0 -0
- package/cli/menus/dashboard.js +223 -0
- package/cli/menus/domains.js +5 -0
- package/cli/menus/nginx.js +5 -0
- package/cli/menus/nodejs.js +5 -0
- package/cli/menus/settings.js +83 -0
- package/cli/menus/ssl.js +5 -0
- package/core/config.js +37 -0
- package/core/db.js +30 -0
- package/core/detector.js +257 -0
- package/core/nginx-conf-generator.js +309 -0
- package/core/shell.js +151 -0
- package/dashboard/lib/.gitkeep +0 -0
- package/dashboard/lib/cert-reader.js +59 -0
- package/dashboard/lib/domains-db.js +51 -0
- package/dashboard/lib/nginx-conf-generator.js +16 -0
- package/dashboard/lib/nginx-service.js +282 -0
- package/dashboard/public/js/app.js +486 -0
- package/dashboard/routes/.gitkeep +0 -0
- package/dashboard/routes/auth.js +30 -0
- package/dashboard/routes/domains.js +300 -0
- package/dashboard/routes/nginx.js +151 -0
- package/dashboard/routes/settings.js +78 -0
- package/dashboard/routes/ssl.js +105 -0
- package/dashboard/server.js +79 -0
- package/dashboard/views/index.ejs +327 -0
- package/dashboard/views/partials/domain-form.ejs +229 -0
- package/dashboard/views/partials/domains-panel.ejs +66 -0
- package/dashboard/views/partials/login.ejs +50 -0
- package/dashboard/views/partials/nginx-panel.ejs +90 -0
- package/dashboard/views/partials/overview.ejs +67 -0
- package/dashboard/views/partials/settings-panel.ejs +37 -0
- package/dashboard/views/partials/sidebar.ejs +45 -0
- package/dashboard/views/partials/ssl-panel.ejs +53 -0
- package/data/.gitkeep +0 -0
- package/install.bat +41 -0
- package/install.ps1 +653 -0
- package/install.sh +452 -0
- package/lib/installer/.gitkeep +0 -0
- package/lib/installer/detect.sh +88 -0
- package/lib/installer/node-versions.sh +109 -0
- package/lib/installer/nvm-bootstrap.sh +77 -0
- package/lib/installer/picker.sh +163 -0
- package/lib/installer/progress.sh +25 -0
- package/package.json +67 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/nginx-conf-generator.js
|
|
3
|
+
*
|
|
4
|
+
* Shared nginx configuration generator used by both dashboard routes and CLI.
|
|
5
|
+
* Generates complete nginx reverse proxy configs from domain objects.
|
|
6
|
+
*
|
|
7
|
+
* This module is pure (no side effects) — only generateConf() writes files.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { loadConfig } from './config.js';
|
|
13
|
+
|
|
14
|
+
// ─── DOMAIN DEFAULTS (v2 schema) ─────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export const DOMAIN_DEFAULTS = {
|
|
17
|
+
backendHost: '127.0.0.1',
|
|
18
|
+
upstreamType: 'http', // 'http' | 'https' | 'ws'
|
|
19
|
+
www: false,
|
|
20
|
+
ssl: {
|
|
21
|
+
enabled: false,
|
|
22
|
+
certPath: '',
|
|
23
|
+
keyPath: '',
|
|
24
|
+
redirect: true,
|
|
25
|
+
hsts: false,
|
|
26
|
+
hstsMaxAge: 31536000, // 1 year
|
|
27
|
+
},
|
|
28
|
+
performance: {
|
|
29
|
+
maxBodySize: '10m',
|
|
30
|
+
readTimeout: 60,
|
|
31
|
+
connectTimeout: 10,
|
|
32
|
+
proxyBuffers: false,
|
|
33
|
+
gzip: true,
|
|
34
|
+
gzipTypes: 'text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript',
|
|
35
|
+
},
|
|
36
|
+
security: {
|
|
37
|
+
rateLimit: false,
|
|
38
|
+
rateLimitRate: 10,
|
|
39
|
+
rateLimitBurst: 20,
|
|
40
|
+
securityHeaders: false,
|
|
41
|
+
custom404: false,
|
|
42
|
+
custom50x: false,
|
|
43
|
+
},
|
|
44
|
+
advanced: {
|
|
45
|
+
accessLog: true,
|
|
46
|
+
customLocations: '',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ─── MIGRATION: v1 → v2 ──────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export function migrateDomain(d) {
|
|
53
|
+
if (!d) return d;
|
|
54
|
+
|
|
55
|
+
// Already v2 format (has nested ssl object)
|
|
56
|
+
if (d.ssl && typeof d.ssl === 'object') {
|
|
57
|
+
// Ensure all nested properties exist with defaults
|
|
58
|
+
return {
|
|
59
|
+
...DOMAIN_DEFAULTS,
|
|
60
|
+
...d,
|
|
61
|
+
ssl: { ...DOMAIN_DEFAULTS.ssl, ...d.ssl },
|
|
62
|
+
performance: { ...DOMAIN_DEFAULTS.performance, ...d.performance },
|
|
63
|
+
security: { ...DOMAIN_DEFAULTS.security, ...d.security },
|
|
64
|
+
advanced: { ...DOMAIN_DEFAULTS.advanced, ...d.advanced },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// v1 flat schema → v2 nested
|
|
69
|
+
return {
|
|
70
|
+
...DOMAIN_DEFAULTS,
|
|
71
|
+
name: d.name,
|
|
72
|
+
port: d.port,
|
|
73
|
+
www: d.www ?? false,
|
|
74
|
+
backendHost: d.backendHost ?? '127.0.0.1',
|
|
75
|
+
upstreamType: d.upstreamType ?? 'http',
|
|
76
|
+
ssl: {
|
|
77
|
+
...DOMAIN_DEFAULTS.ssl,
|
|
78
|
+
enabled: d.sslEnabled ?? false,
|
|
79
|
+
certPath: d.certPath ?? '',
|
|
80
|
+
keyPath: d.keyPath ?? '',
|
|
81
|
+
},
|
|
82
|
+
performance: {
|
|
83
|
+
...DOMAIN_DEFAULTS.performance,
|
|
84
|
+
maxBodySize: d.maxBodySize ?? '10m',
|
|
85
|
+
},
|
|
86
|
+
security: { ...DOMAIN_DEFAULTS.security },
|
|
87
|
+
advanced: { ...DOMAIN_DEFAULTS.advanced },
|
|
88
|
+
configFile: d.configFile ?? null,
|
|
89
|
+
createdAt: d.createdAt ?? new Date().toISOString(),
|
|
90
|
+
updatedAt: d.updatedAt ?? new Date().toISOString(),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── CONF BUILDER ─────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Builds an nginx server block configuration from a domain object.
|
|
98
|
+
* @param {Object} domain - Domain configuration object (v2 schema)
|
|
99
|
+
* @param {string} nginxDir - Nginx directory path
|
|
100
|
+
* @param {string} certbotDir - Certbot directory path
|
|
101
|
+
* @returns {string} Complete nginx conf content for the domain
|
|
102
|
+
*/
|
|
103
|
+
export function buildConf(domain, nginxDir, certbotDir) {
|
|
104
|
+
const {
|
|
105
|
+
name,
|
|
106
|
+
port,
|
|
107
|
+
backendHost = '127.0.0.1',
|
|
108
|
+
upstreamType = 'http',
|
|
109
|
+
www = false,
|
|
110
|
+
ssl,
|
|
111
|
+
performance,
|
|
112
|
+
security,
|
|
113
|
+
advanced,
|
|
114
|
+
} = domain;
|
|
115
|
+
|
|
116
|
+
// Determine proxy scheme based on upstreamType
|
|
117
|
+
const proxyScheme = upstreamType === 'https' ? 'https' : 'http';
|
|
118
|
+
|
|
119
|
+
// Build proxy headers (always include standard set)
|
|
120
|
+
let proxyHeaders = `
|
|
121
|
+
proxy_set_header Host $host;
|
|
122
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
123
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
124
|
+
proxy_set_header X-Forwarded-Proto $scheme;`;
|
|
125
|
+
|
|
126
|
+
// Add WebSocket headers if upstreamType is 'ws'
|
|
127
|
+
if (upstreamType === 'ws') {
|
|
128
|
+
proxyHeaders = `
|
|
129
|
+
proxy_http_version 1.1;
|
|
130
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
131
|
+
proxy_set_header Connection "upgrade";${proxyHeaders}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Build rate limiting zone name (dots → underscores)
|
|
135
|
+
const rateLimitZone = name.replace(/\./g, '_').replace(/\*/g, 'wildcard');
|
|
136
|
+
|
|
137
|
+
// SSL cert/key paths - use certbot convention if empty
|
|
138
|
+
const certPath = ssl?.certPath || `${certbotDir}/live/${name}/fullchain.pem`;
|
|
139
|
+
const keyPath = ssl?.keyPath || `${certbotDir}/live/${name}/privkey.pem`;
|
|
140
|
+
|
|
141
|
+
// Build the config sections
|
|
142
|
+
const sections = [];
|
|
143
|
+
|
|
144
|
+
// ─── Rate Limit Zone Comment ────────────────────────────────────────────────
|
|
145
|
+
if (security?.rateLimit) {
|
|
146
|
+
sections.push(`# Rate limit zone — add to nginx.conf http block:
|
|
147
|
+
# limit_req_zone $binary_remote_addr zone=${rateLimitZone}:10m rate=${security.rateLimitRate}r/s;`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── WWW Redirect (non-SSL only) ────────────────────────────────────────────
|
|
151
|
+
if (www && !ssl?.enabled) {
|
|
152
|
+
sections.push(`server {
|
|
153
|
+
listen 80;
|
|
154
|
+
server_name www.${name};
|
|
155
|
+
return 301 http://${name}$request_uri;
|
|
156
|
+
}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── HTTP → HTTPS Redirect ──────────────────────────────────────────────────
|
|
160
|
+
if (ssl?.enabled && ssl?.redirect) {
|
|
161
|
+
const serverNames = www ? `${name} www.${name}` : name;
|
|
162
|
+
sections.push(`server {
|
|
163
|
+
listen 80;
|
|
164
|
+
server_name ${serverNames};
|
|
165
|
+
return 301 https://${name}$request_uri;
|
|
166
|
+
}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─── Main Server Block ──────────────────────────────────────────────────────
|
|
170
|
+
const listenPort = ssl?.enabled ? '443 ssl' : '80';
|
|
171
|
+
const serverNames = www ? `${name} www.${name}` : name;
|
|
172
|
+
|
|
173
|
+
const mainBlock = [];
|
|
174
|
+
|
|
175
|
+
mainBlock.push(`server {`);
|
|
176
|
+
mainBlock.push(` listen ${listenPort};`);
|
|
177
|
+
mainBlock.push(` server_name ${serverNames};`);
|
|
178
|
+
|
|
179
|
+
// SSL configuration
|
|
180
|
+
if (ssl?.enabled) {
|
|
181
|
+
mainBlock.push(``);
|
|
182
|
+
mainBlock.push(` ssl_certificate ${certPath};`);
|
|
183
|
+
mainBlock.push(` ssl_certificate_key ${keyPath};`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Performance: client_max_body_size
|
|
187
|
+
mainBlock.push(``);
|
|
188
|
+
mainBlock.push(` client_max_body_size ${performance?.maxBodySize || '10m'};`);
|
|
189
|
+
|
|
190
|
+
// Performance: gzip
|
|
191
|
+
if (performance?.gzip) {
|
|
192
|
+
mainBlock.push(` gzip on;`);
|
|
193
|
+
mainBlock.push(` gzip_types ${performance?.gzipTypes || DOMAIN_DEFAULTS.performance.gzipTypes};`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Performance: proxy buffering
|
|
197
|
+
if (performance?.proxyBuffers) {
|
|
198
|
+
mainBlock.push(` proxy_buffering on;`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Logging
|
|
202
|
+
if (advanced?.accessLog) {
|
|
203
|
+
mainBlock.push(` access_log /var/log/nginx/${name}.access.log;`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── Location Block ─────────────────────────────────────────────────────────
|
|
207
|
+
mainBlock.push(``);
|
|
208
|
+
mainBlock.push(` location / {`);
|
|
209
|
+
mainBlock.push(` proxy_pass ${proxyScheme}://${backendHost}:${port};`);
|
|
210
|
+
mainBlock.push(proxyHeaders);
|
|
211
|
+
mainBlock.push(``);
|
|
212
|
+
mainBlock.push(` proxy_read_timeout ${performance?.readTimeout || 60}s;`);
|
|
213
|
+
mainBlock.push(` proxy_connect_timeout ${performance?.connectTimeout || 10}s;`);
|
|
214
|
+
|
|
215
|
+
// Rate limiting
|
|
216
|
+
if (security?.rateLimit) {
|
|
217
|
+
mainBlock.push(``);
|
|
218
|
+
mainBlock.push(` limit_req zone=${rateLimitZone} burst=${security.rateLimitBurst} nodelay;`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Security headers
|
|
222
|
+
if (security?.securityHeaders) {
|
|
223
|
+
mainBlock.push(``);
|
|
224
|
+
mainBlock.push(` add_header X-Frame-Options "SAMEORIGIN" always;`);
|
|
225
|
+
mainBlock.push(` add_header X-Content-Type-Options "nosniff" always;`);
|
|
226
|
+
mainBlock.push(` add_header Referrer-Policy "strict-origin-when-cross-origin" always;`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// HSTS
|
|
230
|
+
if (ssl?.enabled && ssl?.hsts) {
|
|
231
|
+
mainBlock.push(``);
|
|
232
|
+
mainBlock.push(` add_header Strict-Transport-Security "max-age=${ssl.hstsMaxAge}; includeSubDomains" always;`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
mainBlock.push(` }`);
|
|
236
|
+
|
|
237
|
+
// Custom error pages
|
|
238
|
+
if (security?.custom404 || security?.custom50x) {
|
|
239
|
+
mainBlock.push(``);
|
|
240
|
+
if (security.custom404) {
|
|
241
|
+
mainBlock.push(` error_page 404 /404.html;`);
|
|
242
|
+
}
|
|
243
|
+
if (security.custom50x) {
|
|
244
|
+
mainBlock.push(` error_page 500 502 503 504 /50x.html;`);
|
|
245
|
+
}
|
|
246
|
+
if (security.custom404) {
|
|
247
|
+
mainBlock.push(` location = /404.html { root /usr/share/nginx/html; internal; }`);
|
|
248
|
+
}
|
|
249
|
+
if (security.custom50x) {
|
|
250
|
+
mainBlock.push(` location = /50x.html { root /usr/share/nginx/html; internal; }`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Custom location blocks
|
|
255
|
+
if (advanced?.customLocations && advanced.customLocations.trim()) {
|
|
256
|
+
mainBlock.push(``);
|
|
257
|
+
mainBlock.push(` # Custom locations`);
|
|
258
|
+
mainBlock.push(advanced.customLocations);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
mainBlock.push(`}`);
|
|
262
|
+
|
|
263
|
+
sections.push(mainBlock.join('\n'));
|
|
264
|
+
|
|
265
|
+
return sections.join('\n\n') + '\n';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── FILE GENERATION ──────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Generates and writes an nginx conf file for a domain.
|
|
272
|
+
* @param {Object} domain - Domain configuration object
|
|
273
|
+
* @returns {Promise<string>} Path to the generated conf file
|
|
274
|
+
*/
|
|
275
|
+
export async function generateConf(domain) {
|
|
276
|
+
const { nginxDir, certbotDir } = loadConfig();
|
|
277
|
+
const confPath = path.join(nginxDir, 'conf.d', `${domain.name}.conf`);
|
|
278
|
+
const confContent = buildConf(domain, nginxDir, certbotDir);
|
|
279
|
+
|
|
280
|
+
// Ensure conf.d directory exists
|
|
281
|
+
await fs.mkdir(path.dirname(confPath), { recursive: true });
|
|
282
|
+
await fs.writeFile(confPath, confContent, 'utf8');
|
|
283
|
+
|
|
284
|
+
// Update domain with config file path
|
|
285
|
+
domain.configFile = confPath;
|
|
286
|
+
domain.updatedAt = new Date().toISOString();
|
|
287
|
+
|
|
288
|
+
return confPath;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Generates a default certbot path for a domain based on platform.
|
|
293
|
+
* @param {string} domainName - Domain name
|
|
294
|
+
* @param {string} platform - 'win32' or 'linux'
|
|
295
|
+
* @param {string} certbotDir - Certbot directory from config
|
|
296
|
+
* @returns {Object} { certPath, keyPath }
|
|
297
|
+
*/
|
|
298
|
+
export function getDefaultCertPaths(domainName, platform, certbotDir) {
|
|
299
|
+
if (platform === 'win32') {
|
|
300
|
+
return {
|
|
301
|
+
certPath: `C:\\Certbot\\live\\${domainName}\\fullchain.pem`,
|
|
302
|
+
keyPath: `C:\\Certbot\\live\\${domainName}\\privkey.pem`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
certPath: `${certbotDir}/live/${domainName}/fullchain.pem`,
|
|
307
|
+
keyPath: `${certbotDir}/live/${domainName}/privkey.pem`,
|
|
308
|
+
};
|
|
309
|
+
}
|
package/core/shell.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/shell.js
|
|
3
|
+
*
|
|
4
|
+
* Cross-platform shell command executor for Easy DevOps.
|
|
5
|
+
*
|
|
6
|
+
* All modules must use this utility instead of calling child_process APIs directly.
|
|
7
|
+
*
|
|
8
|
+
* Exported functions:
|
|
9
|
+
* - getShell() — Returns OS-resolved shell descriptor { shell, flag }
|
|
10
|
+
* - run(cmd, options) — Executes a command and captures output; never throws
|
|
11
|
+
* - runLive(cmd, options) — Executes a command, streaming output to the terminal; never throws
|
|
12
|
+
*
|
|
13
|
+
* CommandResult shape (returned by run()):
|
|
14
|
+
* { success: boolean, stdout: string, stderr: string, exitCode: number|null, command: string }
|
|
15
|
+
*
|
|
16
|
+
* CommandOptions (accepted by run() and runLive()):
|
|
17
|
+
* { timeout?: number, cwd?: string }
|
|
18
|
+
* Defaults: timeout = 30 000 ms, cwd = process.cwd()
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { spawn } from 'child_process';
|
|
22
|
+
|
|
23
|
+
// ─── internal helpers ─────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function stripAnsi(str) {
|
|
26
|
+
return str.replace(/\x1B\[[0-9;]*[mGKHFJ]/g, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── getShell ─────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns the OS-appropriate shell descriptor.
|
|
33
|
+
*
|
|
34
|
+
* On Windows (win32) returns PowerShell; on all other platforms returns bash.
|
|
35
|
+
*
|
|
36
|
+
* @returns {{ shell: string, flag: string }}
|
|
37
|
+
*/
|
|
38
|
+
export function getShell() {
|
|
39
|
+
if (process.platform === 'win32') {
|
|
40
|
+
return { shell: 'powershell', flag: '-Command' };
|
|
41
|
+
}
|
|
42
|
+
return { shell: 'bash', flag: '-c' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── run ──────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Executes a shell command and captures its stdout and stderr.
|
|
49
|
+
*
|
|
50
|
+
* Never throws. ANSI escape codes are stripped from all captured output.
|
|
51
|
+
* When the timeout expires the child process is killed and exitCode is null.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} cmd - The command string to execute.
|
|
54
|
+
* @param {{ timeout?: number, cwd?: string }} [options]
|
|
55
|
+
* @param {number} [options.timeout=30000] - Milliseconds before the process is killed.
|
|
56
|
+
* @param {string} [options.cwd=process.cwd()] - Working directory for the child process.
|
|
57
|
+
* @returns {Promise<{ success: boolean, stdout: string, stderr: string, exitCode: number|null, command: string }>}
|
|
58
|
+
*/
|
|
59
|
+
export function run(cmd, options = {}) {
|
|
60
|
+
const { timeout = 30000, cwd = process.cwd() } = options;
|
|
61
|
+
const { shell, flag } = getShell();
|
|
62
|
+
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
const ac = new AbortController();
|
|
65
|
+
const timer = setTimeout(() => ac.abort(), timeout);
|
|
66
|
+
|
|
67
|
+
const child = spawn(shell, [flag, cmd], { cwd, signal: ac.signal, encoding: 'utf8', windowsHide: true });
|
|
68
|
+
|
|
69
|
+
let stdoutBuf = '';
|
|
70
|
+
let stderrBuf = '';
|
|
71
|
+
|
|
72
|
+
child.stdout.on('data', (chunk) => { stdoutBuf += chunk; });
|
|
73
|
+
child.stderr.on('data', (chunk) => { stderrBuf += chunk; });
|
|
74
|
+
|
|
75
|
+
child.on('close', (exitCode) => {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
resolve({
|
|
78
|
+
success: exitCode === 0,
|
|
79
|
+
stdout: stripAnsi(stdoutBuf).trim(),
|
|
80
|
+
stderr: stripAnsi(stderrBuf).trim(),
|
|
81
|
+
exitCode,
|
|
82
|
+
command: cmd,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
child.on('error', (err) => {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
if (err.name === 'AbortError') {
|
|
89
|
+
resolve({
|
|
90
|
+
success: false,
|
|
91
|
+
stdout: '',
|
|
92
|
+
stderr: `Timeout after ${timeout}ms`,
|
|
93
|
+
exitCode: null,
|
|
94
|
+
command: cmd,
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
resolve({
|
|
98
|
+
success: false,
|
|
99
|
+
stdout: '',
|
|
100
|
+
stderr: err.message,
|
|
101
|
+
exitCode: null,
|
|
102
|
+
command: cmd,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── runLive ──────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Executes a shell command, streaming stdout and stderr directly to the terminal.
|
|
113
|
+
*
|
|
114
|
+
* Never throws. Returns the process exit code, or null if the process was killed
|
|
115
|
+
* (e.g. timeout) or an error occurred.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} cmd - The command string to execute.
|
|
118
|
+
* @param {{ timeout?: number, cwd?: string }} [options]
|
|
119
|
+
* @param {number} [options.timeout=30000] - Milliseconds before the process is killed.
|
|
120
|
+
* @param {string} [options.cwd=process.cwd()] - Working directory for the child process.
|
|
121
|
+
* @returns {Promise<number|null>}
|
|
122
|
+
*/
|
|
123
|
+
export function runLive(cmd, options = {}) {
|
|
124
|
+
const { timeout = 30000, cwd = process.cwd() } = options;
|
|
125
|
+
const { shell, flag } = getShell();
|
|
126
|
+
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
const ac = new AbortController();
|
|
129
|
+
const timer = setTimeout(() => ac.abort(), timeout);
|
|
130
|
+
|
|
131
|
+
const child = spawn(shell, [flag, cmd], {
|
|
132
|
+
cwd,
|
|
133
|
+
signal: ac.signal,
|
|
134
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
135
|
+
windowsHide: true,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
child.on('close', (exitCode) => {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
resolve(exitCode);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
child.on('error', (err) => {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
if (err.name !== 'AbortError') {
|
|
146
|
+
process.stderr.write(err.message);
|
|
147
|
+
}
|
|
148
|
+
resolve(null);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { run } from '../../core/shell.js';
|
|
4
|
+
import { loadConfig } from '../../core/config.js';
|
|
5
|
+
|
|
6
|
+
export async function listAllCerts() {
|
|
7
|
+
const { certbotDir } = loadConfig();
|
|
8
|
+
const liveDir = path.join(certbotDir, 'live');
|
|
9
|
+
let entries;
|
|
10
|
+
try {
|
|
11
|
+
entries = await fs.readdir(liveDir, { withFileTypes: true });
|
|
12
|
+
} catch (err) {
|
|
13
|
+
if (err.code === 'ENOENT') return [];
|
|
14
|
+
throw err;
|
|
15
|
+
}
|
|
16
|
+
const dirs = entries.filter(e => e.isDirectory());
|
|
17
|
+
const certs = [];
|
|
18
|
+
for (const dir of dirs) {
|
|
19
|
+
const { expiry, daysLeft } = await getCertExpiry(dir.name);
|
|
20
|
+
let status;
|
|
21
|
+
if (daysLeft === null) {
|
|
22
|
+
status = 'error';
|
|
23
|
+
} else if (daysLeft <= 0) {
|
|
24
|
+
status = 'expired';
|
|
25
|
+
} else if (daysLeft < 30) {
|
|
26
|
+
status = 'expiring';
|
|
27
|
+
} else {
|
|
28
|
+
status = 'valid';
|
|
29
|
+
}
|
|
30
|
+
certs.push({ domain: dir.name, expiry: expiry?.toISOString() ?? null, daysLeft, status });
|
|
31
|
+
}
|
|
32
|
+
return certs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getCertExpiry(name) {
|
|
36
|
+
const { certbotDir } = loadConfig();
|
|
37
|
+
const certPath = path.join(certbotDir, 'live', name, 'cert.pem');
|
|
38
|
+
|
|
39
|
+
const result = await run(`openssl x509 -enddate -noout -in "${certPath}"`);
|
|
40
|
+
|
|
41
|
+
if (result.success && result.stdout) {
|
|
42
|
+
const match = result.stdout.match(/notAfter=(.+)/);
|
|
43
|
+
if (match) {
|
|
44
|
+
const expiry = new Date(match[1].trim());
|
|
45
|
+
const daysLeft = Math.floor((expiry - Date.now()) / 86400000);
|
|
46
|
+
return { expiry, daysLeft, valid: daysLeft > 0 };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Fallback: mtime + 90 days if file exists but openssl failed
|
|
51
|
+
try {
|
|
52
|
+
const stat = await fs.stat(certPath);
|
|
53
|
+
const expiry = new Date(stat.mtime.getTime() + 90 * 86400000);
|
|
54
|
+
const daysLeft = Math.floor((expiry - Date.now()) / 86400000);
|
|
55
|
+
return { expiry, daysLeft, valid: daysLeft > 0 };
|
|
56
|
+
} catch {
|
|
57
|
+
return { expiry: null, daysLeft: null, valid: null };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { dbGet, dbSet } from '../../core/db.js';
|
|
2
|
+
import { DOMAIN_DEFAULTS, migrateDomain } from '../../core/nginx-conf-generator.js';
|
|
3
|
+
|
|
4
|
+
// Re-export DOMAIN_DEFAULTS for consumers
|
|
5
|
+
export { DOMAIN_DEFAULTS, migrateDomain };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get all domains, applying v2 migration to each record.
|
|
9
|
+
* @returns {Array} Array of domain objects (v2 schema)
|
|
10
|
+
*/
|
|
11
|
+
export function getDomains() {
|
|
12
|
+
const raw = dbGet('domains') ?? [];
|
|
13
|
+
return raw.map(migrateDomain);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Save domains array to storage.
|
|
18
|
+
* @param {Array} arr - Array of domain objects
|
|
19
|
+
*/
|
|
20
|
+
export function saveDomains(arr) {
|
|
21
|
+
dbSet('domains', arr);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find a domain by name.
|
|
26
|
+
* @param {string} name - Domain name to find
|
|
27
|
+
* @returns {Object|undefined} Domain object or undefined
|
|
28
|
+
*/
|
|
29
|
+
export function findDomain(name) {
|
|
30
|
+
return getDomains().find((d) => d.name === name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a new domain object with defaults merged.
|
|
35
|
+
* @param {Object} partial - Partial domain data
|
|
36
|
+
* @returns {Object} Complete domain object with defaults
|
|
37
|
+
*/
|
|
38
|
+
export function createDomain(partial) {
|
|
39
|
+
const now = new Date().toISOString();
|
|
40
|
+
return {
|
|
41
|
+
...DOMAIN_DEFAULTS,
|
|
42
|
+
...partial,
|
|
43
|
+
ssl: { ...DOMAIN_DEFAULTS.ssl, ...partial.ssl },
|
|
44
|
+
performance: { ...DOMAIN_DEFAULTS.performance, ...partial.performance },
|
|
45
|
+
security: { ...DOMAIN_DEFAULTS.security, ...partial.security },
|
|
46
|
+
advanced: { ...DOMAIN_DEFAULTS.advanced, ...partial.advanced },
|
|
47
|
+
configFile: null,
|
|
48
|
+
createdAt: now,
|
|
49
|
+
updatedAt: now,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard/lib/nginx-conf-generator.js
|
|
3
|
+
*
|
|
4
|
+
* Re-exports from core/nginx-conf-generator.js for backward compatibility.
|
|
5
|
+
* Dashboard routes should import from either location:
|
|
6
|
+
* - import { generateConf } from '../lib/nginx-conf-generator.js' (existing imports)
|
|
7
|
+
* - import { generateConf } from '../../core/nginx-conf-generator.js' (CLI pattern)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
generateConf,
|
|
12
|
+
buildConf,
|
|
13
|
+
migrateDomain,
|
|
14
|
+
DOMAIN_DEFAULTS,
|
|
15
|
+
getDefaultCertPaths,
|
|
16
|
+
} from '../../core/nginx-conf-generator.js';
|