@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
@@ -22,6 +22,7 @@
22
22
  <p class="text-sm text-gray-600 mt-1">Manage system users</p>
23
23
  </div>
24
24
  <div class="flex items-center gap-4">
25
+ <button id="btn-register-user" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">Register New User</button>
25
26
  </div>
26
27
  </div>
27
28
  </div>
@@ -152,6 +153,37 @@
152
153
  <option value="trialing">trialing</option>
153
154
  </select>
154
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>
155
187
  </div>
156
188
  <div class="flex justify-end gap-2 mt-6">
157
189
  <button id="btn-modal-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
@@ -199,6 +231,46 @@
199
231
  </div>
200
232
  </div>
201
233
 
234
+ <div id="modal-register" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50 flex items-center justify-center">
235
+ <div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md mx-4">
236
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">Register New User</h3>
237
+ <form id="register-form" class="space-y-4">
238
+ <div>
239
+ <label class="block text-sm font-medium text-gray-700 mb-1">Email *</label>
240
+ <input id="register-email" type="email" required class="w-full border rounded px-3 py-2" placeholder="user@example.com">
241
+ </div>
242
+ <div>
243
+ <label class="block text-sm font-medium text-gray-700 mb-1">Password *</label>
244
+ <div class="relative">
245
+ <input id="register-password" type="password" required minlength="6" class="w-full border rounded px-3 py-2 pr-10" placeholder="Min 6 characters">
246
+ <button type="button" id="toggle-password" class="absolute right-2 top-2 text-gray-500 hover:text-gray-700">
247
+ <svg id="eye-icon" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
248
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
249
+ <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>
250
+ </svg>
251
+ </button>
252
+ </div>
253
+ </div>
254
+ <div>
255
+ <label class="block text-sm font-medium text-gray-700 mb-1">Name</label>
256
+ <input id="register-name" type="text" class="w-full border rounded px-3 py-2" placeholder="Optional">
257
+ </div>
258
+ <div>
259
+ <label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
260
+ <select id="register-role" class="w-full border rounded px-3 py-2">
261
+ <option value="user">User</option>
262
+ <option value="admin">Admin</option>
263
+ </select>
264
+ </div>
265
+ <div id="register-error" class="hidden text-red-600 text-sm"></div>
266
+ </form>
267
+ <div class="flex justify-end gap-2 mt-6">
268
+ <button id="btn-register-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
269
+ <button id="btn-register-submit" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">Register</button>
270
+ </div>
271
+ </div>
272
+ </div>
273
+
202
274
  <div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
203
275
 
204
276
  <script>
@@ -301,10 +373,12 @@
301
373
  <td class="px-4 py-3 text-sm text-gray-700 whitespace-nowrap">
302
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>
303
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>
304
377
  ${u.disabled
305
- ? `<button class="text-green-600 hover:text-green-800" data-enable="${escapeHtml(u._id)}">Enable</button>`
306
- : `<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>`
307
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>
308
382
  </td>
309
383
  </tr>
310
384
  `;
@@ -318,6 +392,18 @@
318
392
  btn.addEventListener('click', () => openNotifyModal(btn.dataset.notify, btn.dataset.email));
319
393
  });
320
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
+
321
407
  tbody.querySelectorAll('[data-disable]').forEach(btn => {
322
408
  btn.addEventListener('click', async () => {
323
409
  if (!confirm('Disable this user?')) return;
@@ -353,6 +439,15 @@
353
439
  document.getElementById('edit-role').value = role || 'user';
354
440
  document.getElementById('edit-plan').value = plan || 'free';
355
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
+
356
451
  document.getElementById('modal-edit').classList.remove('hidden');
357
452
  }
358
453
 
@@ -360,22 +455,135 @@
360
455
  document.getElementById('modal-edit').classList.add('hidden');
361
456
  }
362
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
+
363
539
  async function saveUser() {
364
540
  const userId = document.getElementById('edit-user-id').value;
365
541
  const name = document.getElementById('edit-name').value.trim();
366
542
  const role = document.getElementById('edit-role').value;
367
543
  const currentPlan = document.getElementById('edit-plan').value;
368
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
+ }
369
568
 
370
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
+
371
577
  const res = await fetch(`${API_BASE}/api/admin/users/${encodeURIComponent(userId)}`, {
372
578
  method: 'PATCH',
373
579
  headers: { 'Content-Type': 'application/json' },
374
- body: JSON.stringify({ name, role, currentPlan, subscriptionStatus }),
580
+ body: JSON.stringify(updateData),
375
581
  });
376
582
  const data = await res.json();
377
583
  if (!res.ok) { showToast(data?.error || 'Failed to update user', 'error'); return; }
378
- showToast('User updated', 'success');
584
+
585
+ const message = resetPassword ? 'User and password updated' : 'User updated';
586
+ showToast(message, 'success');
379
587
  closeEditModal();
380
588
  await Promise.all([loadUsers(), loadStats()]);
381
589
  } catch (e) { showToast(e.message, 'error'); }
@@ -417,6 +625,204 @@
417
625
  } catch (e) { showToast(e.message, 'error'); }
418
626
  }
419
627
 
628
+ function openRegisterModal() {
629
+ document.getElementById('register-email').value = '';
630
+ document.getElementById('register-password').value = '';
631
+ document.getElementById('register-name').value = '';
632
+ document.getElementById('register-role').value = 'user';
633
+ document.getElementById('register-error').classList.add('hidden');
634
+ document.getElementById('modal-register').classList.remove('hidden');
635
+ }
636
+
637
+ function closeRegisterModal() {
638
+ document.getElementById('modal-register').classList.add('hidden');
639
+ }
640
+
641
+ function togglePasswordVisibility() {
642
+ const passwordInput = document.getElementById('register-password');
643
+ const eyeIcon = document.getElementById('eye-icon');
644
+
645
+ if (passwordInput.type === 'password') {
646
+ passwordInput.type = 'text';
647
+ eyeIcon.innerHTML = `
648
+ <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>
649
+ `;
650
+ } else {
651
+ passwordInput.type = 'password';
652
+ eyeIcon.innerHTML = `
653
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
654
+ <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>
655
+ `;
656
+ }
657
+ }
658
+
659
+ async function registerUser() {
660
+ const email = document.getElementById('register-email').value.trim();
661
+ const password = document.getElementById('register-password').value;
662
+ const name = document.getElementById('register-name').value.trim();
663
+ const role = document.getElementById('register-role').value;
664
+ const errorDiv = document.getElementById('register-error');
665
+
666
+ // Clear previous errors
667
+ errorDiv.classList.add('hidden');
668
+ errorDiv.textContent = '';
669
+
670
+ // Basic validation
671
+ if (!email || !password) {
672
+ errorDiv.textContent = 'Email and password are required';
673
+ errorDiv.classList.remove('hidden');
674
+ return;
675
+ }
676
+
677
+ if (password.length < 6) {
678
+ errorDiv.textContent = 'Password must be at least 6 characters';
679
+ errorDiv.classList.remove('hidden');
680
+ return;
681
+ }
682
+
683
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
684
+ if (!emailRegex.test(email)) {
685
+ errorDiv.textContent = 'Invalid email format';
686
+ errorDiv.classList.remove('hidden');
687
+ return;
688
+ }
689
+
690
+ // Disable submit button
691
+ const submitBtn = document.getElementById('btn-register-submit');
692
+ const originalText = submitBtn.textContent;
693
+ submitBtn.disabled = true;
694
+ submitBtn.textContent = 'Registering...';
695
+
696
+ try {
697
+ // Determine API base URL considering relative path mounting
698
+ const apiBase = getApiUrl('/api/admin/users/register');
699
+
700
+ const response = await fetch(apiBase, {
701
+ method: 'POST',
702
+ headers: {
703
+ 'Content-Type': 'application/json'
704
+ },
705
+ body: JSON.stringify({ email, password, name, role })
706
+ });
707
+
708
+ const data = await response.json();
709
+
710
+ if (!response.ok) {
711
+ if (response.status === 401) {
712
+ throw new Error('Admin authentication required. Please refresh the page and log in again.');
713
+ }
714
+ throw new Error(data.error || 'Registration failed');
715
+ }
716
+
717
+ showToast(`User ${email} registered successfully`, 'success');
718
+ closeRegisterModal();
719
+ await Promise.all([loadUsers(), loadStats()]);
720
+ } catch (error) {
721
+ errorDiv.textContent = error.message || 'Registration failed';
722
+ errorDiv.classList.remove('hidden');
723
+ } finally {
724
+ submitBtn.disabled = false;
725
+ submitBtn.textContent = originalText;
726
+ }
727
+ }
728
+
729
+ function getApiUrl(endpoint) {
730
+ // Detect if we're mounted on a relative path
731
+ const pathname = window.location.pathname;
732
+ const pathSegments = pathname.split('/').filter(Boolean);
733
+
734
+ // Check for common mounting patterns
735
+ if (pathSegments.includes('admin')) {
736
+ // If we're in /admin/ context, try to detect the base path
737
+ // Look for patterns like /super/admin/ or /api/admin/
738
+ const adminIndex = pathSegments.indexOf('admin');
739
+
740
+ if (adminIndex > 0) {
741
+ // There's a prefix before 'admin', use it as base
742
+ const basePath = '/' + pathSegments.slice(0, adminIndex).join('/');
743
+ return basePath + endpoint;
744
+ } else {
745
+ // Admin is at root, just use the endpoint
746
+ return endpoint;
747
+ }
748
+ }
749
+
750
+ // Default case - use current origin with endpoint
751
+ return window.location.origin + endpoint;
752
+ }
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
+
420
826
  function bindEvents() {
421
827
  document.getElementById('btn-refresh').onclick = () => Promise.all([loadUsers(), loadStats()]);
422
828
  document.getElementById('btn-apply').onclick = () => { state.offset = 0; loadUsers(); };
@@ -435,6 +841,36 @@
435
841
  document.getElementById('btn-modal-save').onclick = saveUser;
436
842
  document.getElementById('btn-notify-cancel').onclick = closeNotifyModal;
437
843
  document.getElementById('btn-notify-send').onclick = sendNotification;
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
+
863
+ // Registration modal events
864
+ document.getElementById('btn-register-user').onclick = openRegisterModal;
865
+ document.getElementById('btn-register-cancel').onclick = closeRegisterModal;
866
+ document.getElementById('btn-register-submit').onclick = registerUser;
867
+ document.getElementById('toggle-password').onclick = togglePasswordVisibility;
868
+
869
+ // Form submission
870
+ document.getElementById('register-form').onsubmit = (e) => {
871
+ e.preventDefault();
872
+ registerUser();
873
+ };
438
874
  }
439
875
 
440
876
  bindEvents();
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Webhook Management | SaaSBackend</title>
6
+ <title>Webhook Management | SuperBackend</title>
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
8
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
9
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Workflow Editor - SaaSBackend</title>
6
+ <title>Workflow Editor - SuperBackend</title>
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
8
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
9
9
  <style>
@@ -1436,10 +1436,10 @@
1436
1436
  const publicUrl = selected?.publicUrl ? `${API_BASE}${selected.publicUrl}` : null;
1437
1437
 
1438
1438
  const snippet =
1439
- `const saasbackend = require('saasbackend');
1439
+ `const superbackend = require('@intranefr/superbackend');
1440
1440
 
1441
- // Works when your host app mounts saasbackend.middleware(...) and shares the same process/DB connection
1442
- const { assets } = saasbackend.services;
1441
+ // Works when your host app mounts superbackend.middleware(...) and shares the same process/DB connection
1442
+ const { assets } = superbackend.services;
1443
1443
 
1444
1444
  // 1) List assets (metadata)
1445
1445
  const { assets: list } = await assets.listAssets({
@@ -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
  ];
@@ -57,7 +57,7 @@
57
57
  <span><kbd class="bg-white border rounded px-1.5 py-0.5">Enter</kbd> Select</span>
58
58
  <span><kbd class="bg-white border rounded px-1.5 py-0.5">Esc</kbd> Close</span>
59
59
  </div>
60
- <div>Superbackend <span class="text-[10px] font-normal opacity-60 ml-1">(saasbackend)</span></div>
60
+ <div>SuperBackend <span class="text-[10px] font-normal opacity-60 ml-1">(@intranefr/superbackend)</span></div>
61
61
  </div>
62
62
  </div>
63
63
  </div>