@intranefr/superbackend 1.5.0 → 1.5.2

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 (198) hide show
  1. package/.env.example +15 -0
  2. package/README.md +11 -0
  3. package/analysis-only.skill +0 -0
  4. package/index.js +23 -0
  5. package/package.json +8 -2
  6. package/src/admin/endpointRegistry.js +120 -0
  7. package/src/controllers/admin.controller.js +90 -6
  8. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  9. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  10. package/src/controllers/adminCache.controller.js +342 -0
  11. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  12. package/src/controllers/adminCrons.controller.js +388 -0
  13. package/src/controllers/adminDbBrowser.controller.js +124 -0
  14. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  15. package/src/controllers/adminExperiments.controller.js +200 -0
  16. package/src/controllers/adminHeadless.controller.js +9 -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 +126 -4
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/blogAdmin.controller.js +279 -0
  28. package/src/controllers/blogAiAdmin.controller.js +224 -0
  29. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  30. package/src/controllers/blogInternal.controller.js +26 -0
  31. package/src/controllers/blogPublic.controller.js +89 -0
  32. package/src/controllers/experiments.controller.js +85 -0
  33. package/src/controllers/fileManager.controller.js +190 -0
  34. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  35. package/src/controllers/healthChecksPublic.controller.js +196 -0
  36. package/src/controllers/internalExperiments.controller.js +17 -0
  37. package/src/controllers/metrics.controller.js +64 -4
  38. package/src/controllers/orgAdmin.controller.js +80 -0
  39. package/src/helpers/mongooseHelper.js +258 -0
  40. package/src/helpers/scriptBase.js +230 -0
  41. package/src/helpers/scriptRunner.js +335 -0
  42. package/src/middleware/rbac.js +62 -0
  43. package/src/middleware.js +810 -48
  44. package/src/models/BlockDefinition.js +27 -0
  45. package/src/models/BlogAutomationLock.js +14 -0
  46. package/src/models/BlogAutomationRun.js +39 -0
  47. package/src/models/BlogPost.js +42 -0
  48. package/src/models/CacheEntry.js +26 -0
  49. package/src/models/ConsoleEntry.js +32 -0
  50. package/src/models/ConsoleLog.js +23 -0
  51. package/src/models/ContextBlockDefinition.js +33 -0
  52. package/src/models/CronExecution.js +47 -0
  53. package/src/models/CronJob.js +70 -0
  54. package/src/models/Experiment.js +75 -0
  55. package/src/models/ExperimentAssignment.js +23 -0
  56. package/src/models/ExperimentEvent.js +26 -0
  57. package/src/models/ExperimentMetricBucket.js +30 -0
  58. package/src/models/ExternalDbConnection.js +49 -0
  59. package/src/models/FileEntry.js +22 -0
  60. package/src/models/GlobalSetting.js +1 -2
  61. package/src/models/HealthAutoHealAttempt.js +57 -0
  62. package/src/models/HealthCheck.js +132 -0
  63. package/src/models/HealthCheckRun.js +51 -0
  64. package/src/models/HealthIncident.js +49 -0
  65. package/src/models/Page.js +95 -0
  66. package/src/models/PageCollection.js +42 -0
  67. package/src/models/ProxyEntry.js +66 -0
  68. package/src/models/RateLimitCounter.js +19 -0
  69. package/src/models/RateLimitMetricBucket.js +20 -0
  70. package/src/models/RbacGrant.js +25 -0
  71. package/src/models/RbacGroup.js +16 -0
  72. package/src/models/RbacGroupMember.js +13 -0
  73. package/src/models/RbacGroupRole.js +13 -0
  74. package/src/models/RbacRole.js +25 -0
  75. package/src/models/RbacUserRole.js +13 -0
  76. package/src/models/ScriptDefinition.js +1 -0
  77. package/src/models/Webhook.js +2 -0
  78. package/src/routes/admin.routes.js +2 -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/adminExperiments.routes.js +29 -0
  88. package/src/routes/adminHeadless.routes.js +2 -1
  89. package/src/routes/adminHealthChecks.routes.js +28 -0
  90. package/src/routes/adminI18n.routes.js +4 -3
  91. package/src/routes/adminLlm.routes.js +4 -2
  92. package/src/routes/adminPages.routes.js +55 -0
  93. package/src/routes/adminProxy.routes.js +15 -0
  94. package/src/routes/adminRateLimits.routes.js +17 -0
  95. package/src/routes/adminRbac.routes.js +38 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminUiComponents.routes.js +2 -1
  98. package/src/routes/blogInternal.routes.js +14 -0
  99. package/src/routes/blogPublic.routes.js +9 -0
  100. package/src/routes/experiments.routes.js +30 -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/internalExperiments.routes.js +15 -0
  105. package/src/routes/log.routes.js +43 -60
  106. package/src/routes/metrics.routes.js +4 -2
  107. package/src/routes/orgAdmin.routes.js +1 -0
  108. package/src/routes/pages.routes.js +123 -0
  109. package/src/routes/proxy.routes.js +46 -0
  110. package/src/routes/rbac.routes.js +47 -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 +185 -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 +738 -0
  120. package/src/services/consoleOverride.service.js +7 -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/experiments.service.js +273 -0
  125. package/src/services/experimentsAggregation.service.js +308 -0
  126. package/src/services/experimentsCronsBootstrap.service.js +118 -0
  127. package/src/services/experimentsRetention.service.js +43 -0
  128. package/src/services/experimentsWs.service.js +134 -0
  129. package/src/services/fileManager.service.js +475 -0
  130. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  131. package/src/services/globalSettings.service.js +15 -0
  132. package/src/services/healthChecks.service.js +650 -0
  133. package/src/services/healthChecksBootstrap.service.js +109 -0
  134. package/src/services/healthChecksScheduler.service.js +106 -0
  135. package/src/services/jsonConfigs.service.js +2 -2
  136. package/src/services/llmDefaults.service.js +190 -0
  137. package/src/services/migrationAssets/s3.js +2 -2
  138. package/src/services/pages.service.js +602 -0
  139. package/src/services/pagesContext.service.js +331 -0
  140. package/src/services/pagesContextBlocksAi.service.js +349 -0
  141. package/src/services/proxy.service.js +535 -0
  142. package/src/services/rateLimiter.service.js +623 -0
  143. package/src/services/rbac.service.js +212 -0
  144. package/src/services/scriptsRunner.service.js +215 -15
  145. package/src/services/uiComponentsAi.service.js +6 -19
  146. package/src/services/workflow.service.js +23 -8
  147. package/src/utils/orgRoles.js +14 -0
  148. package/src/utils/rbac/engine.js +60 -0
  149. package/src/utils/rbac/rightsRegistry.js +33 -0
  150. package/views/admin-blog-automation.ejs +877 -0
  151. package/views/admin-blog-edit.ejs +542 -0
  152. package/views/admin-blog.ejs +399 -0
  153. package/views/admin-cache.ejs +681 -0
  154. package/views/admin-console-manager.ejs +680 -0
  155. package/views/admin-crons.ejs +645 -0
  156. package/views/admin-dashboard.ejs +28 -8
  157. package/views/admin-db-browser.ejs +445 -0
  158. package/views/admin-ejs-virtual.ejs +16 -10
  159. package/views/admin-experiments.ejs +91 -0
  160. package/views/admin-file-manager.ejs +942 -0
  161. package/views/admin-health-checks.ejs +725 -0
  162. package/views/admin-i18n.ejs +59 -5
  163. package/views/admin-llm.ejs +99 -1
  164. package/views/admin-organizations.ejs +163 -1
  165. package/views/admin-pages.ejs +2424 -0
  166. package/views/admin-proxy.ejs +491 -0
  167. package/views/admin-rate-limiter.ejs +625 -0
  168. package/views/admin-rbac.ejs +1331 -0
  169. package/views/admin-scripts.ejs +597 -3
  170. package/views/admin-seo-config.ejs +61 -7
  171. package/views/admin-ui-components.ejs +57 -25
  172. package/views/admin-workflows.ejs +7 -7
  173. package/views/file-manager.ejs +866 -0
  174. package/views/pages/blocks/contact.ejs +27 -0
  175. package/views/pages/blocks/cta.ejs +18 -0
  176. package/views/pages/blocks/faq.ejs +20 -0
  177. package/views/pages/blocks/features.ejs +19 -0
  178. package/views/pages/blocks/hero.ejs +13 -0
  179. package/views/pages/blocks/html.ejs +5 -0
  180. package/views/pages/blocks/image.ejs +14 -0
  181. package/views/pages/blocks/testimonials.ejs +26 -0
  182. package/views/pages/blocks/text.ejs +10 -0
  183. package/views/pages/layouts/default.ejs +51 -0
  184. package/views/pages/layouts/minimal.ejs +42 -0
  185. package/views/pages/layouts/sidebar.ejs +54 -0
  186. package/views/pages/partials/footer.ejs +13 -0
  187. package/views/pages/partials/header.ejs +12 -0
  188. package/views/pages/partials/sidebar.ejs +8 -0
  189. package/views/pages/runtime/page.ejs +10 -0
  190. package/views/pages/templates/article.ejs +20 -0
  191. package/views/pages/templates/default.ejs +12 -0
  192. package/views/pages/templates/landing.ejs +14 -0
  193. package/views/pages/templates/listing.ejs +15 -0
  194. package/views/partials/admin-image-upload-modal.ejs +221 -0
  195. package/views/partials/dashboard/nav-items.ejs +12 -0
  196. package/views/partials/dashboard/palette.ejs +5 -3
  197. package/views/partials/llm-provider-model-picker.ejs +183 -0
  198. package/src/routes/llmUi.routes.js +0 -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,85 @@
1
+ const experimentsService = require('../services/experiments.service');
2
+
3
+ function toSafeJsonError(error) {
4
+ const msg = error?.message || 'Operation failed';
5
+ const code = error?.code;
6
+ if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
7
+ if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
8
+ if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
9
+ return { status: 500, body: { error: msg } };
10
+ }
11
+
12
+ exports.getAssignment = async (req, res) => {
13
+ try {
14
+ const orgId = req.headers['x-org-id'] || req.query.orgId || req.body?.orgId;
15
+ const subjectId = req.query.subjectId || req.body?.subjectId;
16
+
17
+ const context = req.body?.context && typeof req.body.context === 'object' ? req.body.context : {};
18
+
19
+ const { experiment, assignment } = await experimentsService.getOrCreateAssignment({
20
+ orgId,
21
+ experimentCode: req.params.code,
22
+ subjectId,
23
+ context,
24
+ });
25
+
26
+ const variant = (experiment.variants || []).find((v) => String(v?.key || '') === String(assignment.variantKey));
27
+ const config = await experimentsService.resolveVariantConfig(variant);
28
+
29
+ const { snapshot } = await experimentsService.getWinnerSnapshot({ orgId, experimentCode: req.params.code });
30
+
31
+ return res.json({
32
+ experimentCode: experiment.code,
33
+ variantKey: assignment.variantKey,
34
+ assignedAt: assignment.assignedAt,
35
+ config,
36
+ winner: {
37
+ winnerVariantKey: snapshot.winnerVariantKey,
38
+ decidedAt: snapshot.winnerDecidedAt,
39
+ reason: snapshot.winnerReason,
40
+ status: snapshot.status,
41
+ },
42
+ });
43
+ } catch (err) {
44
+ const safe = toSafeJsonError(err);
45
+ return res.status(safe.status).json(safe.body);
46
+ }
47
+ };
48
+
49
+ exports.postEvents = async (req, res) => {
50
+ try {
51
+ const orgId = req.headers['x-org-id'] || req.query.orgId || req.body?.orgId;
52
+ const subjectId = req.query.subjectId || req.body?.subjectId;
53
+
54
+ const payload = req.body || {};
55
+ const events = Array.isArray(payload.events) ? payload.events : [payload];
56
+
57
+ const result = await experimentsService.ingestEvents({
58
+ orgId,
59
+ experimentCode: req.params.code,
60
+ subjectId,
61
+ events,
62
+ });
63
+
64
+ return res.status(201).json(result);
65
+ } catch (err) {
66
+ const safe = toSafeJsonError(err);
67
+ return res.status(safe.status).json(safe.body);
68
+ }
69
+ };
70
+
71
+ exports.getWinner = async (req, res) => {
72
+ try {
73
+ const orgId = req.headers['x-org-id'] || req.query.orgId || req.body?.orgId;
74
+ const { snapshot } = await experimentsService.getWinnerSnapshot({ orgId, experimentCode: req.params.code });
75
+ return res.json({
76
+ status: snapshot.status,
77
+ winnerVariantKey: snapshot.winnerVariantKey,
78
+ decidedAt: snapshot.winnerDecidedAt,
79
+ reason: snapshot.winnerReason,
80
+ });
81
+ } catch (err) {
82
+ const safe = toSafeJsonError(err);
83
+ return res.status(safe.status).json(safe.body);
84
+ }
85
+ };
@@ -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
+ };
@@ -0,0 +1,196 @@
1
+ const HealthCheck = require('../models/HealthCheck');
2
+ const HealthIncident = require('../models/HealthIncident');
3
+ const globalSettingsService = require('../services/globalSettings.service');
4
+
5
+ const PUBLIC_STATUS_SETTING_KEY = 'healthChecks.publicStatusEnabled';
6
+
7
+ function escapeHtml(unsafe) {
8
+ return String(unsafe || '')
9
+ .replace(/&/g, '&')
10
+ .replace(/</g, '&lt;')
11
+ .replace(/>/g, '&gt;')
12
+ .replace(/"/g, '&quot;')
13
+ .replace(/'/g, '&#39;');
14
+ }
15
+
16
+ function renderHtml(payload) {
17
+ const status = payload.status || 'unknown';
18
+ const badgeClass = status === 'ok' ? 'badge-success' : status === 'degraded' ? 'badge-warning' : 'badge-error';
19
+
20
+ const checks = Array.isArray(payload.checks) ? payload.checks : [];
21
+
22
+ const bodyRows = checks.length > 0
23
+ ? checks
24
+ .map((c) => {
25
+ const cStatus = String(c.status || 'unknown');
26
+ const cBadgeClass = cStatus === 'healthy' ? 'badge-success' : cStatus === 'unhealthy' ? 'badge-error' : 'badge-ghost';
27
+
28
+ const incident = c.incident;
29
+ const incidentLabel = incident ? `${incident.status} (${incident.severity})` : '-';
30
+
31
+ return `
32
+ <tr>
33
+ <td class="font-medium">${escapeHtml(c.name)}</td>
34
+ <td><span class="badge ${cBadgeClass}">${escapeHtml(cStatus)}</span></td>
35
+ <td class="text-slate-500">${c.lastRunAt ? new Date(c.lastRunAt).toLocaleString() : '-'}</td>
36
+ <td class="text-slate-500">${c.lastLatencyMs != null ? escapeHtml(String(c.lastLatencyMs)) + ' ms' : '-'}</td>
37
+
38
+ <td class="text-xs">${escapeHtml(incidentLabel)}</td>
39
+ </tr>`;
40
+ })
41
+ .join('')
42
+ : '<tr><td colspan="5" class="text-slate-500 text-center">No checks found</td></tr>';
43
+
44
+ return `<!DOCTYPE html>
45
+ <html lang="en">
46
+ <head>
47
+ <meta charset="UTF-8" />
48
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
49
+ <title>Health Checks Status</title>
50
+ <script src="https://cdn.tailwindcss.com"></script>
51
+ <link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.10/dist/full.min.css" rel="stylesheet" type="text/css" />
52
+ </head>
53
+ <body class="bg-slate-50">
54
+ <div class="max-w-5xl mx-auto px-6 py-8">
55
+ <div class="flex items-start justify-between gap-4">
56
+ <div>
57
+ <h1 class="text-2xl font-semibold text-slate-900">Health Checks</h1>
58
+ <div class="text-sm text-slate-500">Public status summary</div>
59
+ </div>
60
+ <div class="text-right">
61
+ <div class="text-sm text-slate-500">Overall</div>
62
+ <div class="badge ${badgeClass} badge-lg">${escapeHtml(status)}</div>
63
+ <div class="text-xs text-slate-500 mt-1">Updated: ${escapeHtml(payload.updatedAt || '')}</div>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="mt-6 grid grid-cols-2 gap-3">
68
+ <div class="card bg-white border border-slate-200">
69
+ <div class="card-body py-4">
70
+ <div class="text-sm text-slate-500">Total checks</div>
71
+ <div class="text-xl font-semibold">${payload.totalChecks || 0}</div>
72
+ </div>
73
+ </div>
74
+ <div class="card bg-white border border-slate-200">
75
+ <div class="card-body py-4">
76
+ <div class="text-sm text-slate-500">Unhealthy</div>
77
+ <div class="text-xl font-semibold">${payload.unhealthyCount || 0}</div>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="mt-6 card bg-white border border-slate-200">
83
+ <div class="card-body p-0">
84
+ <div class="overflow-x-auto">
85
+ <table class="table table-zebra w-full">
86
+ <thead>
87
+ <tr>
88
+ <th>Name</th>
89
+ <th>Status</th>
90
+ <th>Last run</th>
91
+ <th>Latency</th>
92
+ <th>Incident</th>
93
+ </tr>
94
+ </thead>
95
+ <tbody>
96
+ ${bodyRows}
97
+ </tbody>
98
+ </table>
99
+ </div>
100
+ </div>
101
+ </div>
102
+
103
+ <div class="mt-4 text-xs text-slate-500">
104
+ Tip: add <code class="px-1 py-0.5 bg-slate-100 rounded">/json</code> to the URL for JSON format.
105
+ </div>
106
+ </div>
107
+ </body>
108
+ </html>`;
109
+ }
110
+
111
+ async function computeStatusPayload() {
112
+ const checks = await HealthCheck.find({ enabled: true }).sort({ name: 1 }).lean();
113
+ const checkIds = checks.map((c) => String(c._id));
114
+
115
+ const incidents = await HealthIncident.find({
116
+ healthCheckId: { $in: checkIds },
117
+ status: { $in: ['open', 'acknowledged'] },
118
+ }).lean();
119
+
120
+
121
+ const incidentMap = {};
122
+ for (const incident of incidents) {
123
+ if (!incidentMap[incident.healthCheckId]) {
124
+ incidentMap[incident.healthCheckId] = incident;
125
+ }
126
+ }
127
+
128
+ const summaries = checks.map((check) => {
129
+ const incident = incidentMap[String(check._id)];
130
+ return {
131
+ id: String(check._id),
132
+ name: check.name,
133
+ status: incident ? incident.status : (check.lastStatus || 'unknown'),
134
+ lastRunAt: check.lastRunAt || null,
135
+ lastLatencyMs: check.lastLatencyMs || null,
136
+ incident: incident
137
+ ? {
138
+ id: String(incident._id),
139
+ status: incident.status,
140
+ severity: incident.severity,
141
+ openedAt: incident.openedAt,
142
+ lastSeenAt: incident.lastSeenAt,
143
+ }
144
+ : null,
145
+ };
146
+ });
147
+
148
+ const unhealthyCount = summaries.filter((s) => s.status === 'unhealthy' || s.incident).length;
149
+ const overallStatus = unhealthyCount > 0 ? 'degraded' : 'ok';
150
+
151
+ return {
152
+ ok: overallStatus === 'ok',
153
+ status: overallStatus,
154
+ updatedAt: new Date().toISOString(),
155
+ totalChecks: summaries.length,
156
+ unhealthyCount,
157
+ checks: summaries,
158
+ };
159
+ }
160
+
161
+ exports.getStatus = async (req, res) => {
162
+ try {
163
+ const raw = await globalSettingsService.getSettingValue(PUBLIC_STATUS_SETTING_KEY, 'false');
164
+ const enabled = String(raw) === 'true';
165
+
166
+ if (!enabled) {
167
+ return res.status(404).json({ error: 'Not found' });
168
+ }
169
+
170
+ const payload = await computeStatusPayload();
171
+ const html = renderHtml(payload);
172
+
173
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
174
+ return res.status(200).send(html);
175
+ } catch (error) {
176
+ console.error('Failed to compute health checks status:', error);
177
+ return res.status(500).json({ error: 'Failed to compute status' });
178
+ }
179
+ };
180
+
181
+ exports.getStatusJson = async (req, res) => {
182
+ try {
183
+ const raw = await globalSettingsService.getSettingValue(PUBLIC_STATUS_SETTING_KEY, 'false');
184
+ const enabled = String(raw) === 'true';
185
+
186
+ if (!enabled) {
187
+ return res.status(404).json({ error: 'Not found' });
188
+ }
189
+
190
+ const payload = await computeStatusPayload();
191
+ return res.json(payload);
192
+ } catch (error) {
193
+ console.error('Failed to compute health checks status json:', error);
194
+ return res.status(500).json({ error: 'Failed to compute status' });
195
+ }
196
+ };
@@ -0,0 +1,17 @@
1
+ const experimentsAggregation = require('../services/experimentsAggregation.service');
2
+ const experimentsRetention = require('../services/experimentsRetention.service');
3
+
4
+ exports.runAggregation = async (req, res) => {
5
+ const body = req.body || {};
6
+ const bucketMs = body.bucketMs;
7
+ const start = body.start;
8
+ const end = body.end;
9
+
10
+ const data = await experimentsAggregation.runAggregationAndWinner({ bucketMs, start, end });
11
+ return res.json(data);
12
+ };
13
+
14
+ exports.runRetention = async (_req, res) => {
15
+ const data = await experimentsRetention.runRetentionCleanup();
16
+ return res.json(data);
17
+ };