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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -0
  3. package/cli/index.js +91 -0
  4. package/cli/managers/domain-manager.js +451 -0
  5. package/cli/managers/nginx-manager.js +329 -0
  6. package/cli/managers/node-manager.js +275 -0
  7. package/cli/managers/ssl-manager.js +397 -0
  8. package/cli/menus/.gitkeep +0 -0
  9. package/cli/menus/dashboard.js +223 -0
  10. package/cli/menus/domains.js +5 -0
  11. package/cli/menus/nginx.js +5 -0
  12. package/cli/menus/nodejs.js +5 -0
  13. package/cli/menus/settings.js +83 -0
  14. package/cli/menus/ssl.js +5 -0
  15. package/core/config.js +37 -0
  16. package/core/db.js +30 -0
  17. package/core/detector.js +257 -0
  18. package/core/nginx-conf-generator.js +309 -0
  19. package/core/shell.js +151 -0
  20. package/dashboard/lib/.gitkeep +0 -0
  21. package/dashboard/lib/cert-reader.js +59 -0
  22. package/dashboard/lib/domains-db.js +51 -0
  23. package/dashboard/lib/nginx-conf-generator.js +16 -0
  24. package/dashboard/lib/nginx-service.js +282 -0
  25. package/dashboard/public/js/app.js +486 -0
  26. package/dashboard/routes/.gitkeep +0 -0
  27. package/dashboard/routes/auth.js +30 -0
  28. package/dashboard/routes/domains.js +300 -0
  29. package/dashboard/routes/nginx.js +151 -0
  30. package/dashboard/routes/settings.js +78 -0
  31. package/dashboard/routes/ssl.js +105 -0
  32. package/dashboard/server.js +79 -0
  33. package/dashboard/views/index.ejs +327 -0
  34. package/dashboard/views/partials/domain-form.ejs +229 -0
  35. package/dashboard/views/partials/domains-panel.ejs +66 -0
  36. package/dashboard/views/partials/login.ejs +50 -0
  37. package/dashboard/views/partials/nginx-panel.ejs +90 -0
  38. package/dashboard/views/partials/overview.ejs +67 -0
  39. package/dashboard/views/partials/settings-panel.ejs +37 -0
  40. package/dashboard/views/partials/sidebar.ejs +45 -0
  41. package/dashboard/views/partials/ssl-panel.ejs +53 -0
  42. package/data/.gitkeep +0 -0
  43. package/install.bat +41 -0
  44. package/install.ps1 +653 -0
  45. package/install.sh +452 -0
  46. package/lib/installer/.gitkeep +0 -0
  47. package/lib/installer/detect.sh +88 -0
  48. package/lib/installer/node-versions.sh +109 -0
  49. package/lib/installer/nvm-bootstrap.sh +77 -0
  50. package/lib/installer/picker.sh +163 -0
  51. package/lib/installer/progress.sh +25 -0
  52. 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';