@intranefr/superbackend 1.5.0 → 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 (171) hide show
  1. package/.env.example +5 -0
  2. package/README.md +11 -0
  3. package/index.js +23 -0
  4. package/package.json +7 -2
  5. package/src/admin/endpointRegistry.js +120 -0
  6. package/src/controllers/admin.controller.js +22 -5
  7. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  8. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  9. package/src/controllers/adminCache.controller.js +342 -0
  10. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  11. package/src/controllers/adminCrons.controller.js +388 -0
  12. package/src/controllers/adminDbBrowser.controller.js +124 -0
  13. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  14. package/src/controllers/adminHeadless.controller.js +9 -2
  15. package/src/controllers/adminHealthChecks.controller.js +570 -0
  16. package/src/controllers/adminI18n.controller.js +51 -29
  17. package/src/controllers/adminLlm.controller.js +126 -2
  18. package/src/controllers/adminPages.controller.js +720 -0
  19. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  20. package/src/controllers/adminProxy.controller.js +113 -0
  21. package/src/controllers/adminRateLimits.controller.js +138 -0
  22. package/src/controllers/adminRbac.controller.js +803 -0
  23. package/src/controllers/adminScripts.controller.js +93 -2
  24. package/src/controllers/adminSeoConfig.controller.js +71 -48
  25. package/src/controllers/blogAdmin.controller.js +279 -0
  26. package/src/controllers/blogAiAdmin.controller.js +224 -0
  27. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  28. package/src/controllers/blogInternal.controller.js +26 -0
  29. package/src/controllers/blogPublic.controller.js +89 -0
  30. package/src/controllers/fileManager.controller.js +190 -0
  31. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  32. package/src/controllers/healthChecksPublic.controller.js +196 -0
  33. package/src/controllers/metrics.controller.js +64 -4
  34. package/src/controllers/orgAdmin.controller.js +80 -0
  35. package/src/middleware/internalCronAuth.js +29 -0
  36. package/src/middleware/rbac.js +62 -0
  37. package/src/middleware.js +756 -48
  38. package/src/models/BlockDefinition.js +27 -0
  39. package/src/models/BlogAutomationLock.js +14 -0
  40. package/src/models/BlogAutomationRun.js +39 -0
  41. package/src/models/BlogPost.js +42 -0
  42. package/src/models/CacheEntry.js +26 -0
  43. package/src/models/ConsoleEntry.js +32 -0
  44. package/src/models/ConsoleLog.js +23 -0
  45. package/src/models/ContextBlockDefinition.js +33 -0
  46. package/src/models/CronExecution.js +47 -0
  47. package/src/models/CronJob.js +70 -0
  48. package/src/models/ExternalDbConnection.js +49 -0
  49. package/src/models/FileEntry.js +22 -0
  50. package/src/models/HealthAutoHealAttempt.js +57 -0
  51. package/src/models/HealthCheck.js +132 -0
  52. package/src/models/HealthCheckRun.js +51 -0
  53. package/src/models/HealthIncident.js +49 -0
  54. package/src/models/Page.js +95 -0
  55. package/src/models/PageCollection.js +42 -0
  56. package/src/models/ProxyEntry.js +66 -0
  57. package/src/models/RateLimitCounter.js +19 -0
  58. package/src/models/RateLimitMetricBucket.js +20 -0
  59. package/src/models/RbacGrant.js +25 -0
  60. package/src/models/RbacGroup.js +16 -0
  61. package/src/models/RbacGroupMember.js +13 -0
  62. package/src/models/RbacGroupRole.js +13 -0
  63. package/src/models/RbacRole.js +25 -0
  64. package/src/models/RbacUserRole.js +13 -0
  65. package/src/routes/adminBlog.routes.js +21 -0
  66. package/src/routes/adminBlogAi.routes.js +16 -0
  67. package/src/routes/adminBlogAutomation.routes.js +27 -0
  68. package/src/routes/adminCache.routes.js +20 -0
  69. package/src/routes/adminConsoleManager.routes.js +302 -0
  70. package/src/routes/adminCrons.routes.js +25 -0
  71. package/src/routes/adminDbBrowser.routes.js +65 -0
  72. package/src/routes/adminEjsVirtual.routes.js +2 -1
  73. package/src/routes/adminHeadless.routes.js +2 -1
  74. package/src/routes/adminHealthChecks.routes.js +28 -0
  75. package/src/routes/adminI18n.routes.js +4 -3
  76. package/src/routes/adminLlm.routes.js +4 -2
  77. package/src/routes/adminPages.routes.js +55 -0
  78. package/src/routes/adminProxy.routes.js +15 -0
  79. package/src/routes/adminRateLimits.routes.js +17 -0
  80. package/src/routes/adminRbac.routes.js +38 -0
  81. package/src/routes/adminSeoConfig.routes.js +5 -4
  82. package/src/routes/adminUiComponents.routes.js +2 -1
  83. package/src/routes/blogInternal.routes.js +14 -0
  84. package/src/routes/blogPublic.routes.js +9 -0
  85. package/src/routes/fileManager.routes.js +62 -0
  86. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  87. package/src/routes/healthChecksPublic.routes.js +9 -0
  88. package/src/routes/log.routes.js +43 -60
  89. package/src/routes/metrics.routes.js +4 -2
  90. package/src/routes/orgAdmin.routes.js +1 -0
  91. package/src/routes/pages.routes.js +123 -0
  92. package/src/routes/proxy.routes.js +46 -0
  93. package/src/routes/rbac.routes.js +47 -0
  94. package/src/routes/webhook.routes.js +2 -1
  95. package/src/routes/workflows.routes.js +4 -0
  96. package/src/services/blockDefinitionsAi.service.js +247 -0
  97. package/src/services/blog.service.js +99 -0
  98. package/src/services/blogAutomation.service.js +978 -0
  99. package/src/services/blogCronsBootstrap.service.js +184 -0
  100. package/src/services/blogPublishing.service.js +58 -0
  101. package/src/services/cacheLayer.service.js +696 -0
  102. package/src/services/consoleManager.service.js +700 -0
  103. package/src/services/consoleOverride.service.js +6 -1
  104. package/src/services/cronScheduler.service.js +350 -0
  105. package/src/services/dbBrowser.service.js +536 -0
  106. package/src/services/ejsVirtual.service.js +102 -32
  107. package/src/services/fileManager.service.js +475 -0
  108. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  109. package/src/services/healthChecks.service.js +650 -0
  110. package/src/services/healthChecksBootstrap.service.js +109 -0
  111. package/src/services/healthChecksScheduler.service.js +106 -0
  112. package/src/services/llmDefaults.service.js +190 -0
  113. package/src/services/migrationAssets/s3.js +2 -2
  114. package/src/services/pages.service.js +602 -0
  115. package/src/services/pagesContext.service.js +331 -0
  116. package/src/services/pagesContextBlocksAi.service.js +349 -0
  117. package/src/services/proxy.service.js +535 -0
  118. package/src/services/rateLimiter.service.js +623 -0
  119. package/src/services/rbac.service.js +212 -0
  120. package/src/services/scriptsRunner.service.js +1 -1
  121. package/src/services/uiComponentsAi.service.js +6 -19
  122. package/src/services/workflow.service.js +23 -8
  123. package/src/utils/orgRoles.js +14 -0
  124. package/src/utils/rbac/engine.js +60 -0
  125. package/src/utils/rbac/rightsRegistry.js +29 -0
  126. package/views/admin-blog-automation.ejs +877 -0
  127. package/views/admin-blog-edit.ejs +542 -0
  128. package/views/admin-blog.ejs +399 -0
  129. package/views/admin-cache.ejs +681 -0
  130. package/views/admin-console-manager.ejs +680 -0
  131. package/views/admin-crons.ejs +645 -0
  132. package/views/admin-db-browser.ejs +445 -0
  133. package/views/admin-ejs-virtual.ejs +16 -10
  134. package/views/admin-file-manager.ejs +942 -0
  135. package/views/admin-health-checks.ejs +725 -0
  136. package/views/admin-i18n.ejs +59 -5
  137. package/views/admin-llm.ejs +99 -1
  138. package/views/admin-organizations.ejs +163 -1
  139. package/views/admin-pages.ejs +2424 -0
  140. package/views/admin-proxy.ejs +491 -0
  141. package/views/admin-rate-limiter.ejs +625 -0
  142. package/views/admin-rbac.ejs +1331 -0
  143. package/views/admin-scripts.ejs +1 -1
  144. package/views/admin-seo-config.ejs +61 -7
  145. package/views/admin-ui-components.ejs +57 -25
  146. package/views/admin-workflows.ejs +7 -7
  147. package/views/file-manager.ejs +866 -0
  148. package/views/pages/blocks/contact.ejs +27 -0
  149. package/views/pages/blocks/cta.ejs +18 -0
  150. package/views/pages/blocks/faq.ejs +20 -0
  151. package/views/pages/blocks/features.ejs +19 -0
  152. package/views/pages/blocks/hero.ejs +13 -0
  153. package/views/pages/blocks/html.ejs +5 -0
  154. package/views/pages/blocks/image.ejs +14 -0
  155. package/views/pages/blocks/testimonials.ejs +26 -0
  156. package/views/pages/blocks/text.ejs +10 -0
  157. package/views/pages/layouts/default.ejs +51 -0
  158. package/views/pages/layouts/minimal.ejs +42 -0
  159. package/views/pages/layouts/sidebar.ejs +54 -0
  160. package/views/pages/partials/footer.ejs +13 -0
  161. package/views/pages/partials/header.ejs +12 -0
  162. package/views/pages/partials/sidebar.ejs +8 -0
  163. package/views/pages/runtime/page.ejs +10 -0
  164. package/views/pages/templates/article.ejs +20 -0
  165. package/views/pages/templates/default.ejs +12 -0
  166. package/views/pages/templates/landing.ejs +14 -0
  167. package/views/pages/templates/listing.ejs +15 -0
  168. package/views/partials/admin-image-upload-modal.ejs +221 -0
  169. package/views/partials/dashboard/nav-items.ejs +11 -0
  170. package/views/partials/llm-provider-model-picker.ejs +183 -0
  171. package/src/routes/llmUi.routes.js +0 -26
@@ -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
+ };
@@ -55,14 +55,35 @@ async function tryAttachUser(req) {
55
55
 
56
56
  exports.track = async (req, res) => {
57
57
  try {
58
+ // Validate request size to prevent abuse
59
+ const contentLength = req.headers['content-length'];
60
+ if (contentLength && parseInt(contentLength) > 1024 * 10) { // 10KB limit
61
+ return res.status(413).json({ error: 'Request too large' });
62
+ }
63
+
58
64
  await tryAttachUser(req);
59
65
 
60
66
  const action = String(req.body?.action || '').trim();
61
67
  const meta = req.body?.meta ?? null;
62
68
 
69
+ // Validate action field
63
70
  if (!action) {
64
71
  return res.status(400).json({ error: 'action is required' });
65
72
  }
73
+
74
+ if (action.length > 100) {
75
+ return res.status(400).json({ error: 'action too long (max 100 characters)' });
76
+ }
77
+
78
+ // Validate action format (allow only alphanumeric, underscores, hyphens, dots)
79
+ if (!/^[a-zA-Z0-9._-]+$/.test(action)) {
80
+ return res.status(400).json({ error: 'invalid action format' });
81
+ }
82
+
83
+ // Validate meta size
84
+ if (meta && JSON.stringify(meta).length > 1024 * 5) { // 5KB limit
85
+ return res.status(400).json({ error: 'meta data too large' });
86
+ }
66
87
 
67
88
  let actorType = 'anonymous';
68
89
  let actorId = getAnonId(req);
@@ -98,12 +119,48 @@ exports.track = async (req, res) => {
98
119
 
99
120
  exports.getImpact = async (req, res) => {
100
121
  try {
101
- const { start, end } = getMonthRange(new Date());
122
+ // Validate query parameters
123
+ const { start, end } = req.query;
124
+
125
+ // Allow custom time ranges but restrict to reasonable limits
126
+ let startTime, endTime;
127
+
128
+ if (start || end) {
129
+ // Parse custom range if provided
130
+ startTime = start ? new Date(start) : null;
131
+ endTime = end ? new Date(end) : null;
132
+
133
+ // Validate dates
134
+ if ((start && isNaN(startTime.getTime())) || (end && isNaN(endTime.getTime()))) {
135
+ return res.status(400).json({ error: 'Invalid date format' });
136
+ }
137
+
138
+ // Restrict range to maximum 1 year
139
+ if (startTime && endTime) {
140
+ const rangeMs = endTime.getTime() - startTime.getTime();
141
+ const maxRangeMs = 365 * 24 * 60 * 60 * 1000; // 1 year
142
+ if (rangeMs > maxRangeMs) {
143
+ return res.status(400).json({ error: 'Time range too large (max 1 year)' });
144
+ }
145
+ }
146
+
147
+ // Default to current month if range is incomplete
148
+ if (!startTime || !endTime) {
149
+ const currentMonth = getMonthRange(new Date());
150
+ startTime = startTime || currentMonth.start;
151
+ endTime = endTime || currentMonth.end;
152
+ }
153
+ } else {
154
+ // Default to current month
155
+ const currentMonth = getMonthRange(new Date());
156
+ startTime = currentMonth.start;
157
+ endTime = currentMonth.end;
158
+ }
102
159
 
103
160
  const activeActorsAgg = await ActionEvent.aggregate([
104
161
  {
105
162
  $match: {
106
- createdAt: { $gte: start, $lt: end },
163
+ createdAt: { $gte: startTime, $lt: endTime },
107
164
  actorType: { $in: ['user', 'anonymous'] },
108
165
  },
109
166
  },
@@ -122,7 +179,7 @@ exports.getImpact = async (req, res) => {
122
179
 
123
180
  const servicesConsulted = await ActionEvent.countDocuments({
124
181
  action: 'service_view',
125
- createdAt: { $gte: start, $lt: end },
182
+ createdAt: { $gte: startTime, $lt: endTime },
126
183
  });
127
184
 
128
185
  const newsletterSetting = await GlobalSetting.findOne({ key: 'newsletter_list' }).lean();
@@ -136,8 +193,11 @@ exports.getImpact = async (req, res) => {
136
193
  }
137
194
  }
138
195
 
196
+ // Add cache headers for better performance
197
+ res.set('Cache-Control', 'public, max-age=300'); // 5 minutes cache
198
+
139
199
  return res.json({
140
- range: { start: start.toISOString(), end: end.toISOString() },
200
+ range: { start: startTime.toISOString(), end: endTime.toISOString() },
141
201
  activeUsers,
142
202
  servicesConsulted,
143
203
  newsletterSubscribers,
@@ -270,6 +270,86 @@ exports.removeMember = async (req, res) => {
270
270
  }
271
271
  };
272
272
 
273
+ exports.addMember = async (req, res) => {
274
+ try {
275
+ const { orgId } = req.params;
276
+ const { userId, role } = req.body;
277
+
278
+ if (!orgId || !mongoose.Types.ObjectId.isValid(String(orgId))) {
279
+ return res.status(400).json({ error: 'Invalid organization ID' });
280
+ }
281
+
282
+ if (!userId || !mongoose.Types.ObjectId.isValid(String(userId))) {
283
+ return res.status(400).json({ error: 'Invalid user ID' });
284
+ }
285
+
286
+ // Validate role
287
+ const defaultRole = await getDefaultOrgRole();
288
+ const memberRole = role || defaultRole;
289
+ if (!(await isValidOrgRole(memberRole))) {
290
+ const allowed = await getAllowedOrgRoles();
291
+ return res.status(400).json({ error: 'Invalid role', allowedRoles: allowed });
292
+ }
293
+
294
+ // Check if organization exists
295
+ const org = await Organization.findById(orgId);
296
+ if (!org) {
297
+ return res.status(404).json({ error: 'Organization not found' });
298
+ }
299
+
300
+ // Check if user exists
301
+ const user = await User.findById(userId);
302
+ if (!user) {
303
+ return res.status(404).json({ error: 'User not found' });
304
+ }
305
+
306
+ // Check if user is already a member
307
+ const existingMember = await OrganizationMember.findOne({
308
+ orgId,
309
+ userId,
310
+ status: { $in: ['active', 'removed'] }
311
+ });
312
+
313
+ if (existingMember) {
314
+ if (existingMember.status === 'active') {
315
+ return res.status(409).json({ error: 'User is already a member of this organization' });
316
+ } else {
317
+ // Reactivate removed member
318
+ existingMember.status = 'active';
319
+ existingMember.role = memberRole;
320
+ existingMember.addedByUserId = req.user?.id || org.ownerUserId;
321
+ await existingMember.save();
322
+ return res.json({
323
+ message: 'Member reactivated successfully',
324
+ member: existingMember.toObject()
325
+ });
326
+ }
327
+ }
328
+
329
+ // Create new member
330
+ const member = await OrganizationMember.create({
331
+ orgId,
332
+ userId,
333
+ role: memberRole,
334
+ status: 'active',
335
+ addedByUserId: req.user?.id || org.ownerUserId,
336
+ });
337
+
338
+ // Populate user data for response
339
+ const populatedMember = await OrganizationMember.findById(member._id)
340
+ .populate('userId', 'email name')
341
+ .lean();
342
+
343
+ return res.status(201).json({
344
+ message: 'Member added successfully',
345
+ member: populatedMember
346
+ });
347
+ } catch (error) {
348
+ console.error('Admin org member add error:', error);
349
+ return res.status(500).json({ error: 'Failed to add member' });
350
+ }
351
+ };
352
+
273
353
  exports.listInvites = async (req, res) => {
274
354
  try {
275
355
  const { orgId } = req.params;
@@ -0,0 +1,29 @@
1
+ const globalSettingsService = require('../services/globalSettings.service');
2
+ const { INTERNAL_CRON_TOKEN_SETTING_KEY } = require('../services/blogCronsBootstrap.service');
3
+
4
+ async function requireInternalCronToken(req, res, next) {
5
+ try {
6
+ const header = String(req.headers.authorization || '');
7
+ if (!header.startsWith('Bearer ')) {
8
+ return res.status(401).json({ error: 'Authentication required' });
9
+ }
10
+
11
+ const token = header.slice('Bearer '.length).trim();
12
+ const expected = String(
13
+ await globalSettingsService.getSettingValue(INTERNAL_CRON_TOKEN_SETTING_KEY, ''),
14
+ ).trim();
15
+
16
+ if (!expected || token !== expected) {
17
+ return res.status(403).json({ error: 'Forbidden' });
18
+ }
19
+
20
+ next();
21
+ } catch (error) {
22
+ console.error('internal cron auth error:', error);
23
+ res.status(500).json({ error: 'Internal auth failed' });
24
+ }
25
+ }
26
+
27
+ module.exports = {
28
+ requireInternalCronToken,
29
+ };
@@ -0,0 +1,62 @@
1
+ const rbacService = require('../services/rbac.service');
2
+
3
+ function isBasicAuthSuperAdmin(req) {
4
+ const authHeader = req.headers?.authorization || '';
5
+ if (!authHeader.startsWith('Basic ')) return false;
6
+
7
+ try {
8
+ const credentials = Buffer.from(authHeader.substring(6), 'base64').toString('utf-8');
9
+ const [username, password] = credentials.split(':');
10
+
11
+ const adminUsername = process.env.ADMIN_USERNAME || 'admin';
12
+ const adminPassword = process.env.ADMIN_PASSWORD || 'admin';
13
+
14
+ return username === adminUsername && password === adminPassword;
15
+ } catch (e) {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ function requireRight(requiredRight, options = {}) {
21
+ const getOrgId = options.getOrgId || ((req) => req.params?.orgId || req.query?.orgId || req.body?.orgId);
22
+
23
+ return async (req, res, next) => {
24
+ try {
25
+ if (isBasicAuthSuperAdmin(req)) {
26
+ return next();
27
+ }
28
+
29
+ if (!req.user) {
30
+ return res.status(401).json({ error: 'Authentication required' });
31
+ }
32
+
33
+ const orgId = getOrgId(req);
34
+ if (!orgId) {
35
+ return res.status(400).json({ error: 'orgId is required for RBAC checks' });
36
+ }
37
+
38
+ const result = await rbacService.checkRight({
39
+ userId: req.user._id,
40
+ orgId,
41
+ right: requiredRight,
42
+ });
43
+
44
+ if (!result.allowed) {
45
+ return res.status(403).json({
46
+ error: 'Access denied',
47
+ reason: result.reason,
48
+ });
49
+ }
50
+
51
+ return next();
52
+ } catch (error) {
53
+ console.error('RBAC requireRight error:', error);
54
+ return res.status(500).json({ error: 'Failed to evaluate RBAC rights' });
55
+ }
56
+ };
57
+ }
58
+
59
+ module.exports = {
60
+ requireRight,
61
+ isBasicAuthSuperAdmin,
62
+ };