@intranefr/superbackend 1.4.4 → 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/index.js +16 -1
- package/package.json +5 -2
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/controllers/admin.controller.js +89 -0
- package/src/controllers/adminHeadless.controller.js +82 -0
- 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 +115 -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 +1 -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/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/views/admin-headless.ejs +294 -24
- package/views/admin-organizations.ejs +365 -9
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +709 -0
- package/views/admin-users.ejs +261 -4
- package/views/partials/dashboard/nav-items.ejs +3 -0
|
@@ -3,6 +3,9 @@ const mongoose = require('mongoose');
|
|
|
3
3
|
const Organization = require('../models/Organization');
|
|
4
4
|
const OrganizationMember = require('../models/OrganizationMember');
|
|
5
5
|
const Invite = require('../models/Invite');
|
|
6
|
+
const User = require('../models/User');
|
|
7
|
+
const Asset = require('../models/Asset');
|
|
8
|
+
const Notification = require('../models/Notification');
|
|
6
9
|
const emailService = require('../services/email.service');
|
|
7
10
|
const { isValidOrgRole, getAllowedOrgRoles, getDefaultOrgRole } = require('../utils/orgRoles');
|
|
8
11
|
|
|
@@ -489,3 +492,286 @@ exports.resendInvite = async (req, res) => {
|
|
|
489
492
|
return res.status(500).json({ error: 'Failed to resend invite' });
|
|
490
493
|
}
|
|
491
494
|
};
|
|
495
|
+
|
|
496
|
+
// Create organization (admin only)
|
|
497
|
+
exports.createOrganization = async (req, res) => {
|
|
498
|
+
try {
|
|
499
|
+
const { name, description, ownerUserId } = req.body;
|
|
500
|
+
|
|
501
|
+
// Validation
|
|
502
|
+
if (!name || typeof name !== 'string' || name.trim().length < 2) {
|
|
503
|
+
return res.status(400).json({ error: 'Name must be at least 2 characters long' });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (name.trim().length > 100) {
|
|
507
|
+
return res.status(400).json({ error: 'Name must be less than 100 characters' });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (description && description.trim().length > 500) {
|
|
511
|
+
return res.status(400).json({ error: 'Description must be less than 500 characters' });
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Validate owner if specified
|
|
515
|
+
let ownerId = null;
|
|
516
|
+
if (ownerUserId) {
|
|
517
|
+
if (!mongoose.Types.ObjectId.isValid(String(ownerUserId))) {
|
|
518
|
+
return res.status(400).json({ error: 'Invalid owner user ID' });
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const owner = await User.findById(ownerUserId);
|
|
522
|
+
if (!owner) {
|
|
523
|
+
return res.status(400).json({ error: 'Owner user not found' });
|
|
524
|
+
}
|
|
525
|
+
ownerId = owner._id;
|
|
526
|
+
} else {
|
|
527
|
+
// Default to first admin user if no owner specified
|
|
528
|
+
const defaultOwner = await User.findOne({ role: 'admin' });
|
|
529
|
+
if (!defaultOwner) {
|
|
530
|
+
return res.status(400).json({ error: 'No admin user available to assign as owner' });
|
|
531
|
+
}
|
|
532
|
+
ownerId = defaultOwner._id;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Generate unique slug
|
|
536
|
+
let baseSlug = name.trim()
|
|
537
|
+
.toLowerCase()
|
|
538
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
539
|
+
.replace(/\s+/g, '-')
|
|
540
|
+
.replace(/-+/g, '-')
|
|
541
|
+
.replace(/^-|-$/g, '');
|
|
542
|
+
|
|
543
|
+
if (!baseSlug || baseSlug.length < 2) {
|
|
544
|
+
return res.status(400).json({ error: 'Name must contain valid characters for slug generation' });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let slug = baseSlug;
|
|
548
|
+
let counter = 1;
|
|
549
|
+
|
|
550
|
+
while (await Organization.findOne({ slug })) {
|
|
551
|
+
slug = `${baseSlug}-${counter}`;
|
|
552
|
+
counter++;
|
|
553
|
+
if (counter > 1000) {
|
|
554
|
+
return res.status(500).json({ error: 'Unable to generate unique slug' });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Create organization
|
|
559
|
+
const org = await Organization.create({
|
|
560
|
+
name: name.trim(),
|
|
561
|
+
slug,
|
|
562
|
+
description: description ? description.trim() : '',
|
|
563
|
+
ownerUserId: ownerId,
|
|
564
|
+
status: 'active',
|
|
565
|
+
settings: {}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
console.log(`Admin created organization: ${org.name} (${org._id}) with owner: ${ownerId}`);
|
|
569
|
+
|
|
570
|
+
res.status(201).json({
|
|
571
|
+
message: 'Organization created successfully',
|
|
572
|
+
org: org.toObject()
|
|
573
|
+
});
|
|
574
|
+
} catch (error) {
|
|
575
|
+
console.error('Create organization error:', error);
|
|
576
|
+
if (error.code === 11000) {
|
|
577
|
+
// Duplicate key error
|
|
578
|
+
return res.status(400).json({ error: 'Organization with this name or slug already exists' });
|
|
579
|
+
}
|
|
580
|
+
return res.status(500).json({ error: 'Failed to create organization' });
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// Update organization (admin only)
|
|
585
|
+
exports.updateOrganization = async (req, res) => {
|
|
586
|
+
try {
|
|
587
|
+
const { orgId } = req.params;
|
|
588
|
+
const { name, description, ownerUserId, status } = req.body;
|
|
589
|
+
|
|
590
|
+
if (!orgId || !mongoose.Types.ObjectId.isValid(String(orgId))) {
|
|
591
|
+
return res.status(400).json({ error: 'Invalid organization ID' });
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const org = await Organization.findById(orgId);
|
|
595
|
+
if (!org) {
|
|
596
|
+
return res.status(404).json({ error: 'Organization not found' });
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Update name (but not slug - per requirements)
|
|
600
|
+
if (name !== undefined) {
|
|
601
|
+
if (!name || typeof name !== 'string' || name.trim().length < 2) {
|
|
602
|
+
return res.status(400).json({ error: 'Name must be at least 2 characters long' });
|
|
603
|
+
}
|
|
604
|
+
if (name.trim().length > 100) {
|
|
605
|
+
return res.status(400).json({ error: 'Name must be less than 100 characters' });
|
|
606
|
+
}
|
|
607
|
+
org.name = name.trim();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Update description
|
|
611
|
+
if (description !== undefined) {
|
|
612
|
+
if (description && description.trim().length > 500) {
|
|
613
|
+
return res.status(400).json({ error: 'Description must be less than 500 characters' });
|
|
614
|
+
}
|
|
615
|
+
org.description = description ? description.trim() : '';
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Update owner
|
|
619
|
+
if (ownerUserId !== undefined) {
|
|
620
|
+
if (ownerUserId) {
|
|
621
|
+
if (!mongoose.Types.ObjectId.isValid(String(ownerUserId))) {
|
|
622
|
+
return res.status(400).json({ error: 'Invalid owner user ID' });
|
|
623
|
+
}
|
|
624
|
+
const owner = await User.findById(ownerUserId);
|
|
625
|
+
if (!owner) {
|
|
626
|
+
return res.status(400).json({ error: 'Owner user not found' });
|
|
627
|
+
}
|
|
628
|
+
org.ownerUserId = owner._id;
|
|
629
|
+
} else {
|
|
630
|
+
return res.status(400).json({ error: 'Owner cannot be empty' });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Update status
|
|
635
|
+
if (status !== undefined) {
|
|
636
|
+
if (!['active', 'disabled'].includes(status)) {
|
|
637
|
+
return res.status(400).json({ error: 'Status must be either "active" or "disabled"' });
|
|
638
|
+
}
|
|
639
|
+
org.status = status;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
await org.save();
|
|
643
|
+
|
|
644
|
+
console.log(`Admin updated organization: ${org.name} (${org._id})`);
|
|
645
|
+
|
|
646
|
+
res.json({
|
|
647
|
+
message: 'Organization updated successfully',
|
|
648
|
+
org: org.toObject()
|
|
649
|
+
});
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.error('Update organization error:', error);
|
|
652
|
+
return res.status(500).json({ error: 'Failed to update organization' });
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// Disable organization (admin only)
|
|
657
|
+
exports.disableOrganization = async (req, res) => {
|
|
658
|
+
try {
|
|
659
|
+
const { orgId } = req.params;
|
|
660
|
+
|
|
661
|
+
if (!orgId || !mongoose.Types.ObjectId.isValid(String(orgId))) {
|
|
662
|
+
return res.status(400).json({ error: 'Invalid organization ID' });
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const org = await Organization.findById(orgId);
|
|
666
|
+
if (!org) {
|
|
667
|
+
return res.status(404).json({ error: 'Organization not found' });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (org.status === 'disabled') {
|
|
671
|
+
return res.status(400).json({ error: 'Organization is already disabled' });
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
org.status = 'disabled';
|
|
675
|
+
await org.save();
|
|
676
|
+
|
|
677
|
+
console.log(`Admin disabled organization: ${org.name} (${org._id})`);
|
|
678
|
+
|
|
679
|
+
res.json({
|
|
680
|
+
message: 'Organization disabled successfully',
|
|
681
|
+
org: org.toObject()
|
|
682
|
+
});
|
|
683
|
+
} catch (error) {
|
|
684
|
+
console.error('Disable organization error:', error);
|
|
685
|
+
return res.status(500).json({ error: 'Failed to disable organization' });
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// Enable organization (admin only)
|
|
690
|
+
exports.enableOrganization = async (req, res) => {
|
|
691
|
+
try {
|
|
692
|
+
const { orgId } = req.params;
|
|
693
|
+
|
|
694
|
+
if (!orgId || !mongoose.Types.ObjectId.isValid(String(orgId))) {
|
|
695
|
+
return res.status(400).json({ error: 'Invalid organization ID' });
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const org = await Organization.findById(orgId);
|
|
699
|
+
if (!org) {
|
|
700
|
+
return res.status(404).json({ error: 'Organization not found' });
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (org.status === 'active') {
|
|
704
|
+
return res.status(400).json({ error: 'Organization is already active' });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
org.status = 'active';
|
|
708
|
+
await org.save();
|
|
709
|
+
|
|
710
|
+
console.log(`Admin enabled organization: ${org.name} (${org._id})`);
|
|
711
|
+
|
|
712
|
+
res.json({
|
|
713
|
+
message: 'Organization enabled successfully',
|
|
714
|
+
org: org.toObject()
|
|
715
|
+
});
|
|
716
|
+
} catch (error) {
|
|
717
|
+
console.error('Enable organization error:', error);
|
|
718
|
+
return res.status(500).json({ error: 'Failed to enable organization' });
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
// Delete organization (admin only)
|
|
723
|
+
exports.deleteOrganization = async (req, res) => {
|
|
724
|
+
try {
|
|
725
|
+
const { orgId } = req.params;
|
|
726
|
+
|
|
727
|
+
if (!orgId || !mongoose.Types.ObjectId.isValid(String(orgId))) {
|
|
728
|
+
return res.status(400).json({ error: 'Invalid organization ID' });
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const org = await Organization.findById(orgId);
|
|
732
|
+
if (!org) {
|
|
733
|
+
return res.status(404).json({ error: 'Organization not found' });
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Cascade cleanup
|
|
737
|
+
await cleanupOrganizationData(orgId);
|
|
738
|
+
|
|
739
|
+
// Delete organization
|
|
740
|
+
await Organization.findByIdAndDelete(orgId);
|
|
741
|
+
|
|
742
|
+
console.log(`Admin deleted organization: ${org.name} (${org._id})`);
|
|
743
|
+
|
|
744
|
+
res.json({ message: 'Organization deleted successfully' });
|
|
745
|
+
} catch (error) {
|
|
746
|
+
console.error('Delete organization error:', error);
|
|
747
|
+
return res.status(500).json({ error: 'Failed to delete organization' });
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
// Helper function to clean up organization data
|
|
752
|
+
async function cleanupOrganizationData(orgId) {
|
|
753
|
+
try {
|
|
754
|
+
// Delete organization members
|
|
755
|
+
await OrganizationMember.deleteMany({ orgId });
|
|
756
|
+
|
|
757
|
+
// Delete organization invites
|
|
758
|
+
await Invite.deleteMany({ orgId });
|
|
759
|
+
|
|
760
|
+
// Delete organization assets
|
|
761
|
+
await Asset.deleteMany({ ownerUserId: { $in: await getOrganizationUserIds(orgId) } });
|
|
762
|
+
|
|
763
|
+
// Delete organization notifications
|
|
764
|
+
await Notification.deleteMany({ userId: { $in: await getOrganizationUserIds(orgId) } });
|
|
765
|
+
|
|
766
|
+
console.log(`Completed cleanup for organization ${orgId}`);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
console.error('Error during organization cleanup:', error);
|
|
769
|
+
throw error;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Helper function to get all user IDs in an organization
|
|
774
|
+
async function getOrganizationUserIds(orgId) {
|
|
775
|
+
const members = await OrganizationMember.find({ orgId }).distinct('userId');
|
|
776
|
+
return members;
|
|
777
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const UiComponent = require('../models/UiComponent');
|
|
2
|
+
const UiComponentProject = require('../models/UiComponentProject');
|
|
3
|
+
const UiComponentProjectComponent = require('../models/UiComponentProjectComponent');
|
|
4
|
+
|
|
5
|
+
const { verifyKey } = require('../services/uiComponentsCrypto.service');
|
|
6
|
+
|
|
7
|
+
function extractProjectKey(req) {
|
|
8
|
+
const headerToken = req.headers['x-project-key'] || req.headers['x-api-key'];
|
|
9
|
+
if (headerToken) return String(headerToken).trim();
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function loadAndAuthorizeProject(req, res, projectId) {
|
|
14
|
+
const project = await UiComponentProject.findOne({ projectId: String(projectId), isActive: true }).lean();
|
|
15
|
+
if (!project) {
|
|
16
|
+
res.status(404).json({ error: 'Project not found' });
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!project.isPublic) {
|
|
21
|
+
const key = extractProjectKey(req);
|
|
22
|
+
const ok = project.apiKeyHash && verifyKey(key, project.apiKeyHash);
|
|
23
|
+
if (!ok) {
|
|
24
|
+
res.status(401).json({ error: 'Invalid project key' });
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return project;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
exports.getManifest = async (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const { projectId } = req.params;
|
|
35
|
+
const project = await loadAndAuthorizeProject(req, res, projectId);
|
|
36
|
+
if (!project) return;
|
|
37
|
+
|
|
38
|
+
const assignments = await UiComponentProjectComponent.find({ projectId: project.projectId, enabled: true }).lean();
|
|
39
|
+
const codes = assignments.map((a) => a.componentCode);
|
|
40
|
+
|
|
41
|
+
const components = await UiComponent.find({
|
|
42
|
+
code: { $in: codes },
|
|
43
|
+
isActive: true,
|
|
44
|
+
})
|
|
45
|
+
.sort({ code: 1 })
|
|
46
|
+
.lean();
|
|
47
|
+
|
|
48
|
+
const docsOnly = String(req.query.docs || '').toLowerCase() === 'true';
|
|
49
|
+
|
|
50
|
+
const out = components.map((c) => {
|
|
51
|
+
const base = {
|
|
52
|
+
code: c.code,
|
|
53
|
+
name: c.name,
|
|
54
|
+
version: c.version,
|
|
55
|
+
};
|
|
56
|
+
if (docsOnly) {
|
|
57
|
+
base.usageMarkdown = c.usageMarkdown;
|
|
58
|
+
} else {
|
|
59
|
+
base.html = c.html;
|
|
60
|
+
base.js = c.js;
|
|
61
|
+
base.css = c.css;
|
|
62
|
+
}
|
|
63
|
+
return base;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return res.json({
|
|
67
|
+
project: {
|
|
68
|
+
projectId: project.projectId,
|
|
69
|
+
name: project.name,
|
|
70
|
+
isPublic: project.isPublic,
|
|
71
|
+
allowedOrigins: project.allowedOrigins || [],
|
|
72
|
+
},
|
|
73
|
+
components: out,
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('UI Components getManifest error:', error);
|
|
77
|
+
return res.status(500).json({ error: 'Failed to load manifest' });
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
exports.getComponent = async (req, res) => {
|
|
82
|
+
try {
|
|
83
|
+
const { projectId, code } = req.params;
|
|
84
|
+
const project = await loadAndAuthorizeProject(req, res, projectId);
|
|
85
|
+
if (!project) return;
|
|
86
|
+
|
|
87
|
+
const componentCode = String(code || '').trim().toLowerCase();
|
|
88
|
+
|
|
89
|
+
const assignment = await UiComponentProjectComponent.findOne({
|
|
90
|
+
projectId: project.projectId,
|
|
91
|
+
componentCode,
|
|
92
|
+
enabled: true,
|
|
93
|
+
}).lean();
|
|
94
|
+
|
|
95
|
+
if (!assignment) return res.status(404).json({ error: 'Component not enabled for this project' });
|
|
96
|
+
|
|
97
|
+
const component = await UiComponent.findOne({ code: componentCode, isActive: true }).lean();
|
|
98
|
+
if (!component) return res.status(404).json({ error: 'Component not found' });
|
|
99
|
+
|
|
100
|
+
return res.json({
|
|
101
|
+
project: {
|
|
102
|
+
projectId: project.projectId,
|
|
103
|
+
isPublic: project.isPublic,
|
|
104
|
+
},
|
|
105
|
+
component: {
|
|
106
|
+
code: component.code,
|
|
107
|
+
name: component.name,
|
|
108
|
+
version: component.version,
|
|
109
|
+
html: component.html,
|
|
110
|
+
js: component.js,
|
|
111
|
+
css: component.css,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error('UI Components getComponent error:', error);
|
|
116
|
+
return res.status(500).json({ error: 'Failed to load component' });
|
|
117
|
+
}
|
|
118
|
+
};
|
package/src/middleware/auth.js
CHANGED
|
@@ -19,6 +19,7 @@ const authenticate = async (req, res, next) => {
|
|
|
19
19
|
req.user = user;
|
|
20
20
|
next();
|
|
21
21
|
} catch (error) {
|
|
22
|
+
console.error("api authenticate error:", error);
|
|
22
23
|
return res
|
|
23
24
|
.status(401)
|
|
24
25
|
.json({ error: error.message || "Authentication failed" });
|
|
@@ -45,6 +46,12 @@ const basicAuth = (req, res, next) => {
|
|
|
45
46
|
if (username === adminUsername && password === adminPassword) {
|
|
46
47
|
next();
|
|
47
48
|
} else {
|
|
49
|
+
console.error("api basicAuth error:", {
|
|
50
|
+
username,
|
|
51
|
+
password,
|
|
52
|
+
adminUsername,
|
|
53
|
+
adminPassword,
|
|
54
|
+
});
|
|
48
55
|
res.setHeader("WWW-Authenticate", 'Basic realm="Admin Area"');
|
|
49
56
|
return res.status(401).json({ error: "Invalid credentials" });
|
|
50
57
|
}
|
package/src/middleware.js
CHANGED
|
@@ -31,6 +31,13 @@ function createMiddleware(options = {}) {
|
|
|
31
31
|
const router = express.Router();
|
|
32
32
|
const adminPath = options.adminPath || "/admin";
|
|
33
33
|
|
|
34
|
+
// Expose adminPath and WS attachment helper
|
|
35
|
+
router.adminPath = adminPath;
|
|
36
|
+
router.attachWs = (server) => {
|
|
37
|
+
const { attachTerminalWebsocketServer } = require('./services/terminalsWs.service');
|
|
38
|
+
attachTerminalWebsocketServer(server, { basePathPrefix: adminPath });
|
|
39
|
+
};
|
|
40
|
+
|
|
34
41
|
// Initialize console override service early to capture all logs
|
|
35
42
|
consoleOverride.init();
|
|
36
43
|
|
|
@@ -142,6 +149,12 @@ function createMiddleware(options = {}) {
|
|
|
142
149
|
// Serve public static files (e.g. /og/og-default.png)
|
|
143
150
|
router.use(express.static(path.join(__dirname, "..", "public")));
|
|
144
151
|
|
|
152
|
+
// Serve browser SDK bundles
|
|
153
|
+
router.use(
|
|
154
|
+
"/public/sdk",
|
|
155
|
+
express.static(path.join(__dirname, "..", "public", "sdk")),
|
|
156
|
+
);
|
|
157
|
+
|
|
145
158
|
// Serve static files for admin views
|
|
146
159
|
router.use(
|
|
147
160
|
`${adminPath}/assets`,
|
|
@@ -188,6 +201,70 @@ function createMiddleware(options = {}) {
|
|
|
188
201
|
});
|
|
189
202
|
});
|
|
190
203
|
|
|
204
|
+
router.get(`${adminPath}/terminals`, basicAuth, (req, res) => {
|
|
205
|
+
const templatePath = path.join(
|
|
206
|
+
__dirname,
|
|
207
|
+
"..",
|
|
208
|
+
"views",
|
|
209
|
+
"admin-terminals.ejs",
|
|
210
|
+
);
|
|
211
|
+
fs.readFile(templatePath, "utf8", (err, template) => {
|
|
212
|
+
if (err) {
|
|
213
|
+
console.error("Error reading template:", err);
|
|
214
|
+
return res.status(500).send("Error loading page");
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const html = ejs.render(
|
|
218
|
+
template,
|
|
219
|
+
{
|
|
220
|
+
baseUrl: req.baseUrl,
|
|
221
|
+
adminPath,
|
|
222
|
+
endpointRegistry,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
filename: templatePath,
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
res.send(html);
|
|
229
|
+
} catch (renderErr) {
|
|
230
|
+
console.error("Error rendering template:", renderErr);
|
|
231
|
+
res.status(500).send("Error rendering page");
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
router.get(`${adminPath}/scripts`, basicAuth, (req, res) => {
|
|
237
|
+
const templatePath = path.join(
|
|
238
|
+
__dirname,
|
|
239
|
+
"..",
|
|
240
|
+
"views",
|
|
241
|
+
"admin-scripts.ejs",
|
|
242
|
+
);
|
|
243
|
+
fs.readFile(templatePath, "utf8", (err, template) => {
|
|
244
|
+
if (err) {
|
|
245
|
+
console.error("Error reading template:", err);
|
|
246
|
+
return res.status(500).send("Error loading page");
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const html = ejs.render(
|
|
250
|
+
template,
|
|
251
|
+
{
|
|
252
|
+
baseUrl: req.baseUrl,
|
|
253
|
+
adminPath,
|
|
254
|
+
endpointRegistry,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
filename: templatePath,
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
res.send(html);
|
|
261
|
+
} catch (renderErr) {
|
|
262
|
+
console.error("Error rendering template:", renderErr);
|
|
263
|
+
res.status(500).send("Error rendering page");
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
191
268
|
router.use("/api/admin", require("./routes/admin.routes"));
|
|
192
269
|
router.use("/api/admin/settings", require("./routes/globalSettings.routes"));
|
|
193
270
|
router.use(
|
|
@@ -204,11 +281,14 @@ function createMiddleware(options = {}) {
|
|
|
204
281
|
);
|
|
205
282
|
router.use("/api/admin/i18n", require("./routes/adminI18n.routes"));
|
|
206
283
|
router.use("/api/admin/headless", require("./routes/adminHeadless.routes"));
|
|
284
|
+
router.use("/api/admin/scripts", require("./routes/adminScripts.routes"));
|
|
285
|
+
router.use("/api/admin/terminals", require("./routes/adminTerminals.routes"));
|
|
207
286
|
router.use("/api/admin/assets", require("./routes/adminAssets.routes"));
|
|
208
287
|
router.use(
|
|
209
288
|
"/api/admin/upload-namespaces",
|
|
210
289
|
require("./routes/adminUploadNamespaces.routes"),
|
|
211
290
|
);
|
|
291
|
+
router.use("/api/admin/ui-components", require("./routes/adminUiComponents.routes"));
|
|
212
292
|
router.use("/api/admin/migration", require("./routes/adminMigration.routes"));
|
|
213
293
|
router.use("/api/admin/errors", basicAuth, require("./routes/adminErrors.routes"));
|
|
214
294
|
router.use("/api/admin/audit", basicAuth, require("./routes/adminAudit.routes"));
|
|
@@ -229,6 +309,8 @@ function createMiddleware(options = {}) {
|
|
|
229
309
|
router.use("/api/invites", require("./routes/invite.routes"));
|
|
230
310
|
router.use("/api/log", require("./routes/log.routes"));
|
|
231
311
|
router.use("/api/error-tracking", require("./routes/errorTracking.routes"));
|
|
312
|
+
router.use("/api/ui-components", require("./routes/uiComponentsPublic.routes"));
|
|
313
|
+
router.use("/api/llm/ui", require("./routes/llmUi.routes"));
|
|
232
314
|
|
|
233
315
|
// Public assets proxy
|
|
234
316
|
router.use("/public/assets", require("./routes/publicAssets.routes"));
|
|
@@ -541,6 +623,39 @@ function createMiddleware(options = {}) {
|
|
|
541
623
|
});
|
|
542
624
|
});
|
|
543
625
|
|
|
626
|
+
// Admin UI Components page (protected by basic auth)
|
|
627
|
+
router.get(`${adminPath}/ui-components`, basicAuth, (req, res) => {
|
|
628
|
+
const templatePath = path.join(
|
|
629
|
+
__dirname,
|
|
630
|
+
"..",
|
|
631
|
+
"views",
|
|
632
|
+
"admin-ui-components.ejs",
|
|
633
|
+
);
|
|
634
|
+
fs.readFile(templatePath, "utf8", (err, template) => {
|
|
635
|
+
if (err) {
|
|
636
|
+
console.error("Error reading template:", err);
|
|
637
|
+
return res.status(500).send("Error loading page");
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
const html = ejs.render(
|
|
641
|
+
template,
|
|
642
|
+
{
|
|
643
|
+
baseUrl: req.baseUrl,
|
|
644
|
+
adminPath,
|
|
645
|
+
endpointRegistry,
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
filename: templatePath,
|
|
649
|
+
},
|
|
650
|
+
);
|
|
651
|
+
res.send(html);
|
|
652
|
+
} catch (renderErr) {
|
|
653
|
+
console.error("Error rendering template:", renderErr);
|
|
654
|
+
res.status(500).send("Error rendering page");
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
544
659
|
// Admin JSON configs page (protected by basic auth)
|
|
545
660
|
router.get(`${adminPath}/json-configs`, basicAuth, (req, res) => {
|
|
546
661
|
const templatePath = path.join(
|
|
@@ -28,6 +28,16 @@ const headlessModelDefinitionSchema = new mongoose.Schema(
|
|
|
28
28
|
description: { type: String, default: '' },
|
|
29
29
|
fields: { type: [headlessFieldSchema], default: [] },
|
|
30
30
|
indexes: { type: [headlessIndexSchema], default: [] },
|
|
31
|
+
sourceType: { type: String, enum: ['internal', 'external'], default: 'internal', index: true },
|
|
32
|
+
sourceCollectionName: { type: String, default: null, index: true },
|
|
33
|
+
isExternal: { type: Boolean, default: false, index: true },
|
|
34
|
+
inference: {
|
|
35
|
+
enabled: { type: Boolean, default: false },
|
|
36
|
+
lastInferredAt: { type: Date, default: null },
|
|
37
|
+
sampleSize: { type: Number, default: null },
|
|
38
|
+
warnings: { type: [String], default: [] },
|
|
39
|
+
stats: { type: mongoose.Schema.Types.Mixed, default: null },
|
|
40
|
+
},
|
|
31
41
|
isActive: { type: Boolean, default: true, index: true },
|
|
32
42
|
version: { type: Number, default: 1 },
|
|
33
43
|
fieldsHash: { type: String, default: null },
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const envVarSchema = new mongoose.Schema(
|
|
4
|
+
{
|
|
5
|
+
key: { type: String, required: true },
|
|
6
|
+
value: { type: String, required: true },
|
|
7
|
+
},
|
|
8
|
+
{ _id: false },
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const scriptDefinitionSchema = new mongoose.Schema(
|
|
12
|
+
{
|
|
13
|
+
name: { type: String, required: true },
|
|
14
|
+
codeIdentifier: { type: String, required: true, unique: true, index: true },
|
|
15
|
+
description: { type: String, default: '' },
|
|
16
|
+
type: { type: String, enum: ['bash', 'node', 'browser'], required: true },
|
|
17
|
+
runner: { type: String, enum: ['host', 'vm2', 'browser'], required: true },
|
|
18
|
+
script: { type: String, required: true },
|
|
19
|
+
defaultWorkingDirectory: { type: String, default: '' },
|
|
20
|
+
env: { type: [envVarSchema], default: [] },
|
|
21
|
+
timeoutMs: { type: Number, default: 5 * 60 * 1000 },
|
|
22
|
+
enabled: { type: Boolean, default: true, index: true },
|
|
23
|
+
},
|
|
24
|
+
{ timestamps: true, collection: 'script_definitions' },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function normalizeCodeIdentifier(codeIdentifier) {
|
|
28
|
+
return String(codeIdentifier || '')
|
|
29
|
+
.trim()
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/\s+/g, '-')
|
|
32
|
+
.replace(/[^a-z0-9_-]/g, '');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
scriptDefinitionSchema.pre('validate', function preValidate(next) {
|
|
36
|
+
this.codeIdentifier = normalizeCodeIdentifier(this.codeIdentifier);
|
|
37
|
+
next();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
module.exports =
|
|
41
|
+
mongoose.models.ScriptDefinition ||
|
|
42
|
+
mongoose.model('ScriptDefinition', scriptDefinitionSchema);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const scriptRunSchema = new mongoose.Schema(
|
|
4
|
+
{
|
|
5
|
+
scriptId: { type: mongoose.Schema.Types.ObjectId, ref: 'ScriptDefinition', required: true, index: true },
|
|
6
|
+
status: {
|
|
7
|
+
type: String,
|
|
8
|
+
enum: ['queued', 'running', 'succeeded', 'failed', 'canceled', 'timed_out'],
|
|
9
|
+
default: 'queued',
|
|
10
|
+
index: true,
|
|
11
|
+
},
|
|
12
|
+
trigger: { type: String, enum: ['manual', 'schedule', 'api'], default: 'manual', index: true },
|
|
13
|
+
startedAt: { type: Date, default: null },
|
|
14
|
+
finishedAt: { type: Date, default: null },
|
|
15
|
+
exitCode: { type: Number, default: null },
|
|
16
|
+
outputTail: { type: String, default: '' },
|
|
17
|
+
meta: { type: mongoose.Schema.Types.Mixed, default: null },
|
|
18
|
+
},
|
|
19
|
+
{ timestamps: true, collection: 'script_runs' },
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
module.exports = mongoose.models.ScriptRun || mongoose.model('ScriptRun', scriptRunSchema);
|