@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.
- package/.env.example +6 -1
- package/README.md +5 -5
- package/index.js +23 -5
- package/package.json +5 -2
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/error-tracking/browser/package.json +4 -3
- package/sdk/error-tracking/browser/src/embed.js +29 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/controllers/admin.controller.js +139 -1
- package/src/controllers/adminHeadless.controller.js +82 -0
- package/src/controllers/adminMigration.controller.js +5 -1
- package/src/controllers/adminScripts.controller.js +229 -0
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- package/src/controllers/orgAdmin.controller.js +286 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware.js +119 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/ScriptDefinition.js +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminHeadless.routes.js +6 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +29 -0
- package/src/routes/llmUi.routes.js +26 -0
- package/src/routes/orgAdmin.routes.js +5 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/services/consoleOverride.service.js +291 -0
- package/src/services/email.service.js +17 -1
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/scriptsRunner.service.js +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +312 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/webhook.service.js +2 -2
- package/src/services/workflow.service.js +1 -1
- package/src/utils/encryption.js +5 -3
- package/views/admin-coolify-deploy.ejs +1 -1
- package/views/admin-dashboard-home.ejs +1 -1
- package/views/admin-dashboard.ejs +1 -1
- package/views/admin-errors.ejs +2 -2
- package/views/admin-global-settings.ejs +3 -3
- package/views/admin-headless.ejs +294 -24
- package/views/admin-json-configs.ejs +8 -1
- package/views/admin-llm.ejs +2 -2
- package/views/admin-organizations.ejs +365 -9
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +1 -1
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-test.ejs +3 -3
- package/views/admin-ui-components.ejs +709 -0
- package/views/admin-users.ejs +440 -4
- package/views/admin-webhooks.ejs +1 -1
- package/views/admin-workflows.ejs +1 -1
- package/views/partials/admin-assets-script.ejs +3 -3
- package/views/partials/dashboard/nav-items.ejs +3 -0
- package/views/partials/dashboard/palette.ejs +1 -1
package/views/admin-users.ejs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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();
|
package/views/admin-webhooks.ejs
CHANGED
|
@@ -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 |
|
|
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 -
|
|
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
|
|
1439
|
+
`const superbackend = require('@intranefr/superbackend');
|
|
1440
1440
|
|
|
1441
|
-
// Works when your host app mounts
|
|
1442
|
-
const { assets } =
|
|
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>
|
|
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>
|