@intranefr/superbackend 1.4.3
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/.commiat +4 -0
- package/.env.example +47 -0
- package/README.md +110 -0
- package/index.js +94 -0
- package/package.json +67 -0
- package/public/css/styles.css +139 -0
- package/public/js/animations.js +41 -0
- package/sdk/error-tracking/browser/package.json +16 -0
- package/sdk/error-tracking/browser/src/core.js +270 -0
- package/sdk/error-tracking/browser/src/embed.js +18 -0
- package/sdk/error-tracking/browser/src/index.js +1 -0
- package/server.js +5 -0
- package/src/admin/endpointRegistry.js +300 -0
- package/src/controllers/admin.controller.js +321 -0
- package/src/controllers/adminAssets.controller.js +530 -0
- package/src/controllers/adminAssetsStorage.controller.js +260 -0
- package/src/controllers/adminEjsVirtual.controller.js +354 -0
- package/src/controllers/adminFeatureFlags.controller.js +155 -0
- package/src/controllers/adminHeadless.controller.js +1071 -0
- package/src/controllers/adminI18n.controller.js +604 -0
- package/src/controllers/adminJsonConfigs.controller.js +97 -0
- package/src/controllers/adminLlm.controller.js +273 -0
- package/src/controllers/adminMigration.controller.js +257 -0
- package/src/controllers/adminSeoConfig.controller.js +515 -0
- package/src/controllers/adminStats.controller.js +121 -0
- package/src/controllers/adminUploadNamespaces.controller.js +208 -0
- package/src/controllers/assets.controller.js +248 -0
- package/src/controllers/auth.controller.js +93 -0
- package/src/controllers/billing.controller.js +223 -0
- package/src/controllers/featureFlags.controller.js +35 -0
- package/src/controllers/forms.controller.js +217 -0
- package/src/controllers/globalSettings.controller.js +252 -0
- package/src/controllers/headlessCrud.controller.js +126 -0
- package/src/controllers/i18n.controller.js +12 -0
- package/src/controllers/invite.controller.js +249 -0
- package/src/controllers/jsonConfigs.controller.js +19 -0
- package/src/controllers/metrics.controller.js +149 -0
- package/src/controllers/notificationAdmin.controller.js +264 -0
- package/src/controllers/notifications.controller.js +131 -0
- package/src/controllers/org.controller.js +357 -0
- package/src/controllers/orgAdmin.controller.js +491 -0
- package/src/controllers/stripeAdmin.controller.js +410 -0
- package/src/controllers/user.controller.js +361 -0
- package/src/controllers/userAdmin.controller.js +277 -0
- package/src/controllers/waitingList.controller.js +167 -0
- package/src/controllers/webhook.controller.js +200 -0
- package/src/middleware/auth.js +66 -0
- package/src/middleware/errorCapture.js +170 -0
- package/src/middleware/headlessApiTokenAuth.js +57 -0
- package/src/middleware/org.js +108 -0
- package/src/middleware.js +901 -0
- package/src/models/ActionEvent.js +31 -0
- package/src/models/ActivityLog.js +41 -0
- package/src/models/Asset.js +84 -0
- package/src/models/AuditEvent.js +93 -0
- package/src/models/EmailLog.js +28 -0
- package/src/models/ErrorAggregate.js +72 -0
- package/src/models/FormSubmission.js +41 -0
- package/src/models/GlobalSetting.js +38 -0
- package/src/models/HeadlessApiToken.js +24 -0
- package/src/models/HeadlessModelDefinition.js +41 -0
- package/src/models/I18nEntry.js +77 -0
- package/src/models/I18nLocale.js +33 -0
- package/src/models/Invite.js +70 -0
- package/src/models/JsonConfig.js +46 -0
- package/src/models/Notification.js +60 -0
- package/src/models/Organization.js +57 -0
- package/src/models/OrganizationMember.js +43 -0
- package/src/models/StripeCatalogItem.js +77 -0
- package/src/models/StripeWebhookEvent.js +57 -0
- package/src/models/User.js +89 -0
- package/src/models/VirtualEjsFile.js +60 -0
- package/src/models/VirtualEjsFileVersion.js +43 -0
- package/src/models/VirtualEjsGroupChange.js +32 -0
- package/src/models/WaitingList.js +41 -0
- package/src/models/Webhook.js +63 -0
- package/src/models/Workflow.js +29 -0
- package/src/models/WorkflowExecution.js +12 -0
- package/src/routes/admin.routes.js +26 -0
- package/src/routes/adminAssets.routes.js +28 -0
- package/src/routes/adminAssetsStorage.routes.js +13 -0
- package/src/routes/adminAudit.routes.js +196 -0
- package/src/routes/adminEjsVirtual.routes.js +17 -0
- package/src/routes/adminErrors.routes.js +164 -0
- package/src/routes/adminFeatureFlags.routes.js +12 -0
- package/src/routes/adminHeadless.routes.js +38 -0
- package/src/routes/adminI18n.routes.js +22 -0
- package/src/routes/adminJsonConfigs.routes.js +15 -0
- package/src/routes/adminLlm.routes.js +12 -0
- package/src/routes/adminMigration.routes.js +81 -0
- package/src/routes/adminSeoConfig.routes.js +20 -0
- package/src/routes/adminUploadNamespaces.routes.js +13 -0
- package/src/routes/assets.routes.js +21 -0
- package/src/routes/auth.routes.js +12 -0
- package/src/routes/billing.routes.js +11 -0
- package/src/routes/errorTracking.routes.js +31 -0
- package/src/routes/featureFlags.routes.js +9 -0
- package/src/routes/forms.routes.js +9 -0
- package/src/routes/formsAdmin.routes.js +13 -0
- package/src/routes/globalSettings.routes.js +18 -0
- package/src/routes/headless.routes.js +15 -0
- package/src/routes/i18n.routes.js +8 -0
- package/src/routes/invite.routes.js +9 -0
- package/src/routes/jsonConfigs.routes.js +8 -0
- package/src/routes/log.routes.js +111 -0
- package/src/routes/metrics.routes.js +9 -0
- package/src/routes/notificationAdmin.routes.js +15 -0
- package/src/routes/notifications.routes.js +12 -0
- package/src/routes/org.routes.js +31 -0
- package/src/routes/orgAdmin.routes.js +20 -0
- package/src/routes/publicAssets.routes.js +7 -0
- package/src/routes/stripeAdmin.routes.js +20 -0
- package/src/routes/user.routes.js +22 -0
- package/src/routes/userAdmin.routes.js +15 -0
- package/src/routes/waitingList.routes.js +13 -0
- package/src/routes/waitingListAdmin.routes.js +9 -0
- package/src/routes/webhook.routes.js +32 -0
- package/src/routes/workflowWebhook.routes.js +54 -0
- package/src/routes/workflows.routes.js +110 -0
- package/src/services/assets.service.js +110 -0
- package/src/services/audit.service.js +62 -0
- package/src/services/auditLogger.js +165 -0
- package/src/services/ejsVirtual.service.js +614 -0
- package/src/services/email.service.js +351 -0
- package/src/services/errorLogger.js +221 -0
- package/src/services/featureFlags.service.js +202 -0
- package/src/services/forms.service.js +214 -0
- package/src/services/globalSettings.service.js +49 -0
- package/src/services/headlessApiTokens.service.js +158 -0
- package/src/services/headlessCrypto.service.js +31 -0
- package/src/services/headlessModels.service.js +356 -0
- package/src/services/i18n.service.js +314 -0
- package/src/services/i18nInferredKeys.service.js +337 -0
- package/src/services/jsonConfigs.service.js +392 -0
- package/src/services/llm.service.js +749 -0
- package/src/services/migration.service.js +581 -0
- package/src/services/migrationAssets/fsLocal.js +58 -0
- package/src/services/migrationAssets/index.js +134 -0
- package/src/services/migrationAssets/s3.js +75 -0
- package/src/services/migrationAssets/sftp.js +92 -0
- package/src/services/notification.service.js +212 -0
- package/src/services/objectStorage.service.js +514 -0
- package/src/services/seoConfig.service.js +402 -0
- package/src/services/storage.js +150 -0
- package/src/services/stripe.service.js +185 -0
- package/src/services/stripeHelper.service.js +264 -0
- package/src/services/uploadNamespaces.service.js +326 -0
- package/src/services/webhook.service.js +157 -0
- package/src/services/workflow.service.js +271 -0
- package/src/utils/asyncHandler.js +5 -0
- package/src/utils/encryption.js +80 -0
- package/src/utils/jwt.js +40 -0
- package/src/utils/orgRoles.js +156 -0
- package/src/utils/validation.js +26 -0
- package/src/utils/webhookRetry.js +93 -0
- package/views/admin-assets.ejs +444 -0
- package/views/admin-audit.ejs +283 -0
- package/views/admin-coolify-deploy.ejs +207 -0
- package/views/admin-dashboard-home.ejs +291 -0
- package/views/admin-dashboard.ejs +397 -0
- package/views/admin-ejs-virtual.ejs +280 -0
- package/views/admin-errors.ejs +368 -0
- package/views/admin-feature-flags.ejs +390 -0
- package/views/admin-forms.ejs +526 -0
- package/views/admin-global-settings.ejs +436 -0
- package/views/admin-headless.ejs +2020 -0
- package/views/admin-i18n-locales.ejs +221 -0
- package/views/admin-i18n.ejs +728 -0
- package/views/admin-json-configs.ejs +410 -0
- package/views/admin-llm.ejs +884 -0
- package/views/admin-metrics.ejs +274 -0
- package/views/admin-migration.ejs +814 -0
- package/views/admin-notifications.ejs +430 -0
- package/views/admin-organizations.ejs +984 -0
- package/views/admin-seo-config.ejs +673 -0
- package/views/admin-stripe-pricing.ejs +558 -0
- package/views/admin-test.ejs +342 -0
- package/views/admin-users.ejs +452 -0
- package/views/admin-waiting-list.ejs +547 -0
- package/views/admin-webhooks.ejs +329 -0
- package/views/admin-workflows.ejs +310 -0
- package/views/partials/admin-assets-script.ejs +2022 -0
- package/views/partials/admin-test-sidebar.ejs +14 -0
- package/views/partials/dashboard/nav-items.ejs +66 -0
- package/views/partials/dashboard/palette.ejs +63 -0
- package/views/partials/dashboard/sidebar.ejs +21 -0
- package/views/partials/dashboard/tab-bar.ejs +26 -0
- package/views/partials/footer.ejs +3 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const Workflow = require('../models/Workflow');
|
|
4
|
+
const WorkflowExecution = require('../models/WorkflowExecution');
|
|
5
|
+
const workflowService = require('../services/workflow.service');
|
|
6
|
+
|
|
7
|
+
// List workflows
|
|
8
|
+
router.get('/', async (req, res) => {
|
|
9
|
+
const query = req.currentOrganization ? { organizationId: req.currentOrganization.id } : {};
|
|
10
|
+
const workflows = await Workflow.find(query).sort('-createdAt');
|
|
11
|
+
res.json(workflows);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Get single workflow
|
|
15
|
+
router.get('/:id', async (req, res) => {
|
|
16
|
+
const workflow = await Workflow.findById(req.params.id);
|
|
17
|
+
if (!workflow) return res.status(404).json({ error: 'Workflow not found' });
|
|
18
|
+
res.json(workflow);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Create workflow
|
|
22
|
+
router.post('/', async (req, res) => {
|
|
23
|
+
const workflow = await Workflow.create({
|
|
24
|
+
...req.body,
|
|
25
|
+
organizationId: req.currentOrganization?.id
|
|
26
|
+
});
|
|
27
|
+
res.status(201).json(workflow);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Update workflow
|
|
31
|
+
router.put('/:id', async (req, res) => {
|
|
32
|
+
const workflow = await Workflow.findByIdAndUpdate(req.params.id, req.body, { new: true });
|
|
33
|
+
if (!workflow) return res.status(404).json({ error: 'Workflow not found' });
|
|
34
|
+
res.json(workflow);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Delete workflow
|
|
38
|
+
router.delete('/:id', async (req, res) => {
|
|
39
|
+
await Workflow.findByIdAndDelete(req.params.id);
|
|
40
|
+
await WorkflowExecution.deleteMany({ workflowId: req.params.id });
|
|
41
|
+
res.json({ success: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Get executions
|
|
45
|
+
router.get('/:id/runs', async (req, res) => {
|
|
46
|
+
const executions = await WorkflowExecution.find({ workflowId: req.params.id }).sort('-executedAt').limit(50);
|
|
47
|
+
res.json(executions);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Test execution
|
|
51
|
+
router.post('/:id/test', async (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const initialContext = {
|
|
54
|
+
body: req.body.body || {},
|
|
55
|
+
query: req.body.query || {},
|
|
56
|
+
headers: req.body.headers || {},
|
|
57
|
+
method: req.body.method || 'POST'
|
|
58
|
+
};
|
|
59
|
+
const service = await workflowService.execute(req.params.id, initialContext);
|
|
60
|
+
res.json({
|
|
61
|
+
status: service.status,
|
|
62
|
+
log: service.executionLog,
|
|
63
|
+
context: service.context
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
res.status(500).json({ error: err.message });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Isolated node test
|
|
71
|
+
router.post('/:id/nodes/:nodeId/test', async (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const { WorkflowService } = require('../services/workflow.service');
|
|
74
|
+
const Workflow = require('../models/Workflow');
|
|
75
|
+
|
|
76
|
+
const workflow = await Workflow.findById(req.params.id);
|
|
77
|
+
if (!workflow) return res.status(404).json({ error: 'Workflow not found' });
|
|
78
|
+
|
|
79
|
+
// Use dirty node data from request body if provided, otherwise fallback to DB
|
|
80
|
+
const node = req.body.node || workflow.nodes.find(n => n.id === req.params.nodeId);
|
|
81
|
+
if (!node) return res.status(404).json({ error: 'Node not found' });
|
|
82
|
+
|
|
83
|
+
const service = new WorkflowService(req.params.id);
|
|
84
|
+
// Standard runNode logic but with the provided node object
|
|
85
|
+
service.context = {
|
|
86
|
+
...req.body.context,
|
|
87
|
+
entrypoint: req.body.context?.entrypoint || workflow.testDataset || {},
|
|
88
|
+
payload: req.body.context?.payload || workflow.testDataset || {},
|
|
89
|
+
nodes: req.body.context?.nodes || {},
|
|
90
|
+
lastNode: req.body.context?.lastNode || workflow.testDataset || {},
|
|
91
|
+
env: process.env
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const result = await service.executeNode(node);
|
|
95
|
+
|
|
96
|
+
// Persist result in the node's testResult field in DB
|
|
97
|
+
const nodeIndex = workflow.nodes.findIndex(n => n.id === req.params.nodeId);
|
|
98
|
+
if (nodeIndex !== -1) {
|
|
99
|
+
workflow.nodes[nodeIndex].testResult = result;
|
|
100
|
+
workflow.markModified('nodes');
|
|
101
|
+
await workflow.save();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
res.json({ result, context: service.context });
|
|
105
|
+
} catch (err) {
|
|
106
|
+
res.status(500).json({ error: err.message });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
module.exports = router;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const Asset = require('../models/Asset');
|
|
2
|
+
const objectStorage = require('./objectStorage.service');
|
|
3
|
+
|
|
4
|
+
async function getAssetById(id, { status = 'uploaded' } = {}) {
|
|
5
|
+
const asset = await Asset.findById(id);
|
|
6
|
+
if (!asset) {
|
|
7
|
+
const err = new Error('Asset not found');
|
|
8
|
+
err.code = 'NOT_FOUND';
|
|
9
|
+
throw err;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (status && asset.status !== status) {
|
|
13
|
+
const err = new Error('Asset not found');
|
|
14
|
+
err.code = 'NOT_FOUND';
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return asset.toObject();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function getAssetByKey(key, { status = 'uploaded' } = {}) {
|
|
22
|
+
const asset = await Asset.findOne({ key: String(key || '').trim() });
|
|
23
|
+
if (!asset) {
|
|
24
|
+
const err = new Error('Asset not found');
|
|
25
|
+
err.code = 'NOT_FOUND';
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (status && asset.status !== status) {
|
|
30
|
+
const err = new Error('Asset not found');
|
|
31
|
+
err.code = 'NOT_FOUND';
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return asset.toObject();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function listAssets({
|
|
39
|
+
namespace,
|
|
40
|
+
tag,
|
|
41
|
+
visibility,
|
|
42
|
+
status = 'uploaded',
|
|
43
|
+
page = 1,
|
|
44
|
+
limit = 50,
|
|
45
|
+
} = {}) {
|
|
46
|
+
const normalizedLimit = Math.min(100, Math.max(1, Number(limit) || 50));
|
|
47
|
+
const normalizedPage = Math.max(1, Number(page) || 1);
|
|
48
|
+
const skip = (normalizedPage - 1) * normalizedLimit;
|
|
49
|
+
|
|
50
|
+
const filter = {};
|
|
51
|
+
if (status) filter.status = status;
|
|
52
|
+
if (namespace) filter.namespace = String(namespace);
|
|
53
|
+
if (visibility) filter.visibility = String(visibility);
|
|
54
|
+
if (tag) filter.tags = String(tag).trim().toLowerCase();
|
|
55
|
+
|
|
56
|
+
const [assets, total] = await Promise.all([
|
|
57
|
+
Asset.find(filter).sort({ createdAt: -1 }).skip(skip).limit(normalizedLimit).lean(),
|
|
58
|
+
Asset.countDocuments(filter),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
assets,
|
|
63
|
+
pagination: {
|
|
64
|
+
page: normalizedPage,
|
|
65
|
+
limit: normalizedLimit,
|
|
66
|
+
total,
|
|
67
|
+
pages: Math.ceil(total / normalizedLimit),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function getAssetBytesById(id, { status = 'uploaded' } = {}) {
|
|
73
|
+
const asset = await getAssetById(id, { status });
|
|
74
|
+
const result = await objectStorage.getObject({ key: asset.key });
|
|
75
|
+
if (!result || !result.body) {
|
|
76
|
+
const err = new Error('File not found in storage');
|
|
77
|
+
err.code = 'NOT_FOUND';
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
asset,
|
|
83
|
+
contentType: result.contentType || asset.contentType,
|
|
84
|
+
body: result.body,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function getAssetBytesByKey(key, { status = 'uploaded' } = {}) {
|
|
89
|
+
const asset = await getAssetByKey(key, { status });
|
|
90
|
+
const result = await objectStorage.getObject({ key: asset.key });
|
|
91
|
+
if (!result || !result.body) {
|
|
92
|
+
const err = new Error('File not found in storage');
|
|
93
|
+
err.code = 'NOT_FOUND';
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
asset,
|
|
99
|
+
contentType: result.contentType || asset.contentType,
|
|
100
|
+
body: result.body,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
getAssetById,
|
|
106
|
+
getAssetByKey,
|
|
107
|
+
listAssets,
|
|
108
|
+
getAssetBytesById,
|
|
109
|
+
getAssetBytesByKey,
|
|
110
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const AuditEvent = require('../models/AuditEvent');
|
|
2
|
+
const webhookService = require('./webhook.service');
|
|
3
|
+
|
|
4
|
+
async function createAuditEvent({
|
|
5
|
+
actorType,
|
|
6
|
+
actorId,
|
|
7
|
+
action,
|
|
8
|
+
entityType,
|
|
9
|
+
entityId,
|
|
10
|
+
before,
|
|
11
|
+
after,
|
|
12
|
+
meta,
|
|
13
|
+
}) {
|
|
14
|
+
try {
|
|
15
|
+
await AuditEvent.create({
|
|
16
|
+
actorType,
|
|
17
|
+
actorId,
|
|
18
|
+
action,
|
|
19
|
+
entityType,
|
|
20
|
+
entityId,
|
|
21
|
+
before,
|
|
22
|
+
after,
|
|
23
|
+
meta,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Trigger Webhooks for critical events or all audit events
|
|
27
|
+
// Assuming organizationId is available in meta or can be inferred
|
|
28
|
+
const organizationId = meta?.organizationId || meta?.orgId;
|
|
29
|
+
if (organizationId) {
|
|
30
|
+
webhookService.emit('audit.event', {
|
|
31
|
+
action,
|
|
32
|
+
entityType,
|
|
33
|
+
entityId,
|
|
34
|
+
actorType,
|
|
35
|
+
actorId,
|
|
36
|
+
timestamp: new Date().toISOString()
|
|
37
|
+
}, organizationId);
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error creating audit event:', error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getBasicAuthActor(req) {
|
|
45
|
+
const authHeader = req.headers.authorization;
|
|
46
|
+
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
|
47
|
+
return { actorType: 'admin', actorId: null };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const credentials = Buffer.from(authHeader.substring(6), 'base64').toString('utf-8');
|
|
52
|
+
const [username] = credentials.split(':');
|
|
53
|
+
return { actorType: 'admin', actorId: username || null };
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return { actorType: 'admin', actorId: null };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
createAuditEvent,
|
|
61
|
+
getBasicAuthActor,
|
|
62
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const AuditEvent = require('../models/AuditEvent');
|
|
2
|
+
|
|
3
|
+
const SENSITIVE_KEYS = [
|
|
4
|
+
'password',
|
|
5
|
+
'token',
|
|
6
|
+
'secret',
|
|
7
|
+
'authorization',
|
|
8
|
+
'cookie',
|
|
9
|
+
'apikey',
|
|
10
|
+
'api_key',
|
|
11
|
+
'accesstoken',
|
|
12
|
+
'refreshtoken',
|
|
13
|
+
'passwordhash',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
async function getConfig() {
|
|
17
|
+
return {
|
|
18
|
+
auditTrackingEnabled: process.env.AUDIT_TRACKING_ENABLED !== 'false',
|
|
19
|
+
auditLogFailedAttempts: process.env.AUDIT_LOG_FAILED_ATTEMPTS !== 'false',
|
|
20
|
+
auditRetentionDays: parseInt(process.env.AUDIT_RETENTION_DAYS, 10) || 90,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function scrubValue(key, value) {
|
|
25
|
+
const lowerKey = String(key).toLowerCase();
|
|
26
|
+
for (const sensitive of SENSITIVE_KEYS) {
|
|
27
|
+
if (lowerKey.includes(sensitive)) {
|
|
28
|
+
return '[REDACTED]';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function scrubObject(obj, depth = 0) {
|
|
35
|
+
if (depth > 5 || !obj || typeof obj !== 'object') return obj;
|
|
36
|
+
if (Array.isArray(obj)) {
|
|
37
|
+
return obj.slice(0, 10).map((item) => scrubObject(item, depth + 1));
|
|
38
|
+
}
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
41
|
+
if (typeof value === 'object' && value !== null) {
|
|
42
|
+
result[key] = scrubObject(value, depth + 1);
|
|
43
|
+
} else {
|
|
44
|
+
result[key] = scrubValue(key, value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function extractContext(req) {
|
|
51
|
+
if (!req) return {};
|
|
52
|
+
return {
|
|
53
|
+
ip: req.ip || req.headers?.['x-forwarded-for']?.split(',')[0]?.trim() || req.connection?.remoteAddress,
|
|
54
|
+
userAgent: req.headers?.['user-agent']?.slice(0, 500),
|
|
55
|
+
requestId: req.headers?.['x-request-id'] || req.requestId,
|
|
56
|
+
path: req.path || req.url,
|
|
57
|
+
method: req.method,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractActor(req) {
|
|
62
|
+
if (!req) {
|
|
63
|
+
return { actorType: 'system', actorId: null };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (req.user) {
|
|
67
|
+
return {
|
|
68
|
+
actorType: 'user',
|
|
69
|
+
actorId: String(req.user._id || req.user.id),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const authHeader = req.headers?.authorization || '';
|
|
74
|
+
if (authHeader.startsWith('Basic ')) {
|
|
75
|
+
try {
|
|
76
|
+
const credentials = Buffer.from(authHeader.substring(6), 'base64').toString('utf-8');
|
|
77
|
+
const [username] = credentials.split(':');
|
|
78
|
+
return { actorType: 'admin', actorId: username || null };
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return { actorType: 'admin', actorId: null };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { actorType: 'system', actorId: null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function logAudit(event) {
|
|
88
|
+
try {
|
|
89
|
+
const config = await getConfig();
|
|
90
|
+
if (!config.auditTrackingEnabled) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (event.outcome === 'failure' && !config.auditLogFailedAttempts) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const actor = event.actor || extractActor(event.req);
|
|
99
|
+
const context = event.context || extractContext(event.req);
|
|
100
|
+
|
|
101
|
+
const auditEvent = await AuditEvent.create({
|
|
102
|
+
actorType: actor.actorType || 'system',
|
|
103
|
+
actorId: actor.actorId || null,
|
|
104
|
+
action: event.action,
|
|
105
|
+
entityType: event.entityType || event.targetType || 'unknown',
|
|
106
|
+
entityId: event.entityId || event.targetId || null,
|
|
107
|
+
before: event.before || null,
|
|
108
|
+
after: event.after || null,
|
|
109
|
+
meta: scrubObject({
|
|
110
|
+
...(event.meta || {}),
|
|
111
|
+
outcome: event.outcome || 'success',
|
|
112
|
+
context,
|
|
113
|
+
details: scrubObject(event.details || {}),
|
|
114
|
+
}),
|
|
115
|
+
outcome: event.outcome || 'success',
|
|
116
|
+
context,
|
|
117
|
+
targetType: event.targetType || event.entityType,
|
|
118
|
+
targetId: event.targetId || event.entityId,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return auditEvent;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
try {
|
|
124
|
+
console.log('[AuditLogger] Failed to log audit:', err.message);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// ignore
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function logAuditSync(event) {
|
|
133
|
+
setImmediate(() => {
|
|
134
|
+
logAudit(event).catch(() => {});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function auditMiddleware(action, options = {}) {
|
|
139
|
+
return (req, res, next) => {
|
|
140
|
+
const originalEnd = res.end;
|
|
141
|
+
res.end = function (...args) {
|
|
142
|
+
const outcome = res.statusCode >= 400 ? 'failure' : 'success';
|
|
143
|
+
logAuditSync({
|
|
144
|
+
req,
|
|
145
|
+
action,
|
|
146
|
+
outcome,
|
|
147
|
+
entityType: options.entityType || options.targetType,
|
|
148
|
+
entityId: options.getEntityId ? options.getEntityId(req) : (req.params?.id || req.params?.key),
|
|
149
|
+
details: options.getDetails ? options.getDetails(req, res) : undefined,
|
|
150
|
+
});
|
|
151
|
+
return originalEnd.apply(this, args);
|
|
152
|
+
};
|
|
153
|
+
next();
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
logAudit,
|
|
159
|
+
logAuditSync,
|
|
160
|
+
auditMiddleware,
|
|
161
|
+
getConfig,
|
|
162
|
+
extractActor,
|
|
163
|
+
extractContext,
|
|
164
|
+
scrubObject,
|
|
165
|
+
};
|