@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.
- package/.env.example +5 -0
- package/README.md +11 -0
- package/index.js +23 -0
- package/package.json +7 -2
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +22 -5
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminHeadless.controller.js +9 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +93 -2
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +80 -0
- package/src/middleware/internalCronAuth.js +29 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +756 -48
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminHeadless.routes.js +2 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminUiComponents.routes.js +2 -1
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +1 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +184 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +700 -0
- package/src/services/consoleOverride.service.js +6 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +1 -1
- package/src/services/uiComponentsAi.service.js +6 -19
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +29 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +163 -1
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +1 -1
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-ui-components.ejs +57 -25
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +11 -0
- package/views/partials/llm-provider-model-picker.ejs +183 -0
- 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, '<')
|
|
11
|
+
.replace(/>/g, '>')
|
|
12
|
+
.replace(/"/g, '"')
|
|
13
|
+
.replace(/'/g, ''');
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
};
|