@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,326 @@
1
+ const GlobalSetting = require('../models/GlobalSetting');
2
+ const globalSettingsService = require('./globalSettings.service');
3
+ const objectStorage = require('./objectStorage.service');
4
+
5
+ const UPLOAD_NAMESPACE_PREFIX = 'UPLOAD_NAMESPACE.';
6
+
7
+ const stripPrefix = (key) => {
8
+ if (!key) return key;
9
+ if (!key.startsWith(UPLOAD_NAMESPACE_PREFIX)) return key;
10
+ return key.slice(UPLOAD_NAMESPACE_PREFIX.length);
11
+ };
12
+
13
+ const parseJson = (value) => {
14
+ if (!value) return {};
15
+ if (typeof value === 'object') return value;
16
+ try {
17
+ return JSON.parse(value);
18
+ } catch {
19
+ return {};
20
+ }
21
+ };
22
+
23
+ const MAX_FILE_SIZE_HARD_CAP_SETTING_KEY = 'MAX_FILE_SIZE_HARD_CAP';
24
+
25
+ const DEFAULT_MAX_FILE_SIZE_BYTES = 10485760;
26
+
27
+ const getEnvHardCapMaxFileSizeBytes = () => {
28
+ const raw = process.env.MAX_FILE_SIZE_HARD_CAP || process.env.MAX_FILE_SIZE || String(DEFAULT_MAX_FILE_SIZE_BYTES);
29
+ const parsed = Number(raw);
30
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_FILE_SIZE_BYTES;
31
+ return parsed;
32
+ };
33
+
34
+ const getEffectiveHardCapMaxFileSizeBytes = async () => {
35
+ const raw = await globalSettingsService.getSettingValue(MAX_FILE_SIZE_HARD_CAP_SETTING_KEY, null);
36
+ if (raw === null || raw === undefined || raw === '') return getEnvHardCapMaxFileSizeBytes();
37
+
38
+ const parsed = Number(raw);
39
+ if (!Number.isFinite(parsed) || parsed <= 0) return getEnvHardCapMaxFileSizeBytes();
40
+
41
+ return parsed;
42
+ };
43
+
44
+ const getConfiguredHardCapMaxFileSizeBytes = async () => {
45
+ const raw = await globalSettingsService.getSettingValue(MAX_FILE_SIZE_HARD_CAP_SETTING_KEY, null);
46
+ if (raw === null || raw === undefined || raw === '') return null;
47
+
48
+ const parsed = Number(raw);
49
+ if (!Number.isFinite(parsed) || parsed <= 0) return null;
50
+
51
+ return parsed;
52
+ };
53
+
54
+ const normalizePayload = (namespaceKey, payload) => {
55
+ const enabled = payload?.enabled === undefined ? true : Boolean(payload.enabled);
56
+
57
+ let maxFileSizeBytes = payload?.maxFileSizeBytes;
58
+ if (maxFileSizeBytes === undefined || maxFileSizeBytes === null || maxFileSizeBytes === '') {
59
+ maxFileSizeBytes = undefined;
60
+ }
61
+ if (maxFileSizeBytes !== undefined) {
62
+ maxFileSizeBytes = Number(maxFileSizeBytes);
63
+ if (!Number.isFinite(maxFileSizeBytes) || maxFileSizeBytes <= 0) {
64
+ maxFileSizeBytes = undefined;
65
+ }
66
+ }
67
+
68
+ const normalizeArray = (value) => {
69
+ if (value === undefined) return undefined;
70
+ if (value === null) return [];
71
+ if (Array.isArray(value)) return value.map(String).filter(Boolean);
72
+ return [String(value)].filter(Boolean);
73
+ };
74
+
75
+ const allowedContentTypes = normalizeArray(payload?.allowedContentTypes);
76
+
77
+ const defaultVisibility = payload?.defaultVisibility ? String(payload.defaultVisibility) : undefined;
78
+ const enforceVisibility = payload?.enforceVisibility === undefined ? false : Boolean(payload.enforceVisibility);
79
+
80
+ const keyPrefix = payload?.keyPrefix !== undefined ? String(payload.keyPrefix || '') : undefined;
81
+
82
+ return {
83
+ key: String(namespaceKey),
84
+ enabled,
85
+ maxFileSizeBytes,
86
+ allowedContentTypes,
87
+ keyPrefix,
88
+ defaultVisibility,
89
+ enforceVisibility,
90
+ };
91
+ };
92
+
93
+ const getSettingKey = (namespaceKey) => `${UPLOAD_NAMESPACE_PREFIX}${namespaceKey}`;
94
+
95
+ const getDefaultNamespaceConfig = (hardCapMaxFileSizeBytes) => {
96
+ const hardCap = hardCapMaxFileSizeBytes ?? getEnvHardCapMaxFileSizeBytes();
97
+ return {
98
+ key: 'default',
99
+ enabled: true,
100
+ maxFileSizeBytes: hardCap,
101
+ allowedContentTypes: objectStorage.getAllowedContentTypes(),
102
+ keyPrefix: undefined,
103
+ defaultVisibility: 'private',
104
+ enforceVisibility: false,
105
+ };
106
+ };
107
+
108
+ const mergeWithDefault = (config, fallback) => {
109
+ const base = fallback || getDefaultNamespaceConfig();
110
+
111
+ const allowedContentTypes =
112
+ config.allowedContentTypes === undefined ? base.allowedContentTypes : config.allowedContentTypes;
113
+
114
+ const keyPrefix = config.keyPrefix === undefined ? base.keyPrefix : config.keyPrefix;
115
+
116
+ const defaultVisibility = config.defaultVisibility === undefined ? base.defaultVisibility : config.defaultVisibility;
117
+
118
+ return {
119
+ ...base,
120
+ ...config,
121
+ allowedContentTypes,
122
+ keyPrefix,
123
+ defaultVisibility,
124
+ };
125
+ };
126
+
127
+ async function listNamespaces() {
128
+ const settings = await GlobalSetting.find({
129
+ key: { $regex: `^${UPLOAD_NAMESPACE_PREFIX}` },
130
+ type: 'json',
131
+ })
132
+ .sort({ key: 1 })
133
+ .lean();
134
+
135
+ return settings.map((s) => {
136
+ const namespaceKey = stripPrefix(s.key);
137
+ const raw = parseJson(s.value);
138
+ const normalized = normalizePayload(namespaceKey, raw);
139
+ const merged = mergeWithDefault(normalized);
140
+
141
+ return {
142
+ ...merged,
143
+ createdAt: s.createdAt,
144
+ updatedAt: s.updatedAt,
145
+ };
146
+ });
147
+ }
148
+
149
+ async function resolveNamespace(namespaceKey) {
150
+ const requested = namespaceKey ? String(namespaceKey).trim() : '';
151
+ const key = requested || 'default';
152
+
153
+ const effectiveHardCap = await getEffectiveHardCapMaxFileSizeBytes();
154
+
155
+ if (key === 'default') {
156
+ try {
157
+ const value = await globalSettingsService.getSettingValue(getSettingKey('default'), null);
158
+ if (!value) return getDefaultNamespaceConfig(effectiveHardCap);
159
+
160
+ const raw = parseJson(value);
161
+ const normalized = normalizePayload('default', raw);
162
+ const merged = mergeWithDefault(normalized, getDefaultNamespaceConfig(effectiveHardCap));
163
+ const clamped = {
164
+ ...merged,
165
+ maxFileSizeBytes: Math.min(merged.maxFileSizeBytes ?? effectiveHardCap, effectiveHardCap),
166
+ };
167
+ return clamped.enabled ? clamped : getDefaultNamespaceConfig(effectiveHardCap);
168
+ } catch {
169
+ return getDefaultNamespaceConfig(effectiveHardCap);
170
+ }
171
+ }
172
+
173
+ const rawValue = await globalSettingsService.getSettingValue(getSettingKey(key), null);
174
+ if (!rawValue) {
175
+ return resolveNamespace('default');
176
+ }
177
+
178
+ const raw = parseJson(rawValue);
179
+ const normalized = normalizePayload(key, raw);
180
+ const merged = mergeWithDefault(normalized, getDefaultNamespaceConfig(effectiveHardCap));
181
+
182
+ const clamped = {
183
+ ...merged,
184
+ maxFileSizeBytes: Math.min(merged.maxFileSizeBytes ?? effectiveHardCap, effectiveHardCap),
185
+ };
186
+
187
+ if (!clamped.enabled) {
188
+ return resolveNamespace('default');
189
+ }
190
+
191
+ return clamped;
192
+ }
193
+
194
+ async function upsertNamespace(namespaceKey, payload) {
195
+ const key = String(namespaceKey || '').trim();
196
+ if (!key) {
197
+ const err = new Error('namespaceKey is required');
198
+ err.code = 'VALIDATION_ERROR';
199
+ throw err;
200
+ }
201
+
202
+ const normalized = normalizePayload(key, payload || {});
203
+ const settingKey = getSettingKey(key);
204
+
205
+ const existing = await GlobalSetting.findOne({ key: settingKey, type: 'json' });
206
+
207
+ const setting = existing
208
+ ? await GlobalSetting.findOneAndUpdate(
209
+ { key: settingKey, type: 'json' },
210
+ { $set: { value: JSON.stringify(normalized) } },
211
+ { new: true }
212
+ )
213
+ : await GlobalSetting.create({
214
+ key: settingKey,
215
+ type: 'json',
216
+ public: false,
217
+ description: `Upload namespace: ${key}`,
218
+ value: JSON.stringify(normalized),
219
+ });
220
+
221
+ globalSettingsService.clearSettingsCache();
222
+
223
+ const resolved = await resolveNamespace(key);
224
+ return {
225
+ ...resolved,
226
+ createdAt: setting.createdAt,
227
+ updatedAt: setting.updatedAt,
228
+ };
229
+ }
230
+
231
+ function validateUpload({ namespaceConfig, contentType, sizeBytes, hardCapMaxFileSizeBytes }) {
232
+ const errors = [];
233
+
234
+ const hardCap = hardCapMaxFileSizeBytes ?? getEnvHardCapMaxFileSizeBytes();
235
+ const maxSize = Math.min(namespaceConfig?.maxFileSizeBytes ?? hardCap, hardCap);
236
+ if (typeof sizeBytes === 'number' && sizeBytes > maxSize) {
237
+ errors.push({ field: 'sizeBytes', reason: 'File too large', maxFileSizeBytes: maxSize });
238
+ }
239
+
240
+ const normalizeAllowedEntry = (entry) => {
241
+ if (entry === undefined || entry === null) return '';
242
+ return String(entry).trim().toLowerCase();
243
+ };
244
+
245
+ const matchesAllowedContentType = (allowedEntry, actualContentType) => {
246
+ const allowedNormalized = normalizeAllowedEntry(allowedEntry);
247
+ const actualNormalized = normalizeAllowedEntry(actualContentType);
248
+
249
+ if (!allowedNormalized || !actualNormalized) return false;
250
+
251
+ // Exact match: image/png
252
+ if (allowedNormalized === actualNormalized) return true;
253
+
254
+ // Wildcard match: image/*
255
+ if (allowedNormalized.endsWith('/*')) {
256
+ const prefix = allowedNormalized.slice(0, -1); // keep trailing '/'
257
+ return actualNormalized.startsWith(prefix);
258
+ }
259
+
260
+ // Shorthand: image, video, audio, application
261
+ if (!allowedNormalized.includes('/')) {
262
+ return actualNormalized.startsWith(`${allowedNormalized}/`);
263
+ }
264
+
265
+ return false;
266
+ };
267
+
268
+ const allowed = namespaceConfig?.allowedContentTypes;
269
+ if (Array.isArray(allowed) && allowed.length > 0) {
270
+ const ok = allowed.some((entry) => matchesAllowedContentType(entry, contentType));
271
+ if (!ok) {
272
+ errors.push({ field: 'contentType', reason: 'Invalid file type', allowedContentTypes: allowed });
273
+ }
274
+ }
275
+
276
+ return {
277
+ ok: errors.length === 0,
278
+ errors,
279
+ };
280
+ }
281
+
282
+ function computeVisibility({ namespaceConfig, requestedVisibility }) {
283
+ const requested = requestedVisibility === 'public' ? 'public' : requestedVisibility === 'private' ? 'private' : null;
284
+
285
+ const defaultVis = namespaceConfig?.defaultVisibility === 'public' ? 'public' : 'private';
286
+
287
+ if (namespaceConfig?.enforceVisibility) {
288
+ return defaultVis;
289
+ }
290
+
291
+ return requested || defaultVis;
292
+ }
293
+
294
+ function computeKeyPrefix(namespaceConfig) {
295
+ const prefix = namespaceConfig?.keyPrefix;
296
+ const namespaceKey = namespaceConfig?.key ? String(namespaceConfig.key).trim() : 'default';
297
+ const safeNamespaceKey = namespaceKey ? namespaceKey.replace(/^\/+/, '').replace(/\/+$/, '') : 'default';
298
+
299
+ if (prefix === undefined || prefix === null) return `assets/${safeNamespaceKey}`;
300
+ const trimmed = String(prefix).trim();
301
+ return trimmed ? trimmed.replace(/^\/+/, '').replace(/\/+$/, '') : `assets/${safeNamespaceKey}`;
302
+ }
303
+
304
+ function generateObjectKey({ namespaceConfig, originalName }) {
305
+ const prefix = computeKeyPrefix(namespaceConfig);
306
+ return objectStorage.generateKey(originalName, prefix);
307
+ }
308
+
309
+ module.exports = {
310
+ UPLOAD_NAMESPACE_PREFIX,
311
+ MAX_FILE_SIZE_HARD_CAP_SETTING_KEY,
312
+ DEFAULT_MAX_FILE_SIZE_BYTES,
313
+ getEnvHardCapMaxFileSizeBytes,
314
+ getEffectiveHardCapMaxFileSizeBytes,
315
+ getConfiguredHardCapMaxFileSizeBytes,
316
+ getDefaultNamespaceConfig,
317
+ normalizePayload,
318
+ getSettingKey,
319
+ listNamespaces,
320
+ resolveNamespace,
321
+ upsertNamespace,
322
+ validateUpload,
323
+ computeVisibility,
324
+ generateObjectKey,
325
+ computeKeyPrefix,
326
+ };
@@ -0,0 +1,157 @@
1
+ const crypto = require('crypto');
2
+ const axios = require('axios');
3
+ const Webhook = require('../models/Webhook');
4
+
5
+ class WebhookService {
6
+ /**
7
+ * Emit an event to all subscribed webhooks for an organization
8
+ * @param {string} event - Event name (e.g., 'form.submitted')
9
+ * @param {Object} data - Payload data
10
+ * @param {string} organizationId - Organization ID
11
+ */
12
+ async emit(event, data, organizationId) {
13
+ try {
14
+ const webhooks = await Webhook.find({
15
+ organizationId,
16
+ events: event,
17
+ status: 'active'
18
+ });
19
+
20
+ if (!webhooks || webhooks.length === 0) return;
21
+
22
+ const payload = {
23
+ event,
24
+ timestamp: new Date().toISOString(),
25
+ organizationId,
26
+ data
27
+ };
28
+
29
+ const promises = webhooks.map(webhook => this.deliver(webhook, payload));
30
+ // Non-blocking execution
31
+ Promise.all(promises).catch(err => {
32
+ console.error('Error delivering some webhooks:', err);
33
+ });
34
+
35
+ } catch (error) {
36
+ console.error('WebhookService.emit error:', error);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Deliver payload to a specific webhook
42
+ * @param {Object} webhook - Webhook model instance
43
+ * @param {Object} payload - Payload object
44
+ */
45
+ async deliver(webhook, payload) {
46
+ const signature = crypto
47
+ .createHmac('sha256', webhook.secret)
48
+ .update(JSON.stringify(payload))
49
+ .digest('hex');
50
+
51
+ const timeout = webhook.timeout || 5000;
52
+ const isAsync = webhook.isAsync || false;
53
+
54
+ if (isAsync) {
55
+ // Fire and forget
56
+ axios.post(webhook.targetUrl, payload, {
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ 'X-SaaS-Signature': signature,
60
+ 'User-Agent': 'SaaSBackend-Webhook/1.0'
61
+ },
62
+ timeout: timeout
63
+ }).catch(err => {
64
+ console.error(`Async webhook delivery to ${webhook.targetUrl} failed:`, err.message);
65
+ });
66
+
67
+ // Log success immediately for async
68
+ const AuditEvent = require('../models/AuditEvent');
69
+ await AuditEvent.create({
70
+ actorType: 'system',
71
+ actorId: 'webhook-service',
72
+ action: 'WEBHOOK_DELIVERY_ASYNC_DISPATCHED',
73
+ entityType: 'Webhook',
74
+ entityId: webhook._id,
75
+ meta: {
76
+ event: payload.event,
77
+ targetUrl: webhook.targetUrl,
78
+ mode: 'async',
79
+ payload
80
+ }
81
+ });
82
+ return;
83
+ }
84
+
85
+ try {
86
+ await axios.post(webhook.targetUrl, payload, {
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ 'X-SaaS-Signature': signature,
90
+ 'User-Agent': 'SaaSBackend-Webhook/1.0'
91
+ },
92
+ timeout: timeout
93
+ });
94
+
95
+ // Reset status if it was previously failed/paused and now succeeds
96
+ if (webhook.status === 'failed') {
97
+ webhook.status = 'active';
98
+ await webhook.save();
99
+ }
100
+
101
+ // Log success to audit
102
+ const AuditEvent = require('../models/AuditEvent');
103
+ await AuditEvent.create({
104
+ actorType: 'system',
105
+ actorId: 'webhook-service',
106
+ action: 'WEBHOOK_DELIVERY_SUCCESS',
107
+ entityType: 'Webhook',
108
+ entityId: webhook._id,
109
+ meta: {
110
+ event: payload.event,
111
+ targetUrl: webhook.targetUrl,
112
+ statusCode: 200,
113
+ payload
114
+ }
115
+ });
116
+ } catch (error) {
117
+ console.error(`Failed to deliver webhook to ${webhook.targetUrl}:`, error.message);
118
+
119
+ // Log failure to audit
120
+ const AuditEvent = require('../models/AuditEvent');
121
+ await AuditEvent.create({
122
+ actorType: 'system',
123
+ actorId: 'webhook-service',
124
+ action: 'WEBHOOK_DELIVERY_FAILURE',
125
+ entityType: 'Webhook',
126
+ entityId: webhook._id,
127
+ meta: {
128
+ event: payload.event,
129
+ targetUrl: webhook.targetUrl,
130
+ error: error.message,
131
+ statusCode: error.response?.status,
132
+ payload
133
+ }
134
+ });
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Send a test ping to a webhook
140
+ * @param {string} webhookId - Webhook ID
141
+ */
142
+ async test(webhookId) {
143
+ const webhook = await Webhook.findById(webhookId);
144
+ if (!webhook) throw new Error('Webhook not found');
145
+
146
+ const payload = {
147
+ event: 'webhook.test',
148
+ timestamp: new Date().toISOString(),
149
+ organizationId: webhook.organizationId,
150
+ data: { message: 'This is a test delivery' }
151
+ };
152
+
153
+ return this.deliver(webhook, payload);
154
+ }
155
+ }
156
+
157
+ module.exports = new WebhookService();