@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
@@ -0,0 +1,247 @@
1
+ const BlockDefinition = require('../models/BlockDefinition');
2
+ const llmService = require('./llm.service');
3
+ const { resolveLlmProviderModel } = require('./llmDefaults.service');
4
+ const { createAuditEvent } = require('./audit.service');
5
+
6
+ function normalizeCode(code) {
7
+ return String(code || '').trim().toLowerCase();
8
+ }
9
+
10
+ function parseJsonFromModelOutput(raw) {
11
+ const text = String(raw || '').trim();
12
+
13
+ // Try strict JSON first
14
+ try {
15
+ return JSON.parse(text);
16
+ } catch (_) {
17
+ // Try to extract a JSON object from a fenced block
18
+ const m = text.match(/```json\s*([\s\S]*?)\s*```/i) || text.match(/```\s*([\s\S]*?)\s*```/i);
19
+ if (m) {
20
+ return JSON.parse(String(m[1] || '').trim());
21
+ }
22
+
23
+ // Try first { ... } block
24
+ const idx = text.indexOf('{');
25
+ const last = text.lastIndexOf('}');
26
+ if (idx !== -1 && last !== -1 && last > idx) {
27
+ return JSON.parse(text.slice(idx, last + 1));
28
+ }
29
+
30
+ const err = new Error('AI response was not valid JSON');
31
+ err.code = 'AI_INVALID';
32
+ throw err;
33
+ }
34
+ }
35
+
36
+ function validateProposalShape(obj) {
37
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
38
+ const err = new Error('AI proposal must be a JSON object');
39
+ err.code = 'AI_INVALID';
40
+ throw err;
41
+ }
42
+
43
+ const code = normalizeCode(obj.code);
44
+ const label = String(obj.label || '').trim();
45
+
46
+ if (!code) {
47
+ const err = new Error('AI proposal missing required field: code');
48
+ err.code = 'AI_INVALID';
49
+ throw err;
50
+ }
51
+
52
+ if (!label) {
53
+ const err = new Error('AI proposal missing required field: label');
54
+ err.code = 'AI_INVALID';
55
+ throw err;
56
+ }
57
+
58
+ const fields = obj.fields;
59
+ if (fields !== undefined && (fields === null || typeof fields !== 'object' || Array.isArray(fields))) {
60
+ const err = new Error('AI proposal fields must be an object');
61
+ err.code = 'AI_INVALID';
62
+ throw err;
63
+ }
64
+
65
+ return {
66
+ code,
67
+ label,
68
+ description: String(obj.description || ''),
69
+ fields: fields && typeof fields === 'object' ? fields : {},
70
+ };
71
+ }
72
+
73
+ async function resolveLlmDefaults({ systemKey, providerKey, model }) {
74
+ return resolveLlmProviderModel({ systemKey, providerKey, model });
75
+ }
76
+
77
+ function buildSystemPrompt() {
78
+ return [
79
+ 'You are an assistant that outputs a single JSON object describing a Page Builder block definition.',
80
+ 'Return ONLY JSON. No markdown, no extra keys, no explanation.',
81
+ '',
82
+ 'The JSON must have shape:',
83
+ '{',
84
+ ' "code": "string",',
85
+ ' "label": "string",',
86
+ ' "description": "string",',
87
+ ' "fields": {',
88
+ ' "fieldName": {',
89
+ ' "type": "string|html|boolean|number|select|json",',
90
+ ' "label": "string",',
91
+ ' "options": ["..."] (only for select),',
92
+ ' "example": <any JSON value> (only for json)',
93
+ ' }',
94
+ ' }',
95
+ '}',
96
+ ].join('\n');
97
+ }
98
+
99
+ async function generateBlockDefinition({ prompt, providerKey, model, actor }) {
100
+ const instruction = String(prompt || '').trim();
101
+ if (!instruction) {
102
+ const err = new Error('prompt is required');
103
+ err.code = 'VALIDATION';
104
+ throw err;
105
+ }
106
+
107
+ const llmDefaults = await resolveLlmDefaults({
108
+ systemKey: 'pageBuilder.blocks.generate',
109
+ providerKey,
110
+ model,
111
+ });
112
+
113
+ const result = await llmService.callAdhoc(
114
+ {
115
+ providerKey: llmDefaults.providerKey,
116
+ model: llmDefaults.model,
117
+ messages: [
118
+ { role: 'system', content: buildSystemPrompt() },
119
+ { role: 'user', content: instruction },
120
+ ],
121
+ promptKeyForAudit: 'pageBuilder.blocks.ai.generate',
122
+ },
123
+ { temperature: 0.3 },
124
+ );
125
+
126
+ const raw = String(result.content || '');
127
+ const json = parseJsonFromModelOutput(raw);
128
+ const proposal = validateProposalShape(json);
129
+
130
+ await createAuditEvent({
131
+ ...(actor || { actorType: 'system', actorId: null }),
132
+ action: 'pageBuilder.blocks.ai.generate',
133
+ entityType: 'BlockDefinition',
134
+ entityId: proposal.code,
135
+ before: null,
136
+ after: { code: proposal.code },
137
+ meta: {
138
+ providerKey: llmDefaults.providerKey,
139
+ model: llmDefaults.model,
140
+ responsePreview: raw.slice(0, 4000),
141
+ },
142
+ });
143
+
144
+ return {
145
+ proposal,
146
+ providerKey: llmDefaults.providerKey,
147
+ model: llmDefaults.model,
148
+ };
149
+ }
150
+
151
+ async function proposeBlockDefinitionEdit({ code, prompt, providerKey, model, actor }) {
152
+ const blockCode = normalizeCode(code);
153
+ if (!blockCode) {
154
+ const err = new Error('code is required');
155
+ err.code = 'VALIDATION';
156
+ throw err;
157
+ }
158
+
159
+ const instruction = String(prompt || '').trim();
160
+ if (!instruction) {
161
+ const err = new Error('prompt is required');
162
+ err.code = 'VALIDATION';
163
+ throw err;
164
+ }
165
+
166
+ const doc = await BlockDefinition.findOne({ code: blockCode });
167
+ if (!doc) {
168
+ const err = new Error('Block not found');
169
+ err.code = 'NOT_FOUND';
170
+ throw err;
171
+ }
172
+
173
+ const current = doc.toObject();
174
+
175
+ const llmDefaults = await resolveLlmDefaults({
176
+ systemKey: 'pageBuilder.blocks.propose',
177
+ providerKey,
178
+ model,
179
+ });
180
+
181
+ const messages = [
182
+ { role: 'system', content: buildSystemPrompt() },
183
+ { role: 'user', content: `Instruction:\n${instruction}` },
184
+ {
185
+ role: 'user',
186
+ content: [
187
+ 'Current block definition:',
188
+ JSON.stringify(
189
+ {
190
+ code: current.code,
191
+ label: current.label,
192
+ description: current.description,
193
+ fields: current.fields || {},
194
+ },
195
+ null,
196
+ 2,
197
+ ),
198
+ ].join('\n'),
199
+ },
200
+ ];
201
+
202
+ const result = await llmService.callAdhoc(
203
+ {
204
+ providerKey: llmDefaults.providerKey,
205
+ model: llmDefaults.model,
206
+ messages,
207
+ promptKeyForAudit: 'pageBuilder.blocks.ai.propose',
208
+ },
209
+ { temperature: 0.3 },
210
+ );
211
+
212
+ const raw = String(result.content || '');
213
+ const json = parseJsonFromModelOutput(raw);
214
+ const proposal = validateProposalShape(json);
215
+
216
+ if (proposal.code !== current.code) {
217
+ const err = new Error('AI proposal code must match the requested block code');
218
+ err.code = 'AI_INVALID';
219
+ throw err;
220
+ }
221
+
222
+ await createAuditEvent({
223
+ ...(actor || { actorType: 'system', actorId: null }),
224
+ action: 'pageBuilder.blocks.ai.propose',
225
+ entityType: 'BlockDefinition',
226
+ entityId: current.code,
227
+ before: { code: current.code, version: current.version },
228
+ after: { code: current.code, version: current.version },
229
+ meta: {
230
+ providerKey: llmDefaults.providerKey,
231
+ model: llmDefaults.model,
232
+ responsePreview: raw.slice(0, 4000),
233
+ },
234
+ });
235
+
236
+ return {
237
+ block: { code: current.code, version: current.version },
238
+ proposal,
239
+ providerKey: llmDefaults.providerKey,
240
+ model: llmDefaults.model,
241
+ };
242
+ }
243
+
244
+ module.exports = {
245
+ generateBlockDefinition,
246
+ proposeBlockDefinitionEdit,
247
+ };
@@ -0,0 +1,99 @@
1
+ const BlogPost = require('../models/BlogPost');
2
+
3
+ function slugify(input) {
4
+ const s = String(input || '')
5
+ .trim()
6
+ .toLowerCase();
7
+ return s
8
+ .normalize('NFKD')
9
+ .replace(/[\u0300-\u036f]/g, '')
10
+ .replace(/[^a-z0-9]+/g, '-')
11
+ .replace(/^-+|-+$/g, '')
12
+ .replace(/-{2,}/g, '-');
13
+ }
14
+
15
+ function extractExcerptFromMarkdown(markdown) {
16
+ const text = String(markdown || '')
17
+ .replace(/```[\s\S]*?```/g, ' ')
18
+ .replace(/`[^`]*`/g, ' ')
19
+ .replace(/!\[[^\]]*\]\([^\)]*\)/g, ' ')
20
+ .replace(/\[[^\]]*\]\([^\)]*\)/g, ' ')
21
+ .replace(/[#>*_\-]+/g, ' ')
22
+ .replace(/\s+/g, ' ')
23
+ .trim();
24
+ return text.length > 180 ? `${text.slice(0, 177)}...` : text;
25
+ }
26
+
27
+ async function generateUniqueBlogSlug(title, { excludeId } = {}) {
28
+ const base = slugify(title);
29
+ let candidate = base || 'post';
30
+ let n = 2;
31
+
32
+ // Slug uniqueness is only enforced among non-archived posts via partial unique index.
33
+ // We still do a pre-check to avoid duplicate key errors.
34
+ while (true) {
35
+ const query = {
36
+ slug: candidate,
37
+ status: { $in: ['draft', 'scheduled', 'published'] },
38
+ };
39
+ if (excludeId) query._id = { $ne: excludeId };
40
+
41
+ const existing = await BlogPost.findOne(query).select('_id').lean();
42
+ if (!existing) return candidate;
43
+
44
+ candidate = `${base || 'post'}-${n}`;
45
+ n += 1;
46
+ }
47
+ }
48
+
49
+ function normalizeStringArray(value, { maxItems = 25, maxItemLength = 50 } = {}) {
50
+ const arr = Array.isArray(value)
51
+ ? value
52
+ : value === undefined || value === null
53
+ ? []
54
+ : [value];
55
+
56
+ const cleaned = [];
57
+ const seen = new Set();
58
+ for (const item of arr) {
59
+ const s = String(item || '').trim();
60
+ if (!s) continue;
61
+ const capped = s.length > maxItemLength ? s.slice(0, maxItemLength) : s;
62
+ const key = capped.toLowerCase();
63
+ if (seen.has(key)) continue;
64
+ seen.add(key);
65
+ cleaned.push(capped);
66
+ if (cleaned.length >= maxItems) break;
67
+ }
68
+ return cleaned;
69
+ }
70
+
71
+ function normalizeTags(value) {
72
+ if (value === undefined || value === null) return [];
73
+ if (Array.isArray(value)) {
74
+ return normalizeStringArray(value, { maxItems: 25, maxItemLength: 40 });
75
+ }
76
+
77
+ // accept comma-separated string
78
+ const s = String(value || '');
79
+ const parts = s
80
+ .split(',')
81
+ .map((t) => String(t || '').trim())
82
+ .filter(Boolean);
83
+ return normalizeStringArray(parts, { maxItems: 25, maxItemLength: 40 });
84
+ }
85
+
86
+ function parsePagination({ page, limit, maxLimit = 100, defaultLimit = 20 } = {}) {
87
+ const p = Math.max(1, Number(page || 1) || 1);
88
+ const l = Math.min(maxLimit, Math.max(1, Number(limit || defaultLimit) || defaultLimit));
89
+ const skip = (p - 1) * l;
90
+ return { page: p, limit: l, skip };
91
+ }
92
+
93
+ module.exports = {
94
+ slugify,
95
+ extractExcerptFromMarkdown,
96
+ generateUniqueBlogSlug,
97
+ normalizeTags,
98
+ parsePagination,
99
+ };