@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.
- package/.env.example +6 -1
- package/README.md +5 -5
- package/index.js +23 -5
- package/package.json +5 -2
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/error-tracking/browser/package.json +4 -3
- package/sdk/error-tracking/browser/src/embed.js +29 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/controllers/admin.controller.js +139 -1
- package/src/controllers/adminHeadless.controller.js +82 -0
- package/src/controllers/adminMigration.controller.js +5 -1
- package/src/controllers/adminScripts.controller.js +229 -0
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- package/src/controllers/orgAdmin.controller.js +286 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware.js +119 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/ScriptDefinition.js +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminHeadless.routes.js +6 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +29 -0
- package/src/routes/llmUi.routes.js +26 -0
- package/src/routes/orgAdmin.routes.js +5 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/services/consoleOverride.service.js +291 -0
- package/src/services/email.service.js +17 -1
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/scriptsRunner.service.js +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +312 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/webhook.service.js +2 -2
- package/src/services/workflow.service.js +1 -1
- package/src/utils/encryption.js +5 -3
- package/views/admin-coolify-deploy.ejs +1 -1
- package/views/admin-dashboard-home.ejs +1 -1
- package/views/admin-dashboard.ejs +1 -1
- package/views/admin-errors.ejs +2 -2
- package/views/admin-global-settings.ejs +3 -3
- package/views/admin-headless.ejs +294 -24
- package/views/admin-json-configs.ejs +8 -1
- package/views/admin-llm.ejs +2 -2
- package/views/admin-organizations.ejs +365 -9
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +1 -1
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-test.ejs +3 -3
- package/views/admin-ui-components.ejs +709 -0
- package/views/admin-users.ejs +440 -4
- package/views/admin-webhooks.ejs +1 -1
- package/views/admin-workflows.ejs +1 -1
- package/views/partials/admin-assets-script.ejs +3 -3
- package/views/partials/dashboard/nav-items.ejs +3 -0
- 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-
|
|
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
|
-
|
|
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
|
+
};
|