digitaltwin-core 0.9.1 → 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/components/assets_manager.d.ts +123 -1
- package/dist/components/assets_manager.d.ts.map +1 -1
- package/dist/components/assets_manager.js +440 -684
- package/dist/components/assets_manager.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/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/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.
|
|
@@ -177,6 +184,192 @@ export class AssetsManager {
|
|
|
177
184
|
return false;
|
|
178
185
|
}
|
|
179
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
|
+
}
|
|
180
373
|
/**
|
|
181
374
|
* Upload a new asset file with metadata.
|
|
182
375
|
*
|
|
@@ -240,80 +433,59 @@ export class AssetsManager {
|
|
|
240
433
|
async retrieve(req) {
|
|
241
434
|
try {
|
|
242
435
|
const assets = await this.getAllAssets();
|
|
243
|
-
const config = this.getConfiguration();
|
|
244
|
-
// Check if user is admin (admins can see everything)
|
|
245
436
|
const isAdmin = req && ApisixAuthParser.isAdmin(req.headers || {});
|
|
246
|
-
//
|
|
437
|
+
// Admin can see everything
|
|
247
438
|
if (isAdmin) {
|
|
248
|
-
|
|
249
|
-
id: asset.id,
|
|
250
|
-
name: asset.name,
|
|
251
|
-
date: asset.date,
|
|
252
|
-
contentType: asset.contentType,
|
|
253
|
-
description: asset.description || '',
|
|
254
|
-
source: asset.source || '',
|
|
255
|
-
owner_id: asset.owner_id || null,
|
|
256
|
-
filename: asset.filename || '',
|
|
257
|
-
is_public: asset.is_public ?? true,
|
|
258
|
-
url: `/${config.name}/${asset.id}`,
|
|
259
|
-
download_url: `/${config.name}/${asset.id}/download`
|
|
260
|
-
}));
|
|
261
|
-
return {
|
|
262
|
-
status: 200,
|
|
263
|
-
content: JSON.stringify(assetsWithMetadata),
|
|
264
|
-
headers: { 'Content-Type': 'application/json' }
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
// For non-admin users, check authentication and ownership
|
|
268
|
-
let authenticatedUserId = null;
|
|
269
|
-
if (req && ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
270
|
-
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
271
|
-
if (authUser) {
|
|
272
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
273
|
-
authenticatedUserId = userRecord.id || null;
|
|
274
|
-
}
|
|
439
|
+
return successResponse(this.formatAssetsForResponse(assets));
|
|
275
440
|
}
|
|
276
|
-
//
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
// Private assets are only visible to the owner
|
|
283
|
-
return authenticatedUserId !== null && asset.owner_id === authenticatedUserId;
|
|
284
|
-
});
|
|
285
|
-
// Transform to include asset metadata and user-friendly URLs
|
|
286
|
-
// Note: assets are already sorted by date desc from DB query
|
|
287
|
-
const assetsWithMetadata = visibleAssets.map(asset => ({
|
|
288
|
-
id: asset.id,
|
|
289
|
-
name: asset.name,
|
|
290
|
-
date: asset.date,
|
|
291
|
-
contentType: asset.contentType,
|
|
292
|
-
// Asset-specific metadata (from DB columns)
|
|
293
|
-
description: asset.description || '',
|
|
294
|
-
source: asset.source || '',
|
|
295
|
-
owner_id: asset.owner_id || null,
|
|
296
|
-
filename: asset.filename || '',
|
|
297
|
-
is_public: asset.is_public ?? true,
|
|
298
|
-
// Add URLs that the front-end can actually use
|
|
299
|
-
url: `/${config.name}/${asset.id}`, // For display/use
|
|
300
|
-
download_url: `/${config.name}/${asset.id}/download` // For download
|
|
301
|
-
}));
|
|
302
|
-
return {
|
|
303
|
-
status: 200,
|
|
304
|
-
content: JSON.stringify(assetsWithMetadata),
|
|
305
|
-
headers: { 'Content-Type': 'application/json' }
|
|
306
|
-
};
|
|
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));
|
|
307
446
|
}
|
|
308
447
|
catch (error) {
|
|
309
|
-
return
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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;
|
|
460
|
+
}
|
|
461
|
+
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
462
|
+
if (!authUser) {
|
|
463
|
+
return null;
|
|
316
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
|
+
}));
|
|
317
489
|
}
|
|
318
490
|
/**
|
|
319
491
|
* Get all assets for this component type.
|
|
@@ -619,128 +791,53 @@ export class AssetsManager {
|
|
|
619
791
|
async handleUpload(req) {
|
|
620
792
|
try {
|
|
621
793
|
// Validate request structure
|
|
622
|
-
if (!req
|
|
623
|
-
return
|
|
624
|
-
status: 400,
|
|
625
|
-
content: JSON.stringify({
|
|
626
|
-
error: 'Invalid request: missing request body'
|
|
627
|
-
}),
|
|
628
|
-
headers: { 'Content-Type': 'application/json' }
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
// Step 1: Verify user is authenticated via Apache APISIX headers
|
|
632
|
-
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
633
|
-
return {
|
|
634
|
-
status: 401,
|
|
635
|
-
content: JSON.stringify({
|
|
636
|
-
error: 'Authentication required'
|
|
637
|
-
}),
|
|
638
|
-
headers: { 'Content-Type': 'application/json' }
|
|
639
|
-
};
|
|
794
|
+
if (!req?.body) {
|
|
795
|
+
return badRequestResponse('Invalid request: missing request body');
|
|
640
796
|
}
|
|
641
|
-
//
|
|
642
|
-
const
|
|
643
|
-
if (!
|
|
644
|
-
return
|
|
645
|
-
status: 401,
|
|
646
|
-
content: JSON.stringify({
|
|
647
|
-
error: 'Invalid authentication headers'
|
|
648
|
-
}),
|
|
649
|
-
headers: { 'Content-Type': 'application/json' }
|
|
650
|
-
};
|
|
797
|
+
// Authenticate user
|
|
798
|
+
const authResult = await this.authenticateRequest(req);
|
|
799
|
+
if (!authResult.success) {
|
|
800
|
+
return authResult.response;
|
|
651
801
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
return {
|
|
656
|
-
status: 500,
|
|
657
|
-
content: JSON.stringify({
|
|
658
|
-
error: 'Failed to retrieve user information'
|
|
659
|
-
}),
|
|
660
|
-
headers: { 'Content-Type': 'application/json' }
|
|
661
|
-
};
|
|
802
|
+
const userId = authResult.userRecord.id;
|
|
803
|
+
if (!userId) {
|
|
804
|
+
return errorResponse('Failed to retrieve user information');
|
|
662
805
|
}
|
|
663
|
-
//
|
|
664
|
-
const
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if (!filePath || !description || !source) {
|
|
669
|
-
return {
|
|
670
|
-
status: 400,
|
|
671
|
-
content: JSON.stringify({
|
|
672
|
-
error: 'Missing required fields: description, source, file'
|
|
673
|
-
}),
|
|
674
|
-
headers: { 'Content-Type': 'application/json' }
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
if (!filename) {
|
|
678
|
-
return {
|
|
679
|
-
status: 400,
|
|
680
|
-
content: JSON.stringify({
|
|
681
|
-
error: 'Filename could not be determined from uploaded file'
|
|
682
|
-
}),
|
|
683
|
-
headers: { 'Content-Type': 'application/json' }
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
// Validate file extension early to avoid processing invalid files
|
|
687
|
-
if (!this.validateFileExtension(filename)) {
|
|
688
|
-
const config = this.getConfiguration();
|
|
689
|
-
return {
|
|
690
|
-
status: 400,
|
|
691
|
-
content: JSON.stringify({
|
|
692
|
-
error: `Invalid file extension. Expected: ${config.extension}`
|
|
693
|
-
}),
|
|
694
|
-
headers: { 'Content-Type': 'application/json' }
|
|
695
|
-
};
|
|
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;
|
|
696
811
|
}
|
|
812
|
+
const validData = validation.data;
|
|
697
813
|
// Read file from temporary location
|
|
698
814
|
let fileBuffer;
|
|
699
815
|
try {
|
|
700
|
-
fileBuffer = await
|
|
816
|
+
fileBuffer = await this.readTempFile(validData.filePath);
|
|
701
817
|
}
|
|
702
818
|
catch (error) {
|
|
703
|
-
return {
|
|
704
|
-
status: 500,
|
|
705
|
-
content: JSON.stringify({
|
|
706
|
-
error: `Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
707
|
-
}),
|
|
708
|
-
headers: { 'Content-Type': 'application/json' }
|
|
709
|
-
};
|
|
819
|
+
return errorResponse(`Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
710
820
|
}
|
|
821
|
+
// Upload asset and cleanup
|
|
711
822
|
try {
|
|
712
823
|
await this.uploadAsset({
|
|
713
|
-
description,
|
|
714
|
-
source,
|
|
715
|
-
owner_id:
|
|
716
|
-
filename,
|
|
824
|
+
description: validData.description,
|
|
825
|
+
source: validData.source,
|
|
826
|
+
owner_id: userId,
|
|
827
|
+
filename: validData.filename,
|
|
717
828
|
file: fileBuffer,
|
|
718
|
-
is_public: is_public
|
|
719
|
-
});
|
|
720
|
-
// Clean up temporary file after successful upload
|
|
721
|
-
await fs.unlink(filePath).catch(() => {
|
|
722
|
-
// Ignore cleanup errors, but log them in production
|
|
829
|
+
is_public: validData.is_public
|
|
723
830
|
});
|
|
831
|
+
await this.cleanupTempFile(validData.filePath);
|
|
724
832
|
}
|
|
725
833
|
catch (error) {
|
|
726
|
-
|
|
727
|
-
await fs.unlink(filePath).catch(() => { });
|
|
834
|
+
await this.cleanupTempFile(validData.filePath);
|
|
728
835
|
throw error;
|
|
729
836
|
}
|
|
730
|
-
return {
|
|
731
|
-
status: 200,
|
|
732
|
-
content: JSON.stringify({ message: 'Asset uploaded successfully' }),
|
|
733
|
-
headers: { 'Content-Type': 'application/json' }
|
|
734
|
-
};
|
|
837
|
+
return successResponse({ message: 'Asset uploaded successfully' });
|
|
735
838
|
}
|
|
736
839
|
catch (error) {
|
|
737
|
-
return
|
|
738
|
-
status: 500,
|
|
739
|
-
content: JSON.stringify({
|
|
740
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
741
|
-
}),
|
|
742
|
-
headers: { 'Content-Type': 'application/json' }
|
|
743
|
-
};
|
|
840
|
+
return errorResponse(error);
|
|
744
841
|
}
|
|
745
842
|
}
|
|
746
843
|
/**
|
|
@@ -760,87 +857,36 @@ export class AssetsManager {
|
|
|
760
857
|
async handleUpdate(req) {
|
|
761
858
|
try {
|
|
762
859
|
if (!req) {
|
|
763
|
-
return
|
|
764
|
-
status: 400,
|
|
765
|
-
content: JSON.stringify({
|
|
766
|
-
error: 'Invalid request: missing request object'
|
|
767
|
-
}),
|
|
768
|
-
headers: { 'Content-Type': 'application/json' }
|
|
769
|
-
};
|
|
860
|
+
return badRequestResponse('Invalid request: missing request object');
|
|
770
861
|
}
|
|
771
|
-
//
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
content: JSON.stringify({
|
|
776
|
-
error: 'Authentication required'
|
|
777
|
-
}),
|
|
778
|
-
headers: { 'Content-Type': 'application/json' }
|
|
779
|
-
};
|
|
862
|
+
// Authenticate user
|
|
863
|
+
const authResult = await this.authenticateRequest(req);
|
|
864
|
+
if (!authResult.success) {
|
|
865
|
+
return authResult.response;
|
|
780
866
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
return {
|
|
785
|
-
status: 401,
|
|
786
|
-
content: JSON.stringify({
|
|
787
|
-
error: 'Invalid authentication headers'
|
|
788
|
-
}),
|
|
789
|
-
headers: { 'Content-Type': 'application/json' }
|
|
790
|
-
};
|
|
867
|
+
const userId = authResult.userRecord.id;
|
|
868
|
+
if (!userId) {
|
|
869
|
+
return errorResponse('Failed to retrieve user information');
|
|
791
870
|
}
|
|
792
871
|
const { id } = req.params || {};
|
|
793
872
|
const { description, source, is_public } = req.body || {};
|
|
794
873
|
if (!id) {
|
|
795
|
-
return
|
|
796
|
-
status: 400,
|
|
797
|
-
content: JSON.stringify({
|
|
798
|
-
error: 'Asset ID is required'
|
|
799
|
-
}),
|
|
800
|
-
headers: { 'Content-Type': 'application/json' }
|
|
801
|
-
};
|
|
874
|
+
return badRequestResponse('Asset ID is required');
|
|
802
875
|
}
|
|
803
876
|
if (!description && !source && is_public === undefined) {
|
|
804
|
-
return
|
|
805
|
-
status: 400,
|
|
806
|
-
content: JSON.stringify({
|
|
807
|
-
error: 'At least one field (description, source, or is_public) must be provided for update'
|
|
808
|
-
}),
|
|
809
|
-
headers: { 'Content-Type': 'application/json' }
|
|
810
|
-
};
|
|
877
|
+
return badRequestResponse('At least one field (description, source, or is_public) must be provided for update');
|
|
811
878
|
}
|
|
812
|
-
//
|
|
813
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
814
|
-
if (!userRecord.id) {
|
|
815
|
-
return {
|
|
816
|
-
status: 500,
|
|
817
|
-
content: JSON.stringify({
|
|
818
|
-
error: 'Failed to retrieve user information'
|
|
819
|
-
}),
|
|
820
|
-
headers: { 'Content-Type': 'application/json' }
|
|
821
|
-
};
|
|
822
|
-
}
|
|
823
|
-
// Check if asset exists and belongs to this user
|
|
879
|
+
// Check if asset exists
|
|
824
880
|
const asset = await this.getAssetById(id);
|
|
825
881
|
if (!asset) {
|
|
826
|
-
return
|
|
827
|
-
status: 404,
|
|
828
|
-
content: JSON.stringify({
|
|
829
|
-
error: 'Asset not found'
|
|
830
|
-
}),
|
|
831
|
-
headers: { 'Content-Type': 'application/json' }
|
|
832
|
-
};
|
|
882
|
+
return notFoundResponse('Asset not found');
|
|
833
883
|
}
|
|
834
|
-
// Check ownership
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
content: JSON.stringify({
|
|
839
|
-
error: 'You can only modify your own assets'
|
|
840
|
-
}),
|
|
841
|
-
headers: { 'Content-Type': 'application/json' }
|
|
842
|
-
};
|
|
884
|
+
// Check ownership
|
|
885
|
+
const ownershipError = this.validateOwnership(asset, userId);
|
|
886
|
+
if (ownershipError) {
|
|
887
|
+
return ownershipError;
|
|
843
888
|
}
|
|
889
|
+
// Build and apply updates
|
|
844
890
|
const updates = {};
|
|
845
891
|
if (description !== undefined)
|
|
846
892
|
updates.description = description;
|
|
@@ -849,20 +895,10 @@ export class AssetsManager {
|
|
|
849
895
|
if (is_public !== undefined)
|
|
850
896
|
updates.is_public = Boolean(is_public);
|
|
851
897
|
await this.updateAssetMetadata(id, updates);
|
|
852
|
-
return {
|
|
853
|
-
status: 200,
|
|
854
|
-
content: JSON.stringify({ message: 'Asset metadata updated successfully' }),
|
|
855
|
-
headers: { 'Content-Type': 'application/json' }
|
|
856
|
-
};
|
|
898
|
+
return successResponse({ message: 'Asset metadata updated successfully' });
|
|
857
899
|
}
|
|
858
900
|
catch (error) {
|
|
859
|
-
return
|
|
860
|
-
status: 500,
|
|
861
|
-
content: JSON.stringify({
|
|
862
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
863
|
-
}),
|
|
864
|
-
headers: { 'Content-Type': 'application/json' }
|
|
865
|
-
};
|
|
901
|
+
return errorResponse(error);
|
|
866
902
|
}
|
|
867
903
|
}
|
|
868
904
|
/**
|
|
@@ -887,88 +923,15 @@ export class AssetsManager {
|
|
|
887
923
|
*/
|
|
888
924
|
async handleGetAsset(req) {
|
|
889
925
|
try {
|
|
890
|
-
const
|
|
891
|
-
if (!
|
|
892
|
-
return
|
|
893
|
-
status: 400,
|
|
894
|
-
content: JSON.stringify({
|
|
895
|
-
error: 'Asset ID is required'
|
|
896
|
-
}),
|
|
897
|
-
headers: { 'Content-Type': 'application/json' }
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
const asset = await this.getAssetById(id);
|
|
901
|
-
if (!asset) {
|
|
902
|
-
return {
|
|
903
|
-
status: 404,
|
|
904
|
-
content: 'Asset not found',
|
|
905
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
// Verify this asset belongs to our component
|
|
909
|
-
const config = this.getConfiguration();
|
|
910
|
-
if (asset.name !== config.name) {
|
|
911
|
-
return {
|
|
912
|
-
status: 404,
|
|
913
|
-
content: 'Asset not found',
|
|
914
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
// Check if user is admin (admins can access everything)
|
|
918
|
-
const isAdmin = ApisixAuthParser.isAdmin(req.headers || {});
|
|
919
|
-
// Check visibility: if private and user is not admin, verify ownership
|
|
920
|
-
if (!asset.is_public && !isAdmin) {
|
|
921
|
-
// Asset is private, require authentication
|
|
922
|
-
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
923
|
-
return {
|
|
924
|
-
status: 401,
|
|
925
|
-
content: JSON.stringify({
|
|
926
|
-
error: 'Authentication required for private assets'
|
|
927
|
-
}),
|
|
928
|
-
headers: { 'Content-Type': 'application/json' }
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
// Verify ownership
|
|
932
|
-
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
933
|
-
if (!authUser) {
|
|
934
|
-
return {
|
|
935
|
-
status: 401,
|
|
936
|
-
content: JSON.stringify({
|
|
937
|
-
error: 'Invalid authentication headers'
|
|
938
|
-
}),
|
|
939
|
-
headers: { 'Content-Type': 'application/json' }
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
943
|
-
if (!userRecord.id || asset.owner_id !== userRecord.id) {
|
|
944
|
-
return {
|
|
945
|
-
status: 403,
|
|
946
|
-
content: JSON.stringify({
|
|
947
|
-
error: 'This asset is private'
|
|
948
|
-
}),
|
|
949
|
-
headers: { 'Content-Type': 'application/json' }
|
|
950
|
-
};
|
|
951
|
-
}
|
|
926
|
+
const result = await this.fetchAssetWithAccessCheck(req);
|
|
927
|
+
if (!result.success) {
|
|
928
|
+
return result.response;
|
|
952
929
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
return {
|
|
956
|
-
status: 200,
|
|
957
|
-
content: fileContent,
|
|
958
|
-
headers: {
|
|
959
|
-
'Content-Type': config.contentType
|
|
960
|
-
// No Content-Disposition - browser will display/use the file
|
|
961
|
-
}
|
|
962
|
-
};
|
|
930
|
+
const fileContent = await result.asset.data();
|
|
931
|
+
return fileResponse(fileContent, this.getConfiguration().contentType);
|
|
963
932
|
}
|
|
964
933
|
catch (error) {
|
|
965
|
-
return
|
|
966
|
-
status: 500,
|
|
967
|
-
content: JSON.stringify({
|
|
968
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
969
|
-
}),
|
|
970
|
-
headers: { 'Content-Type': 'application/json' }
|
|
971
|
-
};
|
|
934
|
+
return errorResponse(error);
|
|
972
935
|
}
|
|
973
936
|
}
|
|
974
937
|
/**
|
|
@@ -993,90 +956,16 @@ export class AssetsManager {
|
|
|
993
956
|
*/
|
|
994
957
|
async handleDownload(req) {
|
|
995
958
|
try {
|
|
996
|
-
const
|
|
997
|
-
if (!
|
|
998
|
-
return
|
|
999
|
-
status: 400,
|
|
1000
|
-
content: JSON.stringify({
|
|
1001
|
-
error: 'Asset ID is required'
|
|
1002
|
-
}),
|
|
1003
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
const asset = await this.getAssetById(id);
|
|
1007
|
-
if (!asset) {
|
|
1008
|
-
return {
|
|
1009
|
-
status: 404,
|
|
1010
|
-
content: 'Asset not found',
|
|
1011
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
1012
|
-
};
|
|
959
|
+
const result = await this.fetchAssetWithAccessCheck(req);
|
|
960
|
+
if (!result.success) {
|
|
961
|
+
return result.response;
|
|
1013
962
|
}
|
|
1014
|
-
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
return {
|
|
1018
|
-
status: 404,
|
|
1019
|
-
content: 'Asset not found',
|
|
1020
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
// Check if user is admin (admins can download everything)
|
|
1024
|
-
const isAdmin = ApisixAuthParser.isAdmin(req.headers || {});
|
|
1025
|
-
// Check visibility: if private and user is not admin, verify ownership
|
|
1026
|
-
if (!asset.is_public && !isAdmin) {
|
|
1027
|
-
// Asset is private, require authentication
|
|
1028
|
-
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
1029
|
-
return {
|
|
1030
|
-
status: 401,
|
|
1031
|
-
content: JSON.stringify({
|
|
1032
|
-
error: 'Authentication required for private assets'
|
|
1033
|
-
}),
|
|
1034
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
// Verify ownership
|
|
1038
|
-
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
1039
|
-
if (!authUser) {
|
|
1040
|
-
return {
|
|
1041
|
-
status: 401,
|
|
1042
|
-
content: JSON.stringify({
|
|
1043
|
-
error: 'Invalid authentication headers'
|
|
1044
|
-
}),
|
|
1045
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
1049
|
-
if (!userRecord.id || asset.owner_id !== userRecord.id) {
|
|
1050
|
-
return {
|
|
1051
|
-
status: 403,
|
|
1052
|
-
content: JSON.stringify({
|
|
1053
|
-
error: 'This asset is private'
|
|
1054
|
-
}),
|
|
1055
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
// Get the file content
|
|
1060
|
-
const fileContent = await asset.data();
|
|
1061
|
-
// Get original filename from asset metadata
|
|
1062
|
-
const filename = asset.filename || `asset_${id}`;
|
|
1063
|
-
return {
|
|
1064
|
-
status: 200,
|
|
1065
|
-
content: fileContent,
|
|
1066
|
-
headers: {
|
|
1067
|
-
'Content-Type': config.contentType,
|
|
1068
|
-
'Content-Disposition': `attachment; filename="${filename}"`
|
|
1069
|
-
}
|
|
1070
|
-
};
|
|
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);
|
|
1071
966
|
}
|
|
1072
967
|
catch (error) {
|
|
1073
|
-
return
|
|
1074
|
-
status: 500,
|
|
1075
|
-
content: JSON.stringify({
|
|
1076
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1077
|
-
}),
|
|
1078
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1079
|
-
};
|
|
968
|
+
return errorResponse(error);
|
|
1080
969
|
}
|
|
1081
970
|
}
|
|
1082
971
|
/**
|
|
@@ -1094,84 +983,34 @@ export class AssetsManager {
|
|
|
1094
983
|
*/
|
|
1095
984
|
async handleDelete(req) {
|
|
1096
985
|
try {
|
|
1097
|
-
//
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
content: JSON.stringify({
|
|
1102
|
-
error: 'Authentication required'
|
|
1103
|
-
}),
|
|
1104
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1105
|
-
};
|
|
986
|
+
// Authenticate user
|
|
987
|
+
const authResult = await this.authenticateRequest(req);
|
|
988
|
+
if (!authResult.success) {
|
|
989
|
+
return authResult.response;
|
|
1106
990
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
return {
|
|
1111
|
-
status: 401,
|
|
1112
|
-
content: JSON.stringify({
|
|
1113
|
-
error: 'Invalid authentication headers'
|
|
1114
|
-
}),
|
|
1115
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1116
|
-
};
|
|
991
|
+
const userId = authResult.userRecord.id;
|
|
992
|
+
if (!userId) {
|
|
993
|
+
return errorResponse('Failed to retrieve user information');
|
|
1117
994
|
}
|
|
1118
995
|
const { id } = req.params || {};
|
|
1119
996
|
if (!id) {
|
|
1120
|
-
return
|
|
1121
|
-
status: 400,
|
|
1122
|
-
content: JSON.stringify({
|
|
1123
|
-
error: 'Asset ID is required'
|
|
1124
|
-
}),
|
|
1125
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
// Find or create user in database
|
|
1129
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
1130
|
-
if (!userRecord.id) {
|
|
1131
|
-
return {
|
|
1132
|
-
status: 500,
|
|
1133
|
-
content: JSON.stringify({
|
|
1134
|
-
error: 'Failed to retrieve user information'
|
|
1135
|
-
}),
|
|
1136
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1137
|
-
};
|
|
997
|
+
return badRequestResponse('Asset ID is required');
|
|
1138
998
|
}
|
|
1139
|
-
// Check if asset exists
|
|
999
|
+
// Check if asset exists
|
|
1140
1000
|
const asset = await this.getAssetById(id);
|
|
1141
1001
|
if (!asset) {
|
|
1142
|
-
return
|
|
1143
|
-
status: 404,
|
|
1144
|
-
content: JSON.stringify({
|
|
1145
|
-
error: 'Asset not found'
|
|
1146
|
-
}),
|
|
1147
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1148
|
-
};
|
|
1002
|
+
return notFoundResponse('Asset not found');
|
|
1149
1003
|
}
|
|
1150
|
-
// Check ownership
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
content: JSON.stringify({
|
|
1155
|
-
error: 'You can only delete your own assets'
|
|
1156
|
-
}),
|
|
1157
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1158
|
-
};
|
|
1004
|
+
// Check ownership
|
|
1005
|
+
const ownershipError = this.validateOwnership(asset, userId);
|
|
1006
|
+
if (ownershipError) {
|
|
1007
|
+
return ownershipError;
|
|
1159
1008
|
}
|
|
1160
1009
|
await this.deleteAssetById(id);
|
|
1161
|
-
return {
|
|
1162
|
-
status: 200,
|
|
1163
|
-
content: JSON.stringify({ message: 'Asset deleted successfully' }),
|
|
1164
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1165
|
-
};
|
|
1010
|
+
return successResponse({ message: 'Asset deleted successfully' });
|
|
1166
1011
|
}
|
|
1167
1012
|
catch (error) {
|
|
1168
|
-
return
|
|
1169
|
-
status: 500,
|
|
1170
|
-
content: JSON.stringify({
|
|
1171
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1172
|
-
}),
|
|
1173
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1174
|
-
};
|
|
1013
|
+
return errorResponse(error);
|
|
1175
1014
|
}
|
|
1176
1015
|
}
|
|
1177
1016
|
/**
|
|
@@ -1179,243 +1018,160 @@ export class AssetsManager {
|
|
|
1179
1018
|
*/
|
|
1180
1019
|
async handleUploadBatch(req) {
|
|
1181
1020
|
try {
|
|
1182
|
-
if (!req
|
|
1183
|
-
return
|
|
1184
|
-
status: 400,
|
|
1185
|
-
content: JSON.stringify({
|
|
1186
|
-
error: 'Invalid request: missing request body'
|
|
1187
|
-
}),
|
|
1188
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1189
|
-
};
|
|
1190
|
-
}
|
|
1191
|
-
// Check authentication
|
|
1192
|
-
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
1193
|
-
return {
|
|
1194
|
-
status: 401,
|
|
1195
|
-
content: JSON.stringify({
|
|
1196
|
-
error: 'Authentication required'
|
|
1197
|
-
}),
|
|
1198
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1199
|
-
};
|
|
1021
|
+
if (!req?.body) {
|
|
1022
|
+
return badRequestResponse('Invalid request: missing request body');
|
|
1200
1023
|
}
|
|
1201
|
-
//
|
|
1202
|
-
const
|
|
1203
|
-
if (!
|
|
1204
|
-
return
|
|
1205
|
-
status: 401,
|
|
1206
|
-
content: JSON.stringify({
|
|
1207
|
-
error: 'Invalid authentication headers'
|
|
1208
|
-
}),
|
|
1209
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1210
|
-
};
|
|
1024
|
+
// Authenticate user
|
|
1025
|
+
const authResult = await this.authenticateRequest(req);
|
|
1026
|
+
if (!authResult.success) {
|
|
1027
|
+
return authResult.response;
|
|
1211
1028
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
return {
|
|
1216
|
-
status: 500,
|
|
1217
|
-
content: JSON.stringify({
|
|
1218
|
-
error: 'Failed to retrieve user information'
|
|
1219
|
-
}),
|
|
1220
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1221
|
-
};
|
|
1029
|
+
const userId = authResult.userRecord.id;
|
|
1030
|
+
if (!userId) {
|
|
1031
|
+
return errorResponse('Failed to retrieve user information');
|
|
1222
1032
|
}
|
|
1223
1033
|
const requests = req.body.requests;
|
|
1224
1034
|
if (!Array.isArray(requests) || requests.length === 0) {
|
|
1225
|
-
return
|
|
1226
|
-
status: 400,
|
|
1227
|
-
content: JSON.stringify({
|
|
1228
|
-
error: 'Requests array is required and must not be empty'
|
|
1229
|
-
}),
|
|
1230
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1231
|
-
};
|
|
1232
|
-
}
|
|
1233
|
-
// Validate all requests first (including extensions and base64 format)
|
|
1234
|
-
for (const request of requests) {
|
|
1235
|
-
if (!request.file || !request.description || !request.source || !request.filename) {
|
|
1236
|
-
return {
|
|
1237
|
-
status: 400,
|
|
1238
|
-
content: JSON.stringify({
|
|
1239
|
-
error: 'Each request must have description, source, filename, and file'
|
|
1240
|
-
}),
|
|
1241
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1242
|
-
};
|
|
1243
|
-
}
|
|
1244
|
-
// Validate that file is valid base64 string
|
|
1245
|
-
if (!this.validateBase64(request.file)) {
|
|
1246
|
-
return {
|
|
1247
|
-
status: 400,
|
|
1248
|
-
content: JSON.stringify({
|
|
1249
|
-
error: `Invalid base64 data for file: ${request.filename}. File must be a valid base64-encoded string.`
|
|
1250
|
-
}),
|
|
1251
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
// Validate file extension for each request
|
|
1255
|
-
if (!this.validateFileExtension(request.filename)) {
|
|
1256
|
-
const config = this.getConfiguration();
|
|
1257
|
-
return {
|
|
1258
|
-
status: 400,
|
|
1259
|
-
content: JSON.stringify({
|
|
1260
|
-
error: `Invalid file extension for ${request.filename}. Expected: ${config.extension}`
|
|
1261
|
-
}),
|
|
1262
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1263
|
-
};
|
|
1264
|
-
}
|
|
1035
|
+
return badRequestResponse('Requests array is required and must not be empty');
|
|
1265
1036
|
}
|
|
1266
|
-
//
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
await this.uploadAsset({
|
|
1271
|
-
description: request.description,
|
|
1272
|
-
source: request.source,
|
|
1273
|
-
owner_id: userRecord.id,
|
|
1274
|
-
filename: request.filename,
|
|
1275
|
-
file: Buffer.from(request.file, 'base64'), // Already validated as base64 above
|
|
1276
|
-
is_public: request.is_public !== undefined ? Boolean(request.is_public) : true
|
|
1277
|
-
});
|
|
1278
|
-
results.push({ success: true, filename: request.filename });
|
|
1279
|
-
}
|
|
1280
|
-
catch (error) {
|
|
1281
|
-
results.push({
|
|
1282
|
-
success: false,
|
|
1283
|
-
filename: request.filename,
|
|
1284
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1037
|
+
// Validate all requests first
|
|
1038
|
+
const validationError = this.validateBatchRequests(requests);
|
|
1039
|
+
if (validationError) {
|
|
1040
|
+
return validationError;
|
|
1287
1041
|
}
|
|
1042
|
+
// Process each request
|
|
1043
|
+
const results = await this.processBatchUploads(requests, userId);
|
|
1288
1044
|
const successCount = results.filter(r => r.success).length;
|
|
1289
1045
|
const failureCount = results.length - successCount;
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
}),
|
|
1296
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1297
|
-
};
|
|
1046
|
+
const message = `${successCount}/${requests.length} assets uploaded successfully`;
|
|
1047
|
+
if (failureCount > 0) {
|
|
1048
|
+
return multiStatusResponse(message, results);
|
|
1049
|
+
}
|
|
1050
|
+
return successResponse({ message, results });
|
|
1298
1051
|
}
|
|
1299
1052
|
catch (error) {
|
|
1300
|
-
return
|
|
1301
|
-
|
|
1302
|
-
|
|
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,
|
|
1303
1102
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1304
|
-
})
|
|
1305
|
-
|
|
1306
|
-
};
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1307
1105
|
}
|
|
1106
|
+
return results;
|
|
1308
1107
|
}
|
|
1309
1108
|
/**
|
|
1310
1109
|
* Handle batch delete endpoint
|
|
1311
1110
|
*/
|
|
1312
1111
|
async handleDeleteBatch(req) {
|
|
1313
1112
|
try {
|
|
1314
|
-
if (!req
|
|
1315
|
-
return
|
|
1316
|
-
status: 400,
|
|
1317
|
-
content: JSON.stringify({
|
|
1318
|
-
error: 'Invalid request: missing request body'
|
|
1319
|
-
}),
|
|
1320
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
// Check authentication
|
|
1324
|
-
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
1325
|
-
return {
|
|
1326
|
-
status: 401,
|
|
1327
|
-
content: JSON.stringify({
|
|
1328
|
-
error: 'Authentication required'
|
|
1329
|
-
}),
|
|
1330
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1331
|
-
};
|
|
1113
|
+
if (!req?.body) {
|
|
1114
|
+
return badRequestResponse('Invalid request: missing request body');
|
|
1332
1115
|
}
|
|
1333
|
-
//
|
|
1334
|
-
const
|
|
1335
|
-
if (!
|
|
1336
|
-
return
|
|
1337
|
-
status: 401,
|
|
1338
|
-
content: JSON.stringify({
|
|
1339
|
-
error: 'Invalid authentication headers'
|
|
1340
|
-
}),
|
|
1341
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1342
|
-
};
|
|
1116
|
+
// Authenticate user
|
|
1117
|
+
const authResult = await this.authenticateRequest(req);
|
|
1118
|
+
if (!authResult.success) {
|
|
1119
|
+
return authResult.response;
|
|
1343
1120
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
return {
|
|
1348
|
-
status: 500,
|
|
1349
|
-
content: JSON.stringify({
|
|
1350
|
-
error: 'Failed to retrieve user information'
|
|
1351
|
-
}),
|
|
1352
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1353
|
-
};
|
|
1121
|
+
const userId = authResult.userRecord.id;
|
|
1122
|
+
if (!userId) {
|
|
1123
|
+
return errorResponse('Failed to retrieve user information');
|
|
1354
1124
|
}
|
|
1355
1125
|
const { ids } = req.body;
|
|
1356
1126
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
1357
|
-
return
|
|
1358
|
-
status: 400,
|
|
1359
|
-
content: JSON.stringify({
|
|
1360
|
-
error: 'IDs array is required and must not be empty'
|
|
1361
|
-
}),
|
|
1362
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1363
|
-
};
|
|
1364
|
-
}
|
|
1365
|
-
// Process deletions individually with error handling and ownership checks
|
|
1366
|
-
const results = [];
|
|
1367
|
-
for (const id of ids) {
|
|
1368
|
-
try {
|
|
1369
|
-
// Check if asset exists and belongs to this user
|
|
1370
|
-
const asset = await this.getAssetById(id);
|
|
1371
|
-
if (!asset) {
|
|
1372
|
-
results.push({
|
|
1373
|
-
success: false,
|
|
1374
|
-
id,
|
|
1375
|
-
error: 'Asset not found'
|
|
1376
|
-
});
|
|
1377
|
-
continue;
|
|
1378
|
-
}
|
|
1379
|
-
// Check ownership (only owner can delete their assets)
|
|
1380
|
-
if (asset.owner_id !== userRecord.id) {
|
|
1381
|
-
results.push({
|
|
1382
|
-
success: false,
|
|
1383
|
-
id,
|
|
1384
|
-
error: 'You can only delete your own assets'
|
|
1385
|
-
});
|
|
1386
|
-
continue;
|
|
1387
|
-
}
|
|
1388
|
-
await this.deleteAssetById(id);
|
|
1389
|
-
results.push({ success: true, id });
|
|
1390
|
-
}
|
|
1391
|
-
catch (error) {
|
|
1392
|
-
results.push({
|
|
1393
|
-
success: false,
|
|
1394
|
-
id,
|
|
1395
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1396
|
-
});
|
|
1397
|
-
}
|
|
1127
|
+
return badRequestResponse('IDs array is required and must not be empty');
|
|
1398
1128
|
}
|
|
1129
|
+
// Process deletions
|
|
1130
|
+
const results = await this.processBatchDeletes(ids, userId);
|
|
1399
1131
|
const successCount = results.filter(r => r.success).length;
|
|
1400
1132
|
const failureCount = results.length - successCount;
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
}),
|
|
1407
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1408
|
-
};
|
|
1133
|
+
const message = `${successCount}/${ids.length} assets deleted successfully`;
|
|
1134
|
+
if (failureCount > 0) {
|
|
1135
|
+
return multiStatusResponse(message, results);
|
|
1136
|
+
}
|
|
1137
|
+
return successResponse({ message, results });
|
|
1409
1138
|
}
|
|
1410
1139
|
catch (error) {
|
|
1411
|
-
return
|
|
1412
|
-
|
|
1413
|
-
|
|
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,
|
|
1414
1170
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1415
|
-
})
|
|
1416
|
-
|
|
1417
|
-
};
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1418
1173
|
}
|
|
1174
|
+
return results;
|
|
1419
1175
|
}
|
|
1420
1176
|
}
|
|
1421
1177
|
//# sourceMappingURL=assets_manager.js.map
|