@lamalibre/create-portlama 1.0.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/bin/create-portlama.js +15 -0
- package/package.json +50 -0
- package/scripts/bundle-vendor.js +60 -0
- package/src/index.js +484 -0
- package/src/lib/cert-help-page.js +160 -0
- package/src/lib/env.js +136 -0
- package/src/lib/secrets.js +19 -0
- package/src/lib/summary.js +101 -0
- package/src/tasks/harden.js +302 -0
- package/src/tasks/mtls.js +195 -0
- package/src/tasks/nginx.js +184 -0
- package/src/tasks/node.js +110 -0
- package/src/tasks/panel.js +434 -0
- package/vendor/panel-client/dist/assets/index-BDOylgUN.js +323 -0
- package/vendor/panel-client/dist/assets/index-BZTMcuQt.css +1 -0
- package/vendor/panel-client/dist/index.html +13 -0
- package/vendor/panel-server/package.json +31 -0
- package/vendor/panel-server/src/index.js +86 -0
- package/vendor/panel-server/src/lib/app-error.js +14 -0
- package/vendor/panel-server/src/lib/authelia.js +482 -0
- package/vendor/panel-server/src/lib/certbot.js +328 -0
- package/vendor/panel-server/src/lib/chisel.js +357 -0
- package/vendor/panel-server/src/lib/config.js +100 -0
- package/vendor/panel-server/src/lib/files.js +251 -0
- package/vendor/panel-server/src/lib/mtls.js +197 -0
- package/vendor/panel-server/src/lib/nginx.js +529 -0
- package/vendor/panel-server/src/lib/plist.js +65 -0
- package/vendor/panel-server/src/lib/services.js +128 -0
- package/vendor/panel-server/src/lib/state.js +95 -0
- package/vendor/panel-server/src/lib/system-stats.js +58 -0
- package/vendor/panel-server/src/middleware/errors.js +58 -0
- package/vendor/panel-server/src/middleware/mtls.js +30 -0
- package/vendor/panel-server/src/middleware/onboarding-guard.js +30 -0
- package/vendor/panel-server/src/routes/health.js +22 -0
- package/vendor/panel-server/src/routes/management/certs.js +225 -0
- package/vendor/panel-server/src/routes/management/logs.js +132 -0
- package/vendor/panel-server/src/routes/management/services.js +51 -0
- package/vendor/panel-server/src/routes/management/sites.js +448 -0
- package/vendor/panel-server/src/routes/management/system.js +12 -0
- package/vendor/panel-server/src/routes/management/tunnels.js +225 -0
- package/vendor/panel-server/src/routes/management/users.js +237 -0
- package/vendor/panel-server/src/routes/management.js +20 -0
- package/vendor/panel-server/src/routes/onboarding/dns.js +73 -0
- package/vendor/panel-server/src/routes/onboarding/domain.js +35 -0
- package/vendor/panel-server/src/routes/onboarding/index.js +18 -0
- package/vendor/panel-server/src/routes/onboarding/provision.js +291 -0
- package/vendor/panel-server/src/routes/onboarding/status.js +12 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import { getConfig, updateConfig } from '../../lib/config.js';
|
|
5
|
+
import * as chisel from '../../lib/chisel.js';
|
|
6
|
+
import * as authelia from '../../lib/authelia.js';
|
|
7
|
+
import * as certbot from '../../lib/certbot.js';
|
|
8
|
+
import * as nginx from '../../lib/nginx.js';
|
|
9
|
+
|
|
10
|
+
// Module-level state for provisioning
|
|
11
|
+
const emitter = new EventEmitter();
|
|
12
|
+
emitter.setMaxListeners(50);
|
|
13
|
+
|
|
14
|
+
const TASK_DEFINITIONS = [
|
|
15
|
+
{ id: 'install-chisel', title: 'Installing Chisel' },
|
|
16
|
+
{ id: 'install-authelia', title: 'Installing Authelia' },
|
|
17
|
+
{ id: 'issue-certs', title: 'Issuing TLS certificates' },
|
|
18
|
+
{ id: 'configure-nginx', title: 'Configuring nginx' },
|
|
19
|
+
{ id: 'verify-services', title: 'Verifying services' },
|
|
20
|
+
{ id: 'finalize', title: 'Finalizing setup' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
let provisioningState = {
|
|
24
|
+
isRunning: false,
|
|
25
|
+
tasks: TASK_DEFINITIONS.map((t) => ({ ...t, status: 'pending', message: null, log: null })),
|
|
26
|
+
error: null,
|
|
27
|
+
result: null,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Reset provisioning state for a fresh run.
|
|
32
|
+
*/
|
|
33
|
+
function resetState() {
|
|
34
|
+
provisioningState = {
|
|
35
|
+
isRunning: true,
|
|
36
|
+
tasks: TASK_DEFINITIONS.map((t) => ({ ...t, status: 'pending', message: null, log: null })),
|
|
37
|
+
error: null,
|
|
38
|
+
result: null,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Emit a progress event and update the provisioning state.
|
|
44
|
+
*/
|
|
45
|
+
function emitProgress(taskId, status, message, log = null) {
|
|
46
|
+
const taskIndex = provisioningState.tasks.findIndex((t) => t.id === taskId);
|
|
47
|
+
if (taskIndex !== -1) {
|
|
48
|
+
provisioningState.tasks[taskIndex].status = status;
|
|
49
|
+
provisioningState.tasks[taskIndex].message = message;
|
|
50
|
+
if (log !== null) {
|
|
51
|
+
provisioningState.tasks[taskIndex].log = log;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const current = provisioningState.tasks.filter((t) => t.status === 'done').length +
|
|
56
|
+
(status === 'running' ? 1 : 0);
|
|
57
|
+
|
|
58
|
+
const payload = {
|
|
59
|
+
task: taskId,
|
|
60
|
+
title: provisioningState.tasks[taskIndex]?.title || taskId,
|
|
61
|
+
status,
|
|
62
|
+
message,
|
|
63
|
+
log,
|
|
64
|
+
progress: { current, total: TASK_DEFINITIONS.length },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
emitter.emit('progress', payload);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Run the full provisioning sequence.
|
|
72
|
+
*/
|
|
73
|
+
async function runProvisioning(log) {
|
|
74
|
+
const config = getConfig();
|
|
75
|
+
const { domain, email } = config;
|
|
76
|
+
|
|
77
|
+
let adminPassword;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Step 1: Install Chisel
|
|
81
|
+
emitProgress('install-chisel', 'running', 'Downloading Chisel binary...');
|
|
82
|
+
const chiselResult = await chisel.installChisel();
|
|
83
|
+
emitProgress('install-chisel', 'running', 'Writing systemd service...', chiselResult.skipped ? 'Chisel already installed' : `Installed Chisel ${chiselResult.version}`);
|
|
84
|
+
await chisel.writeChiselService();
|
|
85
|
+
emitProgress('install-chisel', 'running', 'Starting Chisel service...');
|
|
86
|
+
await chisel.startChisel();
|
|
87
|
+
emitProgress('install-chisel', 'done', 'Chisel installed and running');
|
|
88
|
+
|
|
89
|
+
// Step 2: Install Authelia
|
|
90
|
+
emitProgress('install-authelia', 'running', 'Downloading Authelia binary...');
|
|
91
|
+
const autheliaResult = await authelia.installAuthelia();
|
|
92
|
+
emitProgress('install-authelia', 'running', 'Writing configuration...', autheliaResult.skipped ? 'Authelia already installed' : `Installed Authelia ${autheliaResult.version}`);
|
|
93
|
+
|
|
94
|
+
const secrets = {
|
|
95
|
+
jwtSecret: crypto.randomBytes(32).toString('hex'),
|
|
96
|
+
sessionSecret: crypto.randomBytes(32).toString('hex'),
|
|
97
|
+
storageEncryptionKey: crypto.randomBytes(32).toString('hex'),
|
|
98
|
+
};
|
|
99
|
+
await authelia.writeAutheliaConfig(domain, secrets);
|
|
100
|
+
|
|
101
|
+
emitProgress('install-authelia', 'running', 'Creating admin user...');
|
|
102
|
+
adminPassword = crypto.randomBytes(16).toString('base64url');
|
|
103
|
+
await authelia.createUser('admin', adminPassword);
|
|
104
|
+
|
|
105
|
+
emitProgress('install-authelia', 'running', 'Writing systemd service...');
|
|
106
|
+
await authelia.writeAutheliaService();
|
|
107
|
+
|
|
108
|
+
emitProgress('install-authelia', 'running', 'Starting Authelia service...');
|
|
109
|
+
await authelia.startAuthelia();
|
|
110
|
+
emitProgress('install-authelia', 'done', 'Authelia installed and running');
|
|
111
|
+
|
|
112
|
+
// Step 3: Issue certificates
|
|
113
|
+
emitProgress('issue-certs', 'running', `Issuing certificate for panel.${domain}...`);
|
|
114
|
+
await certbot.issueCoreCerts(domain, email);
|
|
115
|
+
emitProgress('issue-certs', 'running', 'Setting up auto-renewal...');
|
|
116
|
+
await certbot.setupAutoRenew();
|
|
117
|
+
emitProgress('issue-certs', 'done', 'TLS certificates issued');
|
|
118
|
+
|
|
119
|
+
// Step 4: Configure nginx
|
|
120
|
+
emitProgress('configure-nginx', 'running', 'Writing panel vhost...');
|
|
121
|
+
await nginx.writePanelVhost(domain);
|
|
122
|
+
|
|
123
|
+
emitProgress('configure-nginx', 'running', 'Writing auth vhost...');
|
|
124
|
+
await nginx.writeAuthVhost(domain);
|
|
125
|
+
|
|
126
|
+
emitProgress('configure-nginx', 'running', 'Writing tunnel vhost...');
|
|
127
|
+
await nginx.writeTunnelVhost(domain);
|
|
128
|
+
|
|
129
|
+
emitProgress('configure-nginx', 'running', 'Enabling sites...');
|
|
130
|
+
await nginx.enableSite('portlama-panel-domain');
|
|
131
|
+
await nginx.enableSite('portlama-auth');
|
|
132
|
+
await nginx.enableSite('portlama-tunnel');
|
|
133
|
+
|
|
134
|
+
emitProgress('configure-nginx', 'running', 'Testing nginx configuration...');
|
|
135
|
+
const testResult = await nginx.testConfig();
|
|
136
|
+
if (!testResult.valid) {
|
|
137
|
+
throw new Error(`nginx configuration test failed: ${testResult.error}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
emitProgress('configure-nginx', 'running', 'Reloading nginx...');
|
|
141
|
+
await nginx.reload();
|
|
142
|
+
emitProgress('configure-nginx', 'done', 'nginx configured and reloaded');
|
|
143
|
+
|
|
144
|
+
// Step 5: Verify services
|
|
145
|
+
emitProgress('verify-services', 'running', 'Checking Chisel...');
|
|
146
|
+
const chiselRunning = await chisel.isChiselRunning();
|
|
147
|
+
if (!chiselRunning) {
|
|
148
|
+
throw new Error('Chisel service is not running after provisioning');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
emitProgress('verify-services', 'running', 'Checking Authelia...');
|
|
152
|
+
const autheliaRunning = await authelia.isAutheliaRunning();
|
|
153
|
+
if (!autheliaRunning) {
|
|
154
|
+
throw new Error('Authelia service is not running after provisioning');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
emitProgress('verify-services', 'running', 'Checking nginx...');
|
|
158
|
+
try {
|
|
159
|
+
const { stdout } = await execa('systemctl', ['is-active', 'nginx']);
|
|
160
|
+
if (stdout.trim() !== 'active') {
|
|
161
|
+
throw new Error('nginx is not active');
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
throw new Error('nginx service is not running after provisioning');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
emitProgress('verify-services', 'running', 'Checking panel server...');
|
|
168
|
+
try {
|
|
169
|
+
const { stdout } = await execa('curl', ['-s', 'http://127.0.0.1:3100/api/health']);
|
|
170
|
+
const health = JSON.parse(stdout);
|
|
171
|
+
if (health.status !== 'ok') {
|
|
172
|
+
throw new Error('Panel server health check returned unexpected status');
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
throw new Error(`Panel server health check failed: ${err.message}`);
|
|
176
|
+
}
|
|
177
|
+
emitProgress('verify-services', 'done', 'All services running');
|
|
178
|
+
|
|
179
|
+
// Step 6: Finalize
|
|
180
|
+
emitProgress('finalize', 'running', 'Updating configuration...');
|
|
181
|
+
await updateConfig({ onboarding: { status: 'COMPLETED' } });
|
|
182
|
+
|
|
183
|
+
const result = {
|
|
184
|
+
adminUsername: 'admin',
|
|
185
|
+
adminPassword,
|
|
186
|
+
panelUrl: `https://panel.${domain}`,
|
|
187
|
+
authUrl: `https://auth.${domain}`,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
provisioningState.result = result;
|
|
191
|
+
provisioningState.isRunning = false;
|
|
192
|
+
emitProgress('finalize', 'done', 'Provisioning complete');
|
|
193
|
+
|
|
194
|
+
// Send the completion event
|
|
195
|
+
emitter.emit('progress', {
|
|
196
|
+
task: 'complete',
|
|
197
|
+
status: 'done',
|
|
198
|
+
message: 'Provisioning complete',
|
|
199
|
+
result,
|
|
200
|
+
progress: { current: TASK_DEFINITIONS.length, total: TASK_DEFINITIONS.length },
|
|
201
|
+
});
|
|
202
|
+
} catch (err) {
|
|
203
|
+
const failedTask = provisioningState.tasks.find((t) => t.status === 'running');
|
|
204
|
+
const failedTaskId = failedTask?.id || 'unknown';
|
|
205
|
+
|
|
206
|
+
if (failedTask) {
|
|
207
|
+
failedTask.status = 'error';
|
|
208
|
+
failedTask.message = err.message;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
provisioningState.error = { task: failedTaskId, message: err.message };
|
|
212
|
+
provisioningState.isRunning = false;
|
|
213
|
+
|
|
214
|
+
emitter.emit('progress', {
|
|
215
|
+
task: failedTaskId,
|
|
216
|
+
status: 'error',
|
|
217
|
+
message: `Failed: ${failedTask?.title || failedTaskId}`,
|
|
218
|
+
error: err.message,
|
|
219
|
+
progress: {
|
|
220
|
+
current: provisioningState.tasks.filter((t) => t.status === 'done').length,
|
|
221
|
+
total: TASK_DEFINITIONS.length,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
log.error({ err, task: failedTaskId }, 'Provisioning failed');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export default async function provisionRoute(fastify, _opts) {
|
|
230
|
+
// POST /provision — start provisioning
|
|
231
|
+
fastify.post('/provision', async (request, reply) => {
|
|
232
|
+
const config = getConfig();
|
|
233
|
+
const { status } = config.onboarding;
|
|
234
|
+
|
|
235
|
+
if (status === 'FRESH' || status === 'DOMAIN_SET') {
|
|
236
|
+
return reply.code(409).send({
|
|
237
|
+
error: 'DNS must be verified before provisioning',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (status === 'COMPLETED') {
|
|
242
|
+
return reply.code(410).send({
|
|
243
|
+
error: 'Onboarding already completed',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// If provisioning is actively running, return 409
|
|
248
|
+
if (provisioningState.isRunning) {
|
|
249
|
+
return reply.code(409).send({
|
|
250
|
+
error: 'Provisioning already in progress',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Set status to PROVISIONING
|
|
255
|
+
await updateConfig({ onboarding: { status: 'PROVISIONING' } });
|
|
256
|
+
|
|
257
|
+
// Reset state and start provisioning in the background
|
|
258
|
+
resetState();
|
|
259
|
+
runProvisioning(request.log);
|
|
260
|
+
|
|
261
|
+
return reply.code(202).send({ ok: true, message: 'Provisioning started' });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// WebSocket /provision/stream — real-time progress
|
|
265
|
+
fastify.get('/provision/stream', { websocket: true }, (socket, _request) => {
|
|
266
|
+
// Send current state immediately for late-joining clients
|
|
267
|
+
socket.send(JSON.stringify({
|
|
268
|
+
type: 'state',
|
|
269
|
+
...provisioningState,
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
// Subscribe to progress events
|
|
273
|
+
function onProgress(payload) {
|
|
274
|
+
try {
|
|
275
|
+
socket.send(JSON.stringify(payload));
|
|
276
|
+
} catch {
|
|
277
|
+
// Client disconnected — handled by close event
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
emitter.on('progress', onProgress);
|
|
282
|
+
|
|
283
|
+
socket.on('close', () => {
|
|
284
|
+
emitter.off('progress', onProgress);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
socket.on('error', () => {
|
|
288
|
+
emitter.off('progress', onProgress);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getConfig } from '../../lib/config.js';
|
|
2
|
+
|
|
3
|
+
export default async function statusRoute(fastify, _opts) {
|
|
4
|
+
fastify.get('/status', async (_request, _reply) => {
|
|
5
|
+
const config = getConfig();
|
|
6
|
+
return {
|
|
7
|
+
status: config.onboarding.status,
|
|
8
|
+
domain: config.domain ?? null,
|
|
9
|
+
ip: config.ip,
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
}
|