@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,530 @@
|
|
|
1
|
+
const Asset = require('../models/Asset');
|
|
2
|
+
const objectStorage = require('../services/objectStorage.service');
|
|
3
|
+
const uploadNamespacesService = require('../services/uploadNamespaces.service');
|
|
4
|
+
|
|
5
|
+
const buildPublicUrl = (key) => {
|
|
6
|
+
return `/public/assets/${key}`;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const formatAssetResponse = (asset) => {
|
|
10
|
+
const obj = asset.toObject ? asset.toObject() : asset;
|
|
11
|
+
const response = {
|
|
12
|
+
_id: obj._id,
|
|
13
|
+
key: obj.key,
|
|
14
|
+
provider: obj.provider,
|
|
15
|
+
bucket: obj.bucket,
|
|
16
|
+
originalName: obj.originalName,
|
|
17
|
+
contentType: obj.contentType,
|
|
18
|
+
sizeBytes: obj.sizeBytes,
|
|
19
|
+
visibility: obj.visibility,
|
|
20
|
+
namespace: obj.namespace,
|
|
21
|
+
visibilityEnforced: obj.visibilityEnforced,
|
|
22
|
+
tags: Array.isArray(obj.tags) ? obj.tags : [],
|
|
23
|
+
ownerUserId: obj.ownerUserId,
|
|
24
|
+
orgId: obj.orgId,
|
|
25
|
+
status: obj.status,
|
|
26
|
+
createdAt: obj.createdAt,
|
|
27
|
+
updatedAt: obj.updatedAt
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (Object.prototype.hasOwnProperty.call(obj, 'storageExists')) {
|
|
31
|
+
response.storageExists = obj.storageExists;
|
|
32
|
+
}
|
|
33
|
+
if (Object.prototype.hasOwnProperty.call(obj, 'storageCheckedBackend')) {
|
|
34
|
+
response.storageCheckedBackend = obj.storageCheckedBackend;
|
|
35
|
+
}
|
|
36
|
+
if (Object.prototype.hasOwnProperty.call(obj, 'storageExistsError')) {
|
|
37
|
+
response.storageExistsError = obj.storageExistsError;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (obj.visibility === 'public') {
|
|
41
|
+
response.publicUrl = buildPublicUrl(obj.key);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return response;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const normalizeTags = (value) => {
|
|
48
|
+
if (value === undefined) return undefined;
|
|
49
|
+
const raw = Array.isArray(value)
|
|
50
|
+
? value
|
|
51
|
+
: typeof value === 'string'
|
|
52
|
+
? value.split(',')
|
|
53
|
+
: [value];
|
|
54
|
+
|
|
55
|
+
const tags = raw
|
|
56
|
+
.map((t) => String(t).trim().toLowerCase())
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
|
|
59
|
+
return Array.from(new Set(tags));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
exports.list = async (req, res) => {
|
|
63
|
+
try {
|
|
64
|
+
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
65
|
+
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 20));
|
|
66
|
+
const skip = (page - 1) * limit;
|
|
67
|
+
|
|
68
|
+
const filter = {};
|
|
69
|
+
|
|
70
|
+
if (req.query.status) {
|
|
71
|
+
filter.status = req.query.status;
|
|
72
|
+
} else {
|
|
73
|
+
filter.status = 'uploaded';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (req.query.visibility) {
|
|
77
|
+
filter.visibility = req.query.visibility;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (req.query.contentType) {
|
|
81
|
+
filter.contentType = { $regex: req.query.contentType, $options: 'i' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (req.query.ownerUserId) {
|
|
85
|
+
filter.ownerUserId = req.query.ownerUserId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (req.query.orgId) {
|
|
89
|
+
filter.orgId = req.query.orgId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (req.query.namespace) {
|
|
93
|
+
filter.namespace = String(req.query.namespace);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (req.query.tag) {
|
|
97
|
+
const tag = String(req.query.tag).trim().toLowerCase();
|
|
98
|
+
if (tag) {
|
|
99
|
+
filter.tags = tag;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const [assets, total] = await Promise.all([
|
|
104
|
+
Asset.find(filter).sort({ updatedAt: -1, createdAt: -1 }).skip(skip).limit(limit).lean(),
|
|
105
|
+
Asset.countDocuments(filter)
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const assetsWithStorage = await Promise.all(
|
|
109
|
+
assets.map(async (a) => {
|
|
110
|
+
const backend = a?.provider === 's3' ? 's3' : 'fs';
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const exists = await objectStorage.objectExists({ key: a.key, backend });
|
|
114
|
+
return {
|
|
115
|
+
...a,
|
|
116
|
+
storageCheckedBackend: backend,
|
|
117
|
+
storageExists: Boolean(exists),
|
|
118
|
+
};
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return {
|
|
121
|
+
...a,
|
|
122
|
+
storageCheckedBackend: backend,
|
|
123
|
+
storageExists: null,
|
|
124
|
+
storageExistsError: e?.message ? String(e.message) : 'storage check failed',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
res.json({
|
|
131
|
+
assets: assetsWithStorage.map(formatAssetResponse),
|
|
132
|
+
pagination: {
|
|
133
|
+
page,
|
|
134
|
+
limit,
|
|
135
|
+
total,
|
|
136
|
+
pages: Math.ceil(total / limit)
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('Error listing assets:', error);
|
|
141
|
+
res.status(500).json({ error: 'Failed to list assets' });
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
exports.bulkSetTags = async (req, res) => {
|
|
146
|
+
try {
|
|
147
|
+
const assetIds = Array.isArray(req.body?.assetIds) ? req.body.assetIds : [];
|
|
148
|
+
const normalizedIds = assetIds
|
|
149
|
+
.map((id) => String(id || '').trim())
|
|
150
|
+
.filter(Boolean);
|
|
151
|
+
|
|
152
|
+
if (!normalizedIds.length) {
|
|
153
|
+
return res.status(400).json({ error: 'assetIds must be a non-empty array' });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (normalizedIds.length > 200) {
|
|
157
|
+
return res.status(400).json({ error: 'Too many assets. Max 200 per request.' });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const tags = normalizeTags(req.body?.tags) || [];
|
|
161
|
+
|
|
162
|
+
const assets = await Asset.find({
|
|
163
|
+
_id: { $in: normalizedIds },
|
|
164
|
+
status: 'uploaded',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const results = {
|
|
168
|
+
ok: true,
|
|
169
|
+
requested: normalizedIds.length,
|
|
170
|
+
found: assets.length,
|
|
171
|
+
updated: 0,
|
|
172
|
+
failed: [],
|
|
173
|
+
tags,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
for (const asset of assets) {
|
|
177
|
+
try {
|
|
178
|
+
asset.tags = tags;
|
|
179
|
+
await asset.save();
|
|
180
|
+
results.updated += 1;
|
|
181
|
+
} catch (e) {
|
|
182
|
+
results.failed.push({
|
|
183
|
+
assetId: String(asset._id),
|
|
184
|
+
error: e?.message ? String(e.message) : 'Failed to update tags',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return res.json(results);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('Error bulk setting tags:', error);
|
|
192
|
+
return res.status(500).json({ error: 'Failed to bulk set tags' });
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
exports.replace = async (req, res) => {
|
|
197
|
+
try {
|
|
198
|
+
const asset = await Asset.findById(req.params.id);
|
|
199
|
+
|
|
200
|
+
if (!asset) {
|
|
201
|
+
return res.status(404).json({ error: 'Asset not found' });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (asset.status === 'deleted') {
|
|
205
|
+
return res.status(400).json({ error: 'Cannot replace a deleted asset' });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!req.file && !req.files?.file) {
|
|
209
|
+
return res.status(400).json({ error: 'No file provided' });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const file = req.file || req.files.file;
|
|
213
|
+
const buffer = file.buffer || (file.data ? file.data : null);
|
|
214
|
+
|
|
215
|
+
if (!buffer) {
|
|
216
|
+
return res.status(400).json({ error: 'Unable to read file buffer' });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const contentType = file.mimetype;
|
|
220
|
+
const sizeBytes = buffer.length;
|
|
221
|
+
|
|
222
|
+
const namespaceConfig = await uploadNamespacesService.resolveNamespace(asset.namespace || 'default');
|
|
223
|
+
const hardCapMaxFileSizeBytes = await uploadNamespacesService.getEffectiveHardCapMaxFileSizeBytes();
|
|
224
|
+
|
|
225
|
+
const validation = uploadNamespacesService.validateUpload({
|
|
226
|
+
namespaceConfig,
|
|
227
|
+
contentType,
|
|
228
|
+
sizeBytes,
|
|
229
|
+
hardCapMaxFileSizeBytes,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
if (!validation.ok) {
|
|
233
|
+
return res.status(400).json({
|
|
234
|
+
error: 'Upload rejected by namespace policy',
|
|
235
|
+
namespace: namespaceConfig.key,
|
|
236
|
+
hardCapMaxFileSizeBytes,
|
|
237
|
+
errors: validation.errors,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const { provider, bucket } = await objectStorage.putObject({
|
|
242
|
+
key: asset.key,
|
|
243
|
+
body: buffer,
|
|
244
|
+
contentType,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
asset.contentType = contentType;
|
|
248
|
+
asset.sizeBytes = sizeBytes;
|
|
249
|
+
asset.provider = provider;
|
|
250
|
+
asset.bucket = bucket;
|
|
251
|
+
await asset.save();
|
|
252
|
+
|
|
253
|
+
res.json({ asset: formatAssetResponse(asset) });
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error('Error replacing asset:', error);
|
|
256
|
+
res.status(500).json({ error: 'Failed to replace asset' });
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
exports.get = async (req, res) => {
|
|
261
|
+
try {
|
|
262
|
+
const asset = await Asset.findById(req.params.id).lean();
|
|
263
|
+
|
|
264
|
+
if (!asset) {
|
|
265
|
+
return res.status(404).json({ error: 'Asset not found' });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
res.json({ asset: formatAssetResponse(asset) });
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error('Error getting asset:', error);
|
|
271
|
+
res.status(500).json({ error: 'Failed to get asset' });
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
exports.upload = async (req, res) => {
|
|
276
|
+
try {
|
|
277
|
+
if (!req.file && !req.files?.file) {
|
|
278
|
+
return res.status(400).json({ error: 'No file provided' });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const file = req.file || req.files.file;
|
|
282
|
+
const buffer = file.buffer || (file.data ? file.data : null);
|
|
283
|
+
|
|
284
|
+
if (!buffer) {
|
|
285
|
+
return res.status(400).json({ error: 'Unable to read file buffer' });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const contentType = file.mimetype;
|
|
289
|
+
const originalName = file.originalname || file.name;
|
|
290
|
+
const sizeBytes = buffer.length;
|
|
291
|
+
|
|
292
|
+
const namespaceKey = req.body?.namespace ? String(req.body.namespace).trim() : 'default';
|
|
293
|
+
const namespaceConfig = await uploadNamespacesService.resolveNamespace(namespaceKey);
|
|
294
|
+
|
|
295
|
+
const hardCapMaxFileSizeBytes = await uploadNamespacesService.getEffectiveHardCapMaxFileSizeBytes();
|
|
296
|
+
|
|
297
|
+
const validation = uploadNamespacesService.validateUpload({
|
|
298
|
+
namespaceConfig,
|
|
299
|
+
contentType,
|
|
300
|
+
sizeBytes,
|
|
301
|
+
hardCapMaxFileSizeBytes,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (!validation.ok) {
|
|
305
|
+
return res.status(400).json({
|
|
306
|
+
error: 'Upload rejected by namespace policy',
|
|
307
|
+
namespace: namespaceConfig.key,
|
|
308
|
+
hardCapMaxFileSizeBytes,
|
|
309
|
+
errors: validation.errors,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const key = uploadNamespacesService.generateObjectKey({
|
|
314
|
+
namespaceConfig,
|
|
315
|
+
originalName,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const visibility = uploadNamespacesService.computeVisibility({
|
|
319
|
+
namespaceConfig,
|
|
320
|
+
requestedVisibility: req.body?.visibility,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const { provider, bucket } = await objectStorage.putObject({
|
|
324
|
+
key,
|
|
325
|
+
body: buffer,
|
|
326
|
+
contentType
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const asset = await Asset.create({
|
|
330
|
+
key,
|
|
331
|
+
provider,
|
|
332
|
+
bucket,
|
|
333
|
+
originalName,
|
|
334
|
+
contentType,
|
|
335
|
+
sizeBytes,
|
|
336
|
+
visibility,
|
|
337
|
+
namespace: namespaceConfig.key,
|
|
338
|
+
visibilityEnforced: Boolean(namespaceConfig.enforceVisibility),
|
|
339
|
+
ownerUserId: null,
|
|
340
|
+
orgId: null,
|
|
341
|
+
status: 'uploaded'
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
res.status(201).json({ asset: formatAssetResponse(asset) });
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error('Error uploading asset:', error);
|
|
347
|
+
res.status(500).json({ error: 'Failed to upload asset' });
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
exports.update = async (req, res) => {
|
|
352
|
+
try {
|
|
353
|
+
const asset = await Asset.findById(req.params.id);
|
|
354
|
+
|
|
355
|
+
if (!asset) {
|
|
356
|
+
return res.status(404).json({ error: 'Asset not found' });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const allowedFields = ['visibility', 'orgId', 'tags'];
|
|
360
|
+
const updates = {};
|
|
361
|
+
|
|
362
|
+
for (const field of allowedFields) {
|
|
363
|
+
if (req.body[field] !== undefined) {
|
|
364
|
+
updates[field] = req.body[field];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (updates.tags !== undefined) {
|
|
369
|
+
updates.tags = normalizeTags(updates.tags) || [];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (updates.visibility && !['public', 'private'].includes(updates.visibility)) {
|
|
373
|
+
return res.status(400).json({ error: 'Invalid visibility value' });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (updates.visibility && asset.visibilityEnforced) {
|
|
377
|
+
return res.status(400).json({ error: 'Visibility is enforced by the upload namespace for this asset' });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
Object.assign(asset, updates);
|
|
381
|
+
await asset.save();
|
|
382
|
+
|
|
383
|
+
res.json({ asset: formatAssetResponse(asset) });
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error('Error updating asset:', error);
|
|
386
|
+
res.status(500).json({ error: 'Failed to update asset' });
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
exports.delete = async (req, res) => {
|
|
391
|
+
try {
|
|
392
|
+
const asset = await Asset.findById(req.params.id);
|
|
393
|
+
|
|
394
|
+
if (!asset) {
|
|
395
|
+
return res.status(404).json({ error: 'Asset not found' });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
await objectStorage.deleteObject({ key: asset.key });
|
|
399
|
+
|
|
400
|
+
asset.status = 'deleted';
|
|
401
|
+
await asset.save();
|
|
402
|
+
|
|
403
|
+
res.json({ success: true });
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error('Error deleting asset:', error);
|
|
406
|
+
res.status(500).json({ error: 'Failed to delete asset' });
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
exports.getStorageInfo = async (req, res) => {
|
|
411
|
+
try {
|
|
412
|
+
const multerCeilingMaxFileSizeBytes = Number(process.env.MULTER_FILE_SIZE_LIMIT || '1073741824');
|
|
413
|
+
const envFallbackHardCapMaxFileSizeBytes = uploadNamespacesService.getEnvHardCapMaxFileSizeBytes();
|
|
414
|
+
const configuredHardCapMaxFileSizeBytes = await uploadNamespacesService.getConfiguredHardCapMaxFileSizeBytes();
|
|
415
|
+
const hardCapMaxFileSizeBytes = await uploadNamespacesService.getEffectiveHardCapMaxFileSizeBytes();
|
|
416
|
+
|
|
417
|
+
const [provider, bucket, s3Enabled] = await Promise.all([
|
|
418
|
+
objectStorage.getProvider(),
|
|
419
|
+
objectStorage.getBucket(),
|
|
420
|
+
objectStorage.isS3Enabled(),
|
|
421
|
+
]);
|
|
422
|
+
|
|
423
|
+
res.json({
|
|
424
|
+
provider,
|
|
425
|
+
bucket,
|
|
426
|
+
s3Enabled,
|
|
427
|
+
maxFileSize: objectStorage.getMaxFileSize(),
|
|
428
|
+
multerCeilingMaxFileSizeBytes,
|
|
429
|
+
envFallbackHardCapMaxFileSizeBytes,
|
|
430
|
+
configuredHardCapMaxFileSizeBytes,
|
|
431
|
+
hardCapMaxFileSizeBytes,
|
|
432
|
+
allowedContentTypes: objectStorage.getAllowedContentTypes()
|
|
433
|
+
});
|
|
434
|
+
} catch (error) {
|
|
435
|
+
console.error('Error getting storage info:', error);
|
|
436
|
+
res.status(500).json({ error: 'Failed to get storage info' });
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
exports.bulkMoveNamespace = async (req, res) => {
|
|
441
|
+
try {
|
|
442
|
+
const assetIds = Array.isArray(req.body?.assetIds) ? req.body.assetIds : [];
|
|
443
|
+
const targetNamespaceRaw = req.body?.targetNamespace;
|
|
444
|
+
|
|
445
|
+
const targetNamespaceKey = String(targetNamespaceRaw || '').trim();
|
|
446
|
+
if (!targetNamespaceKey) {
|
|
447
|
+
return res.status(400).json({ error: 'targetNamespace is required' });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const normalizedIds = assetIds
|
|
451
|
+
.map((id) => String(id || '').trim())
|
|
452
|
+
.filter(Boolean);
|
|
453
|
+
|
|
454
|
+
if (!normalizedIds.length) {
|
|
455
|
+
return res.status(400).json({ error: 'assetIds must be a non-empty array' });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (normalizedIds.length > 100) {
|
|
459
|
+
return res.status(400).json({ error: 'Too many assets. Max 100 per request.' });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const namespaceConfig = await uploadNamespacesService.resolveNamespace(targetNamespaceKey);
|
|
463
|
+
if (!namespaceConfig?.enabled) {
|
|
464
|
+
return res.status(400).json({ error: 'Target namespace is disabled' });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const assets = await Asset.find({
|
|
468
|
+
_id: { $in: normalizedIds },
|
|
469
|
+
status: 'uploaded',
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const results = {
|
|
473
|
+
ok: true,
|
|
474
|
+
targetNamespace: namespaceConfig.key,
|
|
475
|
+
requested: normalizedIds.length,
|
|
476
|
+
found: assets.length,
|
|
477
|
+
moved: 0,
|
|
478
|
+
skipped: 0,
|
|
479
|
+
failed: [],
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
for (const asset of assets) {
|
|
483
|
+
try {
|
|
484
|
+
const currentNamespace = String(asset.namespace || 'default');
|
|
485
|
+
if (currentNamespace === namespaceConfig.key) {
|
|
486
|
+
results.skipped += 1;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const newKey = uploadNamespacesService.generateObjectKey({
|
|
491
|
+
namespaceConfig,
|
|
492
|
+
originalName: asset.originalName,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const backend = asset?.provider === 's3' ? 's3' : 'fs';
|
|
496
|
+
const oldKey = asset.key;
|
|
497
|
+
|
|
498
|
+
await objectStorage.moveObject({
|
|
499
|
+
sourceKey: oldKey,
|
|
500
|
+
destKey: newKey,
|
|
501
|
+
backend,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const movedProvider = backend;
|
|
505
|
+
const movedBucket = backend === 's3'
|
|
506
|
+
? (await objectStorage.getS3Config())?.bucket
|
|
507
|
+
: 'fs';
|
|
508
|
+
|
|
509
|
+
asset.key = newKey;
|
|
510
|
+
asset.namespace = namespaceConfig.key;
|
|
511
|
+
asset.provider = movedProvider;
|
|
512
|
+
asset.bucket = movedBucket || asset.bucket;
|
|
513
|
+
asset.visibilityEnforced = Boolean(namespaceConfig.enforceVisibility);
|
|
514
|
+
await asset.save();
|
|
515
|
+
|
|
516
|
+
results.moved += 1;
|
|
517
|
+
} catch (e) {
|
|
518
|
+
results.failed.push({
|
|
519
|
+
assetId: String(asset._id),
|
|
520
|
+
error: e?.message ? String(e.message) : 'Failed to move asset',
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return res.json(results);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
console.error('Error bulk moving assets:', error);
|
|
528
|
+
return res.status(500).json({ error: 'Failed to bulk move assets' });
|
|
529
|
+
}
|
|
530
|
+
};
|