@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.
Files changed (195) hide show
  1. package/.env.example +5 -0
  2. package/README.md +11 -0
  3. package/index.js +39 -1
  4. package/package.json +11 -3
  5. package/public/sdk/ui-components.iife.js +191 -0
  6. package/sdk/ui-components/browser/src/index.js +228 -0
  7. package/src/admin/endpointRegistry.js +120 -0
  8. package/src/controllers/admin.controller.js +111 -5
  9. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  10. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  11. package/src/controllers/adminCache.controller.js +342 -0
  12. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  13. package/src/controllers/adminCrons.controller.js +388 -0
  14. package/src/controllers/adminDbBrowser.controller.js +124 -0
  15. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  16. package/src/controllers/adminHeadless.controller.js +91 -2
  17. package/src/controllers/adminHealthChecks.controller.js +570 -0
  18. package/src/controllers/adminI18n.controller.js +51 -29
  19. package/src/controllers/adminLlm.controller.js +126 -2
  20. package/src/controllers/adminPages.controller.js +720 -0
  21. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  22. package/src/controllers/adminProxy.controller.js +113 -0
  23. package/src/controllers/adminRateLimits.controller.js +138 -0
  24. package/src/controllers/adminRbac.controller.js +803 -0
  25. package/src/controllers/adminScripts.controller.js +320 -0
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/adminTerminals.controller.js +39 -0
  28. package/src/controllers/adminUiComponents.controller.js +315 -0
  29. package/src/controllers/adminUiComponentsAi.controller.js +34 -0
  30. package/src/controllers/blogAdmin.controller.js +279 -0
  31. package/src/controllers/blogAiAdmin.controller.js +224 -0
  32. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  33. package/src/controllers/blogInternal.controller.js +26 -0
  34. package/src/controllers/blogPublic.controller.js +89 -0
  35. package/src/controllers/fileManager.controller.js +190 -0
  36. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  37. package/src/controllers/healthChecksPublic.controller.js +196 -0
  38. package/src/controllers/metrics.controller.js +64 -4
  39. package/src/controllers/orgAdmin.controller.js +366 -0
  40. package/src/controllers/uiComponentsPublic.controller.js +118 -0
  41. package/src/middleware/auth.js +7 -0
  42. package/src/middleware/internalCronAuth.js +29 -0
  43. package/src/middleware/rbac.js +62 -0
  44. package/src/middleware.js +879 -56
  45. package/src/models/BlockDefinition.js +27 -0
  46. package/src/models/BlogAutomationLock.js +14 -0
  47. package/src/models/BlogAutomationRun.js +39 -0
  48. package/src/models/BlogPost.js +42 -0
  49. package/src/models/CacheEntry.js +26 -0
  50. package/src/models/ConsoleEntry.js +32 -0
  51. package/src/models/ConsoleLog.js +23 -0
  52. package/src/models/ContextBlockDefinition.js +33 -0
  53. package/src/models/CronExecution.js +47 -0
  54. package/src/models/CronJob.js +70 -0
  55. package/src/models/ExternalDbConnection.js +49 -0
  56. package/src/models/FileEntry.js +22 -0
  57. package/src/models/HeadlessModelDefinition.js +10 -0
  58. package/src/models/HealthAutoHealAttempt.js +57 -0
  59. package/src/models/HealthCheck.js +132 -0
  60. package/src/models/HealthCheckRun.js +51 -0
  61. package/src/models/HealthIncident.js +49 -0
  62. package/src/models/Page.js +95 -0
  63. package/src/models/PageCollection.js +42 -0
  64. package/src/models/ProxyEntry.js +66 -0
  65. package/src/models/RateLimitCounter.js +19 -0
  66. package/src/models/RateLimitMetricBucket.js +20 -0
  67. package/src/models/RbacGrant.js +25 -0
  68. package/src/models/RbacGroup.js +16 -0
  69. package/src/models/RbacGroupMember.js +13 -0
  70. package/src/models/RbacGroupRole.js +13 -0
  71. package/src/models/RbacRole.js +25 -0
  72. package/src/models/RbacUserRole.js +13 -0
  73. package/src/models/ScriptDefinition.js +42 -0
  74. package/src/models/ScriptRun.js +22 -0
  75. package/src/models/UiComponent.js +29 -0
  76. package/src/models/UiComponentProject.js +26 -0
  77. package/src/models/UiComponentProjectComponent.js +18 -0
  78. package/src/routes/admin.routes.js +1 -0
  79. package/src/routes/adminBlog.routes.js +21 -0
  80. package/src/routes/adminBlogAi.routes.js +16 -0
  81. package/src/routes/adminBlogAutomation.routes.js +27 -0
  82. package/src/routes/adminCache.routes.js +20 -0
  83. package/src/routes/adminConsoleManager.routes.js +302 -0
  84. package/src/routes/adminCrons.routes.js +25 -0
  85. package/src/routes/adminDbBrowser.routes.js +65 -0
  86. package/src/routes/adminEjsVirtual.routes.js +2 -1
  87. package/src/routes/adminHeadless.routes.js +8 -1
  88. package/src/routes/adminHealthChecks.routes.js +28 -0
  89. package/src/routes/adminI18n.routes.js +4 -3
  90. package/src/routes/adminLlm.routes.js +4 -2
  91. package/src/routes/adminPages.routes.js +55 -0
  92. package/src/routes/adminProxy.routes.js +15 -0
  93. package/src/routes/adminRateLimits.routes.js +17 -0
  94. package/src/routes/adminRbac.routes.js +38 -0
  95. package/src/routes/adminScripts.routes.js +21 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminTerminals.routes.js +13 -0
  98. package/src/routes/adminUiComponents.routes.js +30 -0
  99. package/src/routes/blogInternal.routes.js +14 -0
  100. package/src/routes/blogPublic.routes.js +9 -0
  101. package/src/routes/fileManager.routes.js +62 -0
  102. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  103. package/src/routes/healthChecksPublic.routes.js +9 -0
  104. package/src/routes/log.routes.js +43 -60
  105. package/src/routes/metrics.routes.js +4 -2
  106. package/src/routes/orgAdmin.routes.js +6 -0
  107. package/src/routes/pages.routes.js +123 -0
  108. package/src/routes/proxy.routes.js +46 -0
  109. package/src/routes/rbac.routes.js +47 -0
  110. package/src/routes/uiComponentsPublic.routes.js +9 -0
  111. package/src/routes/webhook.routes.js +2 -1
  112. package/src/routes/workflows.routes.js +4 -0
  113. package/src/services/blockDefinitionsAi.service.js +247 -0
  114. package/src/services/blog.service.js +99 -0
  115. package/src/services/blogAutomation.service.js +978 -0
  116. package/src/services/blogCronsBootstrap.service.js +184 -0
  117. package/src/services/blogPublishing.service.js +58 -0
  118. package/src/services/cacheLayer.service.js +696 -0
  119. package/src/services/consoleManager.service.js +700 -0
  120. package/src/services/consoleOverride.service.js +6 -1
  121. package/src/services/cronScheduler.service.js +350 -0
  122. package/src/services/dbBrowser.service.js +536 -0
  123. package/src/services/ejsVirtual.service.js +102 -32
  124. package/src/services/fileManager.service.js +475 -0
  125. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  126. package/src/services/headlessExternalModels.service.js +292 -0
  127. package/src/services/headlessModels.service.js +26 -6
  128. package/src/services/healthChecks.service.js +650 -0
  129. package/src/services/healthChecksBootstrap.service.js +109 -0
  130. package/src/services/healthChecksScheduler.service.js +106 -0
  131. package/src/services/llmDefaults.service.js +190 -0
  132. package/src/services/migrationAssets/s3.js +2 -2
  133. package/src/services/pages.service.js +602 -0
  134. package/src/services/pagesContext.service.js +331 -0
  135. package/src/services/pagesContextBlocksAi.service.js +349 -0
  136. package/src/services/proxy.service.js +535 -0
  137. package/src/services/rateLimiter.service.js +623 -0
  138. package/src/services/rbac.service.js +212 -0
  139. package/src/services/scriptsRunner.service.js +259 -0
  140. package/src/services/terminals.service.js +152 -0
  141. package/src/services/terminalsWs.service.js +100 -0
  142. package/src/services/uiComponentsAi.service.js +299 -0
  143. package/src/services/uiComponentsCrypto.service.js +39 -0
  144. package/src/services/workflow.service.js +23 -8
  145. package/src/utils/orgRoles.js +14 -0
  146. package/src/utils/rbac/engine.js +60 -0
  147. package/src/utils/rbac/rightsRegistry.js +29 -0
  148. package/views/admin-blog-automation.ejs +877 -0
  149. package/views/admin-blog-edit.ejs +542 -0
  150. package/views/admin-blog.ejs +399 -0
  151. package/views/admin-cache.ejs +681 -0
  152. package/views/admin-console-manager.ejs +680 -0
  153. package/views/admin-crons.ejs +645 -0
  154. package/views/admin-db-browser.ejs +445 -0
  155. package/views/admin-ejs-virtual.ejs +16 -10
  156. package/views/admin-file-manager.ejs +942 -0
  157. package/views/admin-headless.ejs +294 -24
  158. package/views/admin-health-checks.ejs +725 -0
  159. package/views/admin-i18n.ejs +59 -5
  160. package/views/admin-llm.ejs +99 -1
  161. package/views/admin-organizations.ejs +528 -10
  162. package/views/admin-pages.ejs +2424 -0
  163. package/views/admin-proxy.ejs +491 -0
  164. package/views/admin-rate-limiter.ejs +625 -0
  165. package/views/admin-rbac.ejs +1331 -0
  166. package/views/admin-scripts.ejs +497 -0
  167. package/views/admin-seo-config.ejs +61 -7
  168. package/views/admin-terminals.ejs +328 -0
  169. package/views/admin-ui-components.ejs +741 -0
  170. package/views/admin-users.ejs +261 -4
  171. package/views/admin-workflows.ejs +7 -7
  172. package/views/file-manager.ejs +866 -0
  173. package/views/pages/blocks/contact.ejs +27 -0
  174. package/views/pages/blocks/cta.ejs +18 -0
  175. package/views/pages/blocks/faq.ejs +20 -0
  176. package/views/pages/blocks/features.ejs +19 -0
  177. package/views/pages/blocks/hero.ejs +13 -0
  178. package/views/pages/blocks/html.ejs +5 -0
  179. package/views/pages/blocks/image.ejs +14 -0
  180. package/views/pages/blocks/testimonials.ejs +26 -0
  181. package/views/pages/blocks/text.ejs +10 -0
  182. package/views/pages/layouts/default.ejs +51 -0
  183. package/views/pages/layouts/minimal.ejs +42 -0
  184. package/views/pages/layouts/sidebar.ejs +54 -0
  185. package/views/pages/partials/footer.ejs +13 -0
  186. package/views/pages/partials/header.ejs +12 -0
  187. package/views/pages/partials/sidebar.ejs +8 -0
  188. package/views/pages/runtime/page.ejs +10 -0
  189. package/views/pages/templates/article.ejs +20 -0
  190. package/views/pages/templates/default.ejs +12 -0
  191. package/views/pages/templates/landing.ejs +14 -0
  192. package/views/pages/templates/listing.ejs +15 -0
  193. package/views/partials/admin-image-upload-modal.ejs +221 -0
  194. package/views/partials/dashboard/nav-items.ejs +14 -0
  195. package/views/partials/llm-provider-model-picker.ejs +183 -0
@@ -70,6 +70,75 @@ const endpointRegistry = [
70
70
  },
71
71
  ],
72
72
  },
73
+ {
74
+ id: "admin-pages",
75
+ title: "Pages (Admin)",
76
+ endpoints: [
77
+ {
78
+ id: "pages-context-block-definitions-list",
79
+ method: "GET",
80
+ path: "/api/admin/pages/context-block-definitions",
81
+ auth: "basic",
82
+ },
83
+ {
84
+ id: "pages-context-block-definitions-create",
85
+ method: "POST",
86
+ path: "/api/admin/pages/context-block-definitions",
87
+ auth: "basic",
88
+ bodyExample: {
89
+ code: "blog-post-by-slug",
90
+ label: "Blog post by slug",
91
+ description: "Used by blog post page",
92
+ type: "context.db_query",
93
+ props: {
94
+ assignTo: "post",
95
+ model: "BlogPost",
96
+ op: "findOne",
97
+ filter: { slug: { $ctx: "params.slug" } },
98
+ },
99
+ },
100
+ },
101
+ {
102
+ id: "pages-context-block-definitions-get",
103
+ method: "GET",
104
+ path: "/api/admin/pages/context-block-definitions/:code",
105
+ auth: "basic",
106
+ },
107
+ {
108
+ id: "pages-context-block-definitions-update",
109
+ method: "PUT",
110
+ path: "/api/admin/pages/context-block-definitions/:code",
111
+ auth: "basic",
112
+ bodyExample: {
113
+ label: "Blog post by slug (updated)",
114
+ description: "Used by blog post page",
115
+ type: "context.db_query",
116
+ props: {
117
+ assignTo: "post",
118
+ model: "BlogPost",
119
+ op: "findOne",
120
+ filter: { slug: { $ctx: "params.slug" } },
121
+ },
122
+ },
123
+ },
124
+ {
125
+ id: "pages-context-block-definitions-delete",
126
+ method: "DELETE",
127
+ path: "/api/admin/pages/context-block-definitions/:code",
128
+ auth: "basic",
129
+ },
130
+ {
131
+ id: "pages-context-blocks-ai-generate",
132
+ method: "POST",
133
+ path: "/api/admin/pages/ai/context-blocks/generate",
134
+ auth: "basic",
135
+ bodyExample: {
136
+ prompt: "Load the published BlogPost for params.slug into vars.post. Add cache 30s.",
137
+ blockType: "context.db_query",
138
+ },
139
+ },
140
+ ],
141
+ },
73
142
  {
74
143
  id: "ejs-virtual",
75
144
  title: "EJS Virtual Codebase",
@@ -295,6 +364,57 @@ const endpointRegistry = [
295
364
  },
296
365
  ],
297
366
  },
367
+ {
368
+ id: "rate-limits",
369
+ title: "Rate Limiter",
370
+ endpoints: [
371
+ {
372
+ id: "rate-limits-list",
373
+ method: "GET",
374
+ path: "/api/admin/rate-limits",
375
+ auth: "basic",
376
+ },
377
+ {
378
+ id: "rate-limits-config-get",
379
+ method: "GET",
380
+ path: "/api/admin/rate-limits/config",
381
+ auth: "basic",
382
+ },
383
+ {
384
+ id: "rate-limits-config-update",
385
+ method: "PUT",
386
+ path: "/api/admin/rate-limits/config",
387
+ auth: "basic",
388
+ bodyExample: { jsonRaw: "{\n \"version\": 1,\n \"defaults\": {},\n \"limiters\": {}\n}" },
389
+ },
390
+ {
391
+ id: "rate-limits-metrics",
392
+ method: "GET",
393
+ path: "/api/admin/rate-limits/metrics?start=2026-01-01T00:00:00.000Z&end=2026-01-02T00:00:00.000Z",
394
+ auth: "basic",
395
+ },
396
+ {
397
+ id: "rate-limits-bulk-enabled",
398
+ method: "POST",
399
+ path: "/api/admin/rate-limits/bulk-enabled",
400
+ auth: "basic",
401
+ bodyExample: { enabled: true, all: true },
402
+ },
403
+ {
404
+ id: "rate-limits-limiter-update",
405
+ method: "PUT",
406
+ path: "/api/admin/rate-limits/globalApiLimiter",
407
+ auth: "basic",
408
+ bodyExample: { override: { enabled: true, mode: "reportOnly", limit: { max: 60, windowMs: 60000 } } },
409
+ },
410
+ {
411
+ id: "rate-limits-limiter-reset",
412
+ method: "POST",
413
+ path: "/api/admin/rate-limits/globalApiLimiter/reset",
414
+ auth: "basic",
415
+ },
416
+ ],
417
+ },
298
418
  ];
299
419
 
300
420
  module.exports = endpointRegistry;
@@ -1,5 +1,12 @@
1
1
  const User = require('../models/User');
2
2
  const StripeWebhookEvent = require('../models/StripeWebhookEvent');
3
+ const Organization = require('../models/Organization');
4
+ const OrganizationMember = require('../models/OrganizationMember');
5
+ const Asset = require('../models/Asset');
6
+ const Notification = require('../models/Notification');
7
+ const Invite = require('../models/Invite');
8
+ const EmailLog = require('../models/EmailLog');
9
+ const FormSubmission = require('../models/FormSubmission');
3
10
  const asyncHandler = require('../utils/asyncHandler');
4
11
  const fs = require('fs');
5
12
  const path = require('path');
@@ -10,12 +17,29 @@ const { auditMiddleware } = require('../services/auditLogger');
10
17
 
11
18
  // Get all users
12
19
  const getUsers = asyncHandler(async (req, res) => {
13
- const users = await User.find()
14
- .select('-passwordHash')
15
- .sort({ createdAt: -1 })
16
- .limit(100);
20
+ const { limit, offset } = req.query;
21
+
22
+ const parsedLimit = Math.min(500, Math.max(1, parseInt(limit, 10) || 50));
23
+ const parsedOffset = Math.max(0, parseInt(offset, 10) || 0);
24
+
25
+ const [users, total] = await Promise.all([
26
+ User.find()
27
+ .select('-passwordHash -passwordResetToken -passwordResetExpiry')
28
+ .sort({ createdAt: -1 })
29
+ .limit(parsedLimit)
30
+ .skip(parsedOffset)
31
+ .lean(),
32
+ User.countDocuments()
33
+ ]);
17
34
 
18
- res.json(users.map(u => u.toJSON()));
35
+ res.json({
36
+ users,
37
+ pagination: {
38
+ total,
39
+ limit: parsedLimit,
40
+ offset: parsedOffset,
41
+ },
42
+ });
19
43
  });
20
44
 
21
45
  // Register new user (admin only)
@@ -353,12 +377,94 @@ const provisionCoolifyDeploy = asyncHandler(async (req, res) => {
353
377
  }
354
378
  });
355
379
 
380
+ // Delete user (admin only)
381
+ const deleteUser = asyncHandler(async (req, res) => {
382
+
383
+ const userId = req.params.id;
384
+
385
+ // 1. Validate user exists
386
+ const user = await User.findById(userId);
387
+ if (!user) {
388
+ return res.status(404).json({ error: 'User not found' });
389
+ }
390
+
391
+ // 2. Prevent self-deletion
392
+ // Note: In a real implementation, you'd get the admin ID from req.admin or similar
393
+ // For now, we'll skip this check as the basic auth doesn't provide user identity
394
+
395
+ // 3. Check if this is the last admin
396
+ const adminCount = await User.countDocuments({ role: 'admin' });
397
+ if (user.role === 'admin' && adminCount <= 1) {
398
+ return res.status(400).json({ error: 'Cannot delete the last admin user' });
399
+ }
400
+
401
+ // 4. Cleanup dependencies
402
+ await cleanupUserData(userId);
403
+
404
+ // 5. Delete user
405
+ await User.findByIdAndDelete(userId);
406
+
407
+ // 6. Log action
408
+ console.log(`Admin deleted user: ${user.email} (${userId})`);
409
+
410
+ res.json({ message: 'User deleted successfully' });
411
+ });
412
+
413
+ // Helper function to clean up user data
414
+ async function cleanupUserData(userId) {
415
+
416
+ try {
417
+ // Handle organizations owned by user
418
+ const ownedOrgs = await Organization.find({ ownerUserId: userId });
419
+ for (const org of ownedOrgs) {
420
+ // Check if organization has other members
421
+ const memberCount = await OrganizationMember.countDocuments({
422
+ orgId: org._id,
423
+ userId: { $ne: userId }
424
+ });
425
+
426
+ if (memberCount === 0) {
427
+ // Delete organization if no other members
428
+ await Organization.findByIdAndDelete(org._id);
429
+ console.log(`Deleted organization ${org.name} (${org._id}) - no other members`);
430
+ } else {
431
+ // Remove owner but keep organization
432
+ org.ownerUserId = null;
433
+ await org.save();
434
+ console.log(`Removed owner from organization ${org.name} (${org._id}) - has other members`);
435
+ }
436
+ }
437
+
438
+ // Remove from all organization memberships
439
+ await OrganizationMember.deleteMany({ userId: userId });
440
+
441
+ // Delete user's assets
442
+ await Asset.deleteMany({ ownerUserId: userId });
443
+
444
+ // Delete notifications
445
+ await Notification.deleteMany({ userId: userId });
446
+
447
+ // Clean up other references
448
+ await Invite.deleteMany({ createdByUserId: userId });
449
+ await EmailLog.deleteMany({ userId: userId });
450
+ await FormSubmission.deleteMany({ userId: userId });
451
+
452
+ // Note: We keep ActivityLog and AuditEvent for audit purposes
453
+
454
+ console.log(`Completed cleanup for user ${userId}`);
455
+ } catch (error) {
456
+ console.error('Error during user cleanup:', error);
457
+ throw error;
458
+ }
459
+ }
460
+
356
461
  module.exports = {
357
462
  getUsers,
358
463
  registerUser,
359
464
  getUser,
360
465
  updateUserSubscription,
361
466
  updateUserPassword,
467
+ deleteUser,
362
468
  reconcileUser,
363
469
  generateToken,
364
470
  getWebhookEvents,
@@ -0,0 +1,127 @@
1
+ const BlockDefinition = require('../models/BlockDefinition');
2
+
3
+ function parseBool(value, fallback) {
4
+ if (value === undefined) return fallback;
5
+ if (typeof value === 'boolean') return value;
6
+ const v = String(value).trim().toLowerCase();
7
+ if (v === 'true' || v === '1' || v === 'yes') return true;
8
+ if (v === 'false' || v === '0' || v === 'no') return false;
9
+ return fallback;
10
+ }
11
+
12
+ function normalizeCode(code) {
13
+ return String(code || '').trim().toLowerCase();
14
+ }
15
+
16
+ function normalizeFields(fields) {
17
+ if (!fields) return {};
18
+ if (typeof fields !== 'object' || Array.isArray(fields)) return null;
19
+ return fields;
20
+ }
21
+
22
+ exports.list = async (req, res) => {
23
+ try {
24
+ const onlyActive = parseBool(req.query?.active, null);
25
+ const filter = {};
26
+ if (onlyActive !== null) filter.isActive = onlyActive;
27
+
28
+ const items = await BlockDefinition.find(filter).sort({ updatedAt: -1 }).lean();
29
+ return res.json({ items });
30
+ } catch (error) {
31
+ console.error('[adminBlockDefinitions] list error:', error);
32
+ return res.status(500).json({ error: 'Failed to list block definitions' });
33
+ }
34
+ };
35
+
36
+ exports.create = async (req, res) => {
37
+ try {
38
+ const code = normalizeCode(req.body?.code);
39
+ const label = String(req.body?.label || '').trim();
40
+ if (!code) return res.status(400).json({ error: 'code is required' });
41
+ if (!label) return res.status(400).json({ error: 'label is required' });
42
+
43
+ const fields = normalizeFields(req.body?.fields);
44
+ if (fields === null) return res.status(400).json({ error: 'fields must be an object' });
45
+
46
+ const doc = await BlockDefinition.create({
47
+ code,
48
+ label,
49
+ description: String(req.body?.description || ''),
50
+ fields: fields || {},
51
+ version: Number(req.body?.version || 1) || 1,
52
+ isActive: parseBool(req.body?.isActive, true),
53
+ });
54
+
55
+ return res.status(201).json({ item: doc.toObject() });
56
+ } catch (error) {
57
+ console.error('[adminBlockDefinitions] create error:', error);
58
+ if (error?.name === 'ValidationError') return res.status(400).json({ error: error.message });
59
+ if (error?.code === 11000) return res.status(409).json({ error: 'Block already exists' });
60
+ return res.status(500).json({ error: 'Failed to create block definition' });
61
+ }
62
+ };
63
+
64
+ exports.get = async (req, res) => {
65
+ try {
66
+ const code = normalizeCode(req.params?.code);
67
+ const item = await BlockDefinition.findOne({ code }).lean();
68
+ if (!item) return res.status(404).json({ error: 'Block not found' });
69
+ return res.json({ item });
70
+ } catch (error) {
71
+ console.error('[adminBlockDefinitions] get error:', error);
72
+ return res.status(500).json({ error: 'Failed to load block definition' });
73
+ }
74
+ };
75
+
76
+ exports.update = async (req, res) => {
77
+ try {
78
+ const code = normalizeCode(req.params?.code);
79
+ const doc = await BlockDefinition.findOne({ code });
80
+ if (!doc) return res.status(404).json({ error: 'Block not found' });
81
+
82
+ if (req.body?.label !== undefined) {
83
+ const label = String(req.body.label || '').trim();
84
+ if (!label) return res.status(400).json({ error: 'label is required' });
85
+ doc.label = label;
86
+ }
87
+
88
+ if (req.body?.description !== undefined) doc.description = String(req.body.description || '');
89
+
90
+ if (req.body?.fields !== undefined) {
91
+ const fields = normalizeFields(req.body.fields);
92
+ if (fields === null) return res.status(400).json({ error: 'fields must be an object' });
93
+ doc.fields = fields;
94
+ }
95
+
96
+ if (req.body?.version !== undefined) {
97
+ const v = Number(req.body.version);
98
+ if (!Number.isFinite(v) || v < 1) return res.status(400).json({ error: 'version must be a positive number' });
99
+ doc.version = v;
100
+ } else {
101
+ doc.version = Number(doc.version || 1) + 1;
102
+ }
103
+
104
+ if (req.body?.isActive !== undefined) doc.isActive = Boolean(req.body.isActive);
105
+
106
+ await doc.save();
107
+ return res.json({ item: doc.toObject() });
108
+ } catch (error) {
109
+ console.error('[adminBlockDefinitions] update error:', error);
110
+ if (error?.name === 'ValidationError') return res.status(400).json({ error: error.message });
111
+ return res.status(500).json({ error: 'Failed to update block definition' });
112
+ }
113
+ };
114
+
115
+ exports.remove = async (req, res) => {
116
+ try {
117
+ const code = normalizeCode(req.params?.code);
118
+ const doc = await BlockDefinition.findOne({ code });
119
+ if (!doc) return res.status(404).json({ error: 'Block not found' });
120
+
121
+ await BlockDefinition.deleteOne({ _id: doc._id });
122
+ return res.json({ success: true });
123
+ } catch (error) {
124
+ console.error('[adminBlockDefinitions] remove error:', error);
125
+ return res.status(500).json({ error: 'Failed to delete block definition' });
126
+ }
127
+ };
@@ -0,0 +1,54 @@
1
+ const {
2
+ generateBlockDefinition,
3
+ proposeBlockDefinitionEdit,
4
+ } = require('../services/blockDefinitionsAi.service');
5
+
6
+ const { getBasicAuthActor } = require('../services/audit.service');
7
+
8
+ function handleError(res, err) {
9
+ const code = err && err.code;
10
+ if (code === 'VALIDATION') return res.status(400).json({ error: err.message });
11
+ if (code === 'NOT_FOUND') return res.status(404).json({ error: err.message });
12
+ if (code === 'AI_INVALID') return res.status(500).json({ error: err.message });
13
+ return res.status(500).json({ error: err.message || 'Operation failed' });
14
+ }
15
+
16
+ exports.generate = async (req, res) => {
17
+ try {
18
+ const actor = getBasicAuthActor(req);
19
+ const { prompt, providerKey, model } = req.body || {};
20
+
21
+ const result = await generateBlockDefinition({
22
+ prompt,
23
+ providerKey,
24
+ model,
25
+ actor,
26
+ });
27
+
28
+ return res.json(result);
29
+ } catch (err) {
30
+ console.error('[adminBlockDefinitionsAi] generate error', err);
31
+ return handleError(res, err);
32
+ }
33
+ };
34
+
35
+ exports.propose = async (req, res) => {
36
+ try {
37
+ const actor = getBasicAuthActor(req);
38
+ const { code } = req.params;
39
+ const { prompt, providerKey, model } = req.body || {};
40
+
41
+ const result = await proposeBlockDefinitionEdit({
42
+ code,
43
+ prompt,
44
+ providerKey,
45
+ model,
46
+ actor,
47
+ });
48
+
49
+ return res.json(result);
50
+ } catch (err) {
51
+ console.error('[adminBlockDefinitionsAi] propose error', err);
52
+ return handleError(res, err);
53
+ }
54
+ };