@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.
- package/.env.example +5 -0
- package/README.md +11 -0
- package/index.js +39 -1
- package/package.json +11 -3
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +111 -5
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminHeadless.controller.js +91 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +320 -0
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- 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/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +366 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware/internalCronAuth.js +29 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +879 -56
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -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 +1 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminHeadless.routes.js +8 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +30 -0
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +6 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +184 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +700 -0
- package/src/services/consoleOverride.service.js +6 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- 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 +299 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +29 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-headless.ejs +294 -24
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +528 -10
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +741 -0
- package/views/admin-users.ejs +261 -4
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +14 -0
- package/views/partials/llm-provider-model-picker.ejs +183 -0
package/views/admin-users.ejs
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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;
|
|
@@ -220,11 +220,11 @@
|
|
|
220
220
|
this.loadWorkflows().then(() => {
|
|
221
221
|
if (workflowIdFromUrl !== 'new' && workflowIdFromUrl.length > 5) {
|
|
222
222
|
const wf = this.workflows.find(w => w._id === workflowIdFromUrl);
|
|
223
|
-
if (wf) this.editWorkflow(wf); else fetch(`/saas/api/workflows/${workflowIdFromUrl}`).then(res => res.ok && res.json().then(data => this.editWorkflow(data)));
|
|
223
|
+
if (wf) this.editWorkflow(wf); else fetch(`/saas/api/admin/workflows/${workflowIdFromUrl}`).then(res => res.ok && res.json().then(data => this.editWorkflow(data)));
|
|
224
224
|
} else if (workflowIdFromUrl === 'new') this.createNew();
|
|
225
225
|
});
|
|
226
226
|
},
|
|
227
|
-
async loadWorkflows() { const res = await fetch('/saas/api/workflows'); this.workflows = await res.json(); },
|
|
227
|
+
async loadWorkflows() { const res = await fetch('/saas/api/admin/workflows'); this.workflows = await res.json(); },
|
|
228
228
|
createNew() { this.workflow = { name: 'New Workflow', status: 'inactive', entrypoint: { awaitResponse: false, allowedMethods: ['POST', 'GET'], auth: { type: 'none' } }, testDataset: { method: 'POST', body: { message: 'Hello' }, query: {}, headers: {} }, nodes: [] }; this.view = 'editor'; },
|
|
229
229
|
editWorkflow(wf) {
|
|
230
230
|
const currentView = this.view; this.workflow = JSON.parse(JSON.stringify(wf)); this.showRuns = false; this.runs = []; this.inspectedRun = null;
|
|
@@ -233,7 +233,7 @@
|
|
|
233
233
|
this.view = (currentView === 'list') ? 'editor' : currentView;
|
|
234
234
|
},
|
|
235
235
|
toggleMethod(m) { this.workflow.entrypoint.allowedMethods = this.workflow.entrypoint.allowedMethods || []; const idx = this.workflow.entrypoint.allowedMethods.indexOf(m); if (idx > -1) this.workflow.entrypoint.allowedMethods.splice(idx, 1); else this.workflow.entrypoint.allowedMethods.push(m); },
|
|
236
|
-
async deleteWorkflow(id) { if (confirm('Are you sure?')) { await fetch(`/saas/api/workflows/${id}`, { method: 'DELETE' }); await this.loadWorkflows(); } },
|
|
236
|
+
async deleteWorkflow(id) { if (confirm('Are you sure?')) { await fetch(`/saas/api/admin/workflows/${id}`, { method: 'DELETE' }); await this.loadWorkflows(); } },
|
|
237
237
|
getWebhookUrl() { return window.location.origin + '/saas/w/' + (this.workflow._id || 'NEW'); },
|
|
238
238
|
copyWebhookUrl() { navigator.clipboard.writeText(this.getWebhookUrl()); this.showToast('Copied!'); },
|
|
239
239
|
addNode(type) { this.workflow.nodes.push({ id: 'node_' + Date.now(), type, name: 'node_' + (this.workflow.nodes.length + 1) }); },
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
getNodeIcon(type) { return { llm: 'ti-brain', if: 'ti-git-branch', http: 'ti-world', exit: 'ti-door-exit', parallel: 'ti-git-merge' }[type] || 'ti-circle'; },
|
|
248
248
|
getNodeColor(type) { return { llm: 'bg-indigo-500', if: 'bg-yellow-500', http: 'bg-blue-500', exit: 'bg-red-500', parallel: 'bg-purple-500' }[type]; },
|
|
249
249
|
async saveWorkflow(silent = false) {
|
|
250
|
-
const res = await fetch(this.workflow._id ? `/saas/api/workflows/${this.workflow._id}` : '/saas/api/workflows', { method: this.workflow._id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.workflow) });
|
|
250
|
+
const res = await fetch(this.workflow._id ? `/saas/api/admin/workflows/${this.workflow._id}` : '/saas/api/admin/workflows', { method: this.workflow._id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.workflow) });
|
|
251
251
|
if (res.ok) { const saved = await res.json(); if (!silent) this.showToast('Saved!'); await this.loadWorkflows(); this.editWorkflow(saved); }
|
|
252
252
|
},
|
|
253
253
|
async runFullTest(entrypointOnly = false) {
|
|
@@ -255,7 +255,7 @@
|
|
|
255
255
|
try {
|
|
256
256
|
const payload = { method: this.testConfig.method, body: JSON.parse(this.testConfig.body), query: JSON.parse(this.testConfig.query), headers: JSON.parse(this.testConfig.headers) };
|
|
257
257
|
if (entrypointOnly) { this.workflow.testDataset = payload; await this.saveWorkflow(true); return this.showToast('Dataset set.'); }
|
|
258
|
-
this.testing = true; const res = await fetch(`/saas/api/workflows/${this.workflow._id}/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
|
258
|
+
this.testing = true; const res = await fetch(`/saas/api/admin/workflows/${this.workflow._id}/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
|
259
259
|
this.testResult = await res.json(); if (this.showRuns) this.loadRuns(); this.showToast('Ran.');
|
|
260
260
|
} catch (e) { this.showToast('Failed', 'error'); } finally { this.testing = false; }
|
|
261
261
|
},
|
|
@@ -263,11 +263,11 @@
|
|
|
263
263
|
async testIsolatedNode(node) {
|
|
264
264
|
const context = { entrypoint: this.workflow.testDataset, payload: this.workflow.testDataset, lastNode: this.workflow.testDataset };
|
|
265
265
|
try {
|
|
266
|
-
const res = await fetch(`/saas/api/workflows/${this.workflow._id}/nodes/${node.id}/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ context, node }) });
|
|
266
|
+
const res = await fetch(`/saas/api/admin/workflows/${this.workflow._id}/nodes/${node.id}/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ context, node }) });
|
|
267
267
|
const data = await res.json(); node.testOutput = data.result; node.testResult = data.result; this.showToast('Tested');
|
|
268
268
|
} catch (e) { this.showToast('Failed', 'error'); }
|
|
269
269
|
},
|
|
270
|
-
async loadRuns() { if (!this.workflow._id) return; const res = await fetch(`/saas/api/workflows/${this.workflow._id}/runs`); this.runs = await res.json(); },
|
|
270
|
+
async loadRuns() { if (!this.workflow._id) return; const res = await fetch(`/saas/api/admin/workflows/${this.workflow._id}/runs`); this.runs = await res.json(); },
|
|
271
271
|
inspectRun(run) { this.inspectedRun = run; }, testWorkflow() { this.showTestPanel = true; },
|
|
272
272
|
showToast(message, type = 'success') {
|
|
273
273
|
const id = Date.now(); this.toasts.push({ id, message, type, show: true });
|