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,486 @@
|
|
|
1
|
+
/* global Vue, io */
|
|
2
|
+
const { createApp } = Vue;
|
|
3
|
+
|
|
4
|
+
// Default form structure matching DOMAIN_DEFAULTS
|
|
5
|
+
const DEFAULT_FORM = {
|
|
6
|
+
name: '',
|
|
7
|
+
port: 3000,
|
|
8
|
+
backendHost: '127.0.0.1',
|
|
9
|
+
upstreamType: 'http',
|
|
10
|
+
www: false,
|
|
11
|
+
ssl: {
|
|
12
|
+
enabled: false,
|
|
13
|
+
certPath: '',
|
|
14
|
+
keyPath: '',
|
|
15
|
+
redirect: true,
|
|
16
|
+
hsts: false,
|
|
17
|
+
hstsMaxAge: 31536000,
|
|
18
|
+
},
|
|
19
|
+
performance: {
|
|
20
|
+
maxBodySize: '10m',
|
|
21
|
+
readTimeout: 60,
|
|
22
|
+
connectTimeout: 10,
|
|
23
|
+
proxyBuffers: false,
|
|
24
|
+
gzip: true,
|
|
25
|
+
gzipTypes: 'text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript',
|
|
26
|
+
},
|
|
27
|
+
security: {
|
|
28
|
+
rateLimit: false,
|
|
29
|
+
rateLimitRate: 10,
|
|
30
|
+
rateLimitBurst: 20,
|
|
31
|
+
securityHeaders: false,
|
|
32
|
+
custom404: false,
|
|
33
|
+
custom50x: false,
|
|
34
|
+
},
|
|
35
|
+
advanced: {
|
|
36
|
+
accessLog: true,
|
|
37
|
+
customLocations: '',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
createApp({
|
|
42
|
+
data() {
|
|
43
|
+
return {
|
|
44
|
+
theme: localStorage.getItem('ed-theme') || 'dark',
|
|
45
|
+
authenticated: false,
|
|
46
|
+
page: 'overview',
|
|
47
|
+
|
|
48
|
+
login: { password: '', error: '', loading: false },
|
|
49
|
+
|
|
50
|
+
navItems: [
|
|
51
|
+
{ id: 'overview', label: 'Overview', icon: '⚡' },
|
|
52
|
+
{ id: 'nginx', label: 'Nginx', icon: '🌐' },
|
|
53
|
+
{ id: 'ssl', label: 'SSL Certs', icon: '🔒' },
|
|
54
|
+
{ id: 'domains', label: 'Domains', icon: '🔗' },
|
|
55
|
+
{ id: 'settings', label: 'Settings', icon: '⚙️' },
|
|
56
|
+
],
|
|
57
|
+
|
|
58
|
+
nginx: {
|
|
59
|
+
status: null, loading: false,
|
|
60
|
+
actionMsg: '', actionError: '',
|
|
61
|
+
configs: [], selectedConfig: '',
|
|
62
|
+
configContent: '', configSaving: false, configMsg: '',
|
|
63
|
+
logs: [], logsLoading: false,
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
ssl: { certs: [], loading: false, error: '', renewingDomain: null },
|
|
67
|
+
|
|
68
|
+
// T017-T019: Updated domains state
|
|
69
|
+
domains: {
|
|
70
|
+
list: [], loading: false, error: '',
|
|
71
|
+
showForm: false,
|
|
72
|
+
editingName: null, // T017: null = adding, string = editing
|
|
73
|
+
saving: false,
|
|
74
|
+
dirty: false, // T019: Track unsaved changes
|
|
75
|
+
form: JSON.parse(JSON.stringify(DEFAULT_FORM)), // T025: Nested v2 structure
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// T018: Collapsible section state
|
|
79
|
+
sections: {
|
|
80
|
+
basic: true,
|
|
81
|
+
ssl: true,
|
|
82
|
+
performance: false,
|
|
83
|
+
security: false,
|
|
84
|
+
advanced: false,
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
settings: {
|
|
88
|
+
dashboardPort: '', nginxDir: '', certbotDir: '',
|
|
89
|
+
password: '', loading: false, msg: '', error: '',
|
|
90
|
+
platform: 'linux', // T022: Platform from settings API
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
computed: {
|
|
96
|
+
isDark() { return this.theme === 'dark'; },
|
|
97
|
+
expiringCount() { return this.ssl.certs.filter(c => c.daysLeft !== null && c.daysLeft < 30).length; },
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
watch: {
|
|
101
|
+
theme(val) {
|
|
102
|
+
localStorage.setItem('ed-theme', val);
|
|
103
|
+
document.documentElement.classList.toggle('dark', val === 'dark');
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// T019: Deep watch form for dirty tracking
|
|
107
|
+
'domains.form': {
|
|
108
|
+
handler() {
|
|
109
|
+
if (this.domains.showForm) {
|
|
110
|
+
this.domains.dirty = true;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
deep: true,
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// T024: Auto-populate SSL paths when SSL is toggled on
|
|
117
|
+
'domains.form.ssl.enabled'(newVal) {
|
|
118
|
+
if (newVal) {
|
|
119
|
+
this.autoPopulateCertPaths();
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// T020: beforeunload guard for unsaved changes
|
|
125
|
+
beforeMount() {
|
|
126
|
+
window.addEventListener('beforeunload', (e) => {
|
|
127
|
+
if (this.domains.dirty && this.domains.showForm) {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
e.returnValue = '';
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
async mounted() {
|
|
135
|
+
document.documentElement.classList.toggle('dark', this.theme === 'dark');
|
|
136
|
+
|
|
137
|
+
// Check if already authenticated
|
|
138
|
+
try {
|
|
139
|
+
const r = await fetch('/api/auth');
|
|
140
|
+
const d = await r.json();
|
|
141
|
+
if (d.authenticated) { this.authenticated = true; this.loadPage('overview'); }
|
|
142
|
+
} catch { /* server may not be up yet */ }
|
|
143
|
+
|
|
144
|
+
// T022: Load settings to get platform for SSL path auto-population
|
|
145
|
+
await this.loadSettings();
|
|
146
|
+
|
|
147
|
+
// Real-time nginx status via Socket.io
|
|
148
|
+
const socket = io();
|
|
149
|
+
socket.on('nginx:status', (status) => {
|
|
150
|
+
this.nginx.status = status;
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
methods: {
|
|
155
|
+
// ── Core API helper ────────────────────────────────────────────────────────
|
|
156
|
+
async api(method, path, body) {
|
|
157
|
+
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
|
158
|
+
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
159
|
+
const r = await fetch(path, opts);
|
|
160
|
+
const data = await r.json().catch(() => ({}));
|
|
161
|
+
return { ok: r.ok, status: r.status, data };
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// ── Theme ─────────────────────────────────────────────────────────────────
|
|
165
|
+
toggleTheme() { this.theme = this.theme === 'dark' ? 'light' : 'dark'; },
|
|
166
|
+
|
|
167
|
+
// ── Auth ──────────────────────────────────────────────────────────────────
|
|
168
|
+
async doLogin() {
|
|
169
|
+
this.login.loading = true; this.login.error = '';
|
|
170
|
+
const r = await this.api('POST', '/api/login', { password: this.login.password });
|
|
171
|
+
this.login.loading = false;
|
|
172
|
+
if (r.ok) { this.authenticated = true; this.login.password = ''; this.loadPage('overview'); }
|
|
173
|
+
else { this.login.error = r.data.error || 'Invalid password'; }
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async doLogout() {
|
|
177
|
+
await this.api('POST', '/api/logout');
|
|
178
|
+
this.authenticated = false;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// ── Navigation (T021: Nav click guard) ────────────────────────────────────
|
|
182
|
+
async navigateTo(pageId) {
|
|
183
|
+
// T021: Check for unsaved changes before navigation
|
|
184
|
+
if (this.domains.dirty && this.domains.showForm) {
|
|
185
|
+
if (!confirm('You have unsaved changes. Discard them and continue?')) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.resetDomainForm();
|
|
189
|
+
}
|
|
190
|
+
await this.loadPage(pageId);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
async loadPage(p) {
|
|
194
|
+
this.page = p;
|
|
195
|
+
if (p === 'overview') {
|
|
196
|
+
await Promise.all([this.loadNginxStatus(), this.loadSSL(), this.loadDomains()]);
|
|
197
|
+
} else if (p === 'nginx') {
|
|
198
|
+
await Promise.all([this.loadNginxStatus(), this.loadNginxConfigs(), this.loadNginxLogs()]);
|
|
199
|
+
} else if (p === 'ssl') { await this.loadSSL(); }
|
|
200
|
+
else if (p === 'domains') { await this.loadDomains(); }
|
|
201
|
+
else if (p === 'settings') { await this.loadSettings(); }
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// ── Collapsible Sections ────────────────────────────────────────────
|
|
205
|
+
toggleSection(sectionId) {
|
|
206
|
+
this.sections[sectionId] = !this.sections[sectionId];
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// ── Nginx ─────────────────────────────────────────────────────────────────
|
|
210
|
+
async loadNginxStatus() {
|
|
211
|
+
this.nginx.loading = true;
|
|
212
|
+
const r = await this.api('GET', '/api/nginx/status');
|
|
213
|
+
this.nginx.loading = false;
|
|
214
|
+
if (r.ok) this.nginx.status = r.data;
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
async nginxAction(action) {
|
|
218
|
+
this.nginx.actionMsg = ''; this.nginx.actionError = '';
|
|
219
|
+
const r = await this.api('POST', `/api/nginx/${action}`);
|
|
220
|
+
if (r.ok) { this.nginx.actionMsg = r.data.output || `${action} successful`; await this.loadNginxStatus(); }
|
|
221
|
+
else { this.nginx.actionError = r.data.output || r.data.error || `${action} failed`; }
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
async loadNginxConfigs() {
|
|
225
|
+
const r = await this.api('GET', '/api/nginx/configs');
|
|
226
|
+
if (r.ok) this.nginx.configs = r.data;
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
async selectConfig(filename) {
|
|
230
|
+
if (!filename) return;
|
|
231
|
+
this.nginx.configMsg = '';
|
|
232
|
+
const r = await this.api('GET', `/api/nginx/config/${filename}`);
|
|
233
|
+
if (r.ok) this.nginx.configContent = r.data.content;
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
async saveNginxConfig() {
|
|
237
|
+
if (!this.nginx.selectedConfig) return;
|
|
238
|
+
this.nginx.configSaving = true; this.nginx.configMsg = '';
|
|
239
|
+
const r = await this.api('POST', `/api/nginx/config/${this.nginx.selectedConfig}`, { content: this.nginx.configContent });
|
|
240
|
+
this.nginx.configSaving = false;
|
|
241
|
+
this.nginx.configMsg = r.ok
|
|
242
|
+
? 'Saved — config test passed ✓'
|
|
243
|
+
: (r.data.output || r.data.error || 'Save failed');
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
async loadNginxLogs() {
|
|
247
|
+
this.nginx.logsLoading = true;
|
|
248
|
+
const r = await this.api('GET', '/api/nginx/logs');
|
|
249
|
+
this.nginx.logsLoading = false;
|
|
250
|
+
if (r.ok) this.nginx.logs = r.data.lines || [];
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
// ── SSL ───────────────────────────────────────────────────────────────────
|
|
254
|
+
async loadSSL() {
|
|
255
|
+
this.ssl.loading = true; this.ssl.error = '';
|
|
256
|
+
const r = await this.api('GET', '/api/ssl');
|
|
257
|
+
this.ssl.loading = false;
|
|
258
|
+
if (r.ok) this.ssl.certs = Array.isArray(r.data) ? r.data : [];
|
|
259
|
+
else this.ssl.error = r.data.error || 'Failed to load certificates';
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
async renewCert(domain) {
|
|
263
|
+
this.ssl.renewingDomain = domain;
|
|
264
|
+
await this.api('POST', `/api/ssl/renew/${domain}`);
|
|
265
|
+
this.ssl.renewingDomain = null;
|
|
266
|
+
await this.loadSSL();
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
async renewAll() {
|
|
270
|
+
this.ssl.renewingDomain = 'all';
|
|
271
|
+
await this.api('POST', '/api/ssl/renew-all');
|
|
272
|
+
this.ssl.renewingDomain = null;
|
|
273
|
+
await this.loadSSL();
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
certStatus(cert) {
|
|
277
|
+
if (cert.daysLeft === null || cert.daysLeft === undefined) return 'Unknown';
|
|
278
|
+
if (cert.daysLeft > 30) return 'Healthy';
|
|
279
|
+
if (cert.daysLeft >= 10) return 'Expiring';
|
|
280
|
+
return 'Critical';
|
|
281
|
+
},
|
|
282
|
+
certDaysColor(cert) {
|
|
283
|
+
if (cert.daysLeft === null || cert.daysLeft === undefined) return 'text-gray-400';
|
|
284
|
+
if (cert.daysLeft > 30) return 'text-green-500';
|
|
285
|
+
if (cert.daysLeft >= 10) return 'text-yellow-500';
|
|
286
|
+
return 'text-red-500';
|
|
287
|
+
},
|
|
288
|
+
certBadgeClass(cert) {
|
|
289
|
+
const s = this.certStatus(cert);
|
|
290
|
+
if (s === 'Healthy') return 'bg-green-100 dark:bg-green-950/60 text-green-700 dark:text-green-400';
|
|
291
|
+
if (s === 'Expiring') return 'bg-yellow-100 dark:bg-yellow-950/60 text-yellow-700 dark:text-yellow-400';
|
|
292
|
+
if (s === 'Critical') return 'bg-red-100 dark:bg-red-950/60 text-red-700 dark:text-red-400';
|
|
293
|
+
return 'bg-gray-100 dark:bg-gray-800 text-gray-500';
|
|
294
|
+
},
|
|
295
|
+
certBadgeType(cert) {
|
|
296
|
+
const s = this.certStatus(cert);
|
|
297
|
+
if (s === 'Healthy') return 'success';
|
|
298
|
+
if (s === 'Expiring') return 'warning';
|
|
299
|
+
if (s === 'Critical') return 'danger';
|
|
300
|
+
return 'neutral';
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
// ── Domains ───────────────────────────────────────────────────────────────
|
|
304
|
+
async loadDomains() {
|
|
305
|
+
this.domains.loading = true;
|
|
306
|
+
const r = await this.api('GET', '/api/domains');
|
|
307
|
+
this.domains.loading = false;
|
|
308
|
+
if (r.ok) this.domains.list = r.data;
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
toggleDomainForm() {
|
|
312
|
+
if (this.domains.showForm && this.domains.dirty) {
|
|
313
|
+
if (!confirm('Discard unsaved changes?')) return;
|
|
314
|
+
}
|
|
315
|
+
this.domains.showForm = !this.domains.showForm;
|
|
316
|
+
if (!this.domains.showForm) {
|
|
317
|
+
this.resetDomainForm();
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
resetDomainForm() {
|
|
322
|
+
this.domains.form = JSON.parse(JSON.stringify(DEFAULT_FORM));
|
|
323
|
+
this.domains.editingName = null;
|
|
324
|
+
this.domains.dirty = false;
|
|
325
|
+
this.domains.error = '';
|
|
326
|
+
// Reset sections
|
|
327
|
+
this.sections = {
|
|
328
|
+
basic: true,
|
|
329
|
+
ssl: true,
|
|
330
|
+
performance: false,
|
|
331
|
+
security: false,
|
|
332
|
+
advanced: false,
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
// T023: editDomain method
|
|
337
|
+
async editDomain(name) {
|
|
338
|
+
// Check for unsaved changes
|
|
339
|
+
if (this.domains.dirty && this.domains.showForm) {
|
|
340
|
+
if (!confirm('Discard unsaved changes?')) return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Fetch domain data
|
|
344
|
+
const r = await this.api('GET', `/api/domains`);
|
|
345
|
+
if (!r.ok) {
|
|
346
|
+
this.domains.error = 'Failed to load domain data';
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const domain = r.data.find(d => d.name === name);
|
|
351
|
+
if (!domain) {
|
|
352
|
+
this.domains.error = 'Domain not found';
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Populate form with domain data
|
|
357
|
+
this.domains.form = {
|
|
358
|
+
name: domain.name,
|
|
359
|
+
port: domain.port,
|
|
360
|
+
backendHost: domain.backendHost || '127.0.0.1',
|
|
361
|
+
upstreamType: domain.upstreamType || 'http',
|
|
362
|
+
www: domain.www || false,
|
|
363
|
+
ssl: {
|
|
364
|
+
enabled: domain.ssl?.enabled || false,
|
|
365
|
+
certPath: domain.ssl?.certPath || '',
|
|
366
|
+
keyPath: domain.ssl?.keyPath || '',
|
|
367
|
+
redirect: domain.ssl?.redirect ?? true,
|
|
368
|
+
hsts: domain.ssl?.hsts || false,
|
|
369
|
+
hstsMaxAge: domain.ssl?.hstsMaxAge || 31536000,
|
|
370
|
+
},
|
|
371
|
+
performance: {
|
|
372
|
+
maxBodySize: domain.performance?.maxBodySize || '10m',
|
|
373
|
+
readTimeout: domain.performance?.readTimeout || 60,
|
|
374
|
+
connectTimeout: domain.performance?.connectTimeout || 10,
|
|
375
|
+
proxyBuffers: domain.performance?.proxyBuffers || false,
|
|
376
|
+
gzip: domain.performance?.gzip ?? true,
|
|
377
|
+
gzipTypes: domain.performance?.gzipTypes || DEFAULT_FORM.performance.gzipTypes,
|
|
378
|
+
},
|
|
379
|
+
security: {
|
|
380
|
+
rateLimit: domain.security?.rateLimit || false,
|
|
381
|
+
rateLimitRate: domain.security?.rateLimitRate || 10,
|
|
382
|
+
rateLimitBurst: domain.security?.rateLimitBurst || 20,
|
|
383
|
+
securityHeaders: domain.security?.securityHeaders || false,
|
|
384
|
+
custom404: domain.security?.custom404 || false,
|
|
385
|
+
custom50x: domain.security?.custom50x || false,
|
|
386
|
+
},
|
|
387
|
+
advanced: {
|
|
388
|
+
accessLog: domain.advanced?.accessLog ?? true,
|
|
389
|
+
customLocations: domain.advanced?.customLocations || '',
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
this.domains.editingName = name;
|
|
394
|
+
this.domains.showForm = true;
|
|
395
|
+
this.domains.dirty = false;
|
|
396
|
+
|
|
397
|
+
// Expand all sections for editing
|
|
398
|
+
this.sections = {
|
|
399
|
+
basic: true,
|
|
400
|
+
ssl: true,
|
|
401
|
+
performance: true,
|
|
402
|
+
security: true,
|
|
403
|
+
advanced: true,
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
async saveDomain() {
|
|
408
|
+
this.domains.error = '';
|
|
409
|
+
this.domains.saving = true;
|
|
410
|
+
|
|
411
|
+
const method = this.domains.editingName ? 'PUT' : 'POST';
|
|
412
|
+
const url = this.domains.editingName
|
|
413
|
+
? `/api/domains/${encodeURIComponent(this.domains.editingName)}`
|
|
414
|
+
: '/api/domains';
|
|
415
|
+
|
|
416
|
+
const r = await this.api(method, url, this.domains.form);
|
|
417
|
+
this.domains.saving = false;
|
|
418
|
+
|
|
419
|
+
if (r.ok) {
|
|
420
|
+
this.domains.showForm = false;
|
|
421
|
+
this.domains.dirty = false;
|
|
422
|
+
this.domains.editingName = null;
|
|
423
|
+
this.resetDomainForm();
|
|
424
|
+
await this.loadDomains();
|
|
425
|
+
} else {
|
|
426
|
+
this.domains.error = r.data.error || 'Failed to save domain';
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
async deleteDomain(name) {
|
|
431
|
+
if (!confirm(`Delete domain "${name}"? This cannot be undone.`)) return;
|
|
432
|
+
await this.api('DELETE', `/api/domains/${name}`);
|
|
433
|
+
await this.loadDomains();
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
async reloadDomain(name) {
|
|
437
|
+
await this.api('POST', `/api/domains/${name}/reload`);
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
// T024: Auto-populate SSL paths when SSL is enabled
|
|
441
|
+
autoPopulateCertPaths() {
|
|
442
|
+
if (this.domains.form.ssl.enabled) {
|
|
443
|
+
if (!this.domains.form.ssl.certPath || !this.domains.form.ssl.keyPath) {
|
|
444
|
+
const domain = this.domains.form.name;
|
|
445
|
+
if (domain) {
|
|
446
|
+
const platform = this.settings.platform;
|
|
447
|
+
if (platform === 'win32') {
|
|
448
|
+
this.domains.form.ssl.certPath = `C:\\Certbot\\live\\${domain}\\fullchain.pem`;
|
|
449
|
+
this.domains.form.ssl.keyPath = `C:\\Certbot\\live\\${domain}\\privkey.pem`;
|
|
450
|
+
} else {
|
|
451
|
+
this.domains.form.ssl.certPath = `/etc/letsencrypt/live/${domain}/fullchain.pem`;
|
|
452
|
+
this.domains.form.ssl.keyPath = `/etc/letsencrypt/live/${domain}/privkey.pem`;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
// ── Settings (T022: Updated to capture platform) ───────────────────────────
|
|
460
|
+
async loadSettings() {
|
|
461
|
+
this.settings.loading = true;
|
|
462
|
+
const r = await this.api('GET', '/api/settings');
|
|
463
|
+
this.settings.loading = false;
|
|
464
|
+
if (r.ok) {
|
|
465
|
+
this.settings.dashboardPort = r.data.dashboardPort;
|
|
466
|
+
this.settings.nginxDir = r.data.nginxDir;
|
|
467
|
+
this.settings.certbotDir = r.data.certbotDir;
|
|
468
|
+
this.settings.platform = r.data.platform || 'linux'; // T022
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
async saveSettings() {
|
|
473
|
+
this.settings.loading = true; this.settings.msg = ''; this.settings.error = '';
|
|
474
|
+
const payload = {
|
|
475
|
+
dashboardPort: this.settings.dashboardPort,
|
|
476
|
+
nginxDir: this.settings.nginxDir,
|
|
477
|
+
certbotDir: this.settings.certbotDir,
|
|
478
|
+
};
|
|
479
|
+
if (this.settings.password) payload.dashboardPassword = this.settings.password;
|
|
480
|
+
const r = await this.api('POST', '/api/settings', payload);
|
|
481
|
+
this.settings.loading = false;
|
|
482
|
+
if (r.ok) { this.settings.msg = 'Settings saved ✓'; this.settings.password = ''; }
|
|
483
|
+
else { this.settings.error = r.data.error || 'Save failed'; }
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
}).mount('#app');
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { loadConfig } from '../../core/config.js';
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
router.post('/login', (req, res) => {
|
|
7
|
+
const { password } = req.body;
|
|
8
|
+
const { dashboardPassword } = loadConfig();
|
|
9
|
+
if (password === dashboardPassword) {
|
|
10
|
+
req.session.authenticated = true;
|
|
11
|
+
res.json({ ok: true });
|
|
12
|
+
} else {
|
|
13
|
+
res.status(401).json({ ok: false, error: 'Invalid password' });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
router.get('/auth', (req, res) => {
|
|
18
|
+
if (req.session.authenticated === true) {
|
|
19
|
+
res.json({ authenticated: true });
|
|
20
|
+
} else {
|
|
21
|
+
res.json({ authenticated: false });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
router.post('/logout', (req, res) => {
|
|
26
|
+
req.session.destroy();
|
|
27
|
+
res.json({ ok: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export default router;
|