@intranefr/superbackend 1.4.4 → 1.5.1

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 (195) hide show
  1. package/.env.example +5 -0
  2. package/README.md +11 -0
  3. package/index.js +39 -1
  4. package/package.json +11 -3
  5. package/public/sdk/ui-components.iife.js +191 -0
  6. package/sdk/ui-components/browser/src/index.js +228 -0
  7. package/src/admin/endpointRegistry.js +120 -0
  8. package/src/controllers/admin.controller.js +111 -5
  9. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  10. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  11. package/src/controllers/adminCache.controller.js +342 -0
  12. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  13. package/src/controllers/adminCrons.controller.js +388 -0
  14. package/src/controllers/adminDbBrowser.controller.js +124 -0
  15. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  16. package/src/controllers/adminHeadless.controller.js +91 -2
  17. package/src/controllers/adminHealthChecks.controller.js +570 -0
  18. package/src/controllers/adminI18n.controller.js +51 -29
  19. package/src/controllers/adminLlm.controller.js +126 -2
  20. package/src/controllers/adminPages.controller.js +720 -0
  21. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  22. package/src/controllers/adminProxy.controller.js +113 -0
  23. package/src/controllers/adminRateLimits.controller.js +138 -0
  24. package/src/controllers/adminRbac.controller.js +803 -0
  25. package/src/controllers/adminScripts.controller.js +320 -0
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/adminTerminals.controller.js +39 -0
  28. package/src/controllers/adminUiComponents.controller.js +315 -0
  29. package/src/controllers/adminUiComponentsAi.controller.js +34 -0
  30. package/src/controllers/blogAdmin.controller.js +279 -0
  31. package/src/controllers/blogAiAdmin.controller.js +224 -0
  32. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  33. package/src/controllers/blogInternal.controller.js +26 -0
  34. package/src/controllers/blogPublic.controller.js +89 -0
  35. package/src/controllers/fileManager.controller.js +190 -0
  36. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  37. package/src/controllers/healthChecksPublic.controller.js +196 -0
  38. package/src/controllers/metrics.controller.js +64 -4
  39. package/src/controllers/orgAdmin.controller.js +366 -0
  40. package/src/controllers/uiComponentsPublic.controller.js +118 -0
  41. package/src/middleware/auth.js +7 -0
  42. package/src/middleware/internalCronAuth.js +29 -0
  43. package/src/middleware/rbac.js +62 -0
  44. package/src/middleware.js +879 -56
  45. package/src/models/BlockDefinition.js +27 -0
  46. package/src/models/BlogAutomationLock.js +14 -0
  47. package/src/models/BlogAutomationRun.js +39 -0
  48. package/src/models/BlogPost.js +42 -0
  49. package/src/models/CacheEntry.js +26 -0
  50. package/src/models/ConsoleEntry.js +32 -0
  51. package/src/models/ConsoleLog.js +23 -0
  52. package/src/models/ContextBlockDefinition.js +33 -0
  53. package/src/models/CronExecution.js +47 -0
  54. package/src/models/CronJob.js +70 -0
  55. package/src/models/ExternalDbConnection.js +49 -0
  56. package/src/models/FileEntry.js +22 -0
  57. package/src/models/HeadlessModelDefinition.js +10 -0
  58. package/src/models/HealthAutoHealAttempt.js +57 -0
  59. package/src/models/HealthCheck.js +132 -0
  60. package/src/models/HealthCheckRun.js +51 -0
  61. package/src/models/HealthIncident.js +49 -0
  62. package/src/models/Page.js +95 -0
  63. package/src/models/PageCollection.js +42 -0
  64. package/src/models/ProxyEntry.js +66 -0
  65. package/src/models/RateLimitCounter.js +19 -0
  66. package/src/models/RateLimitMetricBucket.js +20 -0
  67. package/src/models/RbacGrant.js +25 -0
  68. package/src/models/RbacGroup.js +16 -0
  69. package/src/models/RbacGroupMember.js +13 -0
  70. package/src/models/RbacGroupRole.js +13 -0
  71. package/src/models/RbacRole.js +25 -0
  72. package/src/models/RbacUserRole.js +13 -0
  73. package/src/models/ScriptDefinition.js +42 -0
  74. package/src/models/ScriptRun.js +22 -0
  75. package/src/models/UiComponent.js +29 -0
  76. package/src/models/UiComponentProject.js +26 -0
  77. package/src/models/UiComponentProjectComponent.js +18 -0
  78. package/src/routes/admin.routes.js +1 -0
  79. package/src/routes/adminBlog.routes.js +21 -0
  80. package/src/routes/adminBlogAi.routes.js +16 -0
  81. package/src/routes/adminBlogAutomation.routes.js +27 -0
  82. package/src/routes/adminCache.routes.js +20 -0
  83. package/src/routes/adminConsoleManager.routes.js +302 -0
  84. package/src/routes/adminCrons.routes.js +25 -0
  85. package/src/routes/adminDbBrowser.routes.js +65 -0
  86. package/src/routes/adminEjsVirtual.routes.js +2 -1
  87. package/src/routes/adminHeadless.routes.js +8 -1
  88. package/src/routes/adminHealthChecks.routes.js +28 -0
  89. package/src/routes/adminI18n.routes.js +4 -3
  90. package/src/routes/adminLlm.routes.js +4 -2
  91. package/src/routes/adminPages.routes.js +55 -0
  92. package/src/routes/adminProxy.routes.js +15 -0
  93. package/src/routes/adminRateLimits.routes.js +17 -0
  94. package/src/routes/adminRbac.routes.js +38 -0
  95. package/src/routes/adminScripts.routes.js +21 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminTerminals.routes.js +13 -0
  98. package/src/routes/adminUiComponents.routes.js +30 -0
  99. package/src/routes/blogInternal.routes.js +14 -0
  100. package/src/routes/blogPublic.routes.js +9 -0
  101. package/src/routes/fileManager.routes.js +62 -0
  102. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  103. package/src/routes/healthChecksPublic.routes.js +9 -0
  104. package/src/routes/log.routes.js +43 -60
  105. package/src/routes/metrics.routes.js +4 -2
  106. package/src/routes/orgAdmin.routes.js +6 -0
  107. package/src/routes/pages.routes.js +123 -0
  108. package/src/routes/proxy.routes.js +46 -0
  109. package/src/routes/rbac.routes.js +47 -0
  110. package/src/routes/uiComponentsPublic.routes.js +9 -0
  111. package/src/routes/webhook.routes.js +2 -1
  112. package/src/routes/workflows.routes.js +4 -0
  113. package/src/services/blockDefinitionsAi.service.js +247 -0
  114. package/src/services/blog.service.js +99 -0
  115. package/src/services/blogAutomation.service.js +978 -0
  116. package/src/services/blogCronsBootstrap.service.js +184 -0
  117. package/src/services/blogPublishing.service.js +58 -0
  118. package/src/services/cacheLayer.service.js +696 -0
  119. package/src/services/consoleManager.service.js +700 -0
  120. package/src/services/consoleOverride.service.js +6 -1
  121. package/src/services/cronScheduler.service.js +350 -0
  122. package/src/services/dbBrowser.service.js +536 -0
  123. package/src/services/ejsVirtual.service.js +102 -32
  124. package/src/services/fileManager.service.js +475 -0
  125. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  126. package/src/services/headlessExternalModels.service.js +292 -0
  127. package/src/services/headlessModels.service.js +26 -6
  128. package/src/services/healthChecks.service.js +650 -0
  129. package/src/services/healthChecksBootstrap.service.js +109 -0
  130. package/src/services/healthChecksScheduler.service.js +106 -0
  131. package/src/services/llmDefaults.service.js +190 -0
  132. package/src/services/migrationAssets/s3.js +2 -2
  133. package/src/services/pages.service.js +602 -0
  134. package/src/services/pagesContext.service.js +331 -0
  135. package/src/services/pagesContextBlocksAi.service.js +349 -0
  136. package/src/services/proxy.service.js +535 -0
  137. package/src/services/rateLimiter.service.js +623 -0
  138. package/src/services/rbac.service.js +212 -0
  139. package/src/services/scriptsRunner.service.js +259 -0
  140. package/src/services/terminals.service.js +152 -0
  141. package/src/services/terminalsWs.service.js +100 -0
  142. package/src/services/uiComponentsAi.service.js +299 -0
  143. package/src/services/uiComponentsCrypto.service.js +39 -0
  144. package/src/services/workflow.service.js +23 -8
  145. package/src/utils/orgRoles.js +14 -0
  146. package/src/utils/rbac/engine.js +60 -0
  147. package/src/utils/rbac/rightsRegistry.js +29 -0
  148. package/views/admin-blog-automation.ejs +877 -0
  149. package/views/admin-blog-edit.ejs +542 -0
  150. package/views/admin-blog.ejs +399 -0
  151. package/views/admin-cache.ejs +681 -0
  152. package/views/admin-console-manager.ejs +680 -0
  153. package/views/admin-crons.ejs +645 -0
  154. package/views/admin-db-browser.ejs +445 -0
  155. package/views/admin-ejs-virtual.ejs +16 -10
  156. package/views/admin-file-manager.ejs +942 -0
  157. package/views/admin-headless.ejs +294 -24
  158. package/views/admin-health-checks.ejs +725 -0
  159. package/views/admin-i18n.ejs +59 -5
  160. package/views/admin-llm.ejs +99 -1
  161. package/views/admin-organizations.ejs +528 -10
  162. package/views/admin-pages.ejs +2424 -0
  163. package/views/admin-proxy.ejs +491 -0
  164. package/views/admin-rate-limiter.ejs +625 -0
  165. package/views/admin-rbac.ejs +1331 -0
  166. package/views/admin-scripts.ejs +497 -0
  167. package/views/admin-seo-config.ejs +61 -7
  168. package/views/admin-terminals.ejs +328 -0
  169. package/views/admin-ui-components.ejs +741 -0
  170. package/views/admin-users.ejs +261 -4
  171. package/views/admin-workflows.ejs +7 -7
  172. package/views/file-manager.ejs +866 -0
  173. package/views/pages/blocks/contact.ejs +27 -0
  174. package/views/pages/blocks/cta.ejs +18 -0
  175. package/views/pages/blocks/faq.ejs +20 -0
  176. package/views/pages/blocks/features.ejs +19 -0
  177. package/views/pages/blocks/hero.ejs +13 -0
  178. package/views/pages/blocks/html.ejs +5 -0
  179. package/views/pages/blocks/image.ejs +14 -0
  180. package/views/pages/blocks/testimonials.ejs +26 -0
  181. package/views/pages/blocks/text.ejs +10 -0
  182. package/views/pages/layouts/default.ejs +51 -0
  183. package/views/pages/layouts/minimal.ejs +42 -0
  184. package/views/pages/layouts/sidebar.ejs +54 -0
  185. package/views/pages/partials/footer.ejs +13 -0
  186. package/views/pages/partials/header.ejs +12 -0
  187. package/views/pages/partials/sidebar.ejs +8 -0
  188. package/views/pages/runtime/page.ejs +10 -0
  189. package/views/pages/templates/article.ejs +20 -0
  190. package/views/pages/templates/default.ejs +12 -0
  191. package/views/pages/templates/landing.ejs +14 -0
  192. package/views/pages/templates/listing.ejs +15 -0
  193. package/views/partials/admin-image-upload-modal.ejs +221 -0
  194. package/views/partials/dashboard/nav-items.ejs +14 -0
  195. package/views/partials/llm-provider-model-picker.ejs +183 -0
@@ -33,6 +33,7 @@
33
33
  <p class="text-sm text-gray-600 mt-1">Browse orgs, members and invites</p>
34
34
  </div>
35
35
  <div class="flex items-center gap-4">
36
+ <button id="btn-create-org" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 font-medium">Create Organization</button>
36
37
  </div>
37
38
  </div>
38
39
  </div>
@@ -90,6 +91,7 @@
90
91
  <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
91
92
  <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Slug</th>
92
93
  <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
94
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
93
95
  </tr>
94
96
  </thead>
95
97
  <tbody id="orgs-tbody" class="bg-white divide-y divide-gray-200"></tbody>
@@ -140,6 +142,30 @@
140
142
  <button id="btn-members-refresh" class="bg-gray-100 text-gray-800 px-3 py-2 rounded hover:bg-gray-200" disabled>Refresh</button>
141
143
  </div>
142
144
 
145
+ <!-- Assign User Section -->
146
+ <div class="bg-gray-50 rounded-lg border p-4 mt-4">
147
+ <h4 class="font-semibold text-gray-900">Assign User to Organization</h4>
148
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-3 mt-3">
149
+ <div>
150
+ <label class="block text-sm font-medium text-gray-700 mb-1">User *</label>
151
+ <select id="assign-user-select" class="w-full border rounded px-3 py-2">
152
+ <option value="">Loading users...</option>
153
+ </select>
154
+ </div>
155
+ <div>
156
+ <label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
157
+ <select id="assign-user-role" class="w-full border rounded px-3 py-2">
158
+ <option value="member">member</option>
159
+ <option value="admin">admin</option>
160
+ <option value="viewer">viewer</option>
161
+ </select>
162
+ </div>
163
+ <div class="flex items-end">
164
+ <button id="btn-assign-user" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" disabled>Assign User</button>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
143
169
  <div class="grid grid-cols-1 md:grid-cols-3 gap-3 mt-4">
144
170
  <div>
145
171
  <label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
@@ -275,6 +301,78 @@
275
301
  </div>
276
302
  </div>
277
303
 
304
+ <!-- Create Organization Modal -->
305
+ <div id="modal-create-org" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
306
+ <div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
307
+ <div class="px-6 py-4 border-b border-gray-200">
308
+ <h3 class="text-lg font-medium text-gray-900">Create New Organization</h3>
309
+ </div>
310
+ <div class="px-6 py-4">
311
+ <div class="space-y-4">
312
+ <div>
313
+ <label class="block text-sm font-medium text-gray-700 mb-1">Name *</label>
314
+ <input id="create-org-name" type="text" class="w-full border rounded px-3 py-2" placeholder="Organization name" maxlength="100">
315
+ <div class="text-xs text-gray-500 mt-1">2-100 characters. Slug will be auto-generated.</div>
316
+ </div>
317
+ <div>
318
+ <label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
319
+ <textarea id="create-org-description" class="w-full border rounded px-3 py-2" rows="3" placeholder="Optional description" maxlength="500"></textarea>
320
+ <div class="text-xs text-gray-500 mt-1">Maximum 500 characters.</div>
321
+ </div>
322
+ <div>
323
+ <label class="block text-sm font-medium text-gray-700 mb-1">Owner User ID</label>
324
+ <input id="create-org-owner" type="text" class="w-full border rounded px-3 py-2" placeholder="Leave empty to use first admin">
325
+ <div class="text-xs text-gray-500 mt-1">Optional. Leave empty to assign to first admin user.</div>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ <div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
330
+ <button id="btn-create-org-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
331
+ <button id="btn-create-org-submit" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">Create</button>
332
+ </div>
333
+ </div>
334
+ </div>
335
+
336
+ <!-- Edit Organization Modal -->
337
+ <div id="modal-edit-org" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
338
+ <div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
339
+ <div class="px-6 py-4 border-b border-gray-200">
340
+ <h3 class="text-lg font-medium text-gray-900">Edit Organization</h3>
341
+ </div>
342
+ <div class="px-6 py-4">
343
+ <div class="space-y-4">
344
+ <input id="edit-org-id" type="hidden">
345
+ <div>
346
+ <label class="block text-sm font-medium text-gray-700 mb-1">Name *</label>
347
+ <input id="edit-org-name" type="text" class="w-full border rounded px-3 py-2" placeholder="Organization name" maxlength="100">
348
+ <div class="text-xs text-gray-500 mt-1">2-100 characters.</div>
349
+ </div>
350
+ <div>
351
+ <label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
352
+ <textarea id="edit-org-description" class="w-full border rounded px-3 py-2" rows="3" placeholder="Optional description" maxlength="500"></textarea>
353
+ <div class="text-xs text-gray-500 mt-1">Maximum 500 characters.</div>
354
+ </div>
355
+ <div>
356
+ <label class="block text-sm font-medium text-gray-700 mb-1">Owner User ID</label>
357
+ <input id="edit-org-owner" type="text" class="w-full border rounded px-3 py-2" placeholder="User ID">
358
+ <div class="text-xs text-gray-500 mt-1">Current owner will be replaced.</div>
359
+ </div>
360
+ <div>
361
+ <label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
362
+ <select id="edit-org-status" class="w-full border rounded px-3 py-2">
363
+ <option value="active">Active</option>
364
+ <option value="disabled">Disabled</option>
365
+ </select>
366
+ </div>
367
+ </div>
368
+ </div>
369
+ <div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
370
+ <button id="btn-edit-org-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
371
+ <button id="btn-edit-org-submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Save Changes</button>
372
+ </div>
373
+ </div>
374
+ </div>
375
+
278
376
  <!-- Toast Container -->
279
377
  <div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
280
378
 
@@ -379,8 +477,138 @@
379
477
  orgs: { offset: 0, limit: 25, total: 0, selectedOrgId: null },
380
478
  members: { offset: 0, limit: 50, total: 0 },
381
479
  invites: { offset: 0, limit: 50, total: 0 },
480
+ availableUsers: [],
382
481
  };
383
482
 
483
+ async function loadAvailableUsers() {
484
+
485
+ console.log('Loading available users...');
486
+
487
+ const orgId = state.orgs.selectedOrgId;
488
+ if (!orgId) {
489
+ console.log('No org selected');
490
+ return;
491
+ }
492
+
493
+ try {
494
+ // Load all users
495
+ const usersRes = await fetch(`${API_BASE}/api/admin/users`, {
496
+ headers: { 'Accept': 'application/json' },
497
+ credentials: 'same-origin'
498
+ });
499
+
500
+ const usersData = await usersRes.json();
501
+
502
+ if (!usersRes.ok) {
503
+ console.error('Failed to load users:', usersData?.error);
504
+ const select = document.getElementById('assign-user-select');
505
+ if (select) {
506
+ select.innerHTML = `<option value="">Error: ${usersData?.error || 'Unknown error'}</option>`;
507
+ }
508
+ return;
509
+ }else{
510
+ console.log('Loaded users:', usersData);
511
+ }
512
+
513
+ // Load current org members
514
+ const membersRes = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members?limit=500`, {
515
+ headers: { 'Accept': 'application/json' },
516
+ credentials: 'same-origin'
517
+ });
518
+ const membersData = await membersRes.json();
519
+
520
+ if (!membersRes.ok) {
521
+ console.error('Failed to load members:', membersData?.error);
522
+ return;
523
+ }
524
+
525
+ const allUsers = Array.isArray(usersData?.users) ? usersData.users : [];
526
+ const currentMembers = Array.isArray(membersData?.members) ? membersData.members : [];
527
+ const memberUserIds = new Set(currentMembers.map(m => String(m.userId)));
528
+
529
+ // Filter out users who are already members
530
+ const availableUsers = allUsers.filter(user => !memberUserIds.has(String(user._id)));
531
+ state.availableUsers = availableUsers;
532
+
533
+ // Update the select dropdown
534
+ const select = document.getElementById('assign-user-select');
535
+ if (select) {
536
+ select.innerHTML = '<option value="">Select a user...</option>';
537
+ availableUsers.forEach(user => {
538
+ const option = document.createElement('option');
539
+ option.value = user._id;
540
+ option.textContent = `${user.name || 'No name'} (${user.email})`;
541
+ select.appendChild(option);
542
+ });
543
+ }
544
+
545
+ // Enable/disable assign button based on availability
546
+ const assignBtn = document.getElementById('btn-assign-user');
547
+ if (assignBtn) {
548
+ assignBtn.disabled = availableUsers.length === 0;
549
+ if (availableUsers.length === 0) {
550
+ select.innerHTML = '<option value="">No available users</option>';
551
+ }
552
+ }
553
+ } catch (e) {
554
+ console.error('Failed to load available users:', e);
555
+ const select = document.getElementById('assign-user-select');
556
+ if (select) {
557
+ select.innerHTML = '<option value="">Failed to load users</option>';
558
+ }
559
+ }
560
+ }
561
+
562
+ async function assignUserToOrg() {
563
+ const orgId = state.orgs.selectedOrgId;
564
+ const userId = document.getElementById('assign-user-select')?.value;
565
+ const role = document.getElementById('assign-user-role')?.value;
566
+
567
+ if (!orgId) {
568
+ showToast('No organization selected', 'error');
569
+ return;
570
+ }
571
+
572
+ if (!userId) {
573
+ showToast('Please select a user', 'error');
574
+ return;
575
+ }
576
+
577
+ if (!role) {
578
+ showToast('Please select a role', 'error');
579
+ return;
580
+ }
581
+
582
+ const userName = state.availableUsers.find(u => u._id === userId)?.name || userId;
583
+
584
+ try {
585
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members`, {
586
+ method: 'POST',
587
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
588
+ credentials: 'same-origin',
589
+ body: JSON.stringify({ userId, role }),
590
+ });
591
+
592
+ const data = await res.json();
593
+
594
+ if (!res.ok) {
595
+ showToast(data?.error || 'Failed to assign user', 'error');
596
+ return;
597
+ }
598
+
599
+ showToast(data?.message || 'User assigned successfully', 'success');
600
+
601
+ // Reset form
602
+ document.getElementById('assign-user-select').value = '';
603
+ document.getElementById('assign-user-role').value = 'member';
604
+
605
+ // Reload members list and available users
606
+ await Promise.all([loadMembers(), loadAvailableUsers()]);
607
+ } catch (e) {
608
+ showToast(e.message || 'Failed to assign user', 'error');
609
+ }
610
+ }
611
+
384
612
  async function loadOrgs() {
385
613
  const q = document.getElementById('orgs-q')?.value?.trim();
386
614
  const status = document.getElementById('orgs-status')?.value?.trim();
@@ -400,7 +628,10 @@
400
628
  limit,
401
629
  offset: state.orgs.offset,
402
630
  })}`;
403
- const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
631
+ const res = await fetch(url, {
632
+ headers: { 'Accept': 'application/json' },
633
+ credentials: 'same-origin'
634
+ });
404
635
  const data = await res.json();
405
636
 
406
637
  if (!res.ok) {
@@ -434,7 +665,7 @@
434
665
  if (orgs.length === 0) {
435
666
  tbody.innerHTML = `
436
667
  <tr>
437
- <td class="px-4 py-6 text-sm text-gray-600" colspan="3">No organizations found.</td>
668
+ <td class="px-4 py-6 text-sm text-gray-600" colspan="4">No organizations found.</td>
438
669
  </tr>
439
670
  `;
440
671
  return;
@@ -445,13 +676,30 @@
445
676
  const rowClass = isSelected ? 'bg-blue-50' : 'bg-white';
446
677
 
447
678
  return `
448
- <tr class="${rowClass} hover:bg-gray-50 cursor-pointer" data-org-id="${escapeHtml(o?._id)}">
449
- <td class="px-4 py-3 text-sm text-gray-900">
679
+ <tr class="${rowClass} hover:bg-gray-50" data-org-id="${escapeHtml(o?._id)}">
680
+ <td class="px-4 py-3 text-sm text-gray-900 cursor-pointer" onclick="selectOrg('${escapeHtml(o?._id)}')">
450
681
  <div class="font-medium">${escapeHtml(o?.name)}</div>
451
682
  <div class="text-xs text-gray-500">${escapeHtml(o?.ownerUserId)}</div>
452
683
  </td>
453
- <td class="px-4 py-3 text-sm text-gray-700">${escapeHtml(o?.slug)}</td>
454
- <td class="px-4 py-3 text-sm text-gray-700">${escapeHtml(o?.status)}</td>
684
+ <td class="px-4 py-3 text-sm text-gray-700 cursor-pointer" onclick="selectOrg('${escapeHtml(o?._id)}')">${escapeHtml(o?.slug)}</td>
685
+ <td class="px-4 py-3 text-sm text-gray-700 cursor-pointer" onclick="selectOrg('${escapeHtml(o?._id)}')">
686
+ <span class="px-2 py-1 text-xs rounded-full ${
687
+ o?.status === 'active'
688
+ ? 'bg-green-100 text-green-800'
689
+ : 'bg-red-100 text-red-800'
690
+ }">
691
+ ${escapeHtml(o?.status)}
692
+ </span>
693
+ </td>
694
+ <td class="px-4 py-3 text-sm text-gray-700 whitespace-nowrap">
695
+ <button class="text-blue-600 hover:text-blue-800 mr-2" data-edit="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}" data-description="${escapeHtml(o?.description || '')}" data-owner="${escapeHtml(o?.ownerUserId)}" data-status="${escapeHtml(o?.status)}">Edit</button>
696
+ <button class="text-gray-600 hover:text-gray-800 mr-2" data-copy="${escapeHtml(o?._id)}" title="Copy orgId">Copy ID</button>
697
+ ${o?.status === 'active'
698
+ ? `<button class="text-orange-600 hover:text-orange-800 mr-2" data-disable="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Disable</button>`
699
+ : `<button class="text-green-600 hover:text-green-800 mr-2" data-enable="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Enable</button>`
700
+ }
701
+ <button class="text-red-600 hover:text-red-800" data-delete="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Delete</button>
702
+ </td>
455
703
  </tr>
456
704
  `;
457
705
  }).join('');
@@ -474,6 +722,7 @@
474
722
  const ids = [
475
723
  'btn-selected-refresh',
476
724
  'btn-members-refresh',
725
+ 'btn-assign-user',
477
726
  'btn-members-apply',
478
727
  'btn-members-reset',
479
728
  'btn-members-prev',
@@ -498,7 +747,7 @@
498
747
  state.invites.offset = 0;
499
748
 
500
749
  setSelectedControlsEnabled(true);
501
- await Promise.all([loadSelectedOrg(), loadMembers(), loadInvites()]);
750
+ await Promise.all([loadSelectedOrg(), loadMembers(), loadInvites(), loadAvailableUsers()]);
502
751
  await loadOrgs();
503
752
  }
504
753
 
@@ -511,7 +760,10 @@
511
760
  }
512
761
 
513
762
  try {
514
- const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, { headers: { 'Accept': 'application/json' } });
763
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
764
+ headers: { 'Accept': 'application/json' },
765
+ credentials: 'same-origin'
766
+ });
515
767
  const data = await res.json();
516
768
 
517
769
  if (!res.ok) {
@@ -562,7 +814,10 @@
562
814
  limit: state.members.limit,
563
815
  offset: state.members.offset,
564
816
  })}`;
565
- const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
817
+ const res = await fetch(url, {
818
+ headers: { 'Accept': 'application/json' },
819
+ credentials: 'same-origin'
820
+ });
566
821
  const data = await res.json();
567
822
 
568
823
  if (!res.ok) {
@@ -655,6 +910,7 @@
655
910
  const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members/${encodeURIComponent(memberId)}`, {
656
911
  method: 'PATCH',
657
912
  headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
913
+ credentials: 'same-origin',
658
914
  body: JSON.stringify({ role }),
659
915
  });
660
916
  const data = await res.json();
@@ -680,6 +936,7 @@
680
936
  const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members/${encodeURIComponent(memberId)}`, {
681
937
  method: 'DELETE',
682
938
  headers: { 'Accept': 'application/json' },
939
+ credentials: 'same-origin'
683
940
  });
684
941
  const data = await res.json();
685
942
  if (!res.ok) {
@@ -722,7 +979,10 @@
722
979
  limit: state.invites.limit,
723
980
  offset: state.invites.offset,
724
981
  })}`;
725
- const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
982
+ const res = await fetch(url, {
983
+ headers: { 'Accept': 'application/json' },
984
+ credentials: 'same-origin'
985
+ });
726
986
  const data = await res.json();
727
987
 
728
988
  if (!res.ok) {
@@ -804,6 +1064,7 @@
804
1064
  const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/invites/${encodeURIComponent(inviteId)}`, {
805
1065
  method: 'DELETE',
806
1066
  headers: { 'Accept': 'application/json' },
1067
+ credentials: 'same-origin'
807
1068
  });
808
1069
  const data = await res.json();
809
1070
  if (!res.ok) {
@@ -828,6 +1089,7 @@
828
1089
  const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/invites/${encodeURIComponent(inviteId)}/resend`, {
829
1090
  method: 'POST',
830
1091
  headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
1092
+ credentials: 'same-origin',
831
1093
  body: JSON.stringify({}),
832
1094
  });
833
1095
  const data = await res.json();
@@ -866,6 +1128,7 @@
866
1128
  const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/invites`, {
867
1129
  method: 'POST',
868
1130
  headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
1131
+ credentials: 'same-origin',
869
1132
  body: JSON.stringify({ email, role, expiresInDays }),
870
1133
  });
871
1134
  const data = await res.json();
@@ -911,12 +1174,51 @@
911
1174
  const nextOrgsBtn = document.getElementById('btn-orgs-next');
912
1175
  if (nextOrgsBtn) nextOrgsBtn.onclick = () => { state.orgs.offset = Math.max(0, state.orgs.offset + state.orgs.limit); loadOrgs(); };
913
1176
 
1177
+ // CRUD event handlers
1178
+ const createOrgBtn = document.getElementById('btn-create-org');
1179
+ if (createOrgBtn) createOrgBtn.onclick = () => openCreateOrgModal();
1180
+
1181
+ const createOrgCancelBtn = document.getElementById('btn-create-org-cancel');
1182
+ if (createOrgCancelBtn) createOrgCancelBtn.onclick = () => closeCreateOrgModal();
1183
+
1184
+ const createOrgSubmitBtn = document.getElementById('btn-create-org-submit');
1185
+ if (createOrgSubmitBtn) createOrgSubmitBtn.onclick = () => createOrganization();
1186
+
1187
+ const editOrgCancelBtn = document.getElementById('btn-edit-org-cancel');
1188
+ if (editOrgCancelBtn) editOrgCancelBtn.onclick = () => closeEditOrgModal();
1189
+
1190
+ const editOrgSubmitBtn = document.getElementById('btn-edit-org-submit');
1191
+ if (editOrgSubmitBtn) editOrgSubmitBtn.onclick = () => updateOrganization();
1192
+
1193
+ // Organization action buttons (will be added dynamically when table loads)
1194
+ document.addEventListener('click', (e) => {
1195
+ if (e.target.matches('[data-edit]')) {
1196
+ const btn = e.target;
1197
+ openEditOrgModal(btn.dataset.edit, btn.dataset.name, btn.dataset.description, btn.dataset.owner, btn.dataset.status);
1198
+ } else if (e.target.matches('[data-copy]')) {
1199
+ const btn = e.target;
1200
+ copyText(btn.dataset.copy);
1201
+ } else if (e.target.matches('[data-disable]')) {
1202
+ const btn = e.target;
1203
+ disableOrganization(btn.dataset.disable, btn.dataset.name);
1204
+ } else if (e.target.matches('[data-enable]')) {
1205
+ const btn = e.target;
1206
+ enableOrganization(btn.dataset.enable, btn.dataset.name);
1207
+ } else if (e.target.matches('[data-delete]')) {
1208
+ const btn = e.target;
1209
+ deleteOrganization(btn.dataset.delete, btn.dataset.name);
1210
+ }
1211
+ });
1212
+
914
1213
  const selectedRefreshBtn = document.getElementById('btn-selected-refresh');
915
1214
  if (selectedRefreshBtn) selectedRefreshBtn.onclick = () => Promise.all([loadSelectedOrg(), loadMembers(), loadInvites()]);
916
1215
 
917
1216
  const membersRefreshBtn = document.getElementById('btn-members-refresh');
918
1217
  if (membersRefreshBtn) membersRefreshBtn.onclick = () => loadMembers();
919
1218
 
1219
+ const assignUserBtn = document.getElementById('btn-assign-user');
1220
+ if (assignUserBtn) assignUserBtn.onclick = () => assignUserToOrg();
1221
+
920
1222
  const membersApplyBtn = document.getElementById('btn-members-apply');
921
1223
  if (membersApplyBtn) membersApplyBtn.onclick = () => { state.members.offset = 0; loadMembers(); };
922
1224
 
@@ -969,6 +1271,222 @@
969
1271
  setSelectedControlsEnabled(false);
970
1272
  }
971
1273
 
1274
+ // Modal functions
1275
+ function openCreateOrgModal() {
1276
+ document.getElementById('modal-create-org').classList.remove('hidden');
1277
+ document.getElementById('modal-create-org').classList.add('flex');
1278
+ // Clear form
1279
+ document.getElementById('create-org-name').value = '';
1280
+ document.getElementById('create-org-description').value = '';
1281
+ document.getElementById('create-org-owner').value = '';
1282
+ }
1283
+
1284
+ function closeCreateOrgModal() {
1285
+ document.getElementById('modal-create-org').classList.add('hidden');
1286
+ document.getElementById('modal-create-org').classList.remove('flex');
1287
+ }
1288
+
1289
+ function openEditOrgModal(orgId, name, description, owner, status) {
1290
+ document.getElementById('modal-edit-org').classList.remove('hidden');
1291
+ document.getElementById('modal-edit-org').classList.add('flex');
1292
+ // Populate form
1293
+ document.getElementById('edit-org-id').value = orgId;
1294
+ document.getElementById('edit-org-name').value = name || '';
1295
+ document.getElementById('edit-org-description').value = description || '';
1296
+ document.getElementById('edit-org-owner').value = owner || '';
1297
+ document.getElementById('edit-org-status').value = status || 'active';
1298
+ }
1299
+
1300
+ function closeEditOrgModal() {
1301
+ document.getElementById('modal-edit-org').classList.add('hidden');
1302
+ document.getElementById('modal-edit-org').classList.remove('flex');
1303
+ }
1304
+
1305
+ // CRUD operations
1306
+ async function createOrganization() {
1307
+ const name = document.getElementById('create-org-name').value.trim();
1308
+ const description = document.getElementById('create-org-description').value.trim();
1309
+ const ownerUserId = document.getElementById('create-org-owner').value.trim();
1310
+
1311
+ if (!name) {
1312
+ showToast('Organization name is required', 'error');
1313
+ return;
1314
+ }
1315
+
1316
+ if (name.length < 2) {
1317
+ showToast('Name must be at least 2 characters', 'error');
1318
+ return;
1319
+ }
1320
+
1321
+ try {
1322
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}`, {
1323
+ method: 'POST',
1324
+ headers: { 'Content-Type': 'application/json' },
1325
+ credentials: 'same-origin',
1326
+ body: JSON.stringify({
1327
+ name,
1328
+ description: description || undefined,
1329
+ ownerUserId: ownerUserId || undefined
1330
+ }),
1331
+ });
1332
+
1333
+ const data = await res.json();
1334
+ if (!res.ok) {
1335
+ showToast(data?.error || 'Failed to create organization', 'error');
1336
+ return;
1337
+ }
1338
+
1339
+ showToast('Organization created successfully', 'success');
1340
+ closeCreateOrgModal();
1341
+ await loadOrgs();
1342
+ } catch (e) {
1343
+ showToast(e.message || 'Failed to create organization', 'error');
1344
+ }
1345
+ }
1346
+
1347
+ async function updateOrganization() {
1348
+ const orgId = document.getElementById('edit-org-id').value;
1349
+ const name = document.getElementById('edit-org-name').value.trim();
1350
+ const description = document.getElementById('edit-org-description').value.trim();
1351
+ const ownerUserId = document.getElementById('edit-org-owner').value.trim();
1352
+ const status = document.getElementById('edit-org-status').value;
1353
+
1354
+ if (!name) {
1355
+ showToast('Organization name is required', 'error');
1356
+ return;
1357
+ }
1358
+
1359
+ if (name.length < 2) {
1360
+ showToast('Name must be at least 2 characters', 'error');
1361
+ return;
1362
+ }
1363
+
1364
+ try {
1365
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
1366
+ method: 'PUT',
1367
+ headers: { 'Content-Type': 'application/json' },
1368
+ credentials: 'same-origin',
1369
+ body: JSON.stringify({
1370
+ name,
1371
+ description: description || undefined,
1372
+ ownerUserId: ownerUserId || undefined,
1373
+ status
1374
+ }),
1375
+ });
1376
+
1377
+ const data = await res.json();
1378
+ if (!res.ok) {
1379
+ showToast(data?.error || 'Failed to update organization', 'error');
1380
+ return;
1381
+ }
1382
+
1383
+ showToast('Organization updated successfully', 'success');
1384
+ closeEditOrgModal();
1385
+ await loadOrgs();
1386
+ if (state.orgs.selectedOrgId === orgId) {
1387
+ await loadSelectedOrg();
1388
+ }
1389
+ } catch (e) {
1390
+ showToast(e.message || 'Failed to update organization', 'error');
1391
+ }
1392
+ }
1393
+
1394
+ async function disableOrganization(orgId, orgName) {
1395
+ if (!confirm(`Are you sure you want to disable "${orgName}"?`)) return;
1396
+
1397
+ try {
1398
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/disable`, {
1399
+ method: 'PATCH',
1400
+ credentials: 'same-origin'
1401
+ });
1402
+
1403
+ const data = await res.json();
1404
+ if (!res.ok) {
1405
+ showToast(data?.error || 'Failed to disable organization', 'error');
1406
+ return;
1407
+ }
1408
+
1409
+ showToast('Organization disabled successfully', 'success');
1410
+ await loadOrgs();
1411
+ if (state.orgs.selectedOrgId === orgId) {
1412
+ await loadSelectedOrg();
1413
+ }
1414
+ } catch (e) {
1415
+ showToast(e.message || 'Failed to disable organization', 'error');
1416
+ }
1417
+ }
1418
+
1419
+ async function enableOrganization(orgId, orgName) {
1420
+ if (!confirm(`Are you sure you want to enable "${orgName}"?`)) return;
1421
+
1422
+ try {
1423
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/enable`, {
1424
+ method: 'PATCH',
1425
+ credentials: 'same-origin'
1426
+ });
1427
+
1428
+ const data = await res.json();
1429
+ if (!res.ok) {
1430
+ showToast(data?.error || 'Failed to enable organization', 'error');
1431
+ return;
1432
+ }
1433
+
1434
+ showToast('Organization enabled successfully', 'success');
1435
+ await loadOrgs();
1436
+ if (state.orgs.selectedOrgId === orgId) {
1437
+ await loadSelectedOrg();
1438
+ }
1439
+ } catch (e) {
1440
+ showToast(e.message || 'Failed to enable organization', 'error');
1441
+ }
1442
+ }
1443
+
1444
+ async function deleteOrganization(orgId, orgName) {
1445
+ const confirm1 = confirm(`Are you sure you want to delete "${orgName}"?`);
1446
+ if (!confirm1) return;
1447
+
1448
+ const confirm2 = confirm(
1449
+ 'WARNING: This will permanently delete the organization and ALL its data including:\n' +
1450
+ '• All organization members\n' +
1451
+ '• All pending invites\n' +
1452
+ '• All associated assets and files\n' +
1453
+ '• All notifications and activity\n\n' +
1454
+ 'This action cannot be undone. Continue?'
1455
+ );
1456
+ if (!confirm2) return;
1457
+
1458
+ try {
1459
+ showToast('Deleting organization and cleaning up data...', 'success');
1460
+
1461
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
1462
+ method: 'DELETE',
1463
+ credentials: 'same-origin'
1464
+ });
1465
+
1466
+ const data = await res.json();
1467
+ if (!res.ok) {
1468
+ showToast(data?.error || 'Failed to delete organization', 'error');
1469
+ return;
1470
+ }
1471
+
1472
+ showToast(`Organization "${orgName}" deleted permanently`, 'success');
1473
+
1474
+ // Clear selection if this org was selected
1475
+ if (state.orgs.selectedOrgId === orgId) {
1476
+ state.orgs.selectedOrgId = null;
1477
+ setSelectedControlsEnabled(false);
1478
+ document.getElementById('selected-org-subtitle').textContent = 'Select an org from the list';
1479
+ document.getElementById('kpi-members').textContent = '-';
1480
+ document.getElementById('kpi-invites').textContent = '-';
1481
+ document.getElementById('kpi-org-status').textContent = '-';
1482
+ }
1483
+
1484
+ await loadOrgs();
1485
+ } catch (e) {
1486
+ showToast(e.message || 'Failed to delete organization', 'error');
1487
+ }
1488
+ }
1489
+
972
1490
  bindEvents();
973
1491
  loadOrgs();
974
1492
  </script>