@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
@@ -0,0 +1,315 @@
1
+ const UiComponent = require('../models/UiComponent');
2
+ const UiComponentProject = require('../models/UiComponentProject');
3
+ const UiComponentProjectComponent = require('../models/UiComponentProjectComponent');
4
+
5
+ const {
6
+ generateProjectApiKeyPlaintext,
7
+ hashKey,
8
+ } = require('../services/uiComponentsCrypto.service');
9
+
10
+ function randomLowerAlphaNum(len) {
11
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
12
+ let out = '';
13
+ for (let i = 0; i < len; i += 1) out += chars[Math.floor(Math.random() * chars.length)];
14
+ return out;
15
+ }
16
+
17
+ function generateProjectId() {
18
+ return `prj_${randomLowerAlphaNum(16)}`;
19
+ }
20
+
21
+ function parseBool(value, fallback) {
22
+ if (value === undefined) return fallback;
23
+ if (typeof value === 'boolean') return value;
24
+ const v = String(value).trim().toLowerCase();
25
+ if (v === 'true' || v === '1' || v === 'yes') return true;
26
+ if (v === 'false' || v === '0' || v === 'no') return false;
27
+ return fallback;
28
+ }
29
+
30
+ exports.listProjects = async (req, res) => {
31
+ try {
32
+ const items = await UiComponentProject.find({}).sort({ createdAt: -1 }).lean();
33
+ return res.json({ items });
34
+ } catch (error) {
35
+ console.error('UI Components listProjects error:', error);
36
+ return res.status(500).json({ error: 'Failed to list projects' });
37
+ }
38
+ };
39
+
40
+ exports.createProject = async (req, res) => {
41
+ try {
42
+ const name = String(req.body?.name || '').trim();
43
+ const projectIdIn = req.body?.projectId !== undefined ? String(req.body.projectId).trim() : '';
44
+ const isPublic = parseBool(req.body?.isPublic, true);
45
+
46
+ if (!name) return res.status(400).json({ error: 'name is required' });
47
+
48
+ const projectId = projectIdIn || generateProjectId();
49
+
50
+ const doc = await UiComponentProject.create({
51
+ projectId,
52
+ name,
53
+ isPublic,
54
+ apiKeyHash: null,
55
+ allowedOrigins: Array.isArray(req.body?.allowedOrigins) ? req.body.allowedOrigins : [],
56
+ isActive: true,
57
+ });
58
+
59
+ let apiKey = null;
60
+ if (!isPublic) {
61
+ apiKey = generateProjectApiKeyPlaintext();
62
+ doc.apiKeyHash = hashKey(apiKey);
63
+ await doc.save();
64
+ }
65
+
66
+ return res.status(201).json({ item: doc.toObject(), apiKey });
67
+ } catch (error) {
68
+ console.error('UI Components createProject error:', error);
69
+ if (error?.name === 'ValidationError') return res.status(400).json({ error: error.message });
70
+ if (error?.code === 11000) return res.status(409).json({ error: 'Project already exists' });
71
+ return res.status(500).json({ error: 'Failed to create project' });
72
+ }
73
+ };
74
+
75
+ exports.getProject = async (req, res) => {
76
+ try {
77
+ const { projectId } = req.params;
78
+ const item = await UiComponentProject.findOne({ projectId: String(projectId) }).lean();
79
+ if (!item) return res.status(404).json({ error: 'Project not found' });
80
+
81
+ const assigned = await UiComponentProjectComponent.find({ projectId: item.projectId }).lean();
82
+ return res.json({ item, assigned });
83
+ } catch (error) {
84
+ console.error('UI Components getProject error:', error);
85
+ return res.status(500).json({ error: 'Failed to load project' });
86
+ }
87
+ };
88
+
89
+ exports.updateProject = async (req, res) => {
90
+ try {
91
+ const { projectId } = req.params;
92
+ const doc = await UiComponentProject.findOne({ projectId: String(projectId) });
93
+ if (!doc) return res.status(404).json({ error: 'Project not found' });
94
+
95
+ if (req.body?.name !== undefined) {
96
+ const name = String(req.body.name || '').trim();
97
+ if (!name) return res.status(400).json({ error: 'name is required' });
98
+ doc.name = name;
99
+ }
100
+
101
+ if (req.body?.isPublic !== undefined) {
102
+ const nextPublic = parseBool(req.body.isPublic, doc.isPublic);
103
+ if (nextPublic !== doc.isPublic) {
104
+ doc.isPublic = nextPublic;
105
+ if (doc.isPublic) {
106
+ doc.apiKeyHash = null;
107
+ } else if (!doc.apiKeyHash) {
108
+ const apiKey = generateProjectApiKeyPlaintext();
109
+ doc.apiKeyHash = hashKey(apiKey);
110
+ await doc.save();
111
+ return res.json({ item: doc.toObject(), apiKey });
112
+ }
113
+ }
114
+ }
115
+
116
+ if (req.body?.allowedOrigins !== undefined) {
117
+ doc.allowedOrigins = Array.isArray(req.body.allowedOrigins) ? req.body.allowedOrigins : [];
118
+ }
119
+
120
+ if (req.body?.isActive !== undefined) {
121
+ doc.isActive = Boolean(req.body.isActive);
122
+ }
123
+
124
+ await doc.save();
125
+ return res.json({ item: doc.toObject(), apiKey: null });
126
+ } catch (error) {
127
+ console.error('UI Components updateProject error:', error);
128
+ if (error?.name === 'ValidationError') return res.status(400).json({ error: error.message });
129
+ return res.status(500).json({ error: 'Failed to update project' });
130
+ }
131
+ };
132
+
133
+ exports.rotateProjectKey = async (req, res) => {
134
+ try {
135
+ const { projectId } = req.params;
136
+ const doc = await UiComponentProject.findOne({ projectId: String(projectId) });
137
+ if (!doc) return res.status(404).json({ error: 'Project not found' });
138
+ if (doc.isPublic) return res.status(400).json({ error: 'Project is public' });
139
+
140
+ const apiKey = generateProjectApiKeyPlaintext();
141
+ doc.apiKeyHash = hashKey(apiKey);
142
+ await doc.save();
143
+
144
+ return res.json({ item: doc.toObject(), apiKey });
145
+ } catch (error) {
146
+ console.error('UI Components rotateProjectKey error:', error);
147
+ return res.status(500).json({ error: 'Failed to rotate key' });
148
+ }
149
+ };
150
+
151
+ exports.deleteProject = async (req, res) => {
152
+ try {
153
+ const { projectId } = req.params;
154
+ const doc = await UiComponentProject.findOne({ projectId: String(projectId) });
155
+ if (!doc) return res.status(404).json({ error: 'Project not found' });
156
+
157
+ await UiComponentProjectComponent.deleteMany({ projectId: doc.projectId });
158
+ await UiComponentProject.deleteOne({ _id: doc._id });
159
+ return res.json({ success: true });
160
+ } catch (error) {
161
+ console.error('UI Components deleteProject error:', error);
162
+ return res.status(500).json({ error: 'Failed to delete project' });
163
+ }
164
+ };
165
+
166
+ exports.listComponents = async (req, res) => {
167
+ try {
168
+ const items = await UiComponent.find({}).sort({ updatedAt: -1 }).lean();
169
+ return res.json({ items });
170
+ } catch (error) {
171
+ console.error('UI Components listComponents error:', error);
172
+ return res.status(500).json({ error: 'Failed to list components' });
173
+ }
174
+ };
175
+
176
+ exports.createComponent = async (req, res) => {
177
+ try {
178
+ const code = String(req.body?.code || '').trim().toLowerCase();
179
+ const name = String(req.body?.name || '').trim();
180
+ if (!code) return res.status(400).json({ error: 'code is required' });
181
+ if (!name) return res.status(400).json({ error: 'name is required' });
182
+
183
+ const doc = await UiComponent.create({
184
+ code,
185
+ name,
186
+ html: String(req.body?.html || ''),
187
+ js: String(req.body?.js || ''),
188
+ css: String(req.body?.css || ''),
189
+ api: req.body?.api !== undefined ? req.body.api : null,
190
+ usageMarkdown: String(req.body?.usageMarkdown || ''),
191
+ version: Number(req.body?.version || 1) || 1,
192
+ isActive: parseBool(req.body?.isActive, true),
193
+ });
194
+
195
+ return res.status(201).json({ item: doc.toObject() });
196
+ } catch (error) {
197
+ console.error('UI Components createComponent error:', error);
198
+ if (error?.name === 'ValidationError') return res.status(400).json({ error: error.message });
199
+ if (error?.code === 11000) return res.status(409).json({ error: 'Component already exists' });
200
+ return res.status(500).json({ error: 'Failed to create component' });
201
+ }
202
+ };
203
+
204
+ exports.getComponent = async (req, res) => {
205
+ try {
206
+ const { code } = req.params;
207
+ const item = await UiComponent.findOne({ code: String(code).toLowerCase() }).lean();
208
+ if (!item) return res.status(404).json({ error: 'Component not found' });
209
+ return res.json({ item });
210
+ } catch (error) {
211
+ console.error('UI Components getComponent error:', error);
212
+ return res.status(500).json({ error: 'Failed to load component' });
213
+ }
214
+ };
215
+
216
+ exports.updateComponent = async (req, res) => {
217
+ try {
218
+ const { code } = req.params;
219
+ const doc = await UiComponent.findOne({ code: String(code).toLowerCase() });
220
+ if (!doc) return res.status(404).json({ error: 'Component not found' });
221
+
222
+ if (req.body?.name !== undefined) {
223
+ const name = String(req.body.name || '').trim();
224
+ if (!name) return res.status(400).json({ error: 'name is required' });
225
+ doc.name = name;
226
+ }
227
+ if (req.body?.html !== undefined) doc.html = String(req.body.html || '');
228
+ if (req.body?.js !== undefined) doc.js = String(req.body.js || '');
229
+ if (req.body?.css !== undefined) doc.css = String(req.body.css || '');
230
+ if (req.body?.api !== undefined) doc.api = req.body.api;
231
+ if (req.body?.usageMarkdown !== undefined) doc.usageMarkdown = String(req.body.usageMarkdown || '');
232
+
233
+ if (req.body?.version !== undefined) {
234
+ const v = Number(req.body.version);
235
+ if (!Number.isFinite(v) || v < 1) return res.status(400).json({ error: 'version must be a positive number' });
236
+ doc.version = v;
237
+ } else {
238
+ doc.version = Number(doc.version || 1) + 1;
239
+ }
240
+
241
+ if (req.body?.isActive !== undefined) doc.isActive = Boolean(req.body.isActive);
242
+
243
+ await doc.save();
244
+ return res.json({ item: doc.toObject() });
245
+ } catch (error) {
246
+ console.error('UI Components updateComponent error:', error);
247
+ if (error?.name === 'ValidationError') return res.status(400).json({ error: error.message });
248
+ return res.status(500).json({ error: 'Failed to update component' });
249
+ }
250
+ };
251
+
252
+ exports.deleteComponent = async (req, res) => {
253
+ try {
254
+ const { code } = req.params;
255
+ const doc = await UiComponent.findOne({ code: String(code).toLowerCase() });
256
+ if (!doc) return res.status(404).json({ error: 'Component not found' });
257
+
258
+ await UiComponentProjectComponent.deleteMany({ componentCode: doc.code });
259
+ await UiComponent.deleteOne({ _id: doc._id });
260
+ return res.json({ success: true });
261
+ } catch (error) {
262
+ console.error('UI Components deleteComponent error:', error);
263
+ return res.status(500).json({ error: 'Failed to delete component' });
264
+ }
265
+ };
266
+
267
+ exports.setAssignment = async (req, res) => {
268
+ try {
269
+ const projectId = String(req.params.projectId || '').trim();
270
+ const code = String(req.params.code || '').trim().toLowerCase();
271
+ const enabled = parseBool(req.body?.enabled, true);
272
+
273
+ const project = await UiComponentProject.findOne({ projectId }).lean();
274
+ if (!project) return res.status(404).json({ error: 'Project not found' });
275
+
276
+ const component = await UiComponent.findOne({ code }).lean();
277
+ if (!component) return res.status(404).json({ error: 'Component not found' });
278
+
279
+ const doc = await UiComponentProjectComponent.findOneAndUpdate(
280
+ { projectId, componentCode: code },
281
+ { $set: { enabled } },
282
+ { upsert: true, new: true, setDefaultsOnInsert: true },
283
+ );
284
+
285
+ return res.json({ item: doc.toObject() });
286
+ } catch (error) {
287
+ console.error('UI Components setAssignment error:', error);
288
+ if (error?.code === 11000) return res.status(409).json({ error: 'Assignment already exists' });
289
+ return res.status(500).json({ error: 'Failed to set assignment' });
290
+ }
291
+ };
292
+
293
+ exports.deleteAssignment = async (req, res) => {
294
+ try {
295
+ const projectId = String(req.params.projectId || '').trim();
296
+ const code = String(req.params.code || '').trim().toLowerCase();
297
+
298
+ await UiComponentProjectComponent.deleteOne({ projectId, componentCode: code });
299
+ return res.json({ success: true });
300
+ } catch (error) {
301
+ console.error('UI Components deleteAssignment error:', error);
302
+ return res.status(500).json({ error: 'Failed to delete assignment' });
303
+ }
304
+ };
305
+
306
+ exports.listProjectAssignments = async (req, res) => {
307
+ try {
308
+ const projectId = String(req.params.projectId || '').trim();
309
+ const items = await UiComponentProjectComponent.find({ projectId }).lean();
310
+ return res.json({ items });
311
+ } catch (error) {
312
+ console.error('UI Components listProjectAssignments error:', error);
313
+ return res.status(500).json({ error: 'Failed to list assignments' });
314
+ }
315
+ };
@@ -0,0 +1,34 @@
1
+ const { proposeComponentEdit } = require('../services/uiComponentsAi.service');
2
+ const { getBasicAuthActor } = require('../services/audit.service');
3
+
4
+ function handleError(res, err) {
5
+ const code = err && err.code;
6
+ if (code === 'VALIDATION') return res.status(400).json({ error: err.message });
7
+ if (code === 'NOT_FOUND') return res.status(404).json({ error: err.message });
8
+ if (code === 'AI_INVALID') return res.status(500).json({ error: err.message });
9
+ return res.status(500).json({ error: err.message || 'Operation failed' });
10
+ }
11
+
12
+ exports.propose = async (req, res) => {
13
+ try {
14
+ const actor = getBasicAuthActor(req);
15
+ const { code } = req.params;
16
+
17
+ const { prompt, providerKey, model, targets, mode } = req.body || {};
18
+
19
+ const result = await proposeComponentEdit({
20
+ code,
21
+ prompt,
22
+ providerKey,
23
+ model,
24
+ targets,
25
+ mode,
26
+ actor,
27
+ });
28
+
29
+ return res.json(result);
30
+ } catch (err) {
31
+ console.error('[adminUiComponentsAi] propose error', err);
32
+ return handleError(res, err);
33
+ }
34
+ };
@@ -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
+ }