@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.
Files changed (42) hide show
  1. package/index.js +16 -1
  2. package/package.json +5 -2
  3. package/public/sdk/ui-components.iife.js +191 -0
  4. package/sdk/ui-components/browser/src/index.js +228 -0
  5. package/src/controllers/admin.controller.js +89 -0
  6. package/src/controllers/adminHeadless.controller.js +82 -0
  7. package/src/controllers/adminScripts.controller.js +229 -0
  8. package/src/controllers/adminTerminals.controller.js +39 -0
  9. package/src/controllers/adminUiComponents.controller.js +315 -0
  10. package/src/controllers/adminUiComponentsAi.controller.js +34 -0
  11. package/src/controllers/orgAdmin.controller.js +286 -0
  12. package/src/controllers/uiComponentsPublic.controller.js +118 -0
  13. package/src/middleware/auth.js +7 -0
  14. package/src/middleware.js +115 -0
  15. package/src/models/HeadlessModelDefinition.js +10 -0
  16. package/src/models/ScriptDefinition.js +42 -0
  17. package/src/models/ScriptRun.js +22 -0
  18. package/src/models/UiComponent.js +29 -0
  19. package/src/models/UiComponentProject.js +26 -0
  20. package/src/models/UiComponentProjectComponent.js +18 -0
  21. package/src/routes/admin.routes.js +1 -0
  22. package/src/routes/adminHeadless.routes.js +6 -0
  23. package/src/routes/adminScripts.routes.js +21 -0
  24. package/src/routes/adminTerminals.routes.js +13 -0
  25. package/src/routes/adminUiComponents.routes.js +29 -0
  26. package/src/routes/llmUi.routes.js +26 -0
  27. package/src/routes/orgAdmin.routes.js +5 -0
  28. package/src/routes/uiComponentsPublic.routes.js +9 -0
  29. package/src/services/headlessExternalModels.service.js +292 -0
  30. package/src/services/headlessModels.service.js +26 -6
  31. package/src/services/scriptsRunner.service.js +259 -0
  32. package/src/services/terminals.service.js +152 -0
  33. package/src/services/terminalsWs.service.js +100 -0
  34. package/src/services/uiComponentsAi.service.js +312 -0
  35. package/src/services/uiComponentsCrypto.service.js +39 -0
  36. package/views/admin-headless.ejs +294 -24
  37. package/views/admin-organizations.ejs +365 -9
  38. package/views/admin-scripts.ejs +497 -0
  39. package/views/admin-terminals.ejs +328 -0
  40. package/views/admin-ui-components.ejs +709 -0
  41. package/views/admin-users.ejs +261 -4
  42. package/views/partials/dashboard/nav-items.ejs +3 -0
@@ -153,6 +153,37 @@
153
153
  <option value="trialing">trialing</option>
154
154
  </select>
155
155
  </div>
156
+
157
+ <!-- Password Section -->
158
+ <div class="border-t pt-4">
159
+ <div class="flex items-center justify-between mb-3">
160
+ <label class="text-sm font-medium text-gray-700">Password Management</label>
161
+ <input type="checkbox" id="edit-reset-password" class="rounded text-blue-600 focus:ring-blue-500">
162
+ <label for="edit-reset-password" class="text-sm text-gray-600 ml-2">Reset Password</label>
163
+ </div>
164
+
165
+ <div id="password-fields" class="space-y-3 hidden">
166
+ <div>
167
+ <label class="block text-sm font-medium text-gray-700 mb-1">New Password</label>
168
+ <div class="relative">
169
+ <input id="edit-password" type="password" class="w-full border rounded px-3 py-2 pr-10" placeholder="Enter new password" minlength="6">
170
+ <button type="button" id="toggle-edit-password" class="absolute right-2 top-2 text-gray-500 hover:text-gray-700">
171
+ <svg id="edit-eye-icon" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
172
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
173
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
174
+ </svg>
175
+ </button>
176
+ </div>
177
+ <div id="password-strength" class="mt-1 text-xs"></div>
178
+ </div>
179
+
180
+ <div>
181
+ <label class="block text-sm font-medium text-gray-700 mb-1">Confirm Password</label>
182
+ <input id="edit-confirm-password" type="password" class="w-full border rounded px-3 py-2" placeholder="Confirm new password">
183
+ <div id="password-match" class="mt-1 text-xs"></div>
184
+ </div>
185
+ </div>
186
+ </div>
156
187
  </div>
157
188
  <div class="flex justify-end gap-2 mt-6">
158
189
  <button id="btn-modal-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
@@ -342,10 +373,12 @@
342
373
  <td class="px-4 py-3 text-sm text-gray-700 whitespace-nowrap">
343
374
  <button class="text-blue-600 hover:text-blue-800 mr-2" data-edit="${escapeHtml(u._id)}" data-name="${escapeHtml(u.name || '')}" data-role="${escapeHtml(u.role)}" data-plan="${escapeHtml(u.currentPlan)}" data-subscription="${escapeHtml(u.subscriptionStatus)}">Edit</button>
344
375
  <button class="text-green-600 hover:text-green-800 mr-2" data-notify="${escapeHtml(u._id)}" data-email="${escapeHtml(u.email)}">Notify</button>
376
+ <button class="text-purple-600 hover:text-purple-800 mr-2" data-get-jwt="${escapeHtml(u._id)}" data-email="${escapeHtml(u.email)}">Get JWT</button>
345
377
  ${u.disabled
346
- ? `<button class="text-green-600 hover:text-green-800" data-enable="${escapeHtml(u._id)}">Enable</button>`
347
- : `<button class="text-red-600 hover:text-red-800" data-disable="${escapeHtml(u._id)}">Disable</button>`
378
+ ? `<button class="text-green-600 hover:text-green-800 mr-2" data-enable="${escapeHtml(u._id)}">Enable</button>`
379
+ : `<button class="text-red-600 hover:text-red-800 mr-2" data-disable="${escapeHtml(u._id)}">Disable</button>`
348
380
  }
381
+ <button class="text-red-600 hover:text-red-800 font-semibold" data-delete="${escapeHtml(u._id)}" data-email="${escapeHtml(u.email)}">Delete</button>
349
382
  </td>
350
383
  </tr>
351
384
  `;
@@ -359,6 +392,18 @@
359
392
  btn.addEventListener('click', () => openNotifyModal(btn.dataset.notify, btn.dataset.email));
360
393
  });
361
394
 
395
+ tbody.querySelectorAll('[data-get-jwt]').forEach(btn => {
396
+ btn.addEventListener('click', async () => {
397
+ await getJwtForUser(btn.dataset.getJwt, btn.dataset.email);
398
+ });
399
+ });
400
+
401
+ tbody.querySelectorAll('[data-delete]').forEach(btn => {
402
+ btn.addEventListener('click', async () => {
403
+ await deleteUser(btn.dataset.delete, btn.dataset.email);
404
+ });
405
+ });
406
+
362
407
  tbody.querySelectorAll('[data-disable]').forEach(btn => {
363
408
  btn.addEventListener('click', async () => {
364
409
  if (!confirm('Disable this user?')) return;
@@ -394,6 +439,15 @@
394
439
  document.getElementById('edit-role').value = role || 'user';
395
440
  document.getElementById('edit-plan').value = plan || 'free';
396
441
  document.getElementById('edit-subscription').value = subscription || 'none';
442
+
443
+ // Reset password fields
444
+ document.getElementById('edit-reset-password').checked = false;
445
+ document.getElementById('password-fields').classList.add('hidden');
446
+ document.getElementById('edit-password').value = '';
447
+ document.getElementById('edit-confirm-password').value = '';
448
+ document.getElementById('password-strength').textContent = '';
449
+ document.getElementById('password-match').textContent = '';
450
+
397
451
  document.getElementById('modal-edit').classList.remove('hidden');
398
452
  }
399
453
 
@@ -401,22 +455,135 @@
401
455
  document.getElementById('modal-edit').classList.add('hidden');
402
456
  }
403
457
 
458
+ async function getJwtForUser(userId, email) {
459
+ try {
460
+ // Show loading state
461
+ showToast('Generating JWT...', 'success');
462
+
463
+ const res = await fetch(`${API_BASE}/api/admin/generate-token`, {
464
+ method: 'POST',
465
+ headers: { 'Content-Type': 'application/json' },
466
+ body: JSON.stringify({ userId }),
467
+ });
468
+
469
+ const data = await res.json();
470
+
471
+ if (!res.ok) {
472
+ showToast(data?.error || 'Failed to generate JWT', 'error');
473
+ return;
474
+ }
475
+
476
+ // Copy JWT to clipboard
477
+ try {
478
+ await navigator.clipboard.writeText(data.token);
479
+ showToast(`JWT for ${email} copied to clipboard!`, 'success');
480
+ } catch (clipboardError) {
481
+ // Fallback for browsers that don't support clipboard API
482
+ const textArea = document.createElement('textarea');
483
+ textArea.value = data.token;
484
+ textArea.style.position = 'fixed';
485
+ textArea.style.opacity = '0';
486
+ document.body.appendChild(textArea);
487
+ textArea.select();
488
+ document.execCommand('copy');
489
+ document.body.removeChild(textArea);
490
+ showToast(`JWT for ${email} copied to clipboard!`, 'success');
491
+ }
492
+
493
+ // Also log the token for debugging (in production, you might want to remove this)
494
+ console.log(`Generated JWT for ${email}:`, data.token);
495
+
496
+ } catch (e) {
497
+ showToast(e.message || 'Failed to generate JWT', 'error');
498
+ }
499
+ }
500
+
501
+ async function deleteUser(userId, email) {
502
+ // Double confirmation with clear warnings
503
+ const confirm1 = confirm(`Are you sure you want to delete user ${email}?`);
504
+ if (!confirm1) return;
505
+
506
+ const confirm2 = confirm(
507
+ 'WARNING: This will permanently delete the user and ALL their data including:\n' +
508
+ '• Organizations they own (if no other members)\n' +
509
+ '• All their assets and files\n' +
510
+ '• Organization memberships\n' +
511
+ '• Notifications and invites\n' +
512
+ '• Email logs and form submissions\n\n' +
513
+ 'Activity logs and audit events will be preserved.\n' +
514
+ 'This action cannot be undone. Continue?'
515
+ );
516
+ if (!confirm2) return;
517
+
518
+ try {
519
+ showToast('Deleting user and cleaning up data...', 'success');
520
+
521
+ const res = await fetch(`${API_BASE}/api/admin/users/${encodeURIComponent(userId)}`, {
522
+ method: 'DELETE'
523
+ });
524
+
525
+ const data = await res.json();
526
+
527
+ if (!res.ok) {
528
+ showToast(data?.error || 'Failed to delete user', 'error');
529
+ return;
530
+ }
531
+
532
+ showToast(`User ${email} deleted permanently`, 'success');
533
+ await Promise.all([loadUsers(), loadStats()]);
534
+ } catch (e) {
535
+ showToast(e.message || 'Failed to delete user', 'error');
536
+ }
537
+ }
538
+
404
539
  async function saveUser() {
405
540
  const userId = document.getElementById('edit-user-id').value;
406
541
  const name = document.getElementById('edit-name').value.trim();
407
542
  const role = document.getElementById('edit-role').value;
408
543
  const currentPlan = document.getElementById('edit-plan').value;
409
544
  const subscriptionStatus = document.getElementById('edit-subscription').value;
545
+
546
+ // Password handling
547
+ const resetPassword = document.getElementById('edit-reset-password').checked;
548
+ const password = document.getElementById('edit-password').value;
549
+ const confirmPassword = document.getElementById('edit-confirm-password').value;
550
+
551
+ // Validate password fields if reset is checked
552
+ if (resetPassword) {
553
+ if (!password) {
554
+ showToast('Password is required when reset is enabled', 'error');
555
+ return;
556
+ }
557
+
558
+ if (password.length < 6) {
559
+ showToast('Password must be at least 6 characters', 'error');
560
+ return;
561
+ }
562
+
563
+ if (password !== confirmPassword) {
564
+ showToast('Passwords do not match', 'error');
565
+ return;
566
+ }
567
+ }
410
568
 
411
569
  try {
570
+ const updateData = { name, role, currentPlan, subscriptionStatus };
571
+
572
+ // Only include password if reset is checked
573
+ if (resetPassword && password) {
574
+ updateData.passwordHash = password;
575
+ }
576
+
412
577
  const res = await fetch(`${API_BASE}/api/admin/users/${encodeURIComponent(userId)}`, {
413
578
  method: 'PATCH',
414
579
  headers: { 'Content-Type': 'application/json' },
415
- body: JSON.stringify({ name, role, currentPlan, subscriptionStatus }),
580
+ body: JSON.stringify(updateData),
416
581
  });
417
582
  const data = await res.json();
418
583
  if (!res.ok) { showToast(data?.error || 'Failed to update user', 'error'); return; }
419
- showToast('User updated', 'success');
584
+
585
+ const message = resetPassword ? 'User and password updated' : 'User updated';
586
+ showToast(message, 'success');
420
587
  closeEditModal();
421
588
  await Promise.all([loadUsers(), loadStats()]);
422
589
  } catch (e) { showToast(e.message, 'error'); }
@@ -584,6 +751,78 @@
584
751
  return window.location.origin + endpoint;
585
752
  }
586
753
 
754
+ // Password validation functions
755
+ function checkPasswordStrength(password) {
756
+ if (!password) return '';
757
+
758
+ let strength = 0;
759
+ const feedback = [];
760
+
761
+ if (password.length >= 6) strength++;
762
+ else feedback.push('at least 6 characters');
763
+
764
+ if (password.length >= 10) strength++;
765
+ if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
766
+ if (/\d/.test(password)) strength++;
767
+ if (/[^a-zA-Z\d]/.test(password)) strength++;
768
+
769
+ const strengthText = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong'][strength] || 'Very Weak';
770
+ const strengthColor = ['text-red-500', 'text-orange-500', 'text-yellow-500', 'text-blue-500', 'text-green-500'][strength] || 'text-red-500';
771
+
772
+ return { strength, strengthText, strengthColor, feedback };
773
+ }
774
+
775
+ function updatePasswordStrength() {
776
+ const password = document.getElementById('edit-password').value;
777
+ const strengthDiv = document.getElementById('password-strength');
778
+
779
+ if (!password) {
780
+ strengthDiv.textContent = '';
781
+ return;
782
+ }
783
+
784
+ const { strengthText, strengthColor } = checkPasswordStrength(password);
785
+ strengthDiv.textContent = `Strength: ${strengthText}`;
786
+ strengthDiv.className = `mt-1 text-xs ${strengthColor}`;
787
+ }
788
+
789
+ function updatePasswordMatch() {
790
+ const password = document.getElementById('edit-password').value;
791
+ const confirmPassword = document.getElementById('edit-confirm-password').value;
792
+ const matchDiv = document.getElementById('password-match');
793
+
794
+ if (!confirmPassword) {
795
+ matchDiv.textContent = '';
796
+ return;
797
+ }
798
+
799
+ if (password === confirmPassword) {
800
+ matchDiv.textContent = '✓ Passwords match';
801
+ matchDiv.className = 'mt-1 text-xs text-green-500';
802
+ } else {
803
+ matchDiv.textContent = '✗ Passwords do not match';
804
+ matchDiv.className = 'mt-1 text-xs text-red-500';
805
+ }
806
+ }
807
+
808
+ function toggleEditPasswordVisibility() {
809
+ const passwordInput = document.getElementById('edit-password');
810
+ const eyeIcon = document.getElementById('edit-eye-icon');
811
+
812
+ if (passwordInput.type === 'password') {
813
+ passwordInput.type = 'text';
814
+ eyeIcon.innerHTML = `
815
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>
816
+ `;
817
+ } else {
818
+ passwordInput.type = 'password';
819
+ eyeIcon.innerHTML = `
820
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
821
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
822
+ `;
823
+ }
824
+ }
825
+
587
826
  function bindEvents() {
588
827
  document.getElementById('btn-refresh').onclick = () => Promise.all([loadUsers(), loadStats()]);
589
828
  document.getElementById('btn-apply').onclick = () => { state.offset = 0; loadUsers(); };
@@ -603,6 +842,24 @@
603
842
  document.getElementById('btn-notify-cancel').onclick = closeNotifyModal;
604
843
  document.getElementById('btn-notify-send').onclick = sendNotification;
605
844
 
845
+ // Edit modal password events
846
+ document.getElementById('edit-reset-password').onchange = function() {
847
+ const passwordFields = document.getElementById('password-fields');
848
+ if (this.checked) {
849
+ passwordFields.classList.remove('hidden');
850
+ } else {
851
+ passwordFields.classList.add('hidden');
852
+ document.getElementById('edit-password').value = '';
853
+ document.getElementById('edit-confirm-password').value = '';
854
+ document.getElementById('password-strength').textContent = '';
855
+ document.getElementById('password-match').textContent = '';
856
+ }
857
+ };
858
+
859
+ document.getElementById('toggle-edit-password').onclick = toggleEditPasswordVisibility;
860
+ document.getElementById('edit-password').oninput = updatePasswordStrength;
861
+ document.getElementById('edit-confirm-password').oninput = updatePasswordMatch;
862
+
606
863
  // Registration modal events
607
864
  document.getElementById('btn-register-user').onclick = openRegisterModal;
608
865
  document.getElementById('btn-register-cancel').onclick = closeRegisterModal;
@@ -25,6 +25,7 @@
25
25
  { id: 'json', label: 'JSON Configs', path: adminPath + '/json-configs', icon: 'ti-braces' },
26
26
  { id: 'seo', label: 'SEO Config', path: adminPath + '/seo-config', icon: 'ti-search' },
27
27
  { id: 'assets', label: 'Assets', path: adminPath + '/assets', icon: 'ti-photo' },
28
+ { id: 'ui-components', label: 'UI Components', path: adminPath + '/ui-components', icon: 'ti-components' },
28
29
  { id: 'headless', label: 'Headless CMS', path: adminPath + '/headless', icon: 'ti-table' },
29
30
  ]
30
31
  },
@@ -59,6 +60,8 @@
59
60
  title: 'Automation',
60
61
  items: [
61
62
  { id: 'workflows', label: 'Workflows', path: adminPath + '/workflows/all', icon: 'ti-robot' },
63
+ { id: 'scripts', label: 'Scripts', path: adminPath + '/scripts', icon: 'ti-terminal-2' },
64
+ { id: 'terminals', label: 'Terminals', path: adminPath + '/terminals', icon: 'ti-terminal' },
62
65
  ]
63
66
  }
64
67
  ];