digitaltwin-core 0.9.0 → 0.9.2
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/dist/auth/apisix_parser.d.ts +24 -0
- package/dist/auth/apisix_parser.d.ts.map +1 -1
- package/dist/auth/apisix_parser.js +28 -0
- package/dist/auth/apisix_parser.js.map +1 -1
- package/dist/auth/auth_config.d.ts +26 -0
- package/dist/auth/auth_config.d.ts.map +1 -1
- package/dist/auth/auth_config.js +35 -0
- package/dist/auth/auth_config.js.map +1 -1
- package/dist/components/assets_manager.d.ts +202 -9
- package/dist/components/assets_manager.d.ts.map +1 -1
- package/dist/components/assets_manager.js +552 -491
- package/dist/components/assets_manager.js.map +1 -1
- package/dist/components/types.d.ts +169 -19
- package/dist/components/types.d.ts.map +1 -1
- package/dist/components/types.js +6 -0
- package/dist/components/types.js.map +1 -1
- package/dist/database/adapters/knex_database_adapter.d.ts +10 -0
- package/dist/database/adapters/knex_database_adapter.d.ts.map +1 -1
- package/dist/database/adapters/knex_database_adapter.js +86 -0
- package/dist/database/adapters/knex_database_adapter.js.map +1 -1
- package/dist/database/database_adapter.d.ts +116 -7
- package/dist/database/database_adapter.d.ts.map +1 -1
- package/dist/database/database_adapter.js +41 -1
- package/dist/database/database_adapter.js.map +1 -1
- package/dist/engine/digital_twin_engine.d.ts +12 -0
- package/dist/engine/digital_twin_engine.d.ts.map +1 -1
- package/dist/engine/digital_twin_engine.js +4 -2
- package/dist/engine/digital_twin_engine.js.map +1 -1
- package/dist/engine/initializer.d.ts +8 -5
- package/dist/engine/initializer.d.ts.map +1 -1
- package/dist/engine/initializer.js +32 -13
- package/dist/engine/initializer.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/data_record.d.ts +8 -0
- package/dist/types/data_record.d.ts.map +1 -1
- package/dist/utils/http_responses.d.ts +155 -0
- package/dist/utils/http_responses.d.ts.map +1 -0
- package/dist/utils/http_responses.js +190 -0
- package/dist/utils/http_responses.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/map_to_data_record.d.ts.map +1 -1
- package/dist/utils/map_to_data_record.js +4 -1
- package/dist/utils/map_to_data_record.js.map +1 -1
- package/package.json +1 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApisixAuthParser } from '../auth/apisix_parser.js';
|
|
2
2
|
import { UserService } from '../auth/user_service.js';
|
|
3
|
+
import { successResponse, errorResponse, badRequestResponse, unauthorizedResponse, forbiddenResponse, notFoundResponse, textResponse, fileResponse, multiStatusResponse, HttpStatus } from '../utils/http_responses.js';
|
|
3
4
|
import fs from 'fs/promises';
|
|
4
5
|
/**
|
|
5
6
|
* Abstract base class for Assets Manager components with authentication and access control.
|
|
@@ -65,17 +66,23 @@ export class AssetsManager {
|
|
|
65
66
|
*
|
|
66
67
|
* @param {DatabaseAdapter} db - The database adapter for metadata storage
|
|
67
68
|
* @param {StorageService} storage - The storage service for file persistence
|
|
69
|
+
* @param {UserService} [userService] - Optional user service for authentication (created automatically if not provided)
|
|
68
70
|
*
|
|
69
71
|
* @example
|
|
70
72
|
* ```typescript
|
|
73
|
+
* // Standard usage (UserService created automatically)
|
|
71
74
|
* const assetsManager = new MyAssetsManager()
|
|
72
75
|
* assetsManager.setDependencies(databaseAdapter, storageService)
|
|
76
|
+
*
|
|
77
|
+
* // For testing (inject mock UserService)
|
|
78
|
+
* const mockUserService = new MockUserService()
|
|
79
|
+
* assetsManager.setDependencies(databaseAdapter, storageService, mockUserService)
|
|
73
80
|
* ```
|
|
74
81
|
*/
|
|
75
|
-
setDependencies(db, storage) {
|
|
82
|
+
setDependencies(db, storage, userService) {
|
|
76
83
|
this.db = db;
|
|
77
84
|
this.storage = storage;
|
|
78
|
-
this.userService = new UserService(db);
|
|
85
|
+
this.userService = userService ?? new UserService(db);
|
|
79
86
|
}
|
|
80
87
|
/**
|
|
81
88
|
* Validates that a source string is a valid URL.
|
|
@@ -132,6 +139,237 @@ export class AssetsManager {
|
|
|
132
139
|
// Check if the filename ends with the required extension (case-insensitive)
|
|
133
140
|
return filename.toLowerCase().endsWith(requiredExtension.toLowerCase());
|
|
134
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Validates that a string is valid base64-encoded data.
|
|
144
|
+
*
|
|
145
|
+
* Used internally to ensure file data in batch uploads is properly base64-encoded
|
|
146
|
+
* before attempting to decode it.
|
|
147
|
+
*
|
|
148
|
+
* @private
|
|
149
|
+
* @param {any} data - Data to validate as base64
|
|
150
|
+
* @returns {boolean} True if data is a valid base64 string, false otherwise
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* this.validateBase64('SGVsbG8gV29ybGQ=') // returns true
|
|
155
|
+
* this.validateBase64('not-base64!@#') // returns false
|
|
156
|
+
* this.validateBase64(123) // returns false (not a string)
|
|
157
|
+
* this.validateBase64('') // returns false (empty string)
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
validateBase64(data) {
|
|
161
|
+
// Must be a non-empty string
|
|
162
|
+
if (typeof data !== 'string' || data.length === 0) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
// Base64 regex: only A-Z, a-z, 0-9, +, /, and = for padding
|
|
166
|
+
// Padding (=) can only appear at the end, max 2 times
|
|
167
|
+
const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
168
|
+
const trimmed = data.trim();
|
|
169
|
+
// Must match regex
|
|
170
|
+
if (!base64Regex.test(trimmed)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
// Length must be multiple of 4
|
|
174
|
+
if (trimmed.length % 4 !== 0) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
// Try to decode to verify it's valid base64
|
|
178
|
+
try {
|
|
179
|
+
const decoded = Buffer.from(trimmed, 'base64').toString('base64');
|
|
180
|
+
// Re-encode and compare to ensure no data loss (valid base64)
|
|
181
|
+
return decoded === trimmed;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// Authentication & Request Processing Helpers
|
|
189
|
+
// ============================================================================
|
|
190
|
+
/**
|
|
191
|
+
* Authenticates a request and returns the user record.
|
|
192
|
+
*
|
|
193
|
+
* This method consolidates the authentication flow:
|
|
194
|
+
* 1. Validates APISIX headers are present
|
|
195
|
+
* 2. Parses authentication headers
|
|
196
|
+
* 3. Finds or creates user record in database
|
|
197
|
+
*
|
|
198
|
+
* @param req - HTTP request object
|
|
199
|
+
* @returns AuthResult with either userRecord on success or DataResponse on failure
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```typescript
|
|
203
|
+
* const authResult = await this.authenticateRequest(req)
|
|
204
|
+
* if (!authResult.success) {
|
|
205
|
+
* return authResult.response
|
|
206
|
+
* }
|
|
207
|
+
* const userRecord = authResult.userRecord
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
async authenticateRequest(req) {
|
|
211
|
+
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
212
|
+
return { success: false, response: unauthorizedResponse() };
|
|
213
|
+
}
|
|
214
|
+
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
215
|
+
if (!authUser) {
|
|
216
|
+
return { success: false, response: unauthorizedResponse('Invalid authentication headers') };
|
|
217
|
+
}
|
|
218
|
+
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
219
|
+
if (!userRecord.id) {
|
|
220
|
+
return { success: false, response: errorResponse('Failed to retrieve user information') };
|
|
221
|
+
}
|
|
222
|
+
return { success: true, userRecord };
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Extracts upload data from multipart form request.
|
|
226
|
+
*
|
|
227
|
+
* @param req - HTTP request object with body and file
|
|
228
|
+
* @returns UploadData object with extracted fields
|
|
229
|
+
*/
|
|
230
|
+
extractUploadData(req) {
|
|
231
|
+
return {
|
|
232
|
+
description: req.body?.description,
|
|
233
|
+
source: req.body?.source,
|
|
234
|
+
is_public: req.body?.is_public,
|
|
235
|
+
filePath: req.file?.path,
|
|
236
|
+
filename: req.file?.originalname || req.body?.filename
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Validates required fields for asset upload and returns validated data.
|
|
241
|
+
*
|
|
242
|
+
* @param data - Upload data to validate
|
|
243
|
+
* @returns UploadValidationResult with validated data on success or error response on failure
|
|
244
|
+
*/
|
|
245
|
+
validateUploadFields(data) {
|
|
246
|
+
if (!data.filePath || !data.description || !data.source) {
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
response: badRequestResponse('Missing required fields: description, source, file')
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (!data.filename) {
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
response: badRequestResponse('Filename could not be determined from uploaded file')
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (!this.validateFileExtension(data.filename)) {
|
|
259
|
+
const config = this.getConfiguration();
|
|
260
|
+
return {
|
|
261
|
+
success: false,
|
|
262
|
+
response: badRequestResponse(`Invalid file extension. Expected: ${config.extension}`)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
success: true,
|
|
267
|
+
data: {
|
|
268
|
+
description: data.description,
|
|
269
|
+
source: data.source,
|
|
270
|
+
filePath: data.filePath,
|
|
271
|
+
filename: data.filename,
|
|
272
|
+
is_public: data.is_public !== undefined ? Boolean(data.is_public) : true
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Reads file content from temporary upload path.
|
|
278
|
+
*
|
|
279
|
+
* @param filePath - Path to temporary file
|
|
280
|
+
* @returns Buffer with file content
|
|
281
|
+
* @throws Error if file cannot be read
|
|
282
|
+
*/
|
|
283
|
+
async readTempFile(filePath) {
|
|
284
|
+
return fs.readFile(filePath);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Cleans up temporary file after processing.
|
|
288
|
+
* Silently ignores cleanup errors.
|
|
289
|
+
*
|
|
290
|
+
* @param filePath - Path to temporary file
|
|
291
|
+
*/
|
|
292
|
+
async cleanupTempFile(filePath) {
|
|
293
|
+
await fs.unlink(filePath).catch(() => {
|
|
294
|
+
// Ignore cleanup errors
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Validates ownership of an asset.
|
|
299
|
+
*
|
|
300
|
+
* @param asset - Asset record to check
|
|
301
|
+
* @param userId - User ID to validate against
|
|
302
|
+
* @returns DataResponse with error if not owner, undefined if valid
|
|
303
|
+
*/
|
|
304
|
+
validateOwnership(asset, userId) {
|
|
305
|
+
if (asset.owner_id !== userId) {
|
|
306
|
+
return forbiddenResponse('You can only modify your own assets');
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Checks if a user can access a private asset.
|
|
312
|
+
*
|
|
313
|
+
* @param asset - Asset record to check
|
|
314
|
+
* @param req - HTTP request for authentication context
|
|
315
|
+
* @returns DataResponse with error if access denied, undefined if allowed
|
|
316
|
+
*/
|
|
317
|
+
async checkPrivateAssetAccess(asset, req) {
|
|
318
|
+
// Public assets are always accessible
|
|
319
|
+
if (asset.is_public) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
// Admins can access everything
|
|
323
|
+
if (ApisixAuthParser.isAdmin(req.headers || {})) {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
// Private asset - require authentication
|
|
327
|
+
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
328
|
+
return unauthorizedResponse('Authentication required for private assets');
|
|
329
|
+
}
|
|
330
|
+
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
331
|
+
if (!authUser) {
|
|
332
|
+
return unauthorizedResponse('Invalid authentication headers');
|
|
333
|
+
}
|
|
334
|
+
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
335
|
+
if (!userRecord.id || asset.owner_id !== userRecord.id) {
|
|
336
|
+
return forbiddenResponse('This asset is private');
|
|
337
|
+
}
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Fetches an asset by ID with full access control validation.
|
|
342
|
+
*
|
|
343
|
+
* This method consolidates the common logic for retrieving an asset:
|
|
344
|
+
* 1. Validates that ID is provided
|
|
345
|
+
* 2. Fetches the asset from database
|
|
346
|
+
* 3. Verifies the asset belongs to this component
|
|
347
|
+
* 4. Checks access permissions for private assets
|
|
348
|
+
*
|
|
349
|
+
* @param req - HTTP request with params.id
|
|
350
|
+
* @returns Object with asset on success, or DataResponse on failure
|
|
351
|
+
*/
|
|
352
|
+
async fetchAssetWithAccessCheck(req) {
|
|
353
|
+
const { id } = req.params || {};
|
|
354
|
+
if (!id) {
|
|
355
|
+
return { success: false, response: badRequestResponse('Asset ID is required') };
|
|
356
|
+
}
|
|
357
|
+
const asset = await this.getAssetById(id);
|
|
358
|
+
if (!asset) {
|
|
359
|
+
return { success: false, response: textResponse(HttpStatus.NOT_FOUND, 'Asset not found') };
|
|
360
|
+
}
|
|
361
|
+
// Verify this asset belongs to our component
|
|
362
|
+
const config = this.getConfiguration();
|
|
363
|
+
if (asset.name !== config.name) {
|
|
364
|
+
return { success: false, response: textResponse(HttpStatus.NOT_FOUND, 'Asset not found') };
|
|
365
|
+
}
|
|
366
|
+
// Check access permissions for private assets
|
|
367
|
+
const accessError = await this.checkPrivateAssetAccess(asset, req);
|
|
368
|
+
if (accessError) {
|
|
369
|
+
return { success: false, response: accessError };
|
|
370
|
+
}
|
|
371
|
+
return { success: true, asset };
|
|
372
|
+
}
|
|
135
373
|
/**
|
|
136
374
|
* Upload a new asset file with metadata.
|
|
137
375
|
*
|
|
@@ -148,7 +386,8 @@ export class AssetsManager {
|
|
|
148
386
|
* source: 'https://city-data.example.com/buildings',
|
|
149
387
|
* owner_id: 'user123',
|
|
150
388
|
* filename: 'building.glb',
|
|
151
|
-
* file: fileBuffer
|
|
389
|
+
* file: fileBuffer,
|
|
390
|
+
* is_public: true
|
|
152
391
|
* })
|
|
153
392
|
* ```
|
|
154
393
|
*/
|
|
@@ -173,7 +412,8 @@ export class AssetsManager {
|
|
|
173
412
|
description: request.description,
|
|
174
413
|
source: request.source,
|
|
175
414
|
owner_id: request.owner_id,
|
|
176
|
-
filename: request.filename
|
|
415
|
+
filename: request.filename,
|
|
416
|
+
is_public: request.is_public ?? true // Default to public if not specified
|
|
177
417
|
};
|
|
178
418
|
await this.db.save(metadata);
|
|
179
419
|
}
|
|
@@ -183,43 +423,69 @@ export class AssetsManager {
|
|
|
183
423
|
* Returns a JSON list of all assets with their metadata, following the
|
|
184
424
|
* framework pattern but adapted for assets management.
|
|
185
425
|
*
|
|
426
|
+
* Access control:
|
|
427
|
+
* - Unauthenticated users: Can only see public assets
|
|
428
|
+
* - Authenticated users: Can see public assets + their own private assets
|
|
429
|
+
* - Admin users: Can see all assets (public and private from all users)
|
|
430
|
+
*
|
|
186
431
|
* @returns {Promise<DataResponse>} JSON response with all assets
|
|
187
432
|
*/
|
|
188
|
-
async retrieve() {
|
|
433
|
+
async retrieve(req) {
|
|
189
434
|
try {
|
|
190
435
|
const assets = await this.getAllAssets();
|
|
191
|
-
const
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
source: asset.source || '',
|
|
202
|
-
owner_id: asset.owner_id || null,
|
|
203
|
-
filename: asset.filename || '',
|
|
204
|
-
// Add URLs that the front-end can actually use
|
|
205
|
-
url: `/${config.name}/${asset.id}`, // For display/use
|
|
206
|
-
download_url: `/${config.name}/${asset.id}/download` // For download
|
|
207
|
-
}));
|
|
208
|
-
return {
|
|
209
|
-
status: 200,
|
|
210
|
-
content: JSON.stringify(assetsWithMetadata),
|
|
211
|
-
headers: { 'Content-Type': 'application/json' }
|
|
212
|
-
};
|
|
436
|
+
const isAdmin = req && ApisixAuthParser.isAdmin(req.headers || {});
|
|
437
|
+
// Admin can see everything
|
|
438
|
+
if (isAdmin) {
|
|
439
|
+
return successResponse(this.formatAssetsForResponse(assets));
|
|
440
|
+
}
|
|
441
|
+
// Get authenticated user ID if available
|
|
442
|
+
const authenticatedUserId = await this.getAuthenticatedUserId(req);
|
|
443
|
+
// Filter to visible assets only
|
|
444
|
+
const visibleAssets = assets.filter(asset => asset.is_public || (authenticatedUserId !== null && asset.owner_id === authenticatedUserId));
|
|
445
|
+
return successResponse(this.formatAssetsForResponse(visibleAssets));
|
|
213
446
|
}
|
|
214
447
|
catch (error) {
|
|
215
|
-
return
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
448
|
+
return errorResponse(error);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Gets the authenticated user's database ID from request headers.
|
|
453
|
+
*
|
|
454
|
+
* @param req - HTTP request object
|
|
455
|
+
* @returns User ID or null if not authenticated
|
|
456
|
+
*/
|
|
457
|
+
async getAuthenticatedUserId(req) {
|
|
458
|
+
if (!req || !ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
459
|
+
return null;
|
|
222
460
|
}
|
|
461
|
+
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
462
|
+
if (!authUser) {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
466
|
+
return userRecord.id || null;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Formats assets for API response with metadata and URLs.
|
|
470
|
+
*
|
|
471
|
+
* @param assets - Array of asset records
|
|
472
|
+
* @returns Formatted assets array ready for JSON serialization
|
|
473
|
+
*/
|
|
474
|
+
formatAssetsForResponse(assets) {
|
|
475
|
+
const config = this.getConfiguration();
|
|
476
|
+
return assets.map(asset => ({
|
|
477
|
+
id: asset.id,
|
|
478
|
+
name: asset.name,
|
|
479
|
+
date: asset.date,
|
|
480
|
+
contentType: asset.contentType,
|
|
481
|
+
description: asset.description || '',
|
|
482
|
+
source: asset.source || '',
|
|
483
|
+
owner_id: asset.owner_id || null,
|
|
484
|
+
filename: asset.filename || '',
|
|
485
|
+
is_public: asset.is_public ?? true,
|
|
486
|
+
url: `/${config.name}/${asset.id}`,
|
|
487
|
+
download_url: `/${config.name}/${asset.id}/download`
|
|
488
|
+
}));
|
|
223
489
|
}
|
|
224
490
|
/**
|
|
225
491
|
* Get all assets for this component type.
|
|
@@ -264,7 +530,7 @@ export class AssetsManager {
|
|
|
264
530
|
/**
|
|
265
531
|
* Update asset metadata by ID.
|
|
266
532
|
*
|
|
267
|
-
* Updates the metadata (description and/or
|
|
533
|
+
* Updates the metadata (description, source, and/or visibility) of a specific asset.
|
|
268
534
|
* Asset metadata is stored as dedicated columns in the database.
|
|
269
535
|
*
|
|
270
536
|
* @param {string} id - The ID of the asset to update
|
|
@@ -275,7 +541,8 @@ export class AssetsManager {
|
|
|
275
541
|
* ```typescript
|
|
276
542
|
* await assetsManager.updateAssetMetadata('123', {
|
|
277
543
|
* description: 'Updated building model with new textures',
|
|
278
|
-
* source: 'https://updated-source.example.com'
|
|
544
|
+
* source: 'https://updated-source.example.com',
|
|
545
|
+
* is_public: false
|
|
279
546
|
* })
|
|
280
547
|
* ```
|
|
281
548
|
*/
|
|
@@ -302,7 +569,8 @@ export class AssetsManager {
|
|
|
302
569
|
description: updates.description ?? record.description ?? '',
|
|
303
570
|
source: updates.source ?? record.source ?? '',
|
|
304
571
|
owner_id: record.owner_id ?? null,
|
|
305
|
-
filename: record.filename ?? ''
|
|
572
|
+
filename: record.filename ?? '',
|
|
573
|
+
is_public: updates.is_public !== undefined ? updates.is_public : (record.is_public ?? true)
|
|
306
574
|
};
|
|
307
575
|
// Update the record - delete and re-create with updated metadata
|
|
308
576
|
await this.db.delete(id, this.getConfiguration().name);
|
|
@@ -393,7 +661,8 @@ export class AssetsManager {
|
|
|
393
661
|
description: request.description,
|
|
394
662
|
source: request.source,
|
|
395
663
|
owner_id: request.owner_id,
|
|
396
|
-
filename: request.filename
|
|
664
|
+
filename: request.filename,
|
|
665
|
+
is_public: request.is_public ?? true
|
|
397
666
|
};
|
|
398
667
|
metadataList.push(metadata);
|
|
399
668
|
}
|
|
@@ -491,129 +760,84 @@ export class AssetsManager {
|
|
|
491
760
|
];
|
|
492
761
|
}
|
|
493
762
|
/**
|
|
494
|
-
* Handle upload
|
|
763
|
+
* Handle single asset upload via HTTP POST.
|
|
764
|
+
*
|
|
765
|
+
* Flow:
|
|
766
|
+
* 1. Validate request structure and authentication
|
|
767
|
+
* 2. Extract user identity from Apache APISIX headers
|
|
768
|
+
* 3. Validate file extension and read uploaded file
|
|
769
|
+
* 4. Store file via storage service and metadata in database
|
|
770
|
+
* 5. Set owner_id to authenticated user (prevents ownership spoofing)
|
|
771
|
+
* 6. Apply is_public setting (defaults to true if not specified)
|
|
772
|
+
*
|
|
773
|
+
* Authentication: Required
|
|
774
|
+
* Ownership: Automatically set to authenticated user
|
|
775
|
+
*
|
|
776
|
+
* @param req - HTTP request with multipart/form-data file upload
|
|
777
|
+
* @returns HTTP response with success/error status
|
|
778
|
+
*
|
|
779
|
+
* @example
|
|
780
|
+
* POST /assets
|
|
781
|
+
* Content-Type: multipart/form-data
|
|
782
|
+
* x-user-id: user-uuid
|
|
783
|
+
* x-user-roles: user,premium
|
|
784
|
+
*
|
|
785
|
+
* Form data:
|
|
786
|
+
* - file: <binary file>
|
|
787
|
+
* - description: "3D model of building"
|
|
788
|
+
* - source: "https://source.com"
|
|
789
|
+
* - is_public: true
|
|
495
790
|
*/
|
|
496
791
|
async handleUpload(req) {
|
|
497
792
|
try {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
content: JSON.stringify({
|
|
502
|
-
error: 'Invalid request: missing request body'
|
|
503
|
-
}),
|
|
504
|
-
headers: { 'Content-Type': 'application/json' }
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
// Check authentication
|
|
508
|
-
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
509
|
-
return {
|
|
510
|
-
status: 401,
|
|
511
|
-
content: JSON.stringify({
|
|
512
|
-
error: 'Authentication required'
|
|
513
|
-
}),
|
|
514
|
-
headers: { 'Content-Type': 'application/json' }
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
// Parse authenticated user
|
|
518
|
-
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
519
|
-
if (!authUser) {
|
|
520
|
-
return {
|
|
521
|
-
status: 401,
|
|
522
|
-
content: JSON.stringify({
|
|
523
|
-
error: 'Invalid authentication headers'
|
|
524
|
-
}),
|
|
525
|
-
headers: { 'Content-Type': 'application/json' }
|
|
526
|
-
};
|
|
793
|
+
// Validate request structure
|
|
794
|
+
if (!req?.body) {
|
|
795
|
+
return badRequestResponse('Invalid request: missing request body');
|
|
527
796
|
}
|
|
528
|
-
//
|
|
529
|
-
const
|
|
530
|
-
if (!
|
|
531
|
-
return
|
|
532
|
-
status: 500,
|
|
533
|
-
content: JSON.stringify({
|
|
534
|
-
error: 'Failed to retrieve user information'
|
|
535
|
-
}),
|
|
536
|
-
headers: { 'Content-Type': 'application/json' }
|
|
537
|
-
};
|
|
797
|
+
// Authenticate user
|
|
798
|
+
const authResult = await this.authenticateRequest(req);
|
|
799
|
+
if (!authResult.success) {
|
|
800
|
+
return authResult.response;
|
|
538
801
|
}
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (!filePath || !description || !source) {
|
|
543
|
-
return {
|
|
544
|
-
status: 400,
|
|
545
|
-
content: JSON.stringify({
|
|
546
|
-
error: 'Missing required fields: description, source, file'
|
|
547
|
-
}),
|
|
548
|
-
headers: { 'Content-Type': 'application/json' }
|
|
549
|
-
};
|
|
802
|
+
const userId = authResult.userRecord.id;
|
|
803
|
+
if (!userId) {
|
|
804
|
+
return errorResponse('Failed to retrieve user information');
|
|
550
805
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}),
|
|
557
|
-
headers: { 'Content-Type': 'application/json' }
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
// Validate file extension early to avoid processing invalid files
|
|
561
|
-
if (!this.validateFileExtension(filename)) {
|
|
562
|
-
const config = this.getConfiguration();
|
|
563
|
-
return {
|
|
564
|
-
status: 400,
|
|
565
|
-
content: JSON.stringify({
|
|
566
|
-
error: `Invalid file extension. Expected: ${config.extension}`
|
|
567
|
-
}),
|
|
568
|
-
headers: { 'Content-Type': 'application/json' }
|
|
569
|
-
};
|
|
806
|
+
// Extract and validate upload data
|
|
807
|
+
const uploadData = this.extractUploadData(req);
|
|
808
|
+
const validation = this.validateUploadFields(uploadData);
|
|
809
|
+
if (!validation.success) {
|
|
810
|
+
return validation.response;
|
|
570
811
|
}
|
|
812
|
+
const validData = validation.data;
|
|
571
813
|
// Read file from temporary location
|
|
572
814
|
let fileBuffer;
|
|
573
815
|
try {
|
|
574
|
-
fileBuffer = await
|
|
816
|
+
fileBuffer = await this.readTempFile(validData.filePath);
|
|
575
817
|
}
|
|
576
818
|
catch (error) {
|
|
577
|
-
return {
|
|
578
|
-
status: 500,
|
|
579
|
-
content: JSON.stringify({
|
|
580
|
-
error: `Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
581
|
-
}),
|
|
582
|
-
headers: { 'Content-Type': 'application/json' }
|
|
583
|
-
};
|
|
819
|
+
return errorResponse(`Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
584
820
|
}
|
|
821
|
+
// Upload asset and cleanup
|
|
585
822
|
try {
|
|
586
823
|
await this.uploadAsset({
|
|
587
|
-
description,
|
|
588
|
-
source,
|
|
589
|
-
owner_id:
|
|
590
|
-
filename,
|
|
591
|
-
file: fileBuffer
|
|
592
|
-
|
|
593
|
-
// Clean up temporary file after successful upload
|
|
594
|
-
await fs.unlink(filePath).catch(() => {
|
|
595
|
-
// Ignore cleanup errors, but log them in production
|
|
824
|
+
description: validData.description,
|
|
825
|
+
source: validData.source,
|
|
826
|
+
owner_id: userId,
|
|
827
|
+
filename: validData.filename,
|
|
828
|
+
file: fileBuffer,
|
|
829
|
+
is_public: validData.is_public
|
|
596
830
|
});
|
|
831
|
+
await this.cleanupTempFile(validData.filePath);
|
|
597
832
|
}
|
|
598
833
|
catch (error) {
|
|
599
|
-
|
|
600
|
-
await fs.unlink(filePath).catch(() => { });
|
|
834
|
+
await this.cleanupTempFile(validData.filePath);
|
|
601
835
|
throw error;
|
|
602
836
|
}
|
|
603
|
-
return {
|
|
604
|
-
status: 200,
|
|
605
|
-
content: JSON.stringify({ message: 'Asset uploaded successfully' }),
|
|
606
|
-
headers: { 'Content-Type': 'application/json' }
|
|
607
|
-
};
|
|
837
|
+
return successResponse({ message: 'Asset uploaded successfully' });
|
|
608
838
|
}
|
|
609
839
|
catch (error) {
|
|
610
|
-
return
|
|
611
|
-
status: 500,
|
|
612
|
-
content: JSON.stringify({
|
|
613
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
614
|
-
}),
|
|
615
|
-
headers: { 'Content-Type': 'application/json' }
|
|
616
|
-
};
|
|
840
|
+
return errorResponse(error);
|
|
617
841
|
}
|
|
618
842
|
}
|
|
619
843
|
/**
|
|
@@ -633,107 +857,48 @@ export class AssetsManager {
|
|
|
633
857
|
async handleUpdate(req) {
|
|
634
858
|
try {
|
|
635
859
|
if (!req) {
|
|
636
|
-
return
|
|
637
|
-
status: 400,
|
|
638
|
-
content: JSON.stringify({
|
|
639
|
-
error: 'Invalid request: missing request object'
|
|
640
|
-
}),
|
|
641
|
-
headers: { 'Content-Type': 'application/json' }
|
|
642
|
-
};
|
|
860
|
+
return badRequestResponse('Invalid request: missing request object');
|
|
643
861
|
}
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
content: JSON.stringify({
|
|
649
|
-
error: 'Authentication required'
|
|
650
|
-
}),
|
|
651
|
-
headers: { 'Content-Type': 'application/json' }
|
|
652
|
-
};
|
|
862
|
+
// Authenticate user
|
|
863
|
+
const authResult = await this.authenticateRequest(req);
|
|
864
|
+
if (!authResult.success) {
|
|
865
|
+
return authResult.response;
|
|
653
866
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return {
|
|
658
|
-
status: 401,
|
|
659
|
-
content: JSON.stringify({
|
|
660
|
-
error: 'Invalid authentication headers'
|
|
661
|
-
}),
|
|
662
|
-
headers: { 'Content-Type': 'application/json' }
|
|
663
|
-
};
|
|
867
|
+
const userId = authResult.userRecord.id;
|
|
868
|
+
if (!userId) {
|
|
869
|
+
return errorResponse('Failed to retrieve user information');
|
|
664
870
|
}
|
|
665
871
|
const { id } = req.params || {};
|
|
666
|
-
const { description, source } = req.body || {};
|
|
872
|
+
const { description, source, is_public } = req.body || {};
|
|
667
873
|
if (!id) {
|
|
668
|
-
return
|
|
669
|
-
status: 400,
|
|
670
|
-
content: JSON.stringify({
|
|
671
|
-
error: 'Asset ID is required'
|
|
672
|
-
}),
|
|
673
|
-
headers: { 'Content-Type': 'application/json' }
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
if (!description && !source) {
|
|
677
|
-
return {
|
|
678
|
-
status: 400,
|
|
679
|
-
content: JSON.stringify({
|
|
680
|
-
error: 'At least one field (description or source) must be provided for update'
|
|
681
|
-
}),
|
|
682
|
-
headers: { 'Content-Type': 'application/json' }
|
|
683
|
-
};
|
|
874
|
+
return badRequestResponse('Asset ID is required');
|
|
684
875
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (!userRecord.id) {
|
|
688
|
-
return {
|
|
689
|
-
status: 500,
|
|
690
|
-
content: JSON.stringify({
|
|
691
|
-
error: 'Failed to retrieve user information'
|
|
692
|
-
}),
|
|
693
|
-
headers: { 'Content-Type': 'application/json' }
|
|
694
|
-
};
|
|
876
|
+
if (!description && !source && is_public === undefined) {
|
|
877
|
+
return badRequestResponse('At least one field (description, source, or is_public) must be provided for update');
|
|
695
878
|
}
|
|
696
|
-
// Check if asset exists
|
|
879
|
+
// Check if asset exists
|
|
697
880
|
const asset = await this.getAssetById(id);
|
|
698
881
|
if (!asset) {
|
|
699
|
-
return
|
|
700
|
-
status: 404,
|
|
701
|
-
content: JSON.stringify({
|
|
702
|
-
error: 'Asset not found'
|
|
703
|
-
}),
|
|
704
|
-
headers: { 'Content-Type': 'application/json' }
|
|
705
|
-
};
|
|
882
|
+
return notFoundResponse('Asset not found');
|
|
706
883
|
}
|
|
707
|
-
// Check ownership
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
content: JSON.stringify({
|
|
712
|
-
error: 'You can only modify your own assets'
|
|
713
|
-
}),
|
|
714
|
-
headers: { 'Content-Type': 'application/json' }
|
|
715
|
-
};
|
|
884
|
+
// Check ownership
|
|
885
|
+
const ownershipError = this.validateOwnership(asset, userId);
|
|
886
|
+
if (ownershipError) {
|
|
887
|
+
return ownershipError;
|
|
716
888
|
}
|
|
889
|
+
// Build and apply updates
|
|
717
890
|
const updates = {};
|
|
718
891
|
if (description !== undefined)
|
|
719
892
|
updates.description = description;
|
|
720
893
|
if (source !== undefined)
|
|
721
894
|
updates.source = source;
|
|
895
|
+
if (is_public !== undefined)
|
|
896
|
+
updates.is_public = Boolean(is_public);
|
|
722
897
|
await this.updateAssetMetadata(id, updates);
|
|
723
|
-
return {
|
|
724
|
-
status: 200,
|
|
725
|
-
content: JSON.stringify({ message: 'Asset metadata updated successfully' }),
|
|
726
|
-
headers: { 'Content-Type': 'application/json' }
|
|
727
|
-
};
|
|
898
|
+
return successResponse({ message: 'Asset metadata updated successfully' });
|
|
728
899
|
}
|
|
729
900
|
catch (error) {
|
|
730
|
-
return
|
|
731
|
-
status: 500,
|
|
732
|
-
content: JSON.stringify({
|
|
733
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
734
|
-
}),
|
|
735
|
-
headers: { 'Content-Type': 'application/json' }
|
|
736
|
-
};
|
|
901
|
+
return errorResponse(error);
|
|
737
902
|
}
|
|
738
903
|
}
|
|
739
904
|
/**
|
|
@@ -742,6 +907,11 @@ export class AssetsManager {
|
|
|
742
907
|
* Returns the file content of a specific asset by ID for display/use in front-end.
|
|
743
908
|
* No download headers - just the raw file content.
|
|
744
909
|
*
|
|
910
|
+
* Access control:
|
|
911
|
+
* - Public assets: Accessible to everyone
|
|
912
|
+
* - Private assets: Accessible only to owner
|
|
913
|
+
* - Admin users: Can access all assets (public and private)
|
|
914
|
+
*
|
|
745
915
|
* @param {any} req - HTTP request object with params.id
|
|
746
916
|
* @returns {Promise<DataResponse>} HTTP response with file content
|
|
747
917
|
*
|
|
@@ -753,52 +923,15 @@ export class AssetsManager {
|
|
|
753
923
|
*/
|
|
754
924
|
async handleGetAsset(req) {
|
|
755
925
|
try {
|
|
756
|
-
const
|
|
757
|
-
if (!
|
|
758
|
-
return
|
|
759
|
-
status: 400,
|
|
760
|
-
content: JSON.stringify({
|
|
761
|
-
error: 'Asset ID is required'
|
|
762
|
-
}),
|
|
763
|
-
headers: { 'Content-Type': 'application/json' }
|
|
764
|
-
};
|
|
926
|
+
const result = await this.fetchAssetWithAccessCheck(req);
|
|
927
|
+
if (!result.success) {
|
|
928
|
+
return result.response;
|
|
765
929
|
}
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
return {
|
|
769
|
-
status: 404,
|
|
770
|
-
content: 'Asset not found',
|
|
771
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
// Verify this asset belongs to our component
|
|
775
|
-
const config = this.getConfiguration();
|
|
776
|
-
if (asset.name !== config.name) {
|
|
777
|
-
return {
|
|
778
|
-
status: 404,
|
|
779
|
-
content: 'Asset not found',
|
|
780
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
781
|
-
};
|
|
782
|
-
}
|
|
783
|
-
// Get the file content
|
|
784
|
-
const fileContent = await asset.data();
|
|
785
|
-
return {
|
|
786
|
-
status: 200,
|
|
787
|
-
content: fileContent,
|
|
788
|
-
headers: {
|
|
789
|
-
'Content-Type': config.contentType
|
|
790
|
-
// No Content-Disposition - browser will display/use the file
|
|
791
|
-
}
|
|
792
|
-
};
|
|
930
|
+
const fileContent = await result.asset.data();
|
|
931
|
+
return fileResponse(fileContent, this.getConfiguration().contentType);
|
|
793
932
|
}
|
|
794
933
|
catch (error) {
|
|
795
|
-
return
|
|
796
|
-
status: 500,
|
|
797
|
-
content: JSON.stringify({
|
|
798
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
799
|
-
}),
|
|
800
|
-
headers: { 'Content-Type': 'application/json' }
|
|
801
|
-
};
|
|
934
|
+
return errorResponse(error);
|
|
802
935
|
}
|
|
803
936
|
}
|
|
804
937
|
/**
|
|
@@ -807,6 +940,11 @@ export class AssetsManager {
|
|
|
807
940
|
* Downloads the file content of a specific asset by ID with download headers.
|
|
808
941
|
* Forces browser to download the file rather than display it.
|
|
809
942
|
*
|
|
943
|
+
* Access control:
|
|
944
|
+
* - Public assets: Accessible to everyone
|
|
945
|
+
* - Private assets: Accessible only to owner
|
|
946
|
+
* - Admin users: Can download all assets (public and private)
|
|
947
|
+
*
|
|
810
948
|
* @param {any} req - HTTP request object with params.id
|
|
811
949
|
* @returns {Promise<DataResponse>} HTTP response with file content and download headers
|
|
812
950
|
*
|
|
@@ -818,54 +956,16 @@ export class AssetsManager {
|
|
|
818
956
|
*/
|
|
819
957
|
async handleDownload(req) {
|
|
820
958
|
try {
|
|
821
|
-
const
|
|
822
|
-
if (!
|
|
823
|
-
return
|
|
824
|
-
status: 400,
|
|
825
|
-
content: JSON.stringify({
|
|
826
|
-
error: 'Asset ID is required'
|
|
827
|
-
}),
|
|
828
|
-
headers: { 'Content-Type': 'application/json' }
|
|
829
|
-
};
|
|
959
|
+
const result = await this.fetchAssetWithAccessCheck(req);
|
|
960
|
+
if (!result.success) {
|
|
961
|
+
return result.response;
|
|
830
962
|
}
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
status: 404,
|
|
835
|
-
content: 'Asset not found',
|
|
836
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
// Verify this asset belongs to our component
|
|
840
|
-
const config = this.getConfiguration();
|
|
841
|
-
if (asset.name !== config.name) {
|
|
842
|
-
return {
|
|
843
|
-
status: 404,
|
|
844
|
-
content: 'Asset not found',
|
|
845
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
846
|
-
};
|
|
847
|
-
}
|
|
848
|
-
// Get the file content
|
|
849
|
-
const fileContent = await asset.data();
|
|
850
|
-
// Get original filename from asset metadata
|
|
851
|
-
const filename = asset.filename || `asset_${id}`;
|
|
852
|
-
return {
|
|
853
|
-
status: 200,
|
|
854
|
-
content: fileContent,
|
|
855
|
-
headers: {
|
|
856
|
-
'Content-Type': config.contentType,
|
|
857
|
-
'Content-Disposition': `attachment; filename="${filename}"`
|
|
858
|
-
}
|
|
859
|
-
};
|
|
963
|
+
const fileContent = await result.asset.data();
|
|
964
|
+
const filename = result.asset.filename || `asset_${req.params?.id}`;
|
|
965
|
+
return fileResponse(fileContent, this.getConfiguration().contentType, filename);
|
|
860
966
|
}
|
|
861
967
|
catch (error) {
|
|
862
|
-
return
|
|
863
|
-
status: 500,
|
|
864
|
-
content: JSON.stringify({
|
|
865
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
866
|
-
}),
|
|
867
|
-
headers: { 'Content-Type': 'application/json' }
|
|
868
|
-
};
|
|
968
|
+
return errorResponse(error);
|
|
869
969
|
}
|
|
870
970
|
}
|
|
871
971
|
/**
|
|
@@ -883,84 +983,34 @@ export class AssetsManager {
|
|
|
883
983
|
*/
|
|
884
984
|
async handleDelete(req) {
|
|
885
985
|
try {
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
content: JSON.stringify({
|
|
891
|
-
error: 'Authentication required'
|
|
892
|
-
}),
|
|
893
|
-
headers: { 'Content-Type': 'application/json' }
|
|
894
|
-
};
|
|
986
|
+
// Authenticate user
|
|
987
|
+
const authResult = await this.authenticateRequest(req);
|
|
988
|
+
if (!authResult.success) {
|
|
989
|
+
return authResult.response;
|
|
895
990
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
return {
|
|
900
|
-
status: 401,
|
|
901
|
-
content: JSON.stringify({
|
|
902
|
-
error: 'Invalid authentication headers'
|
|
903
|
-
}),
|
|
904
|
-
headers: { 'Content-Type': 'application/json' }
|
|
905
|
-
};
|
|
991
|
+
const userId = authResult.userRecord.id;
|
|
992
|
+
if (!userId) {
|
|
993
|
+
return errorResponse('Failed to retrieve user information');
|
|
906
994
|
}
|
|
907
995
|
const { id } = req.params || {};
|
|
908
996
|
if (!id) {
|
|
909
|
-
return
|
|
910
|
-
status: 400,
|
|
911
|
-
content: JSON.stringify({
|
|
912
|
-
error: 'Asset ID is required'
|
|
913
|
-
}),
|
|
914
|
-
headers: { 'Content-Type': 'application/json' }
|
|
915
|
-
};
|
|
997
|
+
return badRequestResponse('Asset ID is required');
|
|
916
998
|
}
|
|
917
|
-
//
|
|
918
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
919
|
-
if (!userRecord.id) {
|
|
920
|
-
return {
|
|
921
|
-
status: 500,
|
|
922
|
-
content: JSON.stringify({
|
|
923
|
-
error: 'Failed to retrieve user information'
|
|
924
|
-
}),
|
|
925
|
-
headers: { 'Content-Type': 'application/json' }
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
// Check if asset exists and belongs to this user
|
|
999
|
+
// Check if asset exists
|
|
929
1000
|
const asset = await this.getAssetById(id);
|
|
930
1001
|
if (!asset) {
|
|
931
|
-
return
|
|
932
|
-
status: 404,
|
|
933
|
-
content: JSON.stringify({
|
|
934
|
-
error: 'Asset not found'
|
|
935
|
-
}),
|
|
936
|
-
headers: { 'Content-Type': 'application/json' }
|
|
937
|
-
};
|
|
1002
|
+
return notFoundResponse('Asset not found');
|
|
938
1003
|
}
|
|
939
|
-
// Check ownership
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
content: JSON.stringify({
|
|
944
|
-
error: 'You can only delete your own assets'
|
|
945
|
-
}),
|
|
946
|
-
headers: { 'Content-Type': 'application/json' }
|
|
947
|
-
};
|
|
1004
|
+
// Check ownership
|
|
1005
|
+
const ownershipError = this.validateOwnership(asset, userId);
|
|
1006
|
+
if (ownershipError) {
|
|
1007
|
+
return ownershipError;
|
|
948
1008
|
}
|
|
949
1009
|
await this.deleteAssetById(id);
|
|
950
|
-
return {
|
|
951
|
-
status: 200,
|
|
952
|
-
content: JSON.stringify({ message: 'Asset deleted successfully' }),
|
|
953
|
-
headers: { 'Content-Type': 'application/json' }
|
|
954
|
-
};
|
|
1010
|
+
return successResponse({ message: 'Asset deleted successfully' });
|
|
955
1011
|
}
|
|
956
1012
|
catch (error) {
|
|
957
|
-
return
|
|
958
|
-
status: 500,
|
|
959
|
-
content: JSON.stringify({
|
|
960
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
961
|
-
}),
|
|
962
|
-
headers: { 'Content-Type': 'application/json' }
|
|
963
|
-
};
|
|
1013
|
+
return errorResponse(error);
|
|
964
1014
|
}
|
|
965
1015
|
}
|
|
966
1016
|
/**
|
|
@@ -968,149 +1018,160 @@ export class AssetsManager {
|
|
|
968
1018
|
*/
|
|
969
1019
|
async handleUploadBatch(req) {
|
|
970
1020
|
try {
|
|
971
|
-
if (!req
|
|
972
|
-
return
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1021
|
+
if (!req?.body) {
|
|
1022
|
+
return badRequestResponse('Invalid request: missing request body');
|
|
1023
|
+
}
|
|
1024
|
+
// Authenticate user
|
|
1025
|
+
const authResult = await this.authenticateRequest(req);
|
|
1026
|
+
if (!authResult.success) {
|
|
1027
|
+
return authResult.response;
|
|
1028
|
+
}
|
|
1029
|
+
const userId = authResult.userRecord.id;
|
|
1030
|
+
if (!userId) {
|
|
1031
|
+
return errorResponse('Failed to retrieve user information');
|
|
979
1032
|
}
|
|
980
1033
|
const requests = req.body.requests;
|
|
981
1034
|
if (!Array.isArray(requests) || requests.length === 0) {
|
|
982
|
-
return
|
|
983
|
-
status: 400,
|
|
984
|
-
content: JSON.stringify({
|
|
985
|
-
error: 'Requests array is required and must not be empty'
|
|
986
|
-
}),
|
|
987
|
-
headers: { 'Content-Type': 'application/json' }
|
|
988
|
-
};
|
|
1035
|
+
return badRequestResponse('Requests array is required and must not be empty');
|
|
989
1036
|
}
|
|
990
|
-
// Validate all requests first
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
status: 400,
|
|
995
|
-
content: JSON.stringify({
|
|
996
|
-
error: 'Each request must have description, source, filename, and file'
|
|
997
|
-
}),
|
|
998
|
-
headers: { 'Content-Type': 'application/json' }
|
|
999
|
-
};
|
|
1000
|
-
}
|
|
1001
|
-
// Validate file extension for each request
|
|
1002
|
-
if (!this.validateFileExtension(request.filename)) {
|
|
1003
|
-
const config = this.getConfiguration();
|
|
1004
|
-
return {
|
|
1005
|
-
status: 400,
|
|
1006
|
-
content: JSON.stringify({
|
|
1007
|
-
error: `Invalid file extension for ${request.filename}. Expected: ${config.extension}`
|
|
1008
|
-
}),
|
|
1009
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
// Process each request individually for compatibility
|
|
1014
|
-
const results = [];
|
|
1015
|
-
for (const request of requests) {
|
|
1016
|
-
try {
|
|
1017
|
-
await this.uploadAsset({
|
|
1018
|
-
description: request.description,
|
|
1019
|
-
source: request.source,
|
|
1020
|
-
owner_id: request.owner_id || null,
|
|
1021
|
-
filename: request.filename,
|
|
1022
|
-
file: Buffer.from(request.file, 'base64') // Assuming base64 encoded
|
|
1023
|
-
});
|
|
1024
|
-
results.push({ success: true, filename: request.filename });
|
|
1025
|
-
}
|
|
1026
|
-
catch (error) {
|
|
1027
|
-
results.push({
|
|
1028
|
-
success: false,
|
|
1029
|
-
filename: request.filename,
|
|
1030
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
1037
|
+
// Validate all requests first
|
|
1038
|
+
const validationError = this.validateBatchRequests(requests);
|
|
1039
|
+
if (validationError) {
|
|
1040
|
+
return validationError;
|
|
1033
1041
|
}
|
|
1042
|
+
// Process each request
|
|
1043
|
+
const results = await this.processBatchUploads(requests, userId);
|
|
1034
1044
|
const successCount = results.filter(r => r.success).length;
|
|
1035
1045
|
const failureCount = results.length - successCount;
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
}),
|
|
1042
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1043
|
-
};
|
|
1046
|
+
const message = `${successCount}/${requests.length} assets uploaded successfully`;
|
|
1047
|
+
if (failureCount > 0) {
|
|
1048
|
+
return multiStatusResponse(message, results);
|
|
1049
|
+
}
|
|
1050
|
+
return successResponse({ message, results });
|
|
1044
1051
|
}
|
|
1045
1052
|
catch (error) {
|
|
1046
|
-
return
|
|
1047
|
-
|
|
1048
|
-
|
|
1053
|
+
return errorResponse(error);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Validates all requests in a batch upload.
|
|
1058
|
+
*
|
|
1059
|
+
* @param requests - Array of upload requests to validate
|
|
1060
|
+
* @returns DataResponse with error if validation fails, undefined if valid
|
|
1061
|
+
*/
|
|
1062
|
+
validateBatchRequests(requests) {
|
|
1063
|
+
const config = this.getConfiguration();
|
|
1064
|
+
for (const request of requests) {
|
|
1065
|
+
if (!request.file || !request.description || !request.source || !request.filename) {
|
|
1066
|
+
return badRequestResponse('Each request must have description, source, filename, and file');
|
|
1067
|
+
}
|
|
1068
|
+
if (!this.validateBase64(request.file)) {
|
|
1069
|
+
return badRequestResponse(`Invalid base64 data for file: ${request.filename}. File must be a valid base64-encoded string.`);
|
|
1070
|
+
}
|
|
1071
|
+
if (!this.validateFileExtension(request.filename)) {
|
|
1072
|
+
return badRequestResponse(`Invalid file extension for ${request.filename}. Expected: ${config.extension}`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return undefined;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Processes batch upload requests.
|
|
1079
|
+
*
|
|
1080
|
+
* @param requests - Array of upload requests
|
|
1081
|
+
* @param ownerId - Owner user ID
|
|
1082
|
+
* @returns Array of results for each upload
|
|
1083
|
+
*/
|
|
1084
|
+
async processBatchUploads(requests, ownerId) {
|
|
1085
|
+
const results = [];
|
|
1086
|
+
for (const request of requests) {
|
|
1087
|
+
try {
|
|
1088
|
+
await this.uploadAsset({
|
|
1089
|
+
description: request.description,
|
|
1090
|
+
source: request.source,
|
|
1091
|
+
owner_id: ownerId,
|
|
1092
|
+
filename: request.filename,
|
|
1093
|
+
file: Buffer.from(request.file, 'base64'),
|
|
1094
|
+
is_public: request.is_public !== undefined ? Boolean(request.is_public) : true
|
|
1095
|
+
});
|
|
1096
|
+
results.push({ success: true, filename: request.filename });
|
|
1097
|
+
}
|
|
1098
|
+
catch (error) {
|
|
1099
|
+
results.push({
|
|
1100
|
+
success: false,
|
|
1101
|
+
filename: request.filename,
|
|
1049
1102
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1050
|
-
})
|
|
1051
|
-
|
|
1052
|
-
};
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1053
1105
|
}
|
|
1106
|
+
return results;
|
|
1054
1107
|
}
|
|
1055
1108
|
/**
|
|
1056
1109
|
* Handle batch delete endpoint
|
|
1057
1110
|
*/
|
|
1058
1111
|
async handleDeleteBatch(req) {
|
|
1059
1112
|
try {
|
|
1060
|
-
if (!req
|
|
1061
|
-
return
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1113
|
+
if (!req?.body) {
|
|
1114
|
+
return badRequestResponse('Invalid request: missing request body');
|
|
1115
|
+
}
|
|
1116
|
+
// Authenticate user
|
|
1117
|
+
const authResult = await this.authenticateRequest(req);
|
|
1118
|
+
if (!authResult.success) {
|
|
1119
|
+
return authResult.response;
|
|
1120
|
+
}
|
|
1121
|
+
const userId = authResult.userRecord.id;
|
|
1122
|
+
if (!userId) {
|
|
1123
|
+
return errorResponse('Failed to retrieve user information');
|
|
1068
1124
|
}
|
|
1069
1125
|
const { ids } = req.body;
|
|
1070
1126
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
1071
|
-
return
|
|
1072
|
-
status: 400,
|
|
1073
|
-
content: JSON.stringify({
|
|
1074
|
-
error: 'IDs array is required and must not be empty'
|
|
1075
|
-
}),
|
|
1076
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
// Process deletions individually with error handling
|
|
1080
|
-
const results = [];
|
|
1081
|
-
for (const id of ids) {
|
|
1082
|
-
try {
|
|
1083
|
-
await this.deleteAssetById(id);
|
|
1084
|
-
results.push({ success: true, id });
|
|
1085
|
-
}
|
|
1086
|
-
catch (error) {
|
|
1087
|
-
results.push({
|
|
1088
|
-
success: false,
|
|
1089
|
-
id,
|
|
1090
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1127
|
+
return badRequestResponse('IDs array is required and must not be empty');
|
|
1093
1128
|
}
|
|
1129
|
+
// Process deletions
|
|
1130
|
+
const results = await this.processBatchDeletes(ids, userId);
|
|
1094
1131
|
const successCount = results.filter(r => r.success).length;
|
|
1095
1132
|
const failureCount = results.length - successCount;
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
}),
|
|
1102
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1103
|
-
};
|
|
1133
|
+
const message = `${successCount}/${ids.length} assets deleted successfully`;
|
|
1134
|
+
if (failureCount > 0) {
|
|
1135
|
+
return multiStatusResponse(message, results);
|
|
1136
|
+
}
|
|
1137
|
+
return successResponse({ message, results });
|
|
1104
1138
|
}
|
|
1105
1139
|
catch (error) {
|
|
1106
|
-
return
|
|
1107
|
-
|
|
1108
|
-
|
|
1140
|
+
return errorResponse(error);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Processes batch delete requests.
|
|
1145
|
+
*
|
|
1146
|
+
* @param ids - Array of asset IDs to delete
|
|
1147
|
+
* @param userId - User ID for ownership validation
|
|
1148
|
+
* @returns Array of results for each deletion
|
|
1149
|
+
*/
|
|
1150
|
+
async processBatchDeletes(ids, userId) {
|
|
1151
|
+
const results = [];
|
|
1152
|
+
for (const id of ids) {
|
|
1153
|
+
try {
|
|
1154
|
+
const asset = await this.getAssetById(id);
|
|
1155
|
+
if (!asset) {
|
|
1156
|
+
results.push({ success: false, id, error: 'Asset not found' });
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
if (asset.owner_id !== userId) {
|
|
1160
|
+
results.push({ success: false, id, error: 'You can only delete your own assets' });
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
await this.deleteAssetById(id);
|
|
1164
|
+
results.push({ success: true, id });
|
|
1165
|
+
}
|
|
1166
|
+
catch (error) {
|
|
1167
|
+
results.push({
|
|
1168
|
+
success: false,
|
|
1169
|
+
id,
|
|
1109
1170
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1110
|
-
})
|
|
1111
|
-
|
|
1112
|
-
};
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1113
1173
|
}
|
|
1174
|
+
return results;
|
|
1114
1175
|
}
|
|
1115
1176
|
}
|
|
1116
1177
|
//# sourceMappingURL=assets_manager.js.map
|