@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,167 @@
|
|
|
1
|
+
const WaitingList = require('../models/WaitingList');
|
|
2
|
+
const { validateEmail, sanitizeString } = require('../utils/validation');
|
|
3
|
+
|
|
4
|
+
// Subscribe to waiting list
|
|
5
|
+
exports.subscribe = async (req, res) => {
|
|
6
|
+
try {
|
|
7
|
+
const { email, type, referralSource } = req.body;
|
|
8
|
+
|
|
9
|
+
// Validate and sanitize email
|
|
10
|
+
if (!email) {
|
|
11
|
+
return res.status(400).json({
|
|
12
|
+
error: 'Email address is required',
|
|
13
|
+
field: 'email'
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const sanitizedEmail = sanitizeString(email);
|
|
18
|
+
if (!validateEmail(sanitizedEmail)) {
|
|
19
|
+
return res.status(400).json({
|
|
20
|
+
error: 'Please enter a valid email address',
|
|
21
|
+
field: 'email'
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate type (generic)
|
|
26
|
+
const sanitizedType = sanitizeString(type);
|
|
27
|
+
if (!sanitizedType || typeof sanitizedType !== 'string' || !sanitizedType.trim()) {
|
|
28
|
+
return res.status(400).json({
|
|
29
|
+
error: 'Please select your interest type',
|
|
30
|
+
field: 'type'
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if email already exists
|
|
35
|
+
const existingEntry = await WaitingList.findOne({ email: sanitizedEmail.toLowerCase() });
|
|
36
|
+
if (existingEntry) {
|
|
37
|
+
return res.status(409).json({
|
|
38
|
+
error: 'This email is already on our waiting list',
|
|
39
|
+
field: 'email'
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create new waiting list entry
|
|
44
|
+
const waitingListEntry = new WaitingList({
|
|
45
|
+
email: sanitizedEmail.toLowerCase(),
|
|
46
|
+
type: sanitizedType.trim(),
|
|
47
|
+
referralSource: sanitizeString(referralSource) || 'website'
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await waitingListEntry.save();
|
|
51
|
+
|
|
52
|
+
// Return success response without sensitive data
|
|
53
|
+
const response = waitingListEntry.toJSON();
|
|
54
|
+
delete response.email; // Don't return email in response for privacy
|
|
55
|
+
|
|
56
|
+
res.status(201).json({
|
|
57
|
+
message: 'Successfully joined the waiting list!',
|
|
58
|
+
data: response
|
|
59
|
+
});
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Waiting list subscription error:', error);
|
|
62
|
+
|
|
63
|
+
// Handle specific MongoDB errors
|
|
64
|
+
if (error.code === 11000) {
|
|
65
|
+
return res.status(409).json({
|
|
66
|
+
error: 'This email is already on our waiting list',
|
|
67
|
+
field: 'email'
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (error.name === 'ValidationError') {
|
|
72
|
+
const field = Object.keys(error.errors)[0];
|
|
73
|
+
return res.status(400).json({
|
|
74
|
+
error: error.errors[field].message,
|
|
75
|
+
field: field
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
res.status(500).json({
|
|
80
|
+
error: 'Something went wrong. Please try again later.',
|
|
81
|
+
field: 'general'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Get waiting list stats (public)
|
|
87
|
+
exports.getStats = async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const totalSubscribers = await WaitingList.countDocuments({ status: 'active' });
|
|
90
|
+
|
|
91
|
+
const typeAgg = await WaitingList.aggregate([
|
|
92
|
+
{ $match: { status: 'active' } },
|
|
93
|
+
{ $group: { _id: '$type', count: { $sum: 1 } } },
|
|
94
|
+
{ $sort: { count: -1, _id: 1 } },
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const typeCounts = (typeAgg || []).reduce((acc, row) => {
|
|
98
|
+
if (!row?._id) return acc;
|
|
99
|
+
acc[String(row._id)] = row.count || 0;
|
|
100
|
+
return acc;
|
|
101
|
+
}, {});
|
|
102
|
+
|
|
103
|
+
// Backward compatibility fields (legacy UI/tests)
|
|
104
|
+
const buyerCount = (typeCounts.buyer || 0) + (typeCounts.both || 0);
|
|
105
|
+
const sellerCount = (typeCounts.seller || 0) + (typeCounts.both || 0);
|
|
106
|
+
|
|
107
|
+
// Add some mock growth data for demonstration
|
|
108
|
+
const growthThisWeek = Math.floor(totalSubscribers * 0.05); // 5% growth
|
|
109
|
+
|
|
110
|
+
res.json({
|
|
111
|
+
totalSubscribers,
|
|
112
|
+
buyerCount,
|
|
113
|
+
sellerCount,
|
|
114
|
+
typeCounts,
|
|
115
|
+
growthThisWeek,
|
|
116
|
+
lastUpdated: new Date().toISOString()
|
|
117
|
+
});
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('Waiting list stats error:', error);
|
|
120
|
+
res.status(500).json({
|
|
121
|
+
error: 'Unable to load statistics',
|
|
122
|
+
field: 'general'
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Admin list waiting list entries (includes email)
|
|
128
|
+
exports.adminList = async (req, res) => {
|
|
129
|
+
try {
|
|
130
|
+
const {
|
|
131
|
+
status,
|
|
132
|
+
type,
|
|
133
|
+
email,
|
|
134
|
+
limit = 50,
|
|
135
|
+
offset = 0,
|
|
136
|
+
} = req.query;
|
|
137
|
+
|
|
138
|
+
const query = {};
|
|
139
|
+
if (status) query.status = String(status);
|
|
140
|
+
if (type) query.type = String(type);
|
|
141
|
+
if (email) query.email = String(email).trim().toLowerCase();
|
|
142
|
+
|
|
143
|
+
const parsedLimit = Math.min(500, Math.max(1, parseInt(limit, 10) || 50));
|
|
144
|
+
const parsedOffset = Math.max(0, parseInt(offset, 10) || 0);
|
|
145
|
+
|
|
146
|
+
const entries = await WaitingList.find(query)
|
|
147
|
+
.sort({ createdAt: -1 })
|
|
148
|
+
.limit(parsedLimit)
|
|
149
|
+
.skip(parsedOffset)
|
|
150
|
+
.select('email type status referralSource createdAt updatedAt')
|
|
151
|
+
.lean();
|
|
152
|
+
|
|
153
|
+
const total = await WaitingList.countDocuments(query);
|
|
154
|
+
|
|
155
|
+
return res.json({
|
|
156
|
+
entries,
|
|
157
|
+
pagination: {
|
|
158
|
+
total,
|
|
159
|
+
limit: parsedLimit,
|
|
160
|
+
offset: parsedOffset,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Waiting list admin list error:', error);
|
|
165
|
+
return res.status(500).json({ error: 'Failed to list entries' });
|
|
166
|
+
}
|
|
167
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const Webhook = require('../models/Webhook');
|
|
2
|
+
const webhookService = require('../services/webhook.service');
|
|
3
|
+
|
|
4
|
+
const webhookController = {
|
|
5
|
+
/**
|
|
6
|
+
* Get all webhooks for the current organization
|
|
7
|
+
*/
|
|
8
|
+
async getAll(req, res) {
|
|
9
|
+
try {
|
|
10
|
+
const organizationId = req.orgId || req.currentOrganization?._id || req.org?._id;
|
|
11
|
+
|
|
12
|
+
// If superadmin (Basic Auth), allow fetching all webhooks if no org context
|
|
13
|
+
const isBasicAuth = req.headers.authorization?.startsWith('Basic ');
|
|
14
|
+
|
|
15
|
+
const query = {};
|
|
16
|
+
if (organizationId) {
|
|
17
|
+
query.organizationId = organizationId;
|
|
18
|
+
} else if (!isBasicAuth) {
|
|
19
|
+
return res.status(400).json({ error: 'Organization context required' });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const webhooks = await Webhook.find(query);
|
|
23
|
+
res.json(webhooks);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
res.status(500).json({ error: error.message });
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a new webhook
|
|
31
|
+
*/
|
|
32
|
+
async create(req, res) {
|
|
33
|
+
try {
|
|
34
|
+
const organizationId = req.orgId || req.currentOrganization?._id || req.org?._id || req.body.organizationId || null;
|
|
35
|
+
const { name, targetUrl, events, metadata, timeout, isAsync } = req.body;
|
|
36
|
+
|
|
37
|
+
const isBasicAuth = req.headers.authorization?.startsWith('Basic ');
|
|
38
|
+
|
|
39
|
+
if (!organizationId && !isBasicAuth) {
|
|
40
|
+
return res.status(400).json({ error: 'Organization context required' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!targetUrl || !events || !Array.isArray(events)) {
|
|
44
|
+
return res.status(400).json({ error: 'targetUrl and events (array) are required' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check name uniqueness if provided
|
|
48
|
+
if (name) {
|
|
49
|
+
const existing = await Webhook.findOne({ name, organizationId });
|
|
50
|
+
if (existing) {
|
|
51
|
+
return res.status(400).json({ error: 'A webhook with this name already exists in this organization' });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const webhook = new Webhook({
|
|
56
|
+
name: name || undefined, // Let mongoose default trigger if name is empty
|
|
57
|
+
targetUrl,
|
|
58
|
+
events,
|
|
59
|
+
organizationId,
|
|
60
|
+
timeout: timeout || 5000,
|
|
61
|
+
isAsync: isAsync === true,
|
|
62
|
+
metadata: metadata || {}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await webhook.save();
|
|
66
|
+
res.status(201).json(webhook);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
res.status(500).json({ error: error.message });
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Update a webhook
|
|
74
|
+
*/
|
|
75
|
+
async update(req, res) {
|
|
76
|
+
try {
|
|
77
|
+
const { id } = req.params;
|
|
78
|
+
const organizationId = req.orgId || req.currentOrganization?._id || req.org?._id;
|
|
79
|
+
const isBasicAuth = req.headers.authorization?.startsWith('Basic ');
|
|
80
|
+
const { name, targetUrl, events, status, metadata, timeout, isAsync } = req.body;
|
|
81
|
+
|
|
82
|
+
const query = { _id: id };
|
|
83
|
+
if (!isBasicAuth && organizationId) {
|
|
84
|
+
query.organizationId = organizationId;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const webhook = await Webhook.findOne(query);
|
|
88
|
+
if (!webhook) {
|
|
89
|
+
return res.status(404).json({ error: 'Webhook not found' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (name && name !== webhook.name) {
|
|
93
|
+
const existing = await Webhook.findOne({ name, organizationId: webhook.organizationId, _id: { $ne: id } });
|
|
94
|
+
if (existing) {
|
|
95
|
+
return res.status(400).json({ error: 'A webhook with this name already exists in this organization' });
|
|
96
|
+
}
|
|
97
|
+
webhook.name = name;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (targetUrl) webhook.targetUrl = targetUrl;
|
|
101
|
+
if (events && Array.isArray(events)) webhook.events = events;
|
|
102
|
+
if (status) webhook.status = status;
|
|
103
|
+
if (metadata) webhook.metadata = metadata;
|
|
104
|
+
if (timeout !== undefined) webhook.timeout = timeout;
|
|
105
|
+
if (isAsync !== undefined) webhook.isAsync = isAsync;
|
|
106
|
+
|
|
107
|
+
await webhook.save();
|
|
108
|
+
res.json(webhook);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
res.status(500).json({ error: error.message });
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Delete a webhook
|
|
116
|
+
*/
|
|
117
|
+
async delete(req, res) {
|
|
118
|
+
try {
|
|
119
|
+
const { id } = req.params;
|
|
120
|
+
const organizationId = req.orgId || req.currentOrganization?._id || req.org?._id;
|
|
121
|
+
const isBasicAuth = req.headers.authorization?.startsWith('Basic ');
|
|
122
|
+
|
|
123
|
+
const query = { _id: id };
|
|
124
|
+
if (!isBasicAuth && organizationId) {
|
|
125
|
+
query.organizationId = organizationId;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const result = await Webhook.deleteOne(query);
|
|
129
|
+
if (result.deletedCount === 0) {
|
|
130
|
+
return res.status(404).json({ error: 'Webhook not found' });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
res.json({ message: 'Webhook deleted successfully' });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
res.status(500).json({ error: error.message });
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Test a webhook delivery
|
|
141
|
+
*/
|
|
142
|
+
async test(req, res) {
|
|
143
|
+
try {
|
|
144
|
+
const { id } = req.params;
|
|
145
|
+
const organizationId = req.orgId || req.currentOrganization?._id || req.org?._id;
|
|
146
|
+
const isBasicAuth = req.headers.authorization?.startsWith('Basic ');
|
|
147
|
+
|
|
148
|
+
const query = { _id: id };
|
|
149
|
+
if (!isBasicAuth && organizationId) {
|
|
150
|
+
query.organizationId = organizationId;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const webhook = await Webhook.findOne(query);
|
|
154
|
+
if (!webhook) {
|
|
155
|
+
return res.status(404).json({ error: 'Webhook not found' });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await webhookService.test(id);
|
|
159
|
+
res.json({ message: 'Test payload dispatched' });
|
|
160
|
+
} catch (error) {
|
|
161
|
+
res.status(500).json({ error: error.message });
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get delivery history for a webhook
|
|
167
|
+
*/
|
|
168
|
+
async getHistory(req, res) {
|
|
169
|
+
try {
|
|
170
|
+
const { id } = req.params;
|
|
171
|
+
const organizationId = req.orgId || req.currentOrganization?._id || req.org?._id;
|
|
172
|
+
const isBasicAuth = req.headers.authorization?.startsWith('Basic ');
|
|
173
|
+
const AuditEvent = require('../models/AuditEvent');
|
|
174
|
+
|
|
175
|
+
const query = { _id: id };
|
|
176
|
+
if (!isBasicAuth && organizationId) {
|
|
177
|
+
query.organizationId = organizationId;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const webhook = await Webhook.findOne(query);
|
|
181
|
+
if (!webhook) {
|
|
182
|
+
return res.status(404).json({ error: 'Webhook not found' });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const history = await AuditEvent.find({
|
|
186
|
+
entityType: 'Webhook',
|
|
187
|
+
entityId: id,
|
|
188
|
+
action: { $in: ['WEBHOOK_DELIVERY_SUCCESS', 'WEBHOOK_DELIVERY_FAILURE'] }
|
|
189
|
+
})
|
|
190
|
+
.sort({ createdAt: -1 })
|
|
191
|
+
.limit(50);
|
|
192
|
+
|
|
193
|
+
res.json(history);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
res.status(500).json({ error: error.message });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
module.exports = webhookController;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const { verifyAccessToken } = require("../utils/jwt");
|
|
2
|
+
const User = require("../models/User");
|
|
3
|
+
|
|
4
|
+
const authenticate = async (req, res, next) => {
|
|
5
|
+
try {
|
|
6
|
+
const authHeader = req.headers.authorization;
|
|
7
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
8
|
+
return res.status(401).json({ error: "No token provided" });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const token = authHeader.substring(7);
|
|
12
|
+
const decoded = verifyAccessToken(token);
|
|
13
|
+
|
|
14
|
+
const user = await User.findById(decoded.userId);
|
|
15
|
+
if (!user) {
|
|
16
|
+
return res.status(401).json({ error: "User not found" });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
req.user = user;
|
|
20
|
+
next();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return res
|
|
23
|
+
.status(401)
|
|
24
|
+
.json({ error: error.message || "Authentication failed" });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Basic auth middleware for admin routes
|
|
29
|
+
const basicAuth = (req, res, next) => {
|
|
30
|
+
const authHeader = req.headers.authorization;
|
|
31
|
+
|
|
32
|
+
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
|
33
|
+
res.setHeader("WWW-Authenticate", 'Basic realm="Admin Area"');
|
|
34
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const credentials = Buffer.from(authHeader.substring(6), "base64").toString(
|
|
38
|
+
"utf-8",
|
|
39
|
+
);
|
|
40
|
+
const [username, password] = credentials.split(":");
|
|
41
|
+
|
|
42
|
+
const adminUsername = process.env.ADMIN_USERNAME || "admin";
|
|
43
|
+
const adminPassword = process.env.ADMIN_PASSWORD || "admin";
|
|
44
|
+
|
|
45
|
+
if (username === adminUsername && password === adminPassword) {
|
|
46
|
+
next();
|
|
47
|
+
} else {
|
|
48
|
+
res.setHeader("WWW-Authenticate", 'Basic realm="Admin Area"');
|
|
49
|
+
return res.status(401).json({ error: "Invalid credentials" });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Admin role middleware - requires authentication and admin role
|
|
54
|
+
const requireAdmin = (req, res, next) => {
|
|
55
|
+
if (!req.user) {
|
|
56
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (req.user.role !== 'admin') {
|
|
60
|
+
return res.status(403).json({ error: "Admin access required" });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
next();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
module.exports = { authenticate, basicAuth, requireAdmin };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
const { logErrorSync, logError } = require('../services/errorLogger');
|
|
4
|
+
const { logAuditSync } = require('../services/auditLogger');
|
|
5
|
+
|
|
6
|
+
let originalConsoleError = null;
|
|
7
|
+
let isHooked = false;
|
|
8
|
+
|
|
9
|
+
function hookConsoleError() {
|
|
10
|
+
if (isHooked) return;
|
|
11
|
+
isHooked = true;
|
|
12
|
+
originalConsoleError = console.error;
|
|
13
|
+
|
|
14
|
+
console.error = function (...args) {
|
|
15
|
+
originalConsoleError.apply(console, args);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
let errorObj = null;
|
|
19
|
+
let message = '';
|
|
20
|
+
|
|
21
|
+
for (const arg of args) {
|
|
22
|
+
if (arg instanceof Error) {
|
|
23
|
+
errorObj = arg;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (errorObj) {
|
|
29
|
+
message = errorObj.message;
|
|
30
|
+
} else {
|
|
31
|
+
message = args
|
|
32
|
+
.map((a) => {
|
|
33
|
+
if (typeof a === 'string') return a;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.stringify(a);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return String(a);
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
.join(' ');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
logErrorSync({
|
|
44
|
+
source: 'backend',
|
|
45
|
+
severity: 'error',
|
|
46
|
+
errorName: errorObj?.name || 'ConsoleError',
|
|
47
|
+
message,
|
|
48
|
+
stack: errorObj?.stack,
|
|
49
|
+
extra: { consoleArgs: args.length },
|
|
50
|
+
});
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// avoid loops
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function unhookConsoleError() {
|
|
58
|
+
if (!isHooked || !originalConsoleError) return;
|
|
59
|
+
console.error = originalConsoleError;
|
|
60
|
+
isHooked = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function setupProcessHandlers() {
|
|
64
|
+
process.on('unhandledRejection', (reason) => {
|
|
65
|
+
try {
|
|
66
|
+
originalConsoleError
|
|
67
|
+
? originalConsoleError('[unhandledRejection]', reason)
|
|
68
|
+
: console.error('[unhandledRejection]', reason);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
// ignore
|
|
71
|
+
}
|
|
72
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
73
|
+
logErrorSync({
|
|
74
|
+
source: 'backend',
|
|
75
|
+
severity: 'fatal',
|
|
76
|
+
errorName: 'UnhandledRejection',
|
|
77
|
+
message: error.message,
|
|
78
|
+
stack: error.stack,
|
|
79
|
+
extra: { type: 'unhandledRejection' },
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
process.on('uncaughtException', (error, origin) => {
|
|
84
|
+
try {
|
|
85
|
+
originalConsoleError
|
|
86
|
+
? originalConsoleError('[uncaughtException]', error)
|
|
87
|
+
: console.error('[uncaughtException]', error);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// ignore
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
logError({
|
|
93
|
+
source: 'backend',
|
|
94
|
+
severity: 'fatal',
|
|
95
|
+
errorName: 'UncaughtException',
|
|
96
|
+
message: error.message,
|
|
97
|
+
stack: error.stack,
|
|
98
|
+
extra: { origin },
|
|
99
|
+
}).finally(() => {
|
|
100
|
+
if (process.env.EXIT_ON_UNCAUGHT_EXCEPTION === 'true') {
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function expressErrorMiddleware(err, req, res, next) {
|
|
108
|
+
const statusCode = err.status || err.statusCode || 500;
|
|
109
|
+
|
|
110
|
+
logErrorSync({
|
|
111
|
+
source: 'backend',
|
|
112
|
+
severity: statusCode >= 500 ? 'error' : 'warn',
|
|
113
|
+
errorName: err.name || 'Error',
|
|
114
|
+
errorCode: err.code,
|
|
115
|
+
message: err.message,
|
|
116
|
+
stack: err.stack,
|
|
117
|
+
actor: {
|
|
118
|
+
userId: req.user?._id || req.user?.id,
|
|
119
|
+
role: req.user?.role,
|
|
120
|
+
ip: req.ip || req.headers?.['x-forwarded-for']?.split(',')[0]?.trim(),
|
|
121
|
+
userAgent: req.headers?.['user-agent'],
|
|
122
|
+
},
|
|
123
|
+
request: {
|
|
124
|
+
method: req.method,
|
|
125
|
+
path: req.path,
|
|
126
|
+
statusCode,
|
|
127
|
+
requestId: req.headers?.['x-request-id'] || req.requestId,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (statusCode >= 400) {
|
|
132
|
+
logAuditSync({
|
|
133
|
+
req,
|
|
134
|
+
action: 'request.error',
|
|
135
|
+
outcome: 'failure',
|
|
136
|
+
entityType: 'request',
|
|
137
|
+
entityId: req.path,
|
|
138
|
+
details: {
|
|
139
|
+
errorName: err.name,
|
|
140
|
+
statusCode,
|
|
141
|
+
path: req.path,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (res.headersSent) {
|
|
147
|
+
return next(err);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
res.status(statusCode).json({
|
|
151
|
+
error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function requestIdMiddleware(req, res, next) {
|
|
156
|
+
if (!req.headers['x-request-id']) {
|
|
157
|
+
req.headers['x-request-id'] = crypto.randomUUID();
|
|
158
|
+
}
|
|
159
|
+
req.requestId = req.headers['x-request-id'];
|
|
160
|
+
res.setHeader('X-Request-Id', req.requestId);
|
|
161
|
+
next();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
hookConsoleError,
|
|
166
|
+
unhookConsoleError,
|
|
167
|
+
setupProcessHandlers,
|
|
168
|
+
expressErrorMiddleware,
|
|
169
|
+
requestIdMiddleware,
|
|
170
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const { authenticateApiToken, tokenAllowsOperation } = require('../services/headlessApiTokens.service');
|
|
2
|
+
|
|
3
|
+
function extractToken(req) {
|
|
4
|
+
const headerToken = req.headers['x-api-token'] || req.headers['x-api-key'];
|
|
5
|
+
if (headerToken) return String(headerToken).trim();
|
|
6
|
+
|
|
7
|
+
const auth = req.headers.authorization;
|
|
8
|
+
if (auth && auth.toLowerCase().startsWith('bearer ')) {
|
|
9
|
+
return String(auth.slice(7)).trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getOperationFromMethod(method) {
|
|
16
|
+
const m = String(method || '').toUpperCase();
|
|
17
|
+
if (m === 'GET') return 'read';
|
|
18
|
+
if (m === 'POST') return 'create';
|
|
19
|
+
if (m === 'PUT' || m === 'PATCH') return 'update';
|
|
20
|
+
if (m === 'DELETE') return 'delete';
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function headlessApiTokenAuth() {
|
|
25
|
+
return async (req, res, next) => {
|
|
26
|
+
try {
|
|
27
|
+
const token = extractToken(req);
|
|
28
|
+
const tokenDoc = await authenticateApiToken(token);
|
|
29
|
+
if (!tokenDoc) return res.status(401).json({ error: 'Invalid or expired API token' });
|
|
30
|
+
|
|
31
|
+
req.headlessApiToken = tokenDoc;
|
|
32
|
+
return next();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Headless API token auth error:', error);
|
|
35
|
+
return res.status(500).json({ error: 'Authentication failed' });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function requireHeadlessPermission() {
|
|
41
|
+
return (req, res, next) => {
|
|
42
|
+
const modelCode = req.params.modelCode;
|
|
43
|
+
const operation = getOperationFromMethod(req.method);
|
|
44
|
+
|
|
45
|
+
if (!operation) return res.status(400).json({ error: 'Unsupported operation' });
|
|
46
|
+
|
|
47
|
+
const ok = tokenAllowsOperation(req.headlessApiToken, modelCode, operation);
|
|
48
|
+
if (!ok) return res.status(403).json({ error: 'Insufficient permissions' });
|
|
49
|
+
|
|
50
|
+
return next();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
headlessApiTokenAuth,
|
|
56
|
+
requireHeadlessPermission,
|
|
57
|
+
};
|