@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,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();
|