@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.
Files changed (47) hide show
  1. package/bin/create-portlama.js +15 -0
  2. package/package.json +50 -0
  3. package/scripts/bundle-vendor.js +60 -0
  4. package/src/index.js +484 -0
  5. package/src/lib/cert-help-page.js +160 -0
  6. package/src/lib/env.js +136 -0
  7. package/src/lib/secrets.js +19 -0
  8. package/src/lib/summary.js +101 -0
  9. package/src/tasks/harden.js +302 -0
  10. package/src/tasks/mtls.js +195 -0
  11. package/src/tasks/nginx.js +184 -0
  12. package/src/tasks/node.js +110 -0
  13. package/src/tasks/panel.js +434 -0
  14. package/vendor/panel-client/dist/assets/index-BDOylgUN.js +323 -0
  15. package/vendor/panel-client/dist/assets/index-BZTMcuQt.css +1 -0
  16. package/vendor/panel-client/dist/index.html +13 -0
  17. package/vendor/panel-server/package.json +31 -0
  18. package/vendor/panel-server/src/index.js +86 -0
  19. package/vendor/panel-server/src/lib/app-error.js +14 -0
  20. package/vendor/panel-server/src/lib/authelia.js +482 -0
  21. package/vendor/panel-server/src/lib/certbot.js +328 -0
  22. package/vendor/panel-server/src/lib/chisel.js +357 -0
  23. package/vendor/panel-server/src/lib/config.js +100 -0
  24. package/vendor/panel-server/src/lib/files.js +251 -0
  25. package/vendor/panel-server/src/lib/mtls.js +197 -0
  26. package/vendor/panel-server/src/lib/nginx.js +529 -0
  27. package/vendor/panel-server/src/lib/plist.js +65 -0
  28. package/vendor/panel-server/src/lib/services.js +128 -0
  29. package/vendor/panel-server/src/lib/state.js +95 -0
  30. package/vendor/panel-server/src/lib/system-stats.js +58 -0
  31. package/vendor/panel-server/src/middleware/errors.js +58 -0
  32. package/vendor/panel-server/src/middleware/mtls.js +30 -0
  33. package/vendor/panel-server/src/middleware/onboarding-guard.js +30 -0
  34. package/vendor/panel-server/src/routes/health.js +22 -0
  35. package/vendor/panel-server/src/routes/management/certs.js +225 -0
  36. package/vendor/panel-server/src/routes/management/logs.js +132 -0
  37. package/vendor/panel-server/src/routes/management/services.js +51 -0
  38. package/vendor/panel-server/src/routes/management/sites.js +448 -0
  39. package/vendor/panel-server/src/routes/management/system.js +12 -0
  40. package/vendor/panel-server/src/routes/management/tunnels.js +225 -0
  41. package/vendor/panel-server/src/routes/management/users.js +237 -0
  42. package/vendor/panel-server/src/routes/management.js +20 -0
  43. package/vendor/panel-server/src/routes/onboarding/dns.js +73 -0
  44. package/vendor/panel-server/src/routes/onboarding/domain.js +35 -0
  45. package/vendor/panel-server/src/routes/onboarding/index.js +18 -0
  46. package/vendor/panel-server/src/routes/onboarding/provision.js +291 -0
  47. 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
+ }