lobstakit-cloud 1.3.9 → 1.3.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lobstakit-cloud",
3
- "version": "1.3.9",
3
+ "version": "1.3.10",
4
4
  "description": "LobstaKit Cloud — Setup wizard and management for LobstaCloud gateways",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -96,6 +96,8 @@ const SETTINGS_PROVIDER_FALLBACK = {
96
96
 
97
97
  let settingsProviders = SETTINGS_PROVIDER_FALLBACK;
98
98
  let currentAiSettings = null;
99
+ let terminalHistory = [];
100
+ let terminalHistoryIndex = -1;
99
101
 
100
102
  async function logout() {
101
103
  const token = localStorage.getItem('lobstakit_token');
@@ -229,6 +231,7 @@ document.addEventListener('DOMContentLoaded', async () => {
229
231
  refreshVersionStatus();
230
232
  loadAccountEmail();
231
233
  checkMissionControlAccess();
234
+ initTerminal();
232
235
 
233
236
  // Auto-refresh
234
237
  setInterval(fetchStatus, 10000);
@@ -243,6 +246,115 @@ document.addEventListener('DOMContentLoaded', async () => {
243
246
  setInterval(refreshVersionStatus, 60000);
244
247
  });
245
248
 
249
+ function escapeHtml(value) {
250
+ return String(value || '')
251
+ .replace(/&/g, '&')
252
+ .replace(/</g, '&lt;')
253
+ .replace(/>/g, '&gt;')
254
+ .replace(/"/g, '&quot;')
255
+ .replace(/'/g, '&#39;');
256
+ }
257
+
258
+ function appendTerminalOutput(html) {
259
+ const outputEl = document.getElementById('terminal-output');
260
+ if (!outputEl) return;
261
+ outputEl.insertAdjacentHTML('beforeend', html);
262
+ outputEl.scrollTop = outputEl.scrollHeight;
263
+ }
264
+
265
+ function setTerminalRunning(isRunning) {
266
+ const runBtn = document.getElementById('terminal-run-btn');
267
+ if (!runBtn) return;
268
+ runBtn.disabled = isRunning;
269
+ runBtn.textContent = isRunning ? 'Running...' : 'Run';
270
+ }
271
+
272
+ function terminalHistoryUp(inputEl) {
273
+ if (!terminalHistory.length) return;
274
+ if (terminalHistoryIndex <= 0) {
275
+ terminalHistoryIndex = 0;
276
+ } else {
277
+ terminalHistoryIndex -= 1;
278
+ }
279
+ inputEl.value = terminalHistory[terminalHistoryIndex];
280
+ }
281
+
282
+ function terminalHistoryDown(inputEl) {
283
+ if (!terminalHistory.length) return;
284
+ if (terminalHistoryIndex >= terminalHistory.length - 1) {
285
+ terminalHistoryIndex = terminalHistory.length;
286
+ inputEl.value = '';
287
+ return;
288
+ }
289
+ terminalHistoryIndex += 1;
290
+ inputEl.value = terminalHistory[terminalHistoryIndex];
291
+ }
292
+
293
+ async function runTerminalCommand() {
294
+ const inputEl = document.getElementById('terminal-input');
295
+ if (!inputEl) return;
296
+
297
+ const command = inputEl.value.trim();
298
+ if (!command) return;
299
+
300
+ terminalHistory.push(command);
301
+ terminalHistoryIndex = terminalHistory.length;
302
+ inputEl.value = '';
303
+ setTerminalRunning(true);
304
+
305
+ try {
306
+ const res = await fetch('/api/run-command', authFetchOpts({
307
+ method: 'POST',
308
+ headers: { 'Content-Type': 'application/json' },
309
+ body: JSON.stringify({ command: command })
310
+ }));
311
+ const data = await res.json();
312
+
313
+ if (!res.ok) {
314
+ const requestError = escapeHtml(data.error || 'Request failed');
315
+ appendTerminalOutput(`$ ${escapeHtml(command)}\n<span style="color:#f85149">${requestError}</span>\n\n`);
316
+ return;
317
+ }
318
+
319
+ const stdout = escapeHtml(data.stdout || '');
320
+ const stderr = escapeHtml(data.stderr || '');
321
+ const hasError = Number(data.exitCode) !== 0;
322
+ const stderrBlock = stderr
323
+ ? (hasError ? `<span style="color:#f85149">${stderr}</span>` : stderr)
324
+ : '';
325
+
326
+ appendTerminalOutput(`$ ${escapeHtml(command)}\n${stdout}${stderrBlock}\n\n`);
327
+ } catch (err) {
328
+ appendTerminalOutput(`$ ${escapeHtml(command)}\n<span style="color:#f85149">${escapeHtml(err.message || 'Connection error')}</span>\n\n`);
329
+ } finally {
330
+ setTerminalRunning(false);
331
+ }
332
+ }
333
+
334
+ function initTerminal() {
335
+ const inputEl = document.getElementById('terminal-input');
336
+ const runBtn = document.getElementById('terminal-run-btn');
337
+ if (!inputEl || !runBtn) return;
338
+
339
+ runBtn.addEventListener('click', runTerminalCommand);
340
+ inputEl.addEventListener('keydown', (event) => {
341
+ if (event.key === 'Enter') {
342
+ event.preventDefault();
343
+ runTerminalCommand();
344
+ return;
345
+ }
346
+ if (event.key === 'ArrowUp') {
347
+ event.preventDefault();
348
+ terminalHistoryUp(inputEl);
349
+ return;
350
+ }
351
+ if (event.key === 'ArrowDown') {
352
+ event.preventDefault();
353
+ terminalHistoryDown(inputEl);
354
+ }
355
+ });
356
+ }
357
+
246
358
  // ─── Gateway Status ──────────────────────────────────────────
247
359
 
248
360
  async function fetchStatus() {
@@ -187,6 +187,15 @@
187
187
  </div>
188
188
  </div>
189
189
 
190
+ <div class="card" id="terminal-section">
191
+ <h3>🖥️ Terminal</h3>
192
+ <div class="terminal-output" id="terminal-output" style="min-height:120px;max-height:300px;overflow-y:auto;background:#0d1117;color:#e6edf3;font-family:monospace;font-size:13px;padding:12px;border-radius:6px;margin-bottom:10px;white-space:pre-wrap;"></div>
193
+ <div style="display:flex;gap:8px;">
194
+ <input type="text" id="terminal-input" placeholder="Enter command..." style="flex:1;font-family:monospace;font-size:13px;" />
195
+ <button id="terminal-run-btn" class="btn btn-secondary">Run</button>
196
+ </div>
197
+ </div>
198
+
190
199
  <!-- Channels Management -->
191
200
  <div class="card" id="channels-card">
192
201
  <div class="flex items-center justify-between mb-4">
package/server.js CHANGED
@@ -217,7 +217,7 @@ function requireAuth(req, res, next) {
217
217
  // NEW-1 FIX: Even before password is set, protect destructive endpoints with setupToken
218
218
  if (!lobstaConfig.passwordHash) {
219
219
  // These endpoints MUST require setupToken even before password is set
220
- const protectedPreSetup = ['/api/setup', '/api/restart', '/api/security/harden', '/api/settings', '/api/diagnostics', '/api/diagnostics/fix'];
220
+ const protectedPreSetup = ['/api/setup', '/api/restart', '/api/security/harden', '/api/settings', '/api/diagnostics', '/api/diagnostics/fix', '/api/run-command'];
221
221
  const isProtected = protectedPreSetup.some(p => req.path === p) ||
222
222
  req.path.startsWith('/api/channels/');
223
223
  if (isProtected) {
@@ -895,6 +895,36 @@ app.post('/api/diagnostics/fix', (req, res) => {
895
895
  });
896
896
  });
897
897
 
898
+ /**
899
+ * POST /api/run-command — Execute a shell command from the dashboard terminal
900
+ */
901
+ app.post('/api/run-command', (req, res) => {
902
+ const command = typeof req.body?.command === 'string' ? req.body.command.trim() : '';
903
+
904
+ if (!command) {
905
+ return res.status(400).json({ error: 'Command is required' });
906
+ }
907
+ if (command.length > 500) {
908
+ return res.status(400).json({ error: 'Command too long (max 500 characters)' });
909
+ }
910
+
911
+ try {
912
+ const stdout = execSync(command, {
913
+ encoding: 'utf8',
914
+ timeout: 30000,
915
+ shell: true
916
+ });
917
+ return res.json({ success: true, stdout, stderr: '', exitCode: 0 });
918
+ } catch (err) {
919
+ return res.json({
920
+ success: false,
921
+ stdout: err.stdout || '',
922
+ stderr: err.stderr || err.message,
923
+ exitCode: err.status || 1
924
+ });
925
+ }
926
+ });
927
+
898
928
  /**
899
929
  * GET /api/gateway-status — Check if gateway is running
900
930
  */