@intranefr/superbackend 1.5.3 → 1.6.3

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 (106) hide show
  1. package/cookies.txt +6 -0
  2. package/cookies1.txt +6 -0
  3. package/cookies2.txt +6 -0
  4. package/cookies3.txt +6 -0
  5. package/cookies4.txt +5 -0
  6. package/cookies_old.txt +5 -0
  7. package/cookies_old_test.txt +6 -0
  8. package/cookies_super.txt +5 -0
  9. package/cookies_super_test.txt +6 -0
  10. package/cookies_test.txt +6 -0
  11. package/index.js +7 -0
  12. package/package.json +3 -1
  13. package/plugins/core-waiting-list-migration/README.md +118 -0
  14. package/plugins/core-waiting-list-migration/index.js +438 -0
  15. package/plugins/global-settings-presets/index.js +20 -0
  16. package/plugins/hello-cli/index.js +17 -0
  17. package/plugins/ui-components-seeder/components/suiAlert.js +212 -0
  18. package/plugins/ui-components-seeder/components/suiToast.js +186 -0
  19. package/plugins/ui-components-seeder/index.js +31 -0
  20. package/public/js/admin-ui-components-preview.js +281 -0
  21. package/public/js/admin-ui-components.js +408 -0
  22. package/public/js/llm-provider-model-picker.js +193 -0
  23. package/public/test-iframe-fix.html +63 -0
  24. package/public/test-iframe.html +14 -0
  25. package/src/admin/endpointRegistry.js +68 -0
  26. package/src/controllers/admin.controller.js +25 -5
  27. package/src/controllers/adminDataCleanup.controller.js +45 -0
  28. package/src/controllers/adminLlm.controller.js +0 -8
  29. package/src/controllers/adminLogin.controller.js +269 -0
  30. package/src/controllers/adminPlugins.controller.js +55 -0
  31. package/src/controllers/adminRegistry.controller.js +106 -0
  32. package/src/controllers/adminStats.controller.js +4 -4
  33. package/src/controllers/registry.controller.js +32 -0
  34. package/src/controllers/waitingList.controller.js +52 -74
  35. package/src/middleware/auth.js +71 -1
  36. package/src/middleware/rbac.js +62 -0
  37. package/src/middleware.js +454 -153
  38. package/src/models/GlobalSetting.js +11 -1
  39. package/src/models/UiComponent.js +2 -0
  40. package/src/models/User.js +1 -1
  41. package/src/routes/admin.routes.js +3 -3
  42. package/src/routes/adminAgents.routes.js +2 -2
  43. package/src/routes/adminAssets.routes.js +11 -11
  44. package/src/routes/adminBlog.routes.js +2 -2
  45. package/src/routes/adminBlogAi.routes.js +2 -2
  46. package/src/routes/adminBlogAutomation.routes.js +2 -2
  47. package/src/routes/adminCache.routes.js +2 -2
  48. package/src/routes/adminConsoleManager.routes.js +2 -2
  49. package/src/routes/adminCrons.routes.js +2 -2
  50. package/src/routes/adminDataCleanup.routes.js +26 -0
  51. package/src/routes/adminDbBrowser.routes.js +2 -2
  52. package/src/routes/adminEjsVirtual.routes.js +2 -2
  53. package/src/routes/adminFeatureFlags.routes.js +6 -6
  54. package/src/routes/adminHeadless.routes.js +2 -2
  55. package/src/routes/adminHealthChecks.routes.js +2 -2
  56. package/src/routes/adminI18n.routes.js +2 -2
  57. package/src/routes/adminJsonConfigs.routes.js +8 -8
  58. package/src/routes/adminLlm.routes.js +8 -8
  59. package/src/routes/adminLogin.routes.js +23 -0
  60. package/src/routes/adminMarkdowns.routes.js +3 -9
  61. package/src/routes/adminMigration.routes.js +12 -12
  62. package/src/routes/adminPages.routes.js +2 -2
  63. package/src/routes/adminPlugins.routes.js +15 -0
  64. package/src/routes/adminProxy.routes.js +2 -2
  65. package/src/routes/adminRateLimits.routes.js +8 -8
  66. package/src/routes/adminRbac.routes.js +2 -2
  67. package/src/routes/adminRegistry.routes.js +24 -0
  68. package/src/routes/adminScripts.routes.js +2 -2
  69. package/src/routes/adminSeoConfig.routes.js +10 -10
  70. package/src/routes/adminTelegram.routes.js +2 -2
  71. package/src/routes/adminTerminals.routes.js +2 -2
  72. package/src/routes/adminUiComponents.routes.js +2 -2
  73. package/src/routes/adminUploadNamespaces.routes.js +7 -7
  74. package/src/routes/blogInternal.routes.js +2 -2
  75. package/src/routes/experiments.routes.js +2 -2
  76. package/src/routes/formsAdmin.routes.js +6 -6
  77. package/src/routes/globalSettings.routes.js +8 -8
  78. package/src/routes/internalExperiments.routes.js +2 -2
  79. package/src/routes/notificationAdmin.routes.js +7 -7
  80. package/src/routes/orgAdmin.routes.js +16 -16
  81. package/src/routes/pages.routes.js +3 -3
  82. package/src/routes/registry.routes.js +11 -0
  83. package/src/routes/stripeAdmin.routes.js +12 -12
  84. package/src/routes/userAdmin.routes.js +7 -7
  85. package/src/routes/waitingListAdmin.routes.js +2 -2
  86. package/src/routes/workflows.routes.js +3 -3
  87. package/src/services/dataCleanup.service.js +286 -0
  88. package/src/services/jsonConfigs.service.js +262 -0
  89. package/src/services/plugins.service.js +348 -0
  90. package/src/services/registry.service.js +452 -0
  91. package/src/services/uiComponents.service.js +180 -0
  92. package/src/services/waitingListJson.service.js +401 -0
  93. package/src/utils/rbac/rightsRegistry.js +118 -0
  94. package/test-access.js +63 -0
  95. package/test-iframe-fix.html +63 -0
  96. package/test-iframe.html +14 -0
  97. package/views/admin-403.ejs +92 -0
  98. package/views/admin-dashboard-home.ejs +52 -2
  99. package/views/admin-dashboard.ejs +143 -2
  100. package/views/admin-data-cleanup.ejs +357 -0
  101. package/views/admin-login.ejs +286 -0
  102. package/views/admin-plugins-system.ejs +223 -0
  103. package/views/admin-ui-components.ejs +82 -402
  104. package/views/admin-users.ejs +207 -11
  105. package/views/partials/dashboard/nav-items.ejs +2 -0
  106. package/views/partials/llm-provider-model-picker.ejs +0 -161
@@ -0,0 +1,193 @@
1
+ (function () {
2
+ // Only initialize if Vue has indicated it's ready (prevents premature execution in Vue contexts)
3
+ if (typeof window !== 'undefined' && window.__llmProviderModelPickerReady === true) {
4
+ initializeLlmProviderModelPicker();
5
+ } else if (typeof window !== 'undefined') {
6
+ // Queue initialization for when Vue indicates readiness
7
+ window.__llmProviderModelPickerQueue = window.__llmProviderModelPickerQueue || [];
8
+ window.__llmProviderModelPickerQueue.push(initializeLlmProviderModelPicker);
9
+ }
10
+
11
+ function initializeLlmProviderModelPicker() {
12
+ if (!window.__llmProviderModelPicker) {
13
+ window.__llmProviderModelPicker = { instances: {} };
14
+ }
15
+
16
+ function safeJsonParse(raw, fallback) {
17
+ try {
18
+ return JSON.parse(raw);
19
+ } catch (_) {
20
+ return fallback;
21
+ }
22
+ }
23
+
24
+ async function fetchJson(url) {
25
+ const res = await fetch(url);
26
+ const data = await res.json();
27
+ if (!res.ok) {
28
+ throw new Error(data?.error || 'Request failed');
29
+ }
30
+ return data;
31
+ }
32
+
33
+ function setDatalistOptions(datalistEl, items) {
34
+ datalistEl.innerHTML = '';
35
+ const uniq = Array.from(new Set((items || []).filter(Boolean)));
36
+ for (const item of uniq) {
37
+ const opt = document.createElement('option');
38
+ opt.value = String(item);
39
+ datalistEl.appendChild(opt);
40
+ }
41
+ }
42
+
43
+ function trim(v) {
44
+ return String(v || '').trim();
45
+ }
46
+
47
+ function isOpenRouterProvider({ providerKey, providerConfig }) {
48
+ const pk = String(providerKey || '').trim().toLowerCase();
49
+ if (pk === 'openrouter') return true;
50
+
51
+ const baseUrl = providerConfig && typeof providerConfig === 'object'
52
+ ? String(providerConfig.baseUrl || providerConfig.baseURL || '').trim().toLowerCase()
53
+ : '';
54
+
55
+ return Boolean(baseUrl && baseUrl.includes('openrouter'));
56
+ }
57
+
58
+ function getInstanceKey({ providerInputId, modelInputId }) {
59
+ return `${String(providerInputId || '').trim()}::${String(modelInputId || '').trim()}`;
60
+ }
61
+
62
+ function getOrCreateInstance(opts) {
63
+ const key = getInstanceKey(opts);
64
+ const existing = window.__llmProviderModelPicker.instances[key];
65
+ if (existing) {
66
+ console.log('[LLM Picker Debug] Using existing instance:', key, 'apiBase:', existing.apiBase);
67
+ return existing;
68
+ }
69
+
70
+ const apiBase = opts.apiBase !== undefined ? opts.apiBase : (window.__llmProviderModelPicker.defaultApiBase || null);
71
+ console.log('[LLM Picker Debug] Creating new instance:', key, 'opts.apiBase:', opts.apiBase, 'defaultApiBase:', window.__llmProviderModelPicker.defaultApiBase, 'final apiBase:', apiBase);
72
+
73
+ const inst = {
74
+ apiBase: apiBase,
75
+ providerInputId: opts.providerInputId,
76
+ modelInputId: opts.modelInputId,
77
+ providers: {},
78
+ providerModels: {},
79
+ };
80
+
81
+ window.__llmProviderModelPicker.instances[key] = inst;
82
+ return inst;
83
+ }
84
+
85
+ async function loadConfig(inst) {
86
+ const url = `${inst.apiBase}/api/admin/llm/config`;
87
+ console.log('[LLM Picker Debug] loadConfig called with apiBase:', inst.apiBase, 'full URL:', url);
88
+ const data = await fetchJson(url);
89
+ inst.providers = data.providers || {};
90
+ inst.providerModels = data.providerModels || {};
91
+ return data;
92
+ }
93
+
94
+ function renderProviderOptions(inst) {
95
+ const providerInput = document.getElementById(inst.providerInputId);
96
+ const providerList = document.getElementById(`${inst.providerInputId}__datalist`);
97
+ if (!providerInput || !providerList) return;
98
+
99
+ const providerKeys = Object.keys(inst.providers || {}).sort();
100
+ setDatalistOptions(providerList, providerKeys);
101
+ }
102
+
103
+ function renderModelOptions(inst) {
104
+ const providerInput = document.getElementById(inst.providerInputId);
105
+ const modelList = document.getElementById(`${inst.modelInputId}__datalist`);
106
+ if (!providerInput || !modelList) return;
107
+
108
+ const providerKey = trim(providerInput.value);
109
+ const models = providerKey && inst.providerModels && typeof inst.providerModels === 'object'
110
+ ? inst.providerModels[providerKey]
111
+ : null;
112
+
113
+ setDatalistOptions(modelList, Array.isArray(models) ? models : []);
114
+ }
115
+
116
+ async function maybeAutoFetchOpenRouterModels(inst) {
117
+ try {
118
+ const providerInput = document.getElementById(inst.providerInputId);
119
+ if (!providerInput) return;
120
+
121
+ const providerKey = trim(providerInput.value);
122
+ const providerConfig = inst.providers && typeof inst.providers === 'object' ? inst.providers[providerKey] : null;
123
+ if (!isOpenRouterProvider({ providerKey, providerConfig })) return;
124
+
125
+ const existing = inst.providerModels && typeof inst.providerModels === 'object' ? inst.providerModels.openrouter : null;
126
+ if (Array.isArray(existing) && existing.length > 0) return;
127
+
128
+ await fetchOpenRouterModels({
129
+ apiBase: inst.apiBase,
130
+ providerInputId: inst.providerInputId,
131
+ modelInputId: inst.modelInputId,
132
+ });
133
+ } catch {
134
+ // ignore
135
+ }
136
+ }
137
+
138
+ async function fetchOpenRouterModels(opts) {
139
+ console.log('[LLM Picker Debug] fetchOpenRouterModels called with opts:', opts);
140
+ const inst = getOrCreateInstance(opts || {});
141
+ console.log('[LLM Picker Debug] fetchOpenRouterModels - inst.apiBase before update:', inst.apiBase);
142
+ inst.apiBase = (opts && opts.apiBase !== undefined) ? opts.apiBase : (inst.apiBase !== undefined ? inst.apiBase : (window.__llmProviderModelPicker.defaultApiBase || null));
143
+ console.log('[LLM Picker Debug] fetchOpenRouterModels - inst.apiBase after update:', inst.apiBase);
144
+ if (inst.apiBase == null) {
145
+ console.error('[LLM Picker] No apiBase available for fetchOpenRouterModels');
146
+ return;
147
+ }
148
+
149
+ const data = await fetchJson(`${inst.apiBase}/api/admin/llm/openrouter/models`);
150
+ const models = Array.isArray(data?.models) ? data.models : [];
151
+
152
+ inst.providerModels = inst.providerModels && typeof inst.providerModels === 'object' ? inst.providerModels : {};
153
+ inst.providerModels.openrouter = models;
154
+ renderModelOptions(inst);
155
+ }
156
+
157
+ async function init(opts) {
158
+ console.log('[LLM Picker Debug] init called with opts:', opts);
159
+ const inst = getOrCreateInstance(opts || {});
160
+
161
+ if (opts && opts.apiBase) {
162
+ console.log('[LLM Picker Debug] Setting apiBase to:', opts.apiBase);
163
+ window.__llmProviderModelPicker.defaultApiBase = opts.apiBase;
164
+ // Update apiBase for this instance and all existing instances
165
+ inst.apiBase = opts.apiBase;
166
+ // Update all existing instances to have the correct apiBase
167
+ Object.values(window.__llmProviderModelPicker.instances).forEach(existingInst => {
168
+ console.log('[LLM Picker Debug] Updating existing instance apiBase from', existingInst.apiBase, 'to', opts.apiBase);
169
+ existingInst.apiBase = opts.apiBase;
170
+ });
171
+ }
172
+
173
+ console.log('[LLM Picker Debug] About to call loadConfig, inst.apiBase:', inst.apiBase);
174
+ await loadConfig(inst);
175
+ renderProviderOptions(inst);
176
+ renderModelOptions(inst);
177
+ await maybeAutoFetchOpenRouterModels(inst);
178
+
179
+ const providerInput = document.getElementById(inst.providerInputId);
180
+ if (providerInput) {
181
+ providerInput.addEventListener('change', async () => {
182
+ renderModelOptions(inst);
183
+ await maybeAutoFetchOpenRouterModels(inst);
184
+ });
185
+ providerInput.addEventListener('input', () => renderModelOptions(inst));
186
+ }
187
+ }
188
+
189
+ window.__llmProviderModelPicker.init = init;
190
+ window.__llmProviderModelPicker.fetchOpenRouterModels = fetchOpenRouterModels;
191
+ window.__llmProviderModelPicker._util = { safeJsonParse };
192
+ }
193
+ })();
@@ -0,0 +1,63 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Iframe Fix Test</title>
5
+ <style>
6
+ body { font-family: Arial, sans-serif; padding: 20px; }
7
+ .test-container { margin: 20px 0; }
8
+ iframe { width: 100%; height: 400px; border: 2px solid #ccc; }
9
+ .success { color: green; font-weight: bold; }
10
+ .error { color: red; font-weight: bold; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <h1>Iframe Authentication Fix Test</h1>
15
+
16
+ <div class="test-container">
17
+ <h2>Test 1: Iframe with Token (Should Work)</h2>
18
+ <iframe src="/admin/stats/dashboard-home?iframe_token=authenticated"></iframe>
19
+ <p id="test1-result">Loading...</p>
20
+ </div>
21
+
22
+ <div class="test-container">
23
+ <h2>Test 2: Iframe without Token (Should Redirect to Login)</h2>
24
+ <iframe src="/admin/stats/dashboard-home"></iframe>
25
+ <p id="test2-result">Loading...</p>
26
+ </div>
27
+
28
+ <script>
29
+ // Test if iframe loads correctly
30
+ setTimeout(() => {
31
+ const iframes = document.querySelectorAll('iframe');
32
+
33
+ // Test 1 - should show Command Center
34
+ iframes[0].contentDocument && iframes[0].contentDocument.body) {
35
+ const content = iframes[0].contentDocument.body.innerText;
36
+ if (content.includes('Command Center')) {
37
+ document.getElementById('test1-result').innerHTML = '<span class="success">✅ SUCCESS: Iframe with token loads correctly</span>';
38
+ } else if (content.includes('login') || content.includes('Login')) {
39
+ document.getElementById('test1-result').innerHTML = '<span class="error">❌ FAILED: Iframe with token redirected to login</span>';
40
+ } else {
41
+ document.getElementById('test1-result').innerHTML = '<span class="error">❌ UNKNOWN: Could not determine iframe content</span>';
42
+ }
43
+ } else {
44
+ document.getElementById('test1-result').innerHTML = '<span class="error">❌ FAILED: Could not access iframe content (cross-origin)</span>';
45
+ }
46
+
47
+ // Test 2 - should redirect to login
48
+ if (iframes[1].contentDocument && iframes[1].contentDocument.body) {
49
+ const content = iframes[1].contentDocument.body.innerText;
50
+ if (content.includes('login') || content.includes('Login')) {
51
+ document.getElementById('test2-result').innerHTML = '<span class="success">✅ SUCCESS: Iframe without token correctly redirects to login</span>';
52
+ } else if (content.includes('Command Center')) {
53
+ document.getElementById('test2-result').innerHTML = '<span class="error">❌ FAILED: Iframe without token loaded content (security issue)</span>';
54
+ } else {
55
+ document.getElementById('test2-result').innerHTML = '<span class="error">❌ UNKNOWN: Could not determine iframe content</span>';
56
+ }
57
+ } else {
58
+ document.getElementById('test2-result').innerHTML = '<span class="error">❌ FAILED: Could not access iframe content (cross-origin)</span>';
59
+ }
60
+ }, 3000);
61
+ </script>
62
+ </body>
63
+ </html>
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Iframe Test</title>
5
+ </head>
6
+ <body>
7
+ <h1>Iframe Test</h1>
8
+ <p>Testing iframe loading of admin dashboard content...</p>
9
+
10
+ <iframe src="/admin/stats/dashboard-home" width="100%" height="500" style="border: 1px solid #ccc;"></iframe>
11
+
12
+ <p>If you see the admin dashboard content above, iframes work. If you see a login page, there's a cookie/session issue.</p>
13
+ </body>
14
+ </html>
@@ -415,6 +415,74 @@ const endpointRegistry = [
415
415
  },
416
416
  ],
417
417
  },
418
+ {
419
+ id: "open-registry",
420
+ title: "Open Registry",
421
+ endpoints: [
422
+ {
423
+ id: "registry-auth",
424
+ method: "GET",
425
+ path: "/registry/plugins/auth",
426
+ auth: "none|bearer",
427
+ },
428
+ {
429
+ id: "registry-list",
430
+ method: "GET",
431
+ path: "/registry/plugins/list?category=plugins&minimal=true",
432
+ auth: "none|bearer",
433
+ },
434
+ {
435
+ id: "admin-registries-list",
436
+ method: "GET",
437
+ path: "/api/admin/registries",
438
+ auth: "basic",
439
+ },
440
+ {
441
+ id: "admin-registries-create",
442
+ method: "POST",
443
+ path: "/api/admin/registries",
444
+ auth: "basic",
445
+ bodyExample: { id: "plugins", name: "Plugins Registry", public: true, categories: ["plugins"] },
446
+ },
447
+ {
448
+ id: "admin-registry-item-upsert",
449
+ method: "POST",
450
+ path: "/api/admin/registries/plugins/items",
451
+ auth: "basic",
452
+ bodyExample: { id: "hello-cli", name: "hello-cli", category: "plugins", version: 1, versions: [1], description: "Sample" },
453
+ },
454
+ ],
455
+ },
456
+ {
457
+ id: "plugins-system",
458
+ title: "Plugins System",
459
+ endpoints: [
460
+ {
461
+ id: "plugins-list",
462
+ method: "GET",
463
+ path: "/api/admin/plugins",
464
+ auth: "basic",
465
+ },
466
+ {
467
+ id: "plugins-enable",
468
+ method: "POST",
469
+ path: "/api/admin/plugins/:id/enable",
470
+ auth: "basic",
471
+ },
472
+ {
473
+ id: "plugins-disable",
474
+ method: "POST",
475
+ path: "/api/admin/plugins/:id/disable",
476
+ auth: "basic",
477
+ },
478
+ {
479
+ id: "plugins-install",
480
+ method: "POST",
481
+ path: "/api/admin/plugins/:id/install",
482
+ auth: "basic",
483
+ },
484
+ ],
485
+ },
418
486
  ];
419
487
 
420
488
  module.exports = endpointRegistry;
@@ -7,6 +7,8 @@ const Notification = require('../models/Notification');
7
7
  const Invite = require('../models/Invite');
8
8
  const EmailLog = require('../models/EmailLog');
9
9
  const FormSubmission = require('../models/FormSubmission');
10
+ const RbacRole = require('../models/RbacRole');
11
+ const RbacUserRole = require('../models/RbacUserRole');
10
12
  const asyncHandler = require('../utils/asyncHandler');
11
13
  const fs = require('fs');
12
14
  const path = require('path');
@@ -56,10 +58,6 @@ const registerUser = asyncHandler(async (req, res) => {
56
58
  return res.status(400).json({ error: 'Password must be at least 6 characters' });
57
59
  }
58
60
 
59
- if (!['user', 'admin'].includes(role)) {
60
- return res.status(400).json({ error: 'Role must be either "user" or "admin"' });
61
- }
62
-
63
61
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
64
62
  if (!emailRegex.test(email)) {
65
63
  return res.status(400).json({ error: 'Invalid email format' });
@@ -76,11 +74,33 @@ const registerUser = asyncHandler(async (req, res) => {
76
74
  email: email.toLowerCase(),
77
75
  passwordHash: password, // Will be hashed by pre-save hook
78
76
  name: name || '',
79
- role: role
77
+ role: role // Keep for backward compatibility
80
78
  });
81
79
 
82
80
  await user.save();
83
81
 
82
+ // Assign RBAC role if it's not 'user'
83
+ if (role !== 'user') {
84
+ try {
85
+ // Find the RBAC role
86
+ const rbacRole = await RbacRole.findOne({ key: role, status: 'active' });
87
+ if (rbacRole) {
88
+ // Create user-role assignment
89
+ const userRoleAssignment = new RbacUserRole({
90
+ userId: user._id,
91
+ roleId: rbacRole._id
92
+ });
93
+ await userRoleAssignment.save();
94
+ console.log(`Assigned RBAC role '${role}' to user ${user.email}`);
95
+ } else {
96
+ console.warn(`RBAC role '${role}' not found, user created without RBAC role assignment`);
97
+ }
98
+ } catch (error) {
99
+ console.error('Error assigning RBAC role:', error);
100
+ // Don't fail the user creation if RBAC assignment fails
101
+ }
102
+ }
103
+
84
104
  // Log the admin action
85
105
  console.log(`Admin registered new user: ${user.email} with role: ${user.role}`);
86
106
 
@@ -0,0 +1,45 @@
1
+ const dataCleanup = require('../services/dataCleanup.service');
2
+
3
+ exports.getOverview = async (req, res) => {
4
+ try {
5
+ const data = await dataCleanup.getOverviewStats();
6
+ return res.json(data);
7
+ } catch (err) {
8
+ const safe = dataCleanup.toSafeJsonError(err);
9
+ return res.status(safe.status).json(safe.body);
10
+ }
11
+ };
12
+
13
+ exports.dryRun = async (req, res) => {
14
+ try {
15
+ const out = await dataCleanup.dryRunCollectionCleanup(req.body || {});
16
+ return res.json(out);
17
+ } catch (err) {
18
+ const safe = dataCleanup.toSafeJsonError(err);
19
+ return res.status(safe.status).json(safe.body);
20
+ }
21
+ };
22
+
23
+ exports.execute = async (req, res) => {
24
+ try {
25
+ const out = await dataCleanup.executeCollectionCleanup(req.body || {});
26
+ return res.json(out);
27
+ } catch (err) {
28
+ const safe = dataCleanup.toSafeJsonError(err);
29
+ return res.status(safe.status).json(safe.body);
30
+ }
31
+ };
32
+
33
+ exports.inferFields = async (req, res) => {
34
+ try {
35
+ const { collection } = req.query;
36
+ if (!collection) {
37
+ return res.status(400).json({ error: 'collection query parameter is required' });
38
+ }
39
+ const fields = await dataCleanup.inferCollectionFields(collection);
40
+ return res.json({ fields });
41
+ } catch (err) {
42
+ const safe = dataCleanup.toSafeJsonError(err);
43
+ return res.status(safe.status).json(safe.body);
44
+ }
45
+ };
@@ -43,10 +43,6 @@ async function setStringSetting(key, value, description) {
43
43
  await existing.save();
44
44
  return existing;
45
45
  }
46
- // Ensure we never create a document with an undefined value
47
- if (stringValue === undefined) {
48
- throw new Error(`Cannot save GlobalSetting with undefined value for key: ${key}`);
49
- }
50
46
  const created = new GlobalSetting({
51
47
  key,
52
48
  value: stringValue,
@@ -79,10 +75,6 @@ async function setJsonSetting(key, value) {
79
75
  await existing.save();
80
76
  return existing;
81
77
  }
82
- // Ensure we never create a document with an empty value
83
- if (!stringValue) {
84
- throw new Error(`Cannot save GlobalSetting with empty value for key: ${key}`);
85
- }
86
78
  const created = new GlobalSetting({
87
79
  key,
88
80
  value: stringValue,