@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,224 @@
1
+ const llmService = require('../services/llm.service');
2
+
3
+ function safeJsonExtract(raw) {
4
+ const text = String(raw || '').trim();
5
+ if (!text) return null;
6
+
7
+ let cleaned = text;
8
+ if (cleaned.startsWith('```')) {
9
+ cleaned = cleaned.replace(/^```[a-zA-Z]*\s*/m, '').replace(/```\s*$/m, '').trim();
10
+ }
11
+
12
+ const firstObj = cleaned.indexOf('{');
13
+ const lastObj = cleaned.lastIndexOf('}');
14
+ const firstArr = cleaned.indexOf('[');
15
+ const lastArr = cleaned.lastIndexOf(']');
16
+
17
+ let candidate = cleaned;
18
+ if (firstObj !== -1 && lastObj !== -1 && lastObj > firstObj) {
19
+ candidate = cleaned.slice(firstObj, lastObj + 1);
20
+ } else if (firstArr !== -1 && lastArr !== -1 && lastArr > firstArr) {
21
+ candidate = cleaned.slice(firstArr, lastArr + 1);
22
+ }
23
+
24
+ try {
25
+ return JSON.parse(candidate);
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ exports.generateField = async (req, res) => {
32
+ try {
33
+ const { field, context, providerKey, model } = req.body || {};
34
+ const f = String(field || '').trim();
35
+ if (!f) return res.status(400).json({ error: 'field is required' });
36
+ if (!providerKey) return res.status(400).json({ error: 'providerKey is required' });
37
+
38
+ const ctx = context && typeof context === 'object' ? context : {};
39
+
40
+ const system =
41
+ 'You are an expert blog editor. You write clear, concise, SEO-friendly copy. ' +
42
+ 'Return ONLY the requested output with no extra commentary.';
43
+
44
+ const instructionsByField = {
45
+ title: 'Generate a compelling blog post title. Keep it SEO-friendly. Max 70 characters. Return only the title.',
46
+ excerpt: 'Generate a short excerpt (1-2 sentences). SEO-friendly, no markdown, no quotes. Return only the excerpt.',
47
+ category: 'Suggest a single category (2-4 words). Return only the category.',
48
+ tags: 'Generate 5-10 relevant tags. Return as a comma-separated list, lowercase where appropriate.',
49
+ seoTitle: 'Generate an SEO title. Max 60 characters. Return only the SEO title.',
50
+ seoDescription: 'Generate an SEO meta description. Max 155 characters. Return only the description.',
51
+ };
52
+
53
+ const instruction =
54
+ instructionsByField[f] || `Generate a value for field "${f}". Return only the value.`;
55
+
56
+ const result = await llmService.callAdhoc(
57
+ {
58
+ providerKey,
59
+ model,
60
+ promptKeyForAudit: `blog.ai.generate.${f}`,
61
+ messages: [
62
+ { role: 'system', content: system },
63
+ { role: 'user', content: instruction + '\n\nContext (JSON):\n' + JSON.stringify(ctx, null, 2) },
64
+ ],
65
+ },
66
+ { temperature: 0.6, max_tokens: 256 },
67
+ );
68
+
69
+ res.json({ value: String(result?.content || '').trim(), usage: result?.usage || null });
70
+ } catch (error) {
71
+ console.error('[blog-ai] generate-field error', error);
72
+ res.status(500).json({ error: error.message || 'Failed to generate field' });
73
+ }
74
+ };
75
+
76
+ exports.generateAll = async (req, res) => {
77
+ try {
78
+ const { context, providerKey, model } = req.body || {};
79
+ if (!providerKey) return res.status(400).json({ error: 'providerKey is required' });
80
+
81
+ const ctx = context && typeof context === 'object' ? context : {};
82
+
83
+ const system = 'Return ONLY a valid JSON object, no markdown fences.';
84
+
85
+ const user =
86
+ 'Generate blog metadata fields based on the blog content and context.\n' +
87
+ 'Return a JSON object with keys: title, excerpt, category, tags, seoTitle, seoDescription.\n' +
88
+ 'tags must be an array of strings.\n\n' +
89
+ 'Context (JSON):\n' +
90
+ JSON.stringify(ctx, null, 2);
91
+
92
+ const result = await llmService.callAdhoc(
93
+ {
94
+ providerKey,
95
+ model,
96
+ promptKeyForAudit: 'blog.ai.generate_all',
97
+ messages: [
98
+ { role: 'system', content: system },
99
+ { role: 'user', content: user },
100
+ ],
101
+ },
102
+ { temperature: 0.6, max_tokens: 600 },
103
+ );
104
+
105
+ const parsed = safeJsonExtract(result?.content) || null;
106
+ if (!parsed || typeof parsed !== 'object') {
107
+ return res.status(500).json({ error: 'AI returned invalid JSON', raw: String(result?.content || '') });
108
+ }
109
+
110
+ if (typeof parsed.tags === 'string') {
111
+ parsed.tags = parsed.tags
112
+ .split(',')
113
+ .map((t) => String(t || '').trim())
114
+ .filter(Boolean);
115
+ }
116
+ if (parsed.tags !== undefined && !Array.isArray(parsed.tags)) {
117
+ parsed.tags = [];
118
+ }
119
+
120
+ res.json({ values: parsed, usage: result?.usage || null });
121
+ } catch (error) {
122
+ console.error('[blog-ai] generate-all error', error);
123
+ res.status(500).json({ error: error.message || 'Failed to generate all' });
124
+ }
125
+ };
126
+
127
+ exports.formatMarkdown = async (req, res) => {
128
+ try {
129
+ const { text, context, providerKey, model } = req.body || {};
130
+ if (!providerKey) return res.status(400).json({ error: 'providerKey is required' });
131
+
132
+ const input = String(text || '');
133
+ if (!input.trim()) return res.status(400).json({ error: 'text is required' });
134
+
135
+ const ctx = context && typeof context === 'object' ? context : {};
136
+
137
+ const system = 'You are an expert blog editor. Output only markdown. Do not wrap in code fences.';
138
+
139
+ const user =
140
+ 'Convert the following content into clean, well-structured markdown with headings, lists, and short paragraphs.\n' +
141
+ 'Preserve meaning, improve readability.\n\n' +
142
+ 'Context (JSON):\n' +
143
+ JSON.stringify(ctx, null, 2) +
144
+ '\n\nInput:\n' +
145
+ input;
146
+
147
+ const result = await llmService.callAdhoc(
148
+ {
149
+ providerKey,
150
+ model,
151
+ promptKeyForAudit: 'blog.ai.format_markdown',
152
+ messages: [
153
+ { role: 'system', content: system },
154
+ { role: 'user', content: user },
155
+ ],
156
+ },
157
+ { temperature: 0.4, max_tokens: 2000 },
158
+ );
159
+
160
+ res.json({ markdown: String(result?.content || ''), usage: result?.usage || null });
161
+ } catch (error) {
162
+ console.error('[blog-ai] format-markdown error', error);
163
+ res.status(500).json({ error: error.message || 'Failed to format markdown' });
164
+ }
165
+ };
166
+
167
+ exports.refineMarkdown = async (req, res) => {
168
+ try {
169
+ const { markdown, instruction, selectionStart, selectionEnd, context, providerKey, model } = req.body || {};
170
+ if (!providerKey) return res.status(400).json({ error: 'providerKey is required' });
171
+
172
+ const md = String(markdown || '');
173
+ if (!md.trim()) return res.status(400).json({ error: 'markdown is required' });
174
+
175
+ const instr = String(instruction || '').trim();
176
+ if (!instr) return res.status(400).json({ error: 'instruction is required' });
177
+
178
+ const ctx = context && typeof context === 'object' ? context : {};
179
+
180
+ const start = Number(selectionStart);
181
+ const end = Number(selectionEnd);
182
+ const hasSelection = Number.isFinite(start) && Number.isFinite(end) && end > start;
183
+ const selected = hasSelection ? md.slice(start, end) : '';
184
+
185
+ const system = 'You are an expert blog editor. Output only markdown. Do not wrap in code fences.';
186
+
187
+ const user = hasSelection
188
+ ? 'Refine ONLY the selected markdown according to the instruction. Return ONLY the replacement markdown for the selection.\n\n' +
189
+ 'Instruction:\n' + instr +
190
+ '\n\nContext (JSON):\n' + JSON.stringify(ctx, null, 2) +
191
+ '\n\nSelected markdown:\n' + selected
192
+ : 'Refine the full markdown according to the instruction. Return ONLY the full updated markdown.\n\n' +
193
+ 'Instruction:\n' + instr +
194
+ '\n\nContext (JSON):\n' + JSON.stringify(ctx, null, 2) +
195
+ '\n\nMarkdown:\n' + md;
196
+
197
+ const result = await llmService.callAdhoc(
198
+ {
199
+ providerKey,
200
+ model,
201
+ promptKeyForAudit: hasSelection ? 'blog.ai.refine.selection' : 'blog.ai.refine.full',
202
+ messages: [
203
+ { role: 'system', content: system },
204
+ { role: 'user', content: user },
205
+ ],
206
+ },
207
+ { temperature: 0.5, max_tokens: 2000 },
208
+ );
209
+
210
+ const replacement = String(result?.content || '');
211
+ const nextMarkdown = hasSelection ? md.slice(0, start) + replacement + md.slice(end) : replacement;
212
+
213
+ res.json({
214
+ markdown: nextMarkdown,
215
+ replaced: hasSelection,
216
+ selectionStart: hasSelection ? start : null,
217
+ selectionEnd: hasSelection ? start + replacement.length : null,
218
+ usage: result?.usage || null,
219
+ });
220
+ } catch (error) {
221
+ console.error('[blog-ai] refine-markdown error', error);
222
+ res.status(500).json({ error: error.message || 'Failed to refine markdown' });
223
+ }
224
+ };
@@ -0,0 +1,141 @@
1
+ const blogAutomationService = require('../services/blogAutomation.service');
2
+
3
+ exports.getConfig = async (req, res) => {
4
+ res.status(400).json({
5
+ error: 'Deprecated endpoint. Use /api/admin/blog-automation/configs instead.',
6
+ });
7
+ };
8
+
9
+ exports.saveConfig = async (req, res) => {
10
+ res.status(400).json({
11
+ error: 'Deprecated endpoint. Use /api/admin/blog-automation/configs instead.',
12
+ });
13
+ };
14
+
15
+ exports.listConfigs = async (req, res) => {
16
+ try {
17
+ const configs = await blogAutomationService.getBlogAutomationConfigs();
18
+ res.json({ configs });
19
+ } catch (error) {
20
+ console.error('Error listing blog automation configs:', error);
21
+ res.status(500).json({ error: 'Failed to load configs' });
22
+ }
23
+ };
24
+
25
+ exports.previewPromptsByConfigId = async (req, res) => {
26
+ try {
27
+ const configId = String(req.params.id || '').trim();
28
+ if (!configId) return res.status(400).json({ error: 'configId is required' });
29
+ const prompts = await blogAutomationService.previewPromptsByConfigId(configId);
30
+ res.json({ prompts });
31
+ } catch (error) {
32
+ const status = Number(error?.statusCode || 500);
33
+ if (status !== 500) return res.status(status).json({ error: String(error?.message || 'Invalid request') });
34
+ console.error('Error previewing blog automation prompts:', error);
35
+ res.status(500).json({ error: 'Failed to preview prompts' });
36
+ }
37
+ };
38
+
39
+ exports.getConfigById = async (req, res) => {
40
+ try {
41
+ const cfg = await blogAutomationService.getBlogAutomationConfigById(req.params.id);
42
+ res.json({ config: cfg });
43
+ } catch (error) {
44
+ const status = Number(error?.statusCode || 500);
45
+ if (status !== 500) return res.status(status).json({ error: String(error?.message || 'Invalid request') });
46
+ console.error('Error getting blog automation config:', error);
47
+ res.status(500).json({ error: 'Failed to load config' });
48
+ }
49
+ };
50
+
51
+ exports.createConfig = async (req, res) => {
52
+ try {
53
+ const name = String(req.body?.name || '').trim();
54
+ const created = await blogAutomationService.createAutomationConfig({ name });
55
+ const blogCronsBootstrap = require('../services/blogCronsBootstrap.service');
56
+ await blogCronsBootstrap.bootstrap();
57
+ res.json({ config: created });
58
+ } catch (error) {
59
+ console.error('Error creating blog automation config:', error);
60
+ res.status(500).json({ error: 'Failed to create config' });
61
+ }
62
+ };
63
+
64
+ exports.updateConfigById = async (req, res) => {
65
+ try {
66
+ const config = req.body?.config;
67
+ if (!config || typeof config !== 'object') {
68
+ return res.status(400).json({ error: 'config object is required' });
69
+ }
70
+ const updated = await blogAutomationService.updateAutomationConfig(req.params.id, config);
71
+ const blogCronsBootstrap = require('../services/blogCronsBootstrap.service');
72
+ await blogCronsBootstrap.bootstrap();
73
+ res.json({ config: updated });
74
+ } catch (error) {
75
+ const status = Number(error?.statusCode || 500);
76
+ if (status !== 500) return res.status(status).json({ error: String(error?.message || 'Invalid request') });
77
+ console.error('Error updating blog automation config:', error);
78
+ res.status(500).json({ error: 'Failed to update config' });
79
+ }
80
+ };
81
+
82
+ exports.deleteConfigById = async (req, res) => {
83
+ try {
84
+ await blogAutomationService.deleteAutomationConfig(req.params.id);
85
+ const blogCronsBootstrap = require('../services/blogCronsBootstrap.service');
86
+ await blogCronsBootstrap.bootstrap();
87
+ res.json({ success: true });
88
+ } catch (error) {
89
+ const status = Number(error?.statusCode || 500);
90
+ if (status !== 500) return res.status(status).json({ error: String(error?.message || 'Invalid request') });
91
+ console.error('Error deleting blog automation config:', error);
92
+ res.status(500).json({ error: 'Failed to delete config' });
93
+ }
94
+ };
95
+
96
+ exports.getStyleGuide = async (req, res) => {
97
+ try {
98
+ const styleGuide = await blogAutomationService.getBlogAutomationStyleGuide();
99
+ res.json({ styleGuide });
100
+ } catch (error) {
101
+ console.error('Error getting blog automation style guide:', error);
102
+ res.status(500).json({ error: 'Failed to load style guide' });
103
+ }
104
+ };
105
+
106
+ exports.saveStyleGuide = async (req, res) => {
107
+ try {
108
+ const styleGuide = String(req.body?.styleGuide ?? '');
109
+ await blogAutomationService.updateStyleGuide(styleGuide);
110
+ res.json({ success: true });
111
+ } catch (error) {
112
+ console.error('Error saving blog automation style guide:', error);
113
+ res.status(500).json({ error: 'Failed to save style guide' });
114
+ }
115
+ };
116
+
117
+ exports.listRuns = async (req, res) => {
118
+ try {
119
+ const limit = Math.min(100, Math.max(1, Number(req.query.limit || 30) || 30));
120
+ const configId = String(req.query.configId || '').trim();
121
+ const runs = await blogAutomationService.listRuns({ limit, configId });
122
+ res.json({ runs });
123
+ } catch (error) {
124
+ console.error('Error listing blog automation runs:', error);
125
+ res.status(500).json({ error: 'Failed to load runs' });
126
+ }
127
+ };
128
+
129
+ exports.runNow = async (req, res) => {
130
+ try {
131
+ const configId = String(req.body?.configId || '').trim();
132
+ if (!configId) return res.status(400).json({ error: 'configId is required' });
133
+ const run = await blogAutomationService.runBlogAutomation({ trigger: 'manual', configId });
134
+ res.json({ run });
135
+ } catch (error) {
136
+ const status = Number(error?.statusCode || 500);
137
+ if (status !== 500) return res.status(status).json({ error: String(error?.message || 'Invalid request') });
138
+ console.error('Error starting blog automation run:', error);
139
+ res.status(500).json({ error: 'Failed to run automation' });
140
+ }
141
+ };
@@ -0,0 +1,26 @@
1
+ const blogAutomationService = require('../services/blogAutomation.service');
2
+ const blogPublishingService = require('../services/blogPublishing.service');
3
+
4
+ exports.runAutomation = async (req, res) => {
5
+ try {
6
+ const trigger = req.body?.trigger === 'scheduled' ? 'scheduled' : 'manual';
7
+ const configId = String(req.body?.configId || '').trim();
8
+ if (!configId) return res.status(400).json({ error: 'configId is required' });
9
+ const run = await blogAutomationService.runBlogAutomation({ trigger, configId });
10
+ res.json({ run });
11
+ } catch (error) {
12
+ console.error('internal automation run error:', error);
13
+ res.status(500).json({ error: 'Failed to run automation' });
14
+ }
15
+ };
16
+
17
+ exports.publishScheduled = async (req, res) => {
18
+ try {
19
+ const limit = req.body?.limit;
20
+ const result = await blogPublishingService.publishScheduledDue({ limit });
21
+ res.json({ result });
22
+ } catch (error) {
23
+ console.error('internal publish scheduled error:', error);
24
+ res.status(500).json({ error: 'Failed to publish scheduled posts' });
25
+ }
26
+ };
@@ -0,0 +1,89 @@
1
+ const BlogPost = require('../models/BlogPost');
2
+ const { parsePagination } = require('../services/blog.service');
3
+
4
+ exports.listPublished = async (req, res) => {
5
+ try {
6
+ const { page, limit, skip } = parsePagination({
7
+ page: req.query.page,
8
+ limit: req.query.limit,
9
+ maxLimit: 100,
10
+ defaultLimit: 20,
11
+ });
12
+
13
+ const filter = { status: 'published' };
14
+
15
+ const tag = String(req.query.tag || '').trim();
16
+ if (tag) {
17
+ filter.tags = tag;
18
+ }
19
+
20
+ const category = String(req.query.category || '').trim();
21
+ if (category) {
22
+ filter.category = category;
23
+ }
24
+
25
+ const q = String(req.query.q || '').trim();
26
+ if (q) {
27
+ const re = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
28
+ filter.$or = [{ title: re }, { excerpt: re }];
29
+ }
30
+
31
+ const [items, total] = await Promise.all([
32
+ BlogPost.find(filter)
33
+ .sort({ publishedAt: -1, createdAt: -1 })
34
+ .select('title slug excerpt coverImageUrl category tags authorName publishedAt createdAt updatedAt')
35
+ .skip(skip)
36
+ .limit(limit)
37
+ .lean(),
38
+ BlogPost.countDocuments(filter),
39
+ ]);
40
+
41
+ res.json({
42
+ items,
43
+ pagination: {
44
+ page,
45
+ limit,
46
+ total,
47
+ pages: Math.ceil(total / limit),
48
+ },
49
+ });
50
+ } catch (error) {
51
+ console.error('Error listing blog posts:', error);
52
+ res.status(500).json({ error: 'Failed to list blog posts' });
53
+ }
54
+ };
55
+
56
+ exports.getPublishedBySlug = async (req, res) => {
57
+ try {
58
+ const slug = String(req.params.slug || '').trim();
59
+ if (!slug) return res.status(400).json({ error: 'slug is required' });
60
+
61
+ const post = await BlogPost.findOne({ slug, status: 'published' }).lean();
62
+ if (!post) return res.status(404).json({ error: 'Not found' });
63
+
64
+ res.json({
65
+ post: {
66
+ _id: post._id,
67
+ title: post.title,
68
+ slug: post.slug,
69
+ status: post.status,
70
+ excerpt: post.excerpt,
71
+ markdown: post.markdown,
72
+ html: post.html,
73
+ coverImageUrl: post.coverImageUrl,
74
+ category: post.category,
75
+ tags: post.tags,
76
+ authorName: post.authorName,
77
+ seoTitle: post.seoTitle,
78
+ seoDescription: post.seoDescription,
79
+ scheduledAt: post.scheduledAt,
80
+ publishedAt: post.publishedAt,
81
+ createdAt: post.createdAt,
82
+ updatedAt: post.updatedAt,
83
+ },
84
+ });
85
+ } catch (error) {
86
+ console.error('Error getting blog post:', error);
87
+ res.status(500).json({ error: 'Failed to get blog post' });
88
+ }
89
+ };
@@ -0,0 +1,190 @@
1
+ const fileManagerService = require('../services/fileManager.service');
2
+
3
+ exports.listDrives = async (req, res) => {
4
+ try {
5
+ const orgId = req.query.orgId || req.headers['x-org-id'] || null;
6
+ const payload = await fileManagerService.listDrives({ userId: req.user._id, orgId });
7
+ return res.json(payload);
8
+ } catch (error) {
9
+ const code = error?.code;
10
+ if (code === 'VALIDATION') return res.status(400).json({ error: error.message });
11
+ console.error('Error listing file manager drives:', error);
12
+ return res.status(500).json({ error: 'Failed to list drives' });
13
+ }
14
+ };
15
+
16
+ exports.listFolder = async (req, res) => {
17
+ try {
18
+ const orgId = req.query.orgId || req.body?.orgId || req.headers['x-org-id'] || null;
19
+ const driveType = req.query.driveType || req.body?.driveType;
20
+ const driveId = req.query.driveId || req.body?.driveId;
21
+ const folderPath = req.query.folderPath || req.query.path || req.body?.folderPath || '/';
22
+ const payload = await fileManagerService.listFolder({
23
+ orgId,
24
+ driveType,
25
+ driveId,
26
+ parentPath: folderPath || '/',
27
+ });
28
+ return res.json(payload);
29
+ } catch (error) {
30
+ const code = error?.code;
31
+ if (code === 'VALIDATION') return res.status(400).json({ error: error.message });
32
+ console.error('Error listing file manager folder:', error);
33
+ return res.status(500).json({ error: 'Failed to list folder' });
34
+ }
35
+ };
36
+
37
+ exports.upload = async (req, res) => {
38
+ try {
39
+ if (!req.file && !req.files?.file) {
40
+ return res.status(400).json({ error: 'No file provided' });
41
+ }
42
+
43
+ const file = req.file || req.files.file;
44
+ const buffer = file.buffer || (file.data ? file.data : null);
45
+
46
+ if (!buffer) {
47
+ return res.status(400).json({ error: 'Unable to read file buffer' });
48
+ }
49
+
50
+ const contentType = file.mimetype;
51
+ const name = req.body?.name || file.originalname || file.name;
52
+
53
+ const overwriteRaw = req.body?.overwrite ?? req.query?.overwrite;
54
+ const overwrite = overwriteRaw === 'true' || overwriteRaw === true || overwriteRaw === '1';
55
+
56
+ const orgId = req.body?.orgId ?? req.query?.orgId;
57
+ const driveType = req.body?.driveType ?? req.query?.driveType;
58
+ const driveId = req.body?.driveId ?? req.query?.driveId;
59
+ const folderPath = req.body?.folderPath ?? req.query?.folderPath;
60
+
61
+ const payload = await fileManagerService.uploadFile({
62
+ userId: req.user._id,
63
+ orgId,
64
+ driveType,
65
+ driveId,
66
+ parentPath: folderPath || '/',
67
+ name,
68
+ buffer,
69
+ contentType,
70
+ overwrite,
71
+ requestedVisibility: req.body?.visibility,
72
+ });
73
+
74
+ return res.status(201).json(payload);
75
+ } catch (error) {
76
+ const code = error?.code;
77
+ if (code === 'VALIDATION') return res.status(400).json({ error: error.message });
78
+ if (code === 'CONFLICT') return res.status(409).json({ error: error.message, ...error.details });
79
+ if (code === 'UPLOAD_REJECTED') return res.status(400).json({ error: error.message, ...error.details });
80
+
81
+ console.error('Error uploading file manager file:', error);
82
+ return res.status(500).json({ error: 'Failed to upload file' });
83
+ }
84
+ };
85
+
86
+ exports.download = async (req, res) => {
87
+ try {
88
+ const orgId = req.query.orgId || req.headers['x-org-id'] || null;
89
+ const driveType = req.query.driveType;
90
+ const driveId = req.query.driveId;
91
+
92
+ const result = await fileManagerService.downloadFile({
93
+ orgId,
94
+ driveType,
95
+ driveId,
96
+ fileId: req.params.id,
97
+ });
98
+
99
+ res.set('Content-Type', result.contentType);
100
+ if (result.asset?.originalName) {
101
+ res.set('Content-Disposition', `inline; filename="${result.asset.originalName}"`);
102
+ }
103
+ res.send(result.body);
104
+ } catch (error) {
105
+ const code = error?.code;
106
+ if (code === 'VALIDATION') return res.status(400).json({ error: error.message });
107
+ if (code === 'NOT_FOUND') return res.status(404).json({ error: 'Not found' });
108
+
109
+ console.error('Error downloading file manager file:', error);
110
+ return res.status(500).json({ error: 'Failed to download file' });
111
+ }
112
+ };
113
+
114
+ exports.delete = async (req, res) => {
115
+ try {
116
+ const orgId = req.query.orgId || req.body?.orgId || req.headers['x-org-id'] || null;
117
+ const driveType = req.query.driveType || req.body?.driveType;
118
+ const driveId = req.query.driveId || req.body?.driveId;
119
+
120
+ const payload = await fileManagerService.deleteFile({
121
+ orgId,
122
+ driveType,
123
+ driveId,
124
+ fileId: req.params.id,
125
+ });
126
+
127
+ return res.json(payload);
128
+ } catch (error) {
129
+ const code = error?.code;
130
+ if (code === 'VALIDATION') return res.status(400).json({ error: error.message });
131
+ if (code === 'NOT_FOUND') return res.status(404).json({ error: 'Not found' });
132
+
133
+ console.error('Error deleting file manager file:', error);
134
+ return res.status(500).json({ error: 'Failed to delete file' });
135
+ }
136
+ };
137
+
138
+ exports.update = async (req, res) => {
139
+ try {
140
+ const orgId = req.body?.orgId || req.query.orgId || req.headers['x-org-id'] || null;
141
+ const driveType = req.body?.driveType || req.query.driveType;
142
+ const driveId = req.body?.driveId || req.query.driveId;
143
+
144
+ const payload = await fileManagerService.updateFile({
145
+ orgId,
146
+ driveType,
147
+ driveId,
148
+ fileId: req.params.id,
149
+ name: req.body?.name,
150
+ parentPath: req.body?.folderPath,
151
+ });
152
+
153
+ return res.json(payload);
154
+ } catch (error) {
155
+ const code = error?.code;
156
+ if (code === 'VALIDATION') return res.status(400).json({ error: error.message });
157
+ if (code === 'CONFLICT') return res.status(409).json({ error: error.message, ...error.details });
158
+ if (code === 'NOT_FOUND') return res.status(404).json({ error: 'Not found' });
159
+
160
+ console.error('Error updating file manager file:', error);
161
+ return res.status(500).json({ error: 'Failed to update file' });
162
+ }
163
+ };
164
+
165
+ exports.setShare = async (req, res) => {
166
+ try {
167
+ const orgId = req.body?.orgId || req.query.orgId || req.headers['x-org-id'] || null;
168
+ const driveType = req.body?.driveType || req.query.driveType;
169
+ const driveId = req.body?.driveId || req.query.driveId;
170
+ const enabled = req.body?.enabled === true || req.body?.enabled === 'true' || req.body?.enabled === '1';
171
+
172
+ const payload = await fileManagerService.setShare({
173
+ orgId,
174
+ driveType,
175
+ driveId,
176
+ fileId: req.params.id,
177
+ enabled,
178
+ });
179
+
180
+ return res.json(payload);
181
+ } catch (error) {
182
+ const code = error?.code;
183
+ if (code === 'VALIDATION') return res.status(400).json({ error: error.message });
184
+ if (code === 'NOT_FOUND') return res.status(404).json({ error: 'Not found' });
185
+ if (code === 'VISIBILITY_ENFORCED') return res.status(400).json({ error: error.message });
186
+
187
+ console.error('Error toggling file manager share:', error);
188
+ return res.status(500).json({ error: 'Failed to update share status' });
189
+ }
190
+ };
@@ -0,0 +1,23 @@
1
+ const fileManagerStoragePolicyService = require('../services/fileManagerStoragePolicy.service');
2
+
3
+ exports.getStoragePolicy = async (req, res) => {
4
+ try {
5
+ const orgId = req.query.orgId || req.headers['x-org-id'] || null;
6
+ const driveType = req.query.driveType;
7
+ const driveId = req.query.driveId;
8
+
9
+ const payload = await fileManagerStoragePolicyService.getEffectivePolicy({
10
+ userId: req.user._id,
11
+ orgId,
12
+ driveType,
13
+ driveId,
14
+ });
15
+
16
+ return res.json(payload);
17
+ } catch (error) {
18
+ const code = error?.code;
19
+ if (code === 'VALIDATION') return res.status(400).json({ error: error.message });
20
+ console.error('Error getting file manager storage policy:', error);
21
+ return res.status(500).json({ error: 'Failed to get storage policy' });
22
+ }
23
+ };