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,79 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import session from 'express-session';
|
|
6
|
+
import { Server as SocketIO } from 'socket.io';
|
|
7
|
+
import { loadConfig } from '../core/config.js';
|
|
8
|
+
import authRouter from './routes/auth.js';
|
|
9
|
+
import domainsRouter from './routes/domains.js';
|
|
10
|
+
import sslRouter from './routes/ssl.js';
|
|
11
|
+
import nginxRouter from './routes/nginx.js';
|
|
12
|
+
import settingsRouter from './routes/settings.js';
|
|
13
|
+
import { getStatus } from './lib/nginx-service.js';
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
const app = express();
|
|
18
|
+
|
|
19
|
+
// EJS view engine setup
|
|
20
|
+
app.set('view engine', 'ejs');
|
|
21
|
+
app.set('views', path.join(__dirname, 'views'));
|
|
22
|
+
|
|
23
|
+
app.use(express.json());
|
|
24
|
+
|
|
25
|
+
app.use(session({
|
|
26
|
+
secret: 'easy-devops-secret',
|
|
27
|
+
resave: false,
|
|
28
|
+
saveUninitialized: false,
|
|
29
|
+
cookie: { httpOnly: true },
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
// Static files - disable index serving so EJS handles root
|
|
33
|
+
app.use(express.static(path.join(__dirname, 'public'), { index: false }));
|
|
34
|
+
|
|
35
|
+
app.use('/api', authRouter);
|
|
36
|
+
app.use('/api/domains', domainsRouter);
|
|
37
|
+
app.use('/api/ssl', sslRouter);
|
|
38
|
+
app.use('/api/nginx', nginxRouter);
|
|
39
|
+
app.use('/api', settingsRouter);
|
|
40
|
+
|
|
41
|
+
// Render EJS template for all other routes
|
|
42
|
+
app.use((req, res) => res.render('index'));
|
|
43
|
+
|
|
44
|
+
// ─── HTTP server + Socket.io ──────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
const httpServer = createServer(app);
|
|
47
|
+
const io = new SocketIO(httpServer);
|
|
48
|
+
|
|
49
|
+
let connectedClients = 0;
|
|
50
|
+
|
|
51
|
+
async function emitNginxStatus(target) {
|
|
52
|
+
try {
|
|
53
|
+
const status = await getStatus();
|
|
54
|
+
target.emit('nginx:status', status);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
target.emit('nginx:status', { running: false, version: null, pid: null, error: err.message });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
io.on('connection', (socket) => {
|
|
61
|
+
connectedClients++;
|
|
62
|
+
emitNginxStatus(socket);
|
|
63
|
+
socket.on('disconnect', () => { connectedClients--; });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Broadcast nginx status to all clients every 5 seconds
|
|
67
|
+
setInterval(() => {
|
|
68
|
+
if (connectedClients > 0) emitNginxStatus(io);
|
|
69
|
+
}, 5000);
|
|
70
|
+
|
|
71
|
+
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
const { dashboardPort } = loadConfig();
|
|
74
|
+
const port = Number(process.env.DASHBOARD_PORT) || dashboardPort;
|
|
75
|
+
httpServer.listen(port, () => {
|
|
76
|
+
process.stdout.write(`Dashboard running on port ${port}\n`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export { app };
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Easy DevOps</title>
|
|
7
|
+
|
|
8
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
9
|
+
<script>
|
|
10
|
+
tailwind.config = {
|
|
11
|
+
darkMode: 'class',
|
|
12
|
+
theme: {
|
|
13
|
+
extend: {
|
|
14
|
+
fontFamily: {
|
|
15
|
+
sans: ['Inter', 'system-ui', 'sans-serif'],
|
|
16
|
+
mono: ['JetBrains Mono', 'monospace'],
|
|
17
|
+
},
|
|
18
|
+
colors: {
|
|
19
|
+
brand: {
|
|
20
|
+
50: '#f0fdfa',
|
|
21
|
+
100: '#ccfbf1',
|
|
22
|
+
200: '#99f6e4',
|
|
23
|
+
300: '#5eead4',
|
|
24
|
+
400: '#2dd4bf',
|
|
25
|
+
500: '#14b8a6',
|
|
26
|
+
600: '#0d9488',
|
|
27
|
+
700: '#0f766e',
|
|
28
|
+
800: '#115e59',
|
|
29
|
+
900: '#134e4a',
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
38
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
39
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
40
|
+
|
|
41
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
42
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
[v-cloak] { display: none; }
|
|
46
|
+
|
|
47
|
+
/* Custom scrollbar */
|
|
48
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
49
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
50
|
+
::-webkit-scrollbar-thumb { background: #334155; border-radius: 9999px; }
|
|
51
|
+
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
|
52
|
+
|
|
53
|
+
/* Floating label input group */
|
|
54
|
+
.input-group { position: relative; }
|
|
55
|
+
.input-group input,
|
|
56
|
+
.input-group select,
|
|
57
|
+
.input-group textarea {
|
|
58
|
+
width: 100%;
|
|
59
|
+
padding: 0.875rem 1rem 0.5rem;
|
|
60
|
+
font-size: 0.9375rem;
|
|
61
|
+
background: var(--input-bg);
|
|
62
|
+
border: 1.5px solid var(--border-color);
|
|
63
|
+
border-radius: 0.75rem;
|
|
64
|
+
transition: all 0.2s ease;
|
|
65
|
+
color: inherit;
|
|
66
|
+
}
|
|
67
|
+
.input-group input:focus,
|
|
68
|
+
.input-group select:focus,
|
|
69
|
+
.input-group textarea:focus {
|
|
70
|
+
outline: none;
|
|
71
|
+
border-color: #14b8a6;
|
|
72
|
+
box-shadow: 0 0 0 3px rgba(20, 184, 166, 0.15);
|
|
73
|
+
}
|
|
74
|
+
.input-group label {
|
|
75
|
+
position: absolute;
|
|
76
|
+
left: 1rem;
|
|
77
|
+
top: 50%;
|
|
78
|
+
transform: translateY(-50%);
|
|
79
|
+
font-size: 0.9375rem;
|
|
80
|
+
color: #94a3b8;
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
transition: all 0.2s ease;
|
|
83
|
+
background: transparent;
|
|
84
|
+
}
|
|
85
|
+
.input-group input:focus + label,
|
|
86
|
+
.input-group input:not(:placeholder-shown) + label,
|
|
87
|
+
.input-group select:focus + label,
|
|
88
|
+
.input-group select:not([value=""]):valid + label,
|
|
89
|
+
.input-group textarea:focus + label,
|
|
90
|
+
.input-group textarea:not(:placeholder-shown) + label {
|
|
91
|
+
top: 0.375rem;
|
|
92
|
+
font-size: 0.6875rem;
|
|
93
|
+
color: #14b8a6;
|
|
94
|
+
font-weight: 500;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Toggle switch */
|
|
98
|
+
.switch {
|
|
99
|
+
position: relative;
|
|
100
|
+
width: 52px;
|
|
101
|
+
height: 28px;
|
|
102
|
+
flex-shrink: 0;
|
|
103
|
+
}
|
|
104
|
+
.switch input { opacity: 0; width: 0; height: 0; }
|
|
105
|
+
.switch .slider {
|
|
106
|
+
position: absolute;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
inset: 0;
|
|
109
|
+
background: #334155;
|
|
110
|
+
border-radius: 28px;
|
|
111
|
+
transition: 0.25s ease;
|
|
112
|
+
}
|
|
113
|
+
.switch .slider::before {
|
|
114
|
+
content: "";
|
|
115
|
+
position: absolute;
|
|
116
|
+
height: 22px;
|
|
117
|
+
width: 22px;
|
|
118
|
+
left: 3px;
|
|
119
|
+
bottom: 3px;
|
|
120
|
+
background: white;
|
|
121
|
+
border-radius: 50%;
|
|
122
|
+
transition: 0.25s ease;
|
|
123
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
|
124
|
+
}
|
|
125
|
+
.switch input:checked + .slider { background: #14b8a6; }
|
|
126
|
+
.switch input:checked + .slider::before { transform: translateX(24px); }
|
|
127
|
+
.switch input:focus + .slider { box-shadow: 0 0 0 3px rgba(20, 184, 166, 0.25); }
|
|
128
|
+
|
|
129
|
+
/* Buttons */
|
|
130
|
+
.btn {
|
|
131
|
+
display: inline-flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
gap: 0.5rem;
|
|
135
|
+
padding: 0.625rem 1.25rem;
|
|
136
|
+
font-size: 0.875rem;
|
|
137
|
+
font-weight: 500;
|
|
138
|
+
border-radius: 0.625rem;
|
|
139
|
+
transition: all 0.2s ease;
|
|
140
|
+
cursor: pointer;
|
|
141
|
+
border: none;
|
|
142
|
+
}
|
|
143
|
+
.btn-primary {
|
|
144
|
+
background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);
|
|
145
|
+
color: white;
|
|
146
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.1), 0 0 0 1px rgba(20, 184, 166, 0.2);
|
|
147
|
+
}
|
|
148
|
+
.btn-primary:hover:not(:disabled) {
|
|
149
|
+
background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%);
|
|
150
|
+
transform: translateY(-1px);
|
|
151
|
+
box-shadow: 0 4px 12px rgba(20, 184, 166, 0.3);
|
|
152
|
+
}
|
|
153
|
+
.btn-primary:active:not(:disabled) { transform: translateY(0); }
|
|
154
|
+
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
155
|
+
|
|
156
|
+
.btn-secondary {
|
|
157
|
+
background: transparent;
|
|
158
|
+
color: #94a3b8;
|
|
159
|
+
border: 1.5px solid #334155;
|
|
160
|
+
}
|
|
161
|
+
.btn-secondary:hover:not(:disabled) {
|
|
162
|
+
background: #1e293b;
|
|
163
|
+
color: #e2e8f0;
|
|
164
|
+
border-color: #475569;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.btn-danger {
|
|
168
|
+
background: transparent;
|
|
169
|
+
color: #f87171;
|
|
170
|
+
border: 1.5px solid #7f1d1d;
|
|
171
|
+
}
|
|
172
|
+
.btn-danger:hover:not(:disabled) {
|
|
173
|
+
background: #450a0a;
|
|
174
|
+
color: #fca5a5;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.btn-sm {
|
|
178
|
+
padding: 0.5rem 0.875rem;
|
|
179
|
+
font-size: 0.8125rem;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.btn-icon {
|
|
183
|
+
width: 36px;
|
|
184
|
+
height: 36px;
|
|
185
|
+
padding: 0;
|
|
186
|
+
border-radius: 0.5rem;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Card */
|
|
190
|
+
.card {
|
|
191
|
+
background: var(--card-bg);
|
|
192
|
+
border: 1px solid var(--card-border);
|
|
193
|
+
border-radius: 1rem;
|
|
194
|
+
overflow: hidden;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Status indicator */
|
|
198
|
+
.status-dot {
|
|
199
|
+
width: 10px;
|
|
200
|
+
height: 10px;
|
|
201
|
+
border-radius: 50%;
|
|
202
|
+
position: relative;
|
|
203
|
+
}
|
|
204
|
+
.status-dot.online {
|
|
205
|
+
background: #22c55e;
|
|
206
|
+
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2);
|
|
207
|
+
}
|
|
208
|
+
.status-dot.online::after {
|
|
209
|
+
content: '';
|
|
210
|
+
position: absolute;
|
|
211
|
+
inset: 0;
|
|
212
|
+
border-radius: 50%;
|
|
213
|
+
background: #22c55e;
|
|
214
|
+
animation: pulse 2s ease-in-out infinite;
|
|
215
|
+
}
|
|
216
|
+
.status-dot.offline {
|
|
217
|
+
background: #ef4444;
|
|
218
|
+
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@keyframes pulse {
|
|
222
|
+
0%, 100% { transform: scale(1); opacity: 1; }
|
|
223
|
+
50% { transform: scale(2); opacity: 0; }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* Section header */
|
|
227
|
+
.section-header {
|
|
228
|
+
display: flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
justify-content: space-between;
|
|
231
|
+
padding: 1rem 1.25rem;
|
|
232
|
+
background: var(--section-bg);
|
|
233
|
+
cursor: pointer;
|
|
234
|
+
transition: background 0.2s;
|
|
235
|
+
}
|
|
236
|
+
.section-header:hover { background: var(--section-hover); }
|
|
237
|
+
|
|
238
|
+
/* Table */
|
|
239
|
+
.data-table {
|
|
240
|
+
width: 100%;
|
|
241
|
+
border-collapse: collapse;
|
|
242
|
+
}
|
|
243
|
+
.data-table th {
|
|
244
|
+
text-align: left;
|
|
245
|
+
padding: 0.75rem 1.25rem;
|
|
246
|
+
font-size: 0.6875rem;
|
|
247
|
+
font-weight: 600;
|
|
248
|
+
text-transform: uppercase;
|
|
249
|
+
letter-spacing: 0.05em;
|
|
250
|
+
color: #64748b;
|
|
251
|
+
background: var(--table-header-bg);
|
|
252
|
+
border-bottom: 1px solid var(--card-border);
|
|
253
|
+
}
|
|
254
|
+
.data-table td {
|
|
255
|
+
padding: 1rem 1.25rem;
|
|
256
|
+
border-bottom: 1px solid var(--card-border);
|
|
257
|
+
transition: background 0.15s;
|
|
258
|
+
}
|
|
259
|
+
.data-table tbody tr:hover td { background: var(--row-hover); }
|
|
260
|
+
.data-table tbody tr:last-child td { border-bottom: none; }
|
|
261
|
+
|
|
262
|
+
/* Badges */
|
|
263
|
+
.badge {
|
|
264
|
+
display: inline-flex;
|
|
265
|
+
align-items: center;
|
|
266
|
+
padding: 0.25rem 0.625rem;
|
|
267
|
+
font-size: 0.6875rem;
|
|
268
|
+
font-weight: 600;
|
|
269
|
+
border-radius: 9999px;
|
|
270
|
+
text-transform: uppercase;
|
|
271
|
+
letter-spacing: 0.03em;
|
|
272
|
+
}
|
|
273
|
+
.badge-success { background: rgba(34, 197, 94, 0.15); color: #4ade80; }
|
|
274
|
+
.badge-warning { background: rgba(234, 179, 8, 0.15); color: #facc15; }
|
|
275
|
+
.badge-danger { background: rgba(239, 68, 68, 0.15); color: #f87171; }
|
|
276
|
+
.badge-neutral { background: rgba(100, 116, 139, 0.15); color: #94a3b8; }
|
|
277
|
+
|
|
278
|
+
/* CSS Variables */
|
|
279
|
+
:root {
|
|
280
|
+
--input-bg: #1e293b;
|
|
281
|
+
--border-color: #334155;
|
|
282
|
+
--card-bg: #1e293b;
|
|
283
|
+
--card-border: #334155;
|
|
284
|
+
--section-bg: rgba(15, 23, 42, 0.5);
|
|
285
|
+
--section-hover: rgba(15, 23, 42, 0.8);
|
|
286
|
+
--table-header-bg: rgba(15, 23, 42, 0.5);
|
|
287
|
+
--row-hover: rgba(15, 23, 42, 0.3);
|
|
288
|
+
}
|
|
289
|
+
.light-mode {
|
|
290
|
+
--input-bg: #f8fafc;
|
|
291
|
+
--border-color: #e2e8f0;
|
|
292
|
+
--card-bg: #ffffff;
|
|
293
|
+
--card-border: #e2e8f0;
|
|
294
|
+
--section-bg: #f8fafc;
|
|
295
|
+
--section-hover: #f1f5f9;
|
|
296
|
+
--table-header-bg: #f8fafc;
|
|
297
|
+
--row-hover: #f8fafc;
|
|
298
|
+
}
|
|
299
|
+
</style>
|
|
300
|
+
</head>
|
|
301
|
+
|
|
302
|
+
<body class="bg-slate-950 text-slate-100 font-sans antialiased" :class="{ 'light-mode': !isDark }">
|
|
303
|
+
<div id="app" v-cloak>
|
|
304
|
+
|
|
305
|
+
<!-- Login -->
|
|
306
|
+
<%- include('partials/login.ejs') %>
|
|
307
|
+
|
|
308
|
+
<!-- Main App -->
|
|
309
|
+
<div v-else class="flex h-screen">
|
|
310
|
+
<!-- Sidebar -->
|
|
311
|
+
<%- include('partials/sidebar.ejs') %>
|
|
312
|
+
|
|
313
|
+
<!-- Main Content -->
|
|
314
|
+
<main class="flex-1 overflow-y-auto bg-slate-950 p-8">
|
|
315
|
+
<%- include('partials/overview.ejs') %>
|
|
316
|
+
<%- include('partials/nginx-panel.ejs') %>
|
|
317
|
+
<%- include('partials/ssl-panel.ejs') %>
|
|
318
|
+
<%- include('partials/domains-panel.ejs') %>
|
|
319
|
+
<%- include('partials/settings-panel.ejs') %>
|
|
320
|
+
</main>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<script src="/js/app.js"></script>
|
|
326
|
+
</body>
|
|
327
|
+
</html>
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<!-- Domain Form -->
|
|
2
|
+
<div v-if="domains.showForm" class="card mb-6">
|
|
3
|
+
<div class="flex items-center justify-between p-4 border-b border-slate-800">
|
|
4
|
+
<h3 class="font-semibold">{{ domains.editingName ? 'Edit: ' + domains.editingName : 'New Domain' }}</h3>
|
|
5
|
+
<div class="flex gap-2">
|
|
6
|
+
<button @click="resetDomainForm()" class="btn btn-secondary btn-sm">Cancel</button>
|
|
7
|
+
<button @click="saveDomain()" :disabled="domains.saving" class="btn btn-primary btn-sm">
|
|
8
|
+
{{ domains.saving ? 'Saving...' : 'Save' }}
|
|
9
|
+
</button>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<p v-if="domains.error" class="mx-4 mt-4 p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
|
|
14
|
+
{{ domains.error }}
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<!-- Basic Section -->
|
|
18
|
+
<div class="border-b border-slate-800">
|
|
19
|
+
<div @click="toggleSection('basic')" class="section-header">
|
|
20
|
+
<span class="font-medium">Basic Configuration</span>
|
|
21
|
+
<svg class="w-5 h-5 text-slate-500 transition-transform" :class="{ 'rotate-180': sections.basic }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
22
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
23
|
+
</svg>
|
|
24
|
+
</div>
|
|
25
|
+
<div v-show="sections.basic" class="p-4 grid grid-cols-2 gap-4">
|
|
26
|
+
<div class="input-group">
|
|
27
|
+
<input v-model="domains.form.name" :disabled="!!domains.editingName" placeholder=" " />
|
|
28
|
+
<label>Domain Name</label>
|
|
29
|
+
<p class="text-xs text-slate-500 mt-1 ml-1">e.g., api.example.com or *.example.com</p>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="input-group">
|
|
32
|
+
<input v-model.number="domains.form.port" type="number" placeholder=" " />
|
|
33
|
+
<label>Backend Port</label>
|
|
34
|
+
<p class="text-xs text-slate-500 mt-1 ml-1">Port your app listens on</p>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="input-group">
|
|
37
|
+
<input v-model="domains.form.backendHost" placeholder=" " />
|
|
38
|
+
<label>Backend Host</label>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="input-group">
|
|
41
|
+
<select v-model="domains.form.upstreamType">
|
|
42
|
+
<option value="http">HTTP</option>
|
|
43
|
+
<option value="https">HTTPS</option>
|
|
44
|
+
<option value="ws">WebSocket</option>
|
|
45
|
+
</select>
|
|
46
|
+
<label>Upstream Type</label>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="col-span-2 flex items-center gap-3 py-2">
|
|
49
|
+
<label class="switch">
|
|
50
|
+
<input type="checkbox" v-model="domains.form.www" />
|
|
51
|
+
<span class="slider"></span>
|
|
52
|
+
</label>
|
|
53
|
+
<span class="text-sm">Include www subdomain</span>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- SSL Section -->
|
|
59
|
+
<div class="border-b border-slate-800">
|
|
60
|
+
<div @click="toggleSection('ssl')" class="section-header">
|
|
61
|
+
<span class="font-medium">SSL Configuration</span>
|
|
62
|
+
<svg class="w-5 h-5 text-slate-500 transition-transform" :class="{ 'rotate-180': sections.ssl }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
63
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
64
|
+
</svg>
|
|
65
|
+
</div>
|
|
66
|
+
<div v-show="sections.ssl" class="p-4 space-y-4">
|
|
67
|
+
<div class="flex items-center gap-3">
|
|
68
|
+
<label class="switch">
|
|
69
|
+
<input type="checkbox" v-model="domains.form.ssl.enabled" />
|
|
70
|
+
<span class="slider"></span>
|
|
71
|
+
</label>
|
|
72
|
+
<span class="text-sm">Enable SSL / HTTPS</span>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div v-if="domains.form.ssl.enabled" class="grid grid-cols-2 gap-4 pl-14">
|
|
76
|
+
<div class="input-group">
|
|
77
|
+
<input v-model="domains.form.ssl.certPath" placeholder=" " class="font-mono text-xs" />
|
|
78
|
+
<label>Certificate Path</label>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="input-group">
|
|
81
|
+
<input v-model="domains.form.ssl.keyPath" placeholder=" " class="font-mono text-xs" />
|
|
82
|
+
<label>Private Key Path</label>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="col-span-2 flex items-center gap-3">
|
|
85
|
+
<label class="switch">
|
|
86
|
+
<input type="checkbox" v-model="domains.form.ssl.redirect" />
|
|
87
|
+
<span class="slider"></span>
|
|
88
|
+
</label>
|
|
89
|
+
<span class="text-sm">Redirect HTTP to HTTPS</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="flex items-center gap-3">
|
|
92
|
+
<label class="switch">
|
|
93
|
+
<input type="checkbox" v-model="domains.form.ssl.hsts" />
|
|
94
|
+
<span class="slider"></span>
|
|
95
|
+
</label>
|
|
96
|
+
<span class="text-sm">Enable HSTS</span>
|
|
97
|
+
</div>
|
|
98
|
+
<div v-if="domains.form.ssl.hsts" class="input-group">
|
|
99
|
+
<input v-model.number="domains.form.ssl.hstsMaxAge" type="number" placeholder=" " />
|
|
100
|
+
<label>HSTS Max-Age (seconds)</label>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<!-- Performance Section -->
|
|
107
|
+
<div class="border-b border-slate-800">
|
|
108
|
+
<div @click="toggleSection('performance')" class="section-header">
|
|
109
|
+
<span class="font-medium">Performance</span>
|
|
110
|
+
<svg class="w-5 h-5 text-slate-500 transition-transform" :class="{ 'rotate-180': sections.performance }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
111
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
112
|
+
</svg>
|
|
113
|
+
</div>
|
|
114
|
+
<div v-show="sections.performance" class="p-4 grid grid-cols-2 gap-4">
|
|
115
|
+
<div class="input-group">
|
|
116
|
+
<input v-model="domains.form.performance.maxBodySize" placeholder=" " />
|
|
117
|
+
<label>Max Body Size</label>
|
|
118
|
+
<p class="text-xs text-slate-500 mt-1 ml-1">e.g., 10m, 1g</p>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="input-group">
|
|
121
|
+
<input v-model.number="domains.form.performance.readTimeout" type="number" placeholder=" " />
|
|
122
|
+
<label>Read Timeout (s)</label>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="input-group">
|
|
125
|
+
<input v-model.number="domains.form.performance.connectTimeout" type="number" placeholder=" " />
|
|
126
|
+
<label>Connect Timeout (s)</label>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="flex items-center gap-3">
|
|
129
|
+
<label class="switch">
|
|
130
|
+
<input type="checkbox" v-model="domains.form.performance.proxyBuffers" />
|
|
131
|
+
<span class="slider"></span>
|
|
132
|
+
</label>
|
|
133
|
+
<span class="text-sm">Proxy Buffering</span>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="flex items-center gap-3">
|
|
136
|
+
<label class="switch">
|
|
137
|
+
<input type="checkbox" v-model="domains.form.performance.gzip" />
|
|
138
|
+
<span class="slider"></span>
|
|
139
|
+
</label>
|
|
140
|
+
<span class="text-sm">Gzip Compression</span>
|
|
141
|
+
</div>
|
|
142
|
+
<div v-if="domains.form.performance.gzip" class="input-group">
|
|
143
|
+
<input v-model="domains.form.performance.gzipTypes" placeholder=" " class="text-xs" />
|
|
144
|
+
<label>Gzip MIME Types</label>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<!-- Security Section -->
|
|
150
|
+
<div class="border-b border-slate-800">
|
|
151
|
+
<div @click="toggleSection('security')" class="section-header">
|
|
152
|
+
<span class="font-medium">Security</span>
|
|
153
|
+
<svg class="w-5 h-5 text-slate-500 transition-transform" :class="{ 'rotate-180': sections.security }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
154
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
155
|
+
</svg>
|
|
156
|
+
</div>
|
|
157
|
+
<div v-show="sections.security" class="p-4 space-y-4">
|
|
158
|
+
<div class="flex items-center gap-3">
|
|
159
|
+
<label class="switch">
|
|
160
|
+
<input type="checkbox" v-model="domains.form.security.rateLimit" />
|
|
161
|
+
<span class="slider"></span>
|
|
162
|
+
</label>
|
|
163
|
+
<span class="text-sm">Rate Limiting</span>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div v-if="domains.form.security.rateLimit" class="grid grid-cols-2 gap-4 pl-14">
|
|
167
|
+
<div class="input-group">
|
|
168
|
+
<input v-model.number="domains.form.security.rateLimitRate" type="number" placeholder=" " />
|
|
169
|
+
<label>Requests/second</label>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="input-group">
|
|
172
|
+
<input v-model.number="domains.form.security.rateLimitBurst" type="number" placeholder=" " />
|
|
173
|
+
<label>Burst</label>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div class="flex items-center gap-3">
|
|
178
|
+
<label class="switch">
|
|
179
|
+
<input type="checkbox" v-model="domains.form.security.securityHeaders" />
|
|
180
|
+
<span class="slider"></span>
|
|
181
|
+
</label>
|
|
182
|
+
<span class="text-sm">Security Headers (X-Frame-Options, etc.)</span>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div class="grid grid-cols-2 gap-4">
|
|
186
|
+
<div class="flex items-center gap-3">
|
|
187
|
+
<label class="switch">
|
|
188
|
+
<input type="checkbox" v-model="domains.form.security.custom404" />
|
|
189
|
+
<span class="slider"></span>
|
|
190
|
+
</label>
|
|
191
|
+
<span class="text-sm">Custom 404 Page</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div class="flex items-center gap-3">
|
|
194
|
+
<label class="switch">
|
|
195
|
+
<input type="checkbox" v-model="domains.form.security.custom50x" />
|
|
196
|
+
<span class="slider"></span>
|
|
197
|
+
</label>
|
|
198
|
+
<span class="text-sm">Custom 50x Page</span>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<!-- Advanced Section -->
|
|
205
|
+
<div>
|
|
206
|
+
<div @click="toggleSection('advanced')" class="section-header">
|
|
207
|
+
<span class="font-medium">Advanced</span>
|
|
208
|
+
<svg class="w-5 h-5 text-slate-500 transition-transform" :class="{ 'rotate-180': sections.advanced }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
209
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
210
|
+
</svg>
|
|
211
|
+
</div>
|
|
212
|
+
<div v-show="sections.advanced" class="p-4 space-y-4">
|
|
213
|
+
<div class="flex items-center gap-3">
|
|
214
|
+
<label class="switch">
|
|
215
|
+
<input type="checkbox" v-model="domains.form.advanced.accessLog" />
|
|
216
|
+
<span class="slider"></span>
|
|
217
|
+
</label>
|
|
218
|
+
<span class="text-sm">Domain-specific Access Log</span>
|
|
219
|
+
</div>
|
|
220
|
+
<div>
|
|
221
|
+
<label class="block text-sm font-medium text-slate-400 mb-2">Custom Location Blocks</label>
|
|
222
|
+
<textarea v-model="domains.form.advanced.customLocations" rows="4"
|
|
223
|
+
class="w-full bg-slate-900 border border-slate-800 rounded-xl p-4 font-mono text-sm text-slate-300 focus:outline-none focus:ring-2 focus:ring-brand-500"
|
|
224
|
+
placeholder="location /api/v2 { ... }"></textarea>
|
|
225
|
+
<p class="text-xs text-slate-500 mt-1">Raw nginx location blocks (appended verbatim)</p>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|