@intranefr/superbackend 1.4.3 → 1.5.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 (65) hide show
  1. package/.env.example +6 -1
  2. package/README.md +5 -5
  3. package/index.js +23 -5
  4. package/package.json +5 -2
  5. package/public/sdk/ui-components.iife.js +191 -0
  6. package/sdk/error-tracking/browser/package.json +4 -3
  7. package/sdk/error-tracking/browser/src/embed.js +29 -0
  8. package/sdk/ui-components/browser/src/index.js +228 -0
  9. package/src/controllers/admin.controller.js +139 -1
  10. package/src/controllers/adminHeadless.controller.js +82 -0
  11. package/src/controllers/adminMigration.controller.js +5 -1
  12. package/src/controllers/adminScripts.controller.js +229 -0
  13. package/src/controllers/adminTerminals.controller.js +39 -0
  14. package/src/controllers/adminUiComponents.controller.js +315 -0
  15. package/src/controllers/adminUiComponentsAi.controller.js +34 -0
  16. package/src/controllers/orgAdmin.controller.js +286 -0
  17. package/src/controllers/uiComponentsPublic.controller.js +118 -0
  18. package/src/middleware/auth.js +7 -0
  19. package/src/middleware.js +119 -0
  20. package/src/models/HeadlessModelDefinition.js +10 -0
  21. package/src/models/ScriptDefinition.js +42 -0
  22. package/src/models/ScriptRun.js +22 -0
  23. package/src/models/UiComponent.js +29 -0
  24. package/src/models/UiComponentProject.js +26 -0
  25. package/src/models/UiComponentProjectComponent.js +18 -0
  26. package/src/routes/admin.routes.js +2 -0
  27. package/src/routes/adminHeadless.routes.js +6 -0
  28. package/src/routes/adminScripts.routes.js +21 -0
  29. package/src/routes/adminTerminals.routes.js +13 -0
  30. package/src/routes/adminUiComponents.routes.js +29 -0
  31. package/src/routes/llmUi.routes.js +26 -0
  32. package/src/routes/orgAdmin.routes.js +5 -0
  33. package/src/routes/uiComponentsPublic.routes.js +9 -0
  34. package/src/services/consoleOverride.service.js +291 -0
  35. package/src/services/email.service.js +17 -1
  36. package/src/services/headlessExternalModels.service.js +292 -0
  37. package/src/services/headlessModels.service.js +26 -6
  38. package/src/services/scriptsRunner.service.js +259 -0
  39. package/src/services/terminals.service.js +152 -0
  40. package/src/services/terminalsWs.service.js +100 -0
  41. package/src/services/uiComponentsAi.service.js +312 -0
  42. package/src/services/uiComponentsCrypto.service.js +39 -0
  43. package/src/services/webhook.service.js +2 -2
  44. package/src/services/workflow.service.js +1 -1
  45. package/src/utils/encryption.js +5 -3
  46. package/views/admin-coolify-deploy.ejs +1 -1
  47. package/views/admin-dashboard-home.ejs +1 -1
  48. package/views/admin-dashboard.ejs +1 -1
  49. package/views/admin-errors.ejs +2 -2
  50. package/views/admin-global-settings.ejs +3 -3
  51. package/views/admin-headless.ejs +294 -24
  52. package/views/admin-json-configs.ejs +8 -1
  53. package/views/admin-llm.ejs +2 -2
  54. package/views/admin-organizations.ejs +365 -9
  55. package/views/admin-scripts.ejs +497 -0
  56. package/views/admin-seo-config.ejs +1 -1
  57. package/views/admin-terminals.ejs +328 -0
  58. package/views/admin-test.ejs +3 -3
  59. package/views/admin-ui-components.ejs +709 -0
  60. package/views/admin-users.ejs +440 -4
  61. package/views/admin-webhooks.ejs +1 -1
  62. package/views/admin-workflows.ejs +1 -1
  63. package/views/partials/admin-assets-script.ejs +3 -3
  64. package/views/partials/dashboard/nav-items.ejs +3 -0
  65. package/views/partials/dashboard/palette.ejs +1 -1
@@ -1,11 +1,19 @@
1
1
  const User = require('../models/User');
2
2
  const StripeWebhookEvent = require('../models/StripeWebhookEvent');
3
+ const Organization = require('../models/Organization');
4
+ const OrganizationMember = require('../models/OrganizationMember');
5
+ const Asset = require('../models/Asset');
6
+ const Notification = require('../models/Notification');
7
+ const Invite = require('../models/Invite');
8
+ const EmailLog = require('../models/EmailLog');
9
+ const FormSubmission = require('../models/FormSubmission');
3
10
  const asyncHandler = require('../utils/asyncHandler');
4
11
  const fs = require('fs');
5
12
  const path = require('path');
6
13
  const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
7
14
  const { generateAccessToken, generateRefreshToken } = require('../utils/jwt');
8
15
  const { retryFailedWebhooks, processWebhookEvent } = require('../utils/webhookRetry');
16
+ const { auditMiddleware } = require('../services/auditLogger');
9
17
 
10
18
  // Get all users
11
19
  const getUsers = asyncHandler(async (req, res) => {
@@ -17,6 +25,53 @@ const getUsers = asyncHandler(async (req, res) => {
17
25
  res.json(users.map(u => u.toJSON()));
18
26
  });
19
27
 
28
+ // Register new user (admin only)
29
+ const registerUser = asyncHandler(async (req, res) => {
30
+ const { email, password, name, role = 'user' } = req.body;
31
+
32
+ // Validation
33
+ if (!email || !password) {
34
+ return res.status(400).json({ error: 'Email and password are required' });
35
+ }
36
+
37
+ if (password.length < 6) {
38
+ return res.status(400).json({ error: 'Password must be at least 6 characters' });
39
+ }
40
+
41
+ if (!['user', 'admin'].includes(role)) {
42
+ return res.status(400).json({ error: 'Role must be either "user" or "admin"' });
43
+ }
44
+
45
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
46
+ if (!emailRegex.test(email)) {
47
+ return res.status(400).json({ error: 'Invalid email format' });
48
+ }
49
+
50
+ // Check if user already exists
51
+ const existingUser = await User.findOne({ email: email.toLowerCase() });
52
+ if (existingUser) {
53
+ return res.status(409).json({ error: 'Email already registered' });
54
+ }
55
+
56
+ // Create new user
57
+ const user = new User({
58
+ email: email.toLowerCase(),
59
+ passwordHash: password, // Will be hashed by pre-save hook
60
+ name: name || '',
61
+ role: role
62
+ });
63
+
64
+ await user.save();
65
+
66
+ // Log the admin action
67
+ console.log(`Admin registered new user: ${user.email} with role: ${user.role}`);
68
+
69
+ res.status(201).json({
70
+ success: true,
71
+ user: user.toJSON()
72
+ });
73
+ });
74
+
20
75
  // Get single user
21
76
  const getUser = asyncHandler(async (req, res) => {
22
77
  const user = await User.findById(req.params.id);
@@ -290,7 +345,7 @@ const provisionCoolifyDeploy = asyncHandler(async (req, res) => {
290
345
  });
291
346
  }
292
347
 
293
- // In ref-saasbackend, manage.sh already exists in the root of the repository
348
+ // In ref-superbackend, manage.sh already exists in the root of the repository
294
349
  // If it didn't, we would write it here. For this case, we'll just success.
295
350
  res.json({
296
351
  success: true,
@@ -305,11 +360,94 @@ const provisionCoolifyDeploy = asyncHandler(async (req, res) => {
305
360
  }
306
361
  });
307
362
 
363
+ // Delete user (admin only)
364
+ const deleteUser = asyncHandler(async (req, res) => {
365
+
366
+ const userId = req.params.id;
367
+
368
+ // 1. Validate user exists
369
+ const user = await User.findById(userId);
370
+ if (!user) {
371
+ return res.status(404).json({ error: 'User not found' });
372
+ }
373
+
374
+ // 2. Prevent self-deletion
375
+ // Note: In a real implementation, you'd get the admin ID from req.admin or similar
376
+ // For now, we'll skip this check as the basic auth doesn't provide user identity
377
+
378
+ // 3. Check if this is the last admin
379
+ const adminCount = await User.countDocuments({ role: 'admin' });
380
+ if (user.role === 'admin' && adminCount <= 1) {
381
+ return res.status(400).json({ error: 'Cannot delete the last admin user' });
382
+ }
383
+
384
+ // 4. Cleanup dependencies
385
+ await cleanupUserData(userId);
386
+
387
+ // 5. Delete user
388
+ await User.findByIdAndDelete(userId);
389
+
390
+ // 6. Log action
391
+ console.log(`Admin deleted user: ${user.email} (${userId})`);
392
+
393
+ res.json({ message: 'User deleted successfully' });
394
+ });
395
+
396
+ // Helper function to clean up user data
397
+ async function cleanupUserData(userId) {
398
+
399
+ try {
400
+ // Handle organizations owned by user
401
+ const ownedOrgs = await Organization.find({ ownerUserId: userId });
402
+ for (const org of ownedOrgs) {
403
+ // Check if organization has other members
404
+ const memberCount = await OrganizationMember.countDocuments({
405
+ orgId: org._id,
406
+ userId: { $ne: userId }
407
+ });
408
+
409
+ if (memberCount === 0) {
410
+ // Delete organization if no other members
411
+ await Organization.findByIdAndDelete(org._id);
412
+ console.log(`Deleted organization ${org.name} (${org._id}) - no other members`);
413
+ } else {
414
+ // Remove owner but keep organization
415
+ org.ownerUserId = null;
416
+ await org.save();
417
+ console.log(`Removed owner from organization ${org.name} (${org._id}) - has other members`);
418
+ }
419
+ }
420
+
421
+ // Remove from all organization memberships
422
+ await OrganizationMember.deleteMany({ userId: userId });
423
+
424
+ // Delete user's assets
425
+ await Asset.deleteMany({ ownerUserId: userId });
426
+
427
+ // Delete notifications
428
+ await Notification.deleteMany({ userId: userId });
429
+
430
+ // Clean up other references
431
+ await Invite.deleteMany({ createdByUserId: userId });
432
+ await EmailLog.deleteMany({ userId: userId });
433
+ await FormSubmission.deleteMany({ userId: userId });
434
+
435
+ // Note: We keep ActivityLog and AuditEvent for audit purposes
436
+
437
+ console.log(`Completed cleanup for user ${userId}`);
438
+ } catch (error) {
439
+ console.error('Error during user cleanup:', error);
440
+ throw error;
441
+ }
442
+ }
443
+
308
444
  module.exports = {
309
445
  getUsers,
446
+ registerUser,
310
447
  getUser,
311
448
  updateUserSubscription,
312
449
  updateUserPassword,
450
+ deleteUser,
313
451
  reconcileUser,
314
452
  generateToken,
315
453
  getWebhookEvents,
@@ -7,6 +7,12 @@ const {
7
7
  getDynamicModel,
8
8
  } = require('../services/headlessModels.service');
9
9
 
10
+ const {
11
+ listExternalCollections,
12
+ inferExternalModelFromCollection,
13
+ createOrUpdateExternalModel,
14
+ } = require('../services/headlessExternalModels.service');
15
+
10
16
  const llmService = require('../services/llm.service');
11
17
  const { getSettingValue } = require('../services/globalSettings.service');
12
18
  const { createAuditEvent, getBasicAuthActor } = require('../services/audit.service');
@@ -399,6 +405,82 @@ exports.deleteModel = async (req, res) => {
399
405
  }
400
406
  };
401
407
 
408
+ // External models (Mongo collections)
409
+ exports.listExternalCollections = async (req, res) => {
410
+ try {
411
+ const q = String(req.query.q || '').trim() || null;
412
+ const includeSystem = String(req.query.includeSystem || '').trim().toLowerCase() === 'true';
413
+ const items = await listExternalCollections({ q, includeSystem });
414
+ return res.json({ items });
415
+ } catch (error) {
416
+ console.error('Error listing external mongo collections:', error);
417
+ return handleServiceError(res, error);
418
+ }
419
+ };
420
+
421
+ exports.inferExternalCollection = async (req, res) => {
422
+ try {
423
+ const body = req.body && typeof req.body === 'object' ? req.body : {};
424
+ const collectionName = String(body.collectionName || '').trim();
425
+ const sampleSize = body.sampleSize;
426
+ const result = await inferExternalModelFromCollection({ collectionName, sampleSize });
427
+ return res.json(result);
428
+ } catch (error) {
429
+ console.error('Error inferring external collection schema:', error);
430
+ return handleServiceError(res, error);
431
+ }
432
+ };
433
+
434
+ exports.importExternalModel = async (req, res) => {
435
+ try {
436
+ const body = req.body && typeof req.body === 'object' ? req.body : {};
437
+ const collectionName = String(body.collectionName || '').trim();
438
+ const codeIdentifier = String(body.codeIdentifier || '').trim();
439
+ const displayName = String(body.displayName || '').trim();
440
+ const sampleSize = body.sampleSize;
441
+
442
+ const result = await createOrUpdateExternalModel({
443
+ collectionName,
444
+ codeIdentifier,
445
+ displayName,
446
+ sampleSize,
447
+ });
448
+
449
+ return res.status(result.created ? 201 : 200).json({ item: result.item, inference: result.inference });
450
+ } catch (error) {
451
+ console.error('Error importing external model:', error);
452
+ return handleServiceError(res, error);
453
+ }
454
+ };
455
+
456
+ exports.syncExternalModel = async (req, res) => {
457
+ try {
458
+ const codeIdentifier = String(req.params.codeIdentifier || '').trim();
459
+ const existing = await getModelDefinitionByCode(codeIdentifier);
460
+ if (!existing) return res.status(404).json({ error: 'Model not found' });
461
+
462
+ const isExternal = existing.sourceType === 'external' || existing.isExternal === true;
463
+ if (!isExternal) {
464
+ return res.status(400).json({ error: 'Model is not external' });
465
+ }
466
+
467
+ const body = req.body && typeof req.body === 'object' ? req.body : {};
468
+ const sampleSize = body.sampleSize;
469
+
470
+ const result = await createOrUpdateExternalModel({
471
+ collectionName: existing.sourceCollectionName,
472
+ codeIdentifier: existing.codeIdentifier,
473
+ displayName: existing.displayName,
474
+ sampleSize,
475
+ });
476
+
477
+ return res.json({ item: result.item, inference: result.inference });
478
+ } catch (error) {
479
+ console.error('Error syncing external model:', error);
480
+ return handleServiceError(res, error);
481
+ }
482
+ };
483
+
402
484
  exports.validateModelDefinition = async (req, res) => {
403
485
  try {
404
486
  const body = req.body || {};
@@ -1,7 +1,11 @@
1
1
  const migrationService = require('../services/migration.service');
2
2
 
3
3
  function getModelRegistry() {
4
- return globalThis?.saasbackend?.models || null;
4
+ // Try new registry first, then fallback to old registry for backward compatibility
5
+ if (globalThis?.saasbackend?.models && !globalThis?.superbackend?.models) {
6
+ console.warn('Deprecation: globalThis.saasbackend is deprecated. Use globalThis.superbackend instead.');
7
+ }
8
+ return globalThis?.superbackend?.models || globalThis?.saasbackend?.models || null;
5
9
  }
6
10
 
7
11
  function getModelByName(modelName) {
@@ -0,0 +1,229 @@
1
+ const ScriptDefinition = require('../models/ScriptDefinition');
2
+ const ScriptRun = require('../models/ScriptRun');
3
+ const { startRun, getRunBus } = require('../services/scriptsRunner.service');
4
+
5
+ function toSafeJsonError(error) {
6
+ const msg = error?.message || 'Operation failed';
7
+ const code = error?.code;
8
+ if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
9
+ if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
10
+ if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
11
+ return { status: 500, body: { error: msg } };
12
+ }
13
+
14
+ function normalizeEnv(env) {
15
+ const items = Array.isArray(env) ? env : [];
16
+ const out = [];
17
+ for (const it of items) {
18
+ if (!it || typeof it !== 'object') continue;
19
+ const key = String(it.key || '').trim();
20
+ if (!key) continue;
21
+ out.push({ key, value: String(it.value || '') });
22
+ }
23
+ return out;
24
+ }
25
+
26
+ exports.listScripts = async (req, res) => {
27
+ try {
28
+ const items = await ScriptDefinition.find().sort({ updatedAt: -1 }).lean();
29
+ res.json({ items });
30
+ } catch (err) {
31
+ const safe = toSafeJsonError(err);
32
+ res.status(safe.status).json(safe.body);
33
+ }
34
+ };
35
+
36
+ exports.getScript = async (req, res) => {
37
+ try {
38
+ const doc = await ScriptDefinition.findById(req.params.id).lean();
39
+ if (!doc) return res.status(404).json({ error: 'Not found' });
40
+ res.json({ item: doc });
41
+ } catch (err) {
42
+ const safe = toSafeJsonError(err);
43
+ res.status(safe.status).json(safe.body);
44
+ }
45
+ };
46
+
47
+ exports.createScript = async (req, res) => {
48
+ try {
49
+ const payload = req.body || {};
50
+
51
+ const doc = await ScriptDefinition.create({
52
+ name: String(payload.name || '').trim(),
53
+ codeIdentifier: String(payload.codeIdentifier || '').trim(),
54
+ description: String(payload.description || ''),
55
+ type: String(payload.type || '').trim(),
56
+ runner: String(payload.runner || '').trim(),
57
+ script: String(payload.script || ''),
58
+ defaultWorkingDirectory: String(payload.defaultWorkingDirectory || ''),
59
+ env: normalizeEnv(payload.env),
60
+ timeoutMs: payload.timeoutMs === undefined ? undefined : Number(payload.timeoutMs),
61
+ enabled: payload.enabled === undefined ? true : Boolean(payload.enabled),
62
+ });
63
+
64
+ res.status(201).json({ item: doc.toObject() });
65
+ } catch (err) {
66
+ const safe = toSafeJsonError(err);
67
+ res.status(safe.status).json(safe.body);
68
+ }
69
+ };
70
+
71
+ exports.updateScript = async (req, res) => {
72
+ try {
73
+ const payload = req.body || {};
74
+
75
+ const doc = await ScriptDefinition.findById(req.params.id);
76
+ if (!doc) return res.status(404).json({ error: 'Not found' });
77
+
78
+ if (payload.name !== undefined) doc.name = String(payload.name || '').trim();
79
+ if (payload.codeIdentifier !== undefined) doc.codeIdentifier = String(payload.codeIdentifier || '').trim();
80
+ if (payload.description !== undefined) doc.description = String(payload.description || '');
81
+ if (payload.type !== undefined) doc.type = String(payload.type || '').trim();
82
+ if (payload.runner !== undefined) doc.runner = String(payload.runner || '').trim();
83
+ if (payload.script !== undefined) doc.script = String(payload.script || '');
84
+ if (payload.defaultWorkingDirectory !== undefined) {
85
+ doc.defaultWorkingDirectory = String(payload.defaultWorkingDirectory || '');
86
+ }
87
+ if (payload.env !== undefined) doc.env = normalizeEnv(payload.env);
88
+ if (payload.timeoutMs !== undefined) doc.timeoutMs = Number(payload.timeoutMs || 0);
89
+ if (payload.enabled !== undefined) doc.enabled = Boolean(payload.enabled);
90
+
91
+ await doc.save();
92
+ res.json({ item: doc.toObject() });
93
+ } catch (err) {
94
+ const safe = toSafeJsonError(err);
95
+ res.status(safe.status).json(safe.body);
96
+ }
97
+ };
98
+
99
+ exports.deleteScript = async (req, res) => {
100
+ try {
101
+ const doc = await ScriptDefinition.findById(req.params.id);
102
+ if (!doc) return res.status(404).json({ error: 'Not found' });
103
+ await doc.deleteOne();
104
+ res.json({ ok: true });
105
+ } catch (err) {
106
+ const safe = toSafeJsonError(err);
107
+ res.status(safe.status).json(safe.body);
108
+ }
109
+ };
110
+
111
+ exports.runScript = async (req, res) => {
112
+ try {
113
+ const doc = await ScriptDefinition.findById(req.params.id);
114
+ if (!doc) return res.status(404).json({ error: 'Not found' });
115
+ if (!doc.enabled) return res.status(400).json({ error: 'Script is disabled' });
116
+
117
+ const result = await startRun(doc, { trigger: 'manual', meta: { actorType: 'basicAuth' } });
118
+ res.json(result);
119
+ } catch (err) {
120
+ const safe = toSafeJsonError(err);
121
+ res.status(safe.status).json(safe.body);
122
+ }
123
+ };
124
+
125
+ exports.getRun = async (req, res) => {
126
+ try {
127
+ const run = await ScriptRun.findById(req.params.runId).lean();
128
+ if (!run) return res.status(404).json({ error: 'Not found' });
129
+ res.json({ item: run });
130
+ } catch (err) {
131
+ const safe = toSafeJsonError(err);
132
+ res.status(safe.status).json(safe.body);
133
+ }
134
+ };
135
+
136
+ exports.listRuns = async (req, res) => {
137
+ try {
138
+ const filter = {};
139
+ if (req.query.scriptId) filter.scriptId = req.query.scriptId;
140
+
141
+ const items = await ScriptRun.find(filter)
142
+ .sort({ createdAt: -1 })
143
+ .limit(50)
144
+ .lean();
145
+
146
+ res.json({ items });
147
+ } catch (err) {
148
+ const safe = toSafeJsonError(err);
149
+ res.status(safe.status).json(safe.body);
150
+ }
151
+ };
152
+
153
+ exports.streamRun = async (req, res) => {
154
+ try {
155
+ const runId = String(req.params.runId);
156
+
157
+ res.status(200);
158
+ res.setHeader('Content-Type', 'text/event-stream');
159
+ res.setHeader('Cache-Control', 'no-cache');
160
+ res.setHeader('Connection', 'keep-alive');
161
+
162
+ const bus = getRunBus(runId);
163
+
164
+ const since = Number(req.query.since || 0);
165
+ if (bus) {
166
+ const existing = bus.snapshot(since);
167
+ for (const e of existing) {
168
+ res.write(`event: ${e.type}\n`);
169
+ res.write(`data: ${JSON.stringify(e)}\n\n`);
170
+ }
171
+
172
+ const onEvent = (e) => {
173
+ res.write(`event: ${e.type}\n`);
174
+ res.write(`data: ${JSON.stringify(e)}\n\n`);
175
+ };
176
+ const cleanup = () => {
177
+ clearInterval(heartbeat);
178
+ bus.emitter.off('event', onEvent);
179
+ bus.emitter.off('close', onClose);
180
+ };
181
+
182
+ const onClose = () => {
183
+ cleanup();
184
+ res.end();
185
+ };
186
+
187
+ const heartbeat = setInterval(() => {
188
+ res.write(`: ping\n\n`);
189
+ }, 15000);
190
+ heartbeat.unref();
191
+
192
+ bus.emitter.on('event', onEvent);
193
+ bus.emitter.once('close', onClose);
194
+
195
+ req.on('close', () => {
196
+ cleanup();
197
+ });
198
+
199
+ return;
200
+ }
201
+
202
+ const run = await ScriptRun.findById(runId).lean();
203
+ if (!run) {
204
+ res.write(`event: error\n`);
205
+ res.write(`data: ${JSON.stringify({ error: 'Not found' })}\n\n`);
206
+ return res.end();
207
+ }
208
+
209
+ if (run.outputTail) {
210
+ res.write(`event: log\n`);
211
+ res.write(
212
+ `data: ${JSON.stringify({ seq: 1, type: 'log', ts: new Date().toISOString(), stream: 'stdout', line: run.outputTail })}\n\n`,
213
+ );
214
+ }
215
+ res.write(`event: status\n`);
216
+ res.write(
217
+ `data: ${JSON.stringify({ seq: 2, type: 'status', ts: new Date().toISOString(), status: run.status, exitCode: run.exitCode })}\n\n`,
218
+ );
219
+ res.write(`event: done\n`);
220
+ res.write(
221
+ `data: ${JSON.stringify({ seq: 3, type: 'done', ts: new Date().toISOString(), status: run.status, exitCode: run.exitCode })}\n\n`,
222
+ );
223
+ return res.end();
224
+ } catch (err) {
225
+ res.write(`event: error\n`);
226
+ res.write(`data: ${JSON.stringify({ error: err?.message || 'Stream error' })}\n\n`);
227
+ return res.end();
228
+ }
229
+ };
@@ -0,0 +1,39 @@
1
+ const {
2
+ createSession,
3
+ listSessions,
4
+ killSession,
5
+ } = require('../services/terminals.service');
6
+
7
+ function handleError(res, err) {
8
+ const msg = err?.message || 'Operation failed';
9
+ const code = err?.code;
10
+ if (code === 'NOT_FOUND') return res.status(404).json({ error: msg });
11
+ if (code === 'LIMIT') return res.status(429).json({ error: msg });
12
+ return res.status(500).json({ error: msg });
13
+ }
14
+
15
+ exports.createSession = async (req, res) => {
16
+ try {
17
+ const { cols, rows } = req.body || {};
18
+ const result = createSession({ cols, rows });
19
+ res.json(result);
20
+ } catch (err) {
21
+ handleError(res, err);
22
+ }
23
+ };
24
+
25
+ exports.listSessions = async (req, res) => {
26
+ try {
27
+ res.json({ items: listSessions() });
28
+ } catch (err) {
29
+ handleError(res, err);
30
+ }
31
+ };
32
+
33
+ exports.killSession = async (req, res) => {
34
+ try {
35
+ res.json(killSession(req.params.sessionId));
36
+ } catch (err) {
37
+ handleError(res, err);
38
+ }
39
+ };