@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.
Files changed (188) hide show
  1. package/.commiat +4 -0
  2. package/.env.example +47 -0
  3. package/README.md +110 -0
  4. package/index.js +94 -0
  5. package/package.json +67 -0
  6. package/public/css/styles.css +139 -0
  7. package/public/js/animations.js +41 -0
  8. package/sdk/error-tracking/browser/package.json +16 -0
  9. package/sdk/error-tracking/browser/src/core.js +270 -0
  10. package/sdk/error-tracking/browser/src/embed.js +18 -0
  11. package/sdk/error-tracking/browser/src/index.js +1 -0
  12. package/server.js +5 -0
  13. package/src/admin/endpointRegistry.js +300 -0
  14. package/src/controllers/admin.controller.js +321 -0
  15. package/src/controllers/adminAssets.controller.js +530 -0
  16. package/src/controllers/adminAssetsStorage.controller.js +260 -0
  17. package/src/controllers/adminEjsVirtual.controller.js +354 -0
  18. package/src/controllers/adminFeatureFlags.controller.js +155 -0
  19. package/src/controllers/adminHeadless.controller.js +1071 -0
  20. package/src/controllers/adminI18n.controller.js +604 -0
  21. package/src/controllers/adminJsonConfigs.controller.js +97 -0
  22. package/src/controllers/adminLlm.controller.js +273 -0
  23. package/src/controllers/adminMigration.controller.js +257 -0
  24. package/src/controllers/adminSeoConfig.controller.js +515 -0
  25. package/src/controllers/adminStats.controller.js +121 -0
  26. package/src/controllers/adminUploadNamespaces.controller.js +208 -0
  27. package/src/controllers/assets.controller.js +248 -0
  28. package/src/controllers/auth.controller.js +93 -0
  29. package/src/controllers/billing.controller.js +223 -0
  30. package/src/controllers/featureFlags.controller.js +35 -0
  31. package/src/controllers/forms.controller.js +217 -0
  32. package/src/controllers/globalSettings.controller.js +252 -0
  33. package/src/controllers/headlessCrud.controller.js +126 -0
  34. package/src/controllers/i18n.controller.js +12 -0
  35. package/src/controllers/invite.controller.js +249 -0
  36. package/src/controllers/jsonConfigs.controller.js +19 -0
  37. package/src/controllers/metrics.controller.js +149 -0
  38. package/src/controllers/notificationAdmin.controller.js +264 -0
  39. package/src/controllers/notifications.controller.js +131 -0
  40. package/src/controllers/org.controller.js +357 -0
  41. package/src/controllers/orgAdmin.controller.js +491 -0
  42. package/src/controllers/stripeAdmin.controller.js +410 -0
  43. package/src/controllers/user.controller.js +361 -0
  44. package/src/controllers/userAdmin.controller.js +277 -0
  45. package/src/controllers/waitingList.controller.js +167 -0
  46. package/src/controllers/webhook.controller.js +200 -0
  47. package/src/middleware/auth.js +66 -0
  48. package/src/middleware/errorCapture.js +170 -0
  49. package/src/middleware/headlessApiTokenAuth.js +57 -0
  50. package/src/middleware/org.js +108 -0
  51. package/src/middleware.js +901 -0
  52. package/src/models/ActionEvent.js +31 -0
  53. package/src/models/ActivityLog.js +41 -0
  54. package/src/models/Asset.js +84 -0
  55. package/src/models/AuditEvent.js +93 -0
  56. package/src/models/EmailLog.js +28 -0
  57. package/src/models/ErrorAggregate.js +72 -0
  58. package/src/models/FormSubmission.js +41 -0
  59. package/src/models/GlobalSetting.js +38 -0
  60. package/src/models/HeadlessApiToken.js +24 -0
  61. package/src/models/HeadlessModelDefinition.js +41 -0
  62. package/src/models/I18nEntry.js +77 -0
  63. package/src/models/I18nLocale.js +33 -0
  64. package/src/models/Invite.js +70 -0
  65. package/src/models/JsonConfig.js +46 -0
  66. package/src/models/Notification.js +60 -0
  67. package/src/models/Organization.js +57 -0
  68. package/src/models/OrganizationMember.js +43 -0
  69. package/src/models/StripeCatalogItem.js +77 -0
  70. package/src/models/StripeWebhookEvent.js +57 -0
  71. package/src/models/User.js +89 -0
  72. package/src/models/VirtualEjsFile.js +60 -0
  73. package/src/models/VirtualEjsFileVersion.js +43 -0
  74. package/src/models/VirtualEjsGroupChange.js +32 -0
  75. package/src/models/WaitingList.js +41 -0
  76. package/src/models/Webhook.js +63 -0
  77. package/src/models/Workflow.js +29 -0
  78. package/src/models/WorkflowExecution.js +12 -0
  79. package/src/routes/admin.routes.js +26 -0
  80. package/src/routes/adminAssets.routes.js +28 -0
  81. package/src/routes/adminAssetsStorage.routes.js +13 -0
  82. package/src/routes/adminAudit.routes.js +196 -0
  83. package/src/routes/adminEjsVirtual.routes.js +17 -0
  84. package/src/routes/adminErrors.routes.js +164 -0
  85. package/src/routes/adminFeatureFlags.routes.js +12 -0
  86. package/src/routes/adminHeadless.routes.js +38 -0
  87. package/src/routes/adminI18n.routes.js +22 -0
  88. package/src/routes/adminJsonConfigs.routes.js +15 -0
  89. package/src/routes/adminLlm.routes.js +12 -0
  90. package/src/routes/adminMigration.routes.js +81 -0
  91. package/src/routes/adminSeoConfig.routes.js +20 -0
  92. package/src/routes/adminUploadNamespaces.routes.js +13 -0
  93. package/src/routes/assets.routes.js +21 -0
  94. package/src/routes/auth.routes.js +12 -0
  95. package/src/routes/billing.routes.js +11 -0
  96. package/src/routes/errorTracking.routes.js +31 -0
  97. package/src/routes/featureFlags.routes.js +9 -0
  98. package/src/routes/forms.routes.js +9 -0
  99. package/src/routes/formsAdmin.routes.js +13 -0
  100. package/src/routes/globalSettings.routes.js +18 -0
  101. package/src/routes/headless.routes.js +15 -0
  102. package/src/routes/i18n.routes.js +8 -0
  103. package/src/routes/invite.routes.js +9 -0
  104. package/src/routes/jsonConfigs.routes.js +8 -0
  105. package/src/routes/log.routes.js +111 -0
  106. package/src/routes/metrics.routes.js +9 -0
  107. package/src/routes/notificationAdmin.routes.js +15 -0
  108. package/src/routes/notifications.routes.js +12 -0
  109. package/src/routes/org.routes.js +31 -0
  110. package/src/routes/orgAdmin.routes.js +20 -0
  111. package/src/routes/publicAssets.routes.js +7 -0
  112. package/src/routes/stripeAdmin.routes.js +20 -0
  113. package/src/routes/user.routes.js +22 -0
  114. package/src/routes/userAdmin.routes.js +15 -0
  115. package/src/routes/waitingList.routes.js +13 -0
  116. package/src/routes/waitingListAdmin.routes.js +9 -0
  117. package/src/routes/webhook.routes.js +32 -0
  118. package/src/routes/workflowWebhook.routes.js +54 -0
  119. package/src/routes/workflows.routes.js +110 -0
  120. package/src/services/assets.service.js +110 -0
  121. package/src/services/audit.service.js +62 -0
  122. package/src/services/auditLogger.js +165 -0
  123. package/src/services/ejsVirtual.service.js +614 -0
  124. package/src/services/email.service.js +351 -0
  125. package/src/services/errorLogger.js +221 -0
  126. package/src/services/featureFlags.service.js +202 -0
  127. package/src/services/forms.service.js +214 -0
  128. package/src/services/globalSettings.service.js +49 -0
  129. package/src/services/headlessApiTokens.service.js +158 -0
  130. package/src/services/headlessCrypto.service.js +31 -0
  131. package/src/services/headlessModels.service.js +356 -0
  132. package/src/services/i18n.service.js +314 -0
  133. package/src/services/i18nInferredKeys.service.js +337 -0
  134. package/src/services/jsonConfigs.service.js +392 -0
  135. package/src/services/llm.service.js +749 -0
  136. package/src/services/migration.service.js +581 -0
  137. package/src/services/migrationAssets/fsLocal.js +58 -0
  138. package/src/services/migrationAssets/index.js +134 -0
  139. package/src/services/migrationAssets/s3.js +75 -0
  140. package/src/services/migrationAssets/sftp.js +92 -0
  141. package/src/services/notification.service.js +212 -0
  142. package/src/services/objectStorage.service.js +514 -0
  143. package/src/services/seoConfig.service.js +402 -0
  144. package/src/services/storage.js +150 -0
  145. package/src/services/stripe.service.js +185 -0
  146. package/src/services/stripeHelper.service.js +264 -0
  147. package/src/services/uploadNamespaces.service.js +326 -0
  148. package/src/services/webhook.service.js +157 -0
  149. package/src/services/workflow.service.js +271 -0
  150. package/src/utils/asyncHandler.js +5 -0
  151. package/src/utils/encryption.js +80 -0
  152. package/src/utils/jwt.js +40 -0
  153. package/src/utils/orgRoles.js +156 -0
  154. package/src/utils/validation.js +26 -0
  155. package/src/utils/webhookRetry.js +93 -0
  156. package/views/admin-assets.ejs +444 -0
  157. package/views/admin-audit.ejs +283 -0
  158. package/views/admin-coolify-deploy.ejs +207 -0
  159. package/views/admin-dashboard-home.ejs +291 -0
  160. package/views/admin-dashboard.ejs +397 -0
  161. package/views/admin-ejs-virtual.ejs +280 -0
  162. package/views/admin-errors.ejs +368 -0
  163. package/views/admin-feature-flags.ejs +390 -0
  164. package/views/admin-forms.ejs +526 -0
  165. package/views/admin-global-settings.ejs +436 -0
  166. package/views/admin-headless.ejs +2020 -0
  167. package/views/admin-i18n-locales.ejs +221 -0
  168. package/views/admin-i18n.ejs +728 -0
  169. package/views/admin-json-configs.ejs +410 -0
  170. package/views/admin-llm.ejs +884 -0
  171. package/views/admin-metrics.ejs +274 -0
  172. package/views/admin-migration.ejs +814 -0
  173. package/views/admin-notifications.ejs +430 -0
  174. package/views/admin-organizations.ejs +984 -0
  175. package/views/admin-seo-config.ejs +673 -0
  176. package/views/admin-stripe-pricing.ejs +558 -0
  177. package/views/admin-test.ejs +342 -0
  178. package/views/admin-users.ejs +452 -0
  179. package/views/admin-waiting-list.ejs +547 -0
  180. package/views/admin-webhooks.ejs +329 -0
  181. package/views/admin-workflows.ejs +310 -0
  182. package/views/partials/admin-assets-script.ejs +2022 -0
  183. package/views/partials/admin-test-sidebar.ejs +14 -0
  184. package/views/partials/dashboard/nav-items.ejs +66 -0
  185. package/views/partials/dashboard/palette.ejs +63 -0
  186. package/views/partials/dashboard/sidebar.ejs +21 -0
  187. package/views/partials/dashboard/tab-bar.ejs +26 -0
  188. 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
+ };