digitaltwin-core 0.9.1 → 0.10.0
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/user_service.d.ts.map +1 -1
- package/dist/auth/user_service.js +0 -5
- package/dist/auth/user_service.js.map +1 -1
- 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 +475 -687
- package/dist/components/assets_manager.js.map +1 -1
- package/dist/engine/digital_twin_engine.d.ts.map +1 -1
- package/dist/engine/digital_twin_engine.js +2 -13
- package/dist/engine/digital_twin_engine.js.map +1 -1
- package/dist/engine/initializer.js +5 -11
- package/dist/engine/initializer.js.map +1 -1
- package/dist/engine/scheduler.d.ts.map +1 -1
- package/dist/engine/scheduler.js +10 -14
- package/dist/engine/scheduler.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 +2 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ApisixAuthParser } from '../auth/apisix_parser.js';
|
|
2
2
|
import { UserService } from '../auth/user_service.js';
|
|
3
|
+
import { AuthConfig } from '../auth/auth_config.js';
|
|
4
|
+
import { successResponse, errorResponse, badRequestResponse, unauthorizedResponse, forbiddenResponse, notFoundResponse, textResponse, fileResponse, multiStatusResponse, HttpStatus } from '../utils/http_responses.js';
|
|
3
5
|
import fs from 'fs/promises';
|
|
4
6
|
/**
|
|
5
7
|
* Abstract base class for Assets Manager components with authentication and access control.
|
|
@@ -65,17 +67,23 @@ export class AssetsManager {
|
|
|
65
67
|
*
|
|
66
68
|
* @param {DatabaseAdapter} db - The database adapter for metadata storage
|
|
67
69
|
* @param {StorageService} storage - The storage service for file persistence
|
|
70
|
+
* @param {UserService} [userService] - Optional user service for authentication (created automatically if not provided)
|
|
68
71
|
*
|
|
69
72
|
* @example
|
|
70
73
|
* ```typescript
|
|
74
|
+
* // Standard usage (UserService created automatically)
|
|
71
75
|
* const assetsManager = new MyAssetsManager()
|
|
72
76
|
* assetsManager.setDependencies(databaseAdapter, storageService)
|
|
77
|
+
*
|
|
78
|
+
* // For testing (inject mock UserService)
|
|
79
|
+
* const mockUserService = new MockUserService()
|
|
80
|
+
* assetsManager.setDependencies(databaseAdapter, storageService, mockUserService)
|
|
73
81
|
* ```
|
|
74
82
|
*/
|
|
75
|
-
setDependencies(db, storage) {
|
|
83
|
+
setDependencies(db, storage, userService) {
|
|
76
84
|
this.db = db;
|
|
77
85
|
this.storage = storage;
|
|
78
|
-
this.userService = new UserService(db);
|
|
86
|
+
this.userService = userService ?? new UserService(db);
|
|
79
87
|
}
|
|
80
88
|
/**
|
|
81
89
|
* Validates that a source string is a valid URL.
|
|
@@ -177,6 +185,208 @@ export class AssetsManager {
|
|
|
177
185
|
return false;
|
|
178
186
|
}
|
|
179
187
|
}
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Authentication & Request Processing Helpers
|
|
190
|
+
// ============================================================================
|
|
191
|
+
/**
|
|
192
|
+
* Authenticates a request and returns the user record.
|
|
193
|
+
*
|
|
194
|
+
* This method consolidates the authentication flow:
|
|
195
|
+
* 1. Validates APISIX headers are present
|
|
196
|
+
* 2. Parses authentication headers
|
|
197
|
+
* 3. Finds or creates user record in database
|
|
198
|
+
*
|
|
199
|
+
* @param req - HTTP request object
|
|
200
|
+
* @returns AuthResult with either userRecord on success or DataResponse on failure
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* const authResult = await this.authenticateRequest(req)
|
|
205
|
+
* if (!authResult.success) {
|
|
206
|
+
* return authResult.response
|
|
207
|
+
* }
|
|
208
|
+
* const userRecord = authResult.userRecord
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
async authenticateRequest(req) {
|
|
212
|
+
// If auth is disabled, create an anonymous user
|
|
213
|
+
if (AuthConfig.isAuthDisabled()) {
|
|
214
|
+
const anonymousUser = {
|
|
215
|
+
id: AuthConfig.getAnonymousUserId(),
|
|
216
|
+
roles: []
|
|
217
|
+
};
|
|
218
|
+
const userRecord = await this.userService.findOrCreateUser(anonymousUser);
|
|
219
|
+
return { success: true, userRecord };
|
|
220
|
+
}
|
|
221
|
+
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
222
|
+
return { success: false, response: unauthorizedResponse() };
|
|
223
|
+
}
|
|
224
|
+
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
225
|
+
if (!authUser) {
|
|
226
|
+
return { success: false, response: unauthorizedResponse('Invalid authentication headers') };
|
|
227
|
+
}
|
|
228
|
+
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
229
|
+
if (!userRecord.id) {
|
|
230
|
+
return { success: false, response: errorResponse('Failed to retrieve user information') };
|
|
231
|
+
}
|
|
232
|
+
return { success: true, userRecord };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Extracts upload data from multipart form request.
|
|
236
|
+
*
|
|
237
|
+
* @param req - HTTP request object with body and file
|
|
238
|
+
* @returns UploadData object with extracted fields
|
|
239
|
+
*/
|
|
240
|
+
extractUploadData(req) {
|
|
241
|
+
return {
|
|
242
|
+
description: req.body?.description,
|
|
243
|
+
source: req.body?.source,
|
|
244
|
+
is_public: req.body?.is_public,
|
|
245
|
+
filePath: req.file?.path,
|
|
246
|
+
fileBuffer: req.file?.buffer,
|
|
247
|
+
filename: req.file?.originalname || req.body?.filename
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Validates required fields for asset upload and returns validated data.
|
|
252
|
+
*
|
|
253
|
+
* @param data - Upload data to validate
|
|
254
|
+
* @returns UploadValidationResult with validated data on success or error response on failure
|
|
255
|
+
*/
|
|
256
|
+
validateUploadFields(data) {
|
|
257
|
+
const hasFile = data.filePath || data.fileBuffer;
|
|
258
|
+
if (!hasFile || !data.description || !data.source) {
|
|
259
|
+
return {
|
|
260
|
+
success: false,
|
|
261
|
+
response: badRequestResponse('Missing required fields: description, source, file')
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (!data.filename) {
|
|
265
|
+
return {
|
|
266
|
+
success: false,
|
|
267
|
+
response: badRequestResponse('Filename could not be determined from uploaded file')
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (!this.validateFileExtension(data.filename)) {
|
|
271
|
+
const config = this.getConfiguration();
|
|
272
|
+
return {
|
|
273
|
+
success: false,
|
|
274
|
+
response: badRequestResponse(`Invalid file extension. Expected: ${config.extension}`)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
success: true,
|
|
279
|
+
data: {
|
|
280
|
+
description: data.description,
|
|
281
|
+
source: data.source,
|
|
282
|
+
filePath: data.filePath,
|
|
283
|
+
fileBuffer: data.fileBuffer,
|
|
284
|
+
filename: data.filename,
|
|
285
|
+
is_public: data.is_public !== undefined ? Boolean(data.is_public) : true
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Reads file content from temporary upload path.
|
|
291
|
+
*
|
|
292
|
+
* @param filePath - Path to temporary file
|
|
293
|
+
* @returns Buffer with file content
|
|
294
|
+
* @throws Error if file cannot be read
|
|
295
|
+
*/
|
|
296
|
+
async readTempFile(filePath) {
|
|
297
|
+
return fs.readFile(filePath);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Cleans up temporary file after processing.
|
|
301
|
+
* Silently ignores cleanup errors.
|
|
302
|
+
*
|
|
303
|
+
* @param filePath - Path to temporary file
|
|
304
|
+
*/
|
|
305
|
+
async cleanupTempFile(filePath) {
|
|
306
|
+
await fs.unlink(filePath).catch(() => {
|
|
307
|
+
// Ignore cleanup errors
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Validates ownership of an asset.
|
|
312
|
+
*
|
|
313
|
+
* @param asset - Asset record to check
|
|
314
|
+
* @param userId - User ID to validate against
|
|
315
|
+
* @returns DataResponse with error if not owner, undefined if valid
|
|
316
|
+
*/
|
|
317
|
+
validateOwnership(asset, userId) {
|
|
318
|
+
// Assets with no owner (null) can be modified by anyone
|
|
319
|
+
if (asset.owner_id === null) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
if (asset.owner_id !== userId) {
|
|
323
|
+
return forbiddenResponse('You can only modify your own assets');
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Checks if a user can access a private asset.
|
|
329
|
+
*
|
|
330
|
+
* @param asset - Asset record to check
|
|
331
|
+
* @param req - HTTP request for authentication context
|
|
332
|
+
* @returns DataResponse with error if access denied, undefined if allowed
|
|
333
|
+
*/
|
|
334
|
+
async checkPrivateAssetAccess(asset, req) {
|
|
335
|
+
// Public assets are always accessible
|
|
336
|
+
if (asset.is_public) {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
// Admins can access everything
|
|
340
|
+
if (ApisixAuthParser.isAdmin(req.headers || {})) {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
// Private asset - require authentication
|
|
344
|
+
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
345
|
+
return unauthorizedResponse('Authentication required for private assets');
|
|
346
|
+
}
|
|
347
|
+
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
348
|
+
if (!authUser) {
|
|
349
|
+
return unauthorizedResponse('Invalid authentication headers');
|
|
350
|
+
}
|
|
351
|
+
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
352
|
+
if (!userRecord.id || asset.owner_id !== userRecord.id) {
|
|
353
|
+
return forbiddenResponse('This asset is private');
|
|
354
|
+
}
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Fetches an asset by ID with full access control validation.
|
|
359
|
+
*
|
|
360
|
+
* This method consolidates the common logic for retrieving an asset:
|
|
361
|
+
* 1. Validates that ID is provided
|
|
362
|
+
* 2. Fetches the asset from database
|
|
363
|
+
* 3. Verifies the asset belongs to this component
|
|
364
|
+
* 4. Checks access permissions for private assets
|
|
365
|
+
*
|
|
366
|
+
* @param req - HTTP request with params.id
|
|
367
|
+
* @returns Object with asset on success, or DataResponse on failure
|
|
368
|
+
*/
|
|
369
|
+
async fetchAssetWithAccessCheck(req) {
|
|
370
|
+
const { id } = req.params || {};
|
|
371
|
+
if (!id) {
|
|
372
|
+
return { success: false, response: badRequestResponse('Asset ID is required') };
|
|
373
|
+
}
|
|
374
|
+
const asset = await this.getAssetById(id);
|
|
375
|
+
if (!asset) {
|
|
376
|
+
return { success: false, response: textResponse(HttpStatus.NOT_FOUND, 'Asset not found') };
|
|
377
|
+
}
|
|
378
|
+
// Verify this asset belongs to our component
|
|
379
|
+
const config = this.getConfiguration();
|
|
380
|
+
if (asset.name !== config.name) {
|
|
381
|
+
return { success: false, response: textResponse(HttpStatus.NOT_FOUND, 'Asset not found') };
|
|
382
|
+
}
|
|
383
|
+
// Check access permissions for private assets
|
|
384
|
+
const accessError = await this.checkPrivateAssetAccess(asset, req);
|
|
385
|
+
if (accessError) {
|
|
386
|
+
return { success: false, response: accessError };
|
|
387
|
+
}
|
|
388
|
+
return { success: true, asset };
|
|
389
|
+
}
|
|
180
390
|
/**
|
|
181
391
|
* Upload a new asset file with metadata.
|
|
182
392
|
*
|
|
@@ -240,80 +450,59 @@ export class AssetsManager {
|
|
|
240
450
|
async retrieve(req) {
|
|
241
451
|
try {
|
|
242
452
|
const assets = await this.getAllAssets();
|
|
243
|
-
const config = this.getConfiguration();
|
|
244
|
-
// Check if user is admin (admins can see everything)
|
|
245
453
|
const isAdmin = req && ApisixAuthParser.isAdmin(req.headers || {});
|
|
246
|
-
//
|
|
454
|
+
// Admin can see everything
|
|
247
455
|
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
|
-
};
|
|
456
|
+
return successResponse(this.formatAssetsForResponse(assets));
|
|
266
457
|
}
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
273
|
-
authenticatedUserId = userRecord.id || null;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
// Filter assets based on visibility and ownership
|
|
277
|
-
const visibleAssets = assets.filter(asset => {
|
|
278
|
-
// Public assets are visible to everyone
|
|
279
|
-
if (asset.is_public) {
|
|
280
|
-
return true;
|
|
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
|
-
};
|
|
458
|
+
// Get authenticated user ID if available
|
|
459
|
+
const authenticatedUserId = await this.getAuthenticatedUserId(req);
|
|
460
|
+
// Filter to visible assets only
|
|
461
|
+
const visibleAssets = assets.filter(asset => asset.is_public || (authenticatedUserId !== null && asset.owner_id === authenticatedUserId));
|
|
462
|
+
return successResponse(this.formatAssetsForResponse(visibleAssets));
|
|
307
463
|
}
|
|
308
464
|
catch (error) {
|
|
309
|
-
return
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
465
|
+
return errorResponse(error);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Gets the authenticated user's database ID from request headers.
|
|
470
|
+
*
|
|
471
|
+
* @param req - HTTP request object
|
|
472
|
+
* @returns User ID or null if not authenticated
|
|
473
|
+
*/
|
|
474
|
+
async getAuthenticatedUserId(req) {
|
|
475
|
+
if (!req || !ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
479
|
+
if (!authUser) {
|
|
480
|
+
return null;
|
|
316
481
|
}
|
|
482
|
+
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
483
|
+
return userRecord.id || null;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Formats assets for API response with metadata and URLs.
|
|
487
|
+
*
|
|
488
|
+
* @param assets - Array of asset records
|
|
489
|
+
* @returns Formatted assets array ready for JSON serialization
|
|
490
|
+
*/
|
|
491
|
+
formatAssetsForResponse(assets) {
|
|
492
|
+
const config = this.getConfiguration();
|
|
493
|
+
return assets.map(asset => ({
|
|
494
|
+
id: asset.id,
|
|
495
|
+
name: asset.name,
|
|
496
|
+
date: asset.date,
|
|
497
|
+
contentType: asset.contentType,
|
|
498
|
+
description: asset.description || '',
|
|
499
|
+
source: asset.source || '',
|
|
500
|
+
owner_id: asset.owner_id || null,
|
|
501
|
+
filename: asset.filename || '',
|
|
502
|
+
is_public: asset.is_public ?? true,
|
|
503
|
+
url: `/${config.name}/${asset.id}`,
|
|
504
|
+
download_url: `/${config.name}/${asset.id}/download`
|
|
505
|
+
}));
|
|
317
506
|
}
|
|
318
507
|
/**
|
|
319
508
|
* Get all assets for this component type.
|
|
@@ -619,128 +808,67 @@ export class AssetsManager {
|
|
|
619
808
|
async handleUpload(req) {
|
|
620
809
|
try {
|
|
621
810
|
// 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
|
-
};
|
|
640
|
-
}
|
|
641
|
-
// Step 2: Extract user identity from APISIX headers (x-user-id, x-user-roles)
|
|
642
|
-
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
643
|
-
if (!authUser) {
|
|
644
|
-
return {
|
|
645
|
-
status: 401,
|
|
646
|
-
content: JSON.stringify({
|
|
647
|
-
error: 'Invalid authentication headers'
|
|
648
|
-
}),
|
|
649
|
-
headers: { 'Content-Type': 'application/json' }
|
|
650
|
-
};
|
|
811
|
+
if (!req?.body) {
|
|
812
|
+
return badRequestResponse('Invalid request: missing request body');
|
|
651
813
|
}
|
|
652
|
-
//
|
|
653
|
-
const
|
|
654
|
-
if (!
|
|
655
|
-
return
|
|
656
|
-
status: 500,
|
|
657
|
-
content: JSON.stringify({
|
|
658
|
-
error: 'Failed to retrieve user information'
|
|
659
|
-
}),
|
|
660
|
-
headers: { 'Content-Type': 'application/json' }
|
|
661
|
-
};
|
|
814
|
+
// Authenticate user
|
|
815
|
+
const authResult = await this.authenticateRequest(req);
|
|
816
|
+
if (!authResult.success) {
|
|
817
|
+
return authResult.response;
|
|
662
818
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const filename = req.file?.originalname || req.body.filename;
|
|
667
|
-
// Step 5: Validate required fields are present
|
|
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
|
-
};
|
|
819
|
+
const userId = authResult.userRecord.id;
|
|
820
|
+
if (!userId) {
|
|
821
|
+
return errorResponse('Failed to retrieve user information');
|
|
676
822
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}),
|
|
683
|
-
headers: { 'Content-Type': 'application/json' }
|
|
684
|
-
};
|
|
823
|
+
// Extract and validate upload data
|
|
824
|
+
const uploadData = this.extractUploadData(req);
|
|
825
|
+
const validation = this.validateUploadFields(uploadData);
|
|
826
|
+
if (!validation.success) {
|
|
827
|
+
return validation.response;
|
|
685
828
|
}
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
};
|
|
696
|
-
}
|
|
697
|
-
// Read file from temporary location
|
|
829
|
+
const validData = validation.data;
|
|
830
|
+
// Get file buffer from memory or read from temporary location
|
|
698
831
|
let fileBuffer;
|
|
699
|
-
|
|
700
|
-
|
|
832
|
+
if (validData.fileBuffer) {
|
|
833
|
+
// Memory storage: buffer already available
|
|
834
|
+
fileBuffer = validData.fileBuffer;
|
|
701
835
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
836
|
+
else if (validData.filePath) {
|
|
837
|
+
// Disk storage: read from temp file
|
|
838
|
+
try {
|
|
839
|
+
fileBuffer = await this.readTempFile(validData.filePath);
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
return errorResponse(`Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
return badRequestResponse('No file data available');
|
|
710
847
|
}
|
|
848
|
+
// Upload asset and cleanup
|
|
711
849
|
try {
|
|
712
850
|
await this.uploadAsset({
|
|
713
|
-
description,
|
|
714
|
-
source,
|
|
715
|
-
owner_id:
|
|
716
|
-
filename,
|
|
851
|
+
description: validData.description,
|
|
852
|
+
source: validData.source,
|
|
853
|
+
owner_id: userId,
|
|
854
|
+
filename: validData.filename,
|
|
717
855
|
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
|
|
856
|
+
is_public: validData.is_public
|
|
723
857
|
});
|
|
858
|
+
if (validData.filePath) {
|
|
859
|
+
await this.cleanupTempFile(validData.filePath);
|
|
860
|
+
}
|
|
724
861
|
}
|
|
725
862
|
catch (error) {
|
|
726
|
-
|
|
727
|
-
|
|
863
|
+
if (validData.filePath) {
|
|
864
|
+
await this.cleanupTempFile(validData.filePath);
|
|
865
|
+
}
|
|
728
866
|
throw error;
|
|
729
867
|
}
|
|
730
|
-
return {
|
|
731
|
-
status: 200,
|
|
732
|
-
content: JSON.stringify({ message: 'Asset uploaded successfully' }),
|
|
733
|
-
headers: { 'Content-Type': 'application/json' }
|
|
734
|
-
};
|
|
868
|
+
return successResponse({ message: 'Asset uploaded successfully' });
|
|
735
869
|
}
|
|
736
870
|
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
|
-
};
|
|
871
|
+
return errorResponse(error);
|
|
744
872
|
}
|
|
745
873
|
}
|
|
746
874
|
/**
|
|
@@ -760,87 +888,36 @@ export class AssetsManager {
|
|
|
760
888
|
async handleUpdate(req) {
|
|
761
889
|
try {
|
|
762
890
|
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
|
-
};
|
|
891
|
+
return badRequestResponse('Invalid request: missing request object');
|
|
770
892
|
}
|
|
771
|
-
//
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
content: JSON.stringify({
|
|
776
|
-
error: 'Authentication required'
|
|
777
|
-
}),
|
|
778
|
-
headers: { 'Content-Type': 'application/json' }
|
|
779
|
-
};
|
|
893
|
+
// Authenticate user
|
|
894
|
+
const authResult = await this.authenticateRequest(req);
|
|
895
|
+
if (!authResult.success) {
|
|
896
|
+
return authResult.response;
|
|
780
897
|
}
|
|
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
|
-
};
|
|
898
|
+
const userId = authResult.userRecord.id;
|
|
899
|
+
if (!userId) {
|
|
900
|
+
return errorResponse('Failed to retrieve user information');
|
|
791
901
|
}
|
|
792
902
|
const { id } = req.params || {};
|
|
793
903
|
const { description, source, is_public } = req.body || {};
|
|
794
904
|
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
|
-
};
|
|
905
|
+
return badRequestResponse('Asset ID is required');
|
|
802
906
|
}
|
|
803
907
|
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
|
-
};
|
|
811
|
-
}
|
|
812
|
-
// Find or create user in database
|
|
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
|
-
};
|
|
908
|
+
return badRequestResponse('At least one field (description, source, or is_public) must be provided for update');
|
|
822
909
|
}
|
|
823
|
-
// Check if asset exists
|
|
910
|
+
// Check if asset exists
|
|
824
911
|
const asset = await this.getAssetById(id);
|
|
825
912
|
if (!asset) {
|
|
826
|
-
return
|
|
827
|
-
status: 404,
|
|
828
|
-
content: JSON.stringify({
|
|
829
|
-
error: 'Asset not found'
|
|
830
|
-
}),
|
|
831
|
-
headers: { 'Content-Type': 'application/json' }
|
|
832
|
-
};
|
|
913
|
+
return notFoundResponse('Asset not found');
|
|
833
914
|
}
|
|
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
|
-
};
|
|
915
|
+
// Check ownership
|
|
916
|
+
const ownershipError = this.validateOwnership(asset, userId);
|
|
917
|
+
if (ownershipError) {
|
|
918
|
+
return ownershipError;
|
|
843
919
|
}
|
|
920
|
+
// Build and apply updates
|
|
844
921
|
const updates = {};
|
|
845
922
|
if (description !== undefined)
|
|
846
923
|
updates.description = description;
|
|
@@ -849,20 +926,10 @@ export class AssetsManager {
|
|
|
849
926
|
if (is_public !== undefined)
|
|
850
927
|
updates.is_public = Boolean(is_public);
|
|
851
928
|
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
|
-
};
|
|
929
|
+
return successResponse({ message: 'Asset metadata updated successfully' });
|
|
857
930
|
}
|
|
858
931
|
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
|
-
};
|
|
932
|
+
return errorResponse(error);
|
|
866
933
|
}
|
|
867
934
|
}
|
|
868
935
|
/**
|
|
@@ -887,88 +954,15 @@ export class AssetsManager {
|
|
|
887
954
|
*/
|
|
888
955
|
async handleGetAsset(req) {
|
|
889
956
|
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
|
-
};
|
|
957
|
+
const result = await this.fetchAssetWithAccessCheck(req);
|
|
958
|
+
if (!result.success) {
|
|
959
|
+
return result.response;
|
|
916
960
|
}
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
}
|
|
952
|
-
}
|
|
953
|
-
// Get the file content
|
|
954
|
-
const fileContent = await asset.data();
|
|
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
|
-
};
|
|
961
|
+
const fileContent = await result.asset.data();
|
|
962
|
+
return fileResponse(fileContent, this.getConfiguration().contentType);
|
|
963
963
|
}
|
|
964
964
|
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
|
-
};
|
|
965
|
+
return errorResponse(error);
|
|
972
966
|
}
|
|
973
967
|
}
|
|
974
968
|
/**
|
|
@@ -993,90 +987,16 @@ export class AssetsManager {
|
|
|
993
987
|
*/
|
|
994
988
|
async handleDownload(req) {
|
|
995
989
|
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
|
-
};
|
|
990
|
+
const result = await this.fetchAssetWithAccessCheck(req);
|
|
991
|
+
if (!result.success) {
|
|
992
|
+
return result.response;
|
|
1005
993
|
}
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
status: 404,
|
|
1010
|
-
content: 'Asset not found',
|
|
1011
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
// Verify this asset belongs to our component
|
|
1015
|
-
const config = this.getConfiguration();
|
|
1016
|
-
if (asset.name !== config.name) {
|
|
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
|
-
};
|
|
994
|
+
const fileContent = await result.asset.data();
|
|
995
|
+
const filename = result.asset.filename || `asset_${req.params?.id}`;
|
|
996
|
+
return fileResponse(fileContent, this.getConfiguration().contentType, filename);
|
|
1071
997
|
}
|
|
1072
998
|
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
|
-
};
|
|
999
|
+
return errorResponse(error);
|
|
1080
1000
|
}
|
|
1081
1001
|
}
|
|
1082
1002
|
/**
|
|
@@ -1094,84 +1014,34 @@ export class AssetsManager {
|
|
|
1094
1014
|
*/
|
|
1095
1015
|
async handleDelete(req) {
|
|
1096
1016
|
try {
|
|
1097
|
-
//
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
content: JSON.stringify({
|
|
1102
|
-
error: 'Authentication required'
|
|
1103
|
-
}),
|
|
1104
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1105
|
-
};
|
|
1017
|
+
// Authenticate user
|
|
1018
|
+
const authResult = await this.authenticateRequest(req);
|
|
1019
|
+
if (!authResult.success) {
|
|
1020
|
+
return authResult.response;
|
|
1106
1021
|
}
|
|
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
|
-
};
|
|
1022
|
+
const userId = authResult.userRecord.id;
|
|
1023
|
+
if (!userId) {
|
|
1024
|
+
return errorResponse('Failed to retrieve user information');
|
|
1117
1025
|
}
|
|
1118
1026
|
const { id } = req.params || {};
|
|
1119
1027
|
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
|
-
};
|
|
1028
|
+
return badRequestResponse('Asset ID is required');
|
|
1138
1029
|
}
|
|
1139
|
-
// Check if asset exists
|
|
1030
|
+
// Check if asset exists
|
|
1140
1031
|
const asset = await this.getAssetById(id);
|
|
1141
1032
|
if (!asset) {
|
|
1142
|
-
return
|
|
1143
|
-
status: 404,
|
|
1144
|
-
content: JSON.stringify({
|
|
1145
|
-
error: 'Asset not found'
|
|
1146
|
-
}),
|
|
1147
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1148
|
-
};
|
|
1033
|
+
return notFoundResponse('Asset not found');
|
|
1149
1034
|
}
|
|
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
|
-
};
|
|
1035
|
+
// Check ownership
|
|
1036
|
+
const ownershipError = this.validateOwnership(asset, userId);
|
|
1037
|
+
if (ownershipError) {
|
|
1038
|
+
return ownershipError;
|
|
1159
1039
|
}
|
|
1160
1040
|
await this.deleteAssetById(id);
|
|
1161
|
-
return {
|
|
1162
|
-
status: 200,
|
|
1163
|
-
content: JSON.stringify({ message: 'Asset deleted successfully' }),
|
|
1164
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1165
|
-
};
|
|
1041
|
+
return successResponse({ message: 'Asset deleted successfully' });
|
|
1166
1042
|
}
|
|
1167
1043
|
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
|
-
};
|
|
1044
|
+
return errorResponse(error);
|
|
1175
1045
|
}
|
|
1176
1046
|
}
|
|
1177
1047
|
/**
|
|
@@ -1179,243 +1049,161 @@ export class AssetsManager {
|
|
|
1179
1049
|
*/
|
|
1180
1050
|
async handleUploadBatch(req) {
|
|
1181
1051
|
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
|
-
};
|
|
1052
|
+
if (!req?.body) {
|
|
1053
|
+
return badRequestResponse('Invalid request: missing request body');
|
|
1200
1054
|
}
|
|
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
|
-
};
|
|
1055
|
+
// Authenticate user
|
|
1056
|
+
const authResult = await this.authenticateRequest(req);
|
|
1057
|
+
if (!authResult.success) {
|
|
1058
|
+
return authResult.response;
|
|
1211
1059
|
}
|
|
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
|
-
};
|
|
1060
|
+
const userId = authResult.userRecord.id;
|
|
1061
|
+
if (!userId) {
|
|
1062
|
+
return errorResponse('Failed to retrieve user information');
|
|
1222
1063
|
}
|
|
1223
1064
|
const requests = req.body.requests;
|
|
1224
1065
|
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
|
-
};
|
|
1066
|
+
return badRequestResponse('Requests array is required and must not be empty');
|
|
1232
1067
|
}
|
|
1233
|
-
// Validate all requests first
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
// Process each request individually for compatibility
|
|
1267
|
-
const results = [];
|
|
1268
|
-
for (const request of requests) {
|
|
1269
|
-
try {
|
|
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
|
-
}
|
|
1068
|
+
// Validate all requests first
|
|
1069
|
+
const validationError = this.validateBatchRequests(requests);
|
|
1070
|
+
if (validationError) {
|
|
1071
|
+
return validationError;
|
|
1287
1072
|
}
|
|
1073
|
+
// Process each request
|
|
1074
|
+
const results = await this.processBatchUploads(requests, userId);
|
|
1288
1075
|
const successCount = results.filter(r => r.success).length;
|
|
1289
1076
|
const failureCount = results.length - successCount;
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
}),
|
|
1296
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1297
|
-
};
|
|
1077
|
+
const message = `${successCount}/${requests.length} assets uploaded successfully`;
|
|
1078
|
+
if (failureCount > 0) {
|
|
1079
|
+
return multiStatusResponse(message, results);
|
|
1080
|
+
}
|
|
1081
|
+
return successResponse({ message, results });
|
|
1298
1082
|
}
|
|
1299
1083
|
catch (error) {
|
|
1300
|
-
return
|
|
1301
|
-
|
|
1302
|
-
|
|
1084
|
+
return errorResponse(error);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Validates all requests in a batch upload.
|
|
1089
|
+
*
|
|
1090
|
+
* @param requests - Array of upload requests to validate
|
|
1091
|
+
* @returns DataResponse with error if validation fails, undefined if valid
|
|
1092
|
+
*/
|
|
1093
|
+
validateBatchRequests(requests) {
|
|
1094
|
+
const config = this.getConfiguration();
|
|
1095
|
+
for (const request of requests) {
|
|
1096
|
+
if (!request.file || !request.description || !request.source || !request.filename) {
|
|
1097
|
+
return badRequestResponse('Each request must have description, source, filename, and file');
|
|
1098
|
+
}
|
|
1099
|
+
if (!this.validateBase64(request.file)) {
|
|
1100
|
+
return badRequestResponse(`Invalid base64 data for file: ${request.filename}. File must be a valid base64-encoded string.`);
|
|
1101
|
+
}
|
|
1102
|
+
if (!this.validateFileExtension(request.filename)) {
|
|
1103
|
+
return badRequestResponse(`Invalid file extension for ${request.filename}. Expected: ${config.extension}`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return undefined;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Processes batch upload requests.
|
|
1110
|
+
*
|
|
1111
|
+
* @param requests - Array of upload requests
|
|
1112
|
+
* @param ownerId - Owner user ID
|
|
1113
|
+
* @returns Array of results for each upload
|
|
1114
|
+
*/
|
|
1115
|
+
async processBatchUploads(requests, ownerId) {
|
|
1116
|
+
const results = [];
|
|
1117
|
+
for (const request of requests) {
|
|
1118
|
+
try {
|
|
1119
|
+
await this.uploadAsset({
|
|
1120
|
+
description: request.description,
|
|
1121
|
+
source: request.source,
|
|
1122
|
+
owner_id: ownerId,
|
|
1123
|
+
filename: request.filename,
|
|
1124
|
+
file: Buffer.from(request.file, 'base64'),
|
|
1125
|
+
is_public: request.is_public !== undefined ? Boolean(request.is_public) : true
|
|
1126
|
+
});
|
|
1127
|
+
results.push({ success: true, filename: request.filename });
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
results.push({
|
|
1131
|
+
success: false,
|
|
1132
|
+
filename: request.filename,
|
|
1303
1133
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1304
|
-
})
|
|
1305
|
-
|
|
1306
|
-
};
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1307
1136
|
}
|
|
1137
|
+
return results;
|
|
1308
1138
|
}
|
|
1309
1139
|
/**
|
|
1310
1140
|
* Handle batch delete endpoint
|
|
1311
1141
|
*/
|
|
1312
1142
|
async handleDeleteBatch(req) {
|
|
1313
1143
|
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
|
-
};
|
|
1144
|
+
if (!req?.body) {
|
|
1145
|
+
return badRequestResponse('Invalid request: missing request body');
|
|
1332
1146
|
}
|
|
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
|
-
};
|
|
1147
|
+
// Authenticate user
|
|
1148
|
+
const authResult = await this.authenticateRequest(req);
|
|
1149
|
+
if (!authResult.success) {
|
|
1150
|
+
return authResult.response;
|
|
1343
1151
|
}
|
|
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
|
-
};
|
|
1152
|
+
const userId = authResult.userRecord.id;
|
|
1153
|
+
if (!userId) {
|
|
1154
|
+
return errorResponse('Failed to retrieve user information');
|
|
1354
1155
|
}
|
|
1355
1156
|
const { ids } = req.body;
|
|
1356
1157
|
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
|
-
}
|
|
1158
|
+
return badRequestResponse('IDs array is required and must not be empty');
|
|
1398
1159
|
}
|
|
1160
|
+
// Process deletions
|
|
1161
|
+
const results = await this.processBatchDeletes(ids, userId);
|
|
1399
1162
|
const successCount = results.filter(r => r.success).length;
|
|
1400
1163
|
const failureCount = results.length - successCount;
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
}),
|
|
1407
|
-
headers: { 'Content-Type': 'application/json' }
|
|
1408
|
-
};
|
|
1164
|
+
const message = `${successCount}/${ids.length} assets deleted successfully`;
|
|
1165
|
+
if (failureCount > 0) {
|
|
1166
|
+
return multiStatusResponse(message, results);
|
|
1167
|
+
}
|
|
1168
|
+
return successResponse({ message, results });
|
|
1409
1169
|
}
|
|
1410
1170
|
catch (error) {
|
|
1411
|
-
return
|
|
1412
|
-
|
|
1413
|
-
|
|
1171
|
+
return errorResponse(error);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Processes batch delete requests.
|
|
1176
|
+
*
|
|
1177
|
+
* @param ids - Array of asset IDs to delete
|
|
1178
|
+
* @param userId - User ID for ownership validation
|
|
1179
|
+
* @returns Array of results for each deletion
|
|
1180
|
+
*/
|
|
1181
|
+
async processBatchDeletes(ids, userId) {
|
|
1182
|
+
const results = [];
|
|
1183
|
+
for (const id of ids) {
|
|
1184
|
+
try {
|
|
1185
|
+
const asset = await this.getAssetById(id);
|
|
1186
|
+
if (!asset) {
|
|
1187
|
+
results.push({ success: false, id, error: 'Asset not found' });
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
// Allow deletion if: owner is the current user OR asset has no owner
|
|
1191
|
+
if (asset.owner_id !== null && asset.owner_id !== userId) {
|
|
1192
|
+
results.push({ success: false, id, error: 'You can only delete your own assets' });
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
await this.deleteAssetById(id);
|
|
1196
|
+
results.push({ success: true, id });
|
|
1197
|
+
}
|
|
1198
|
+
catch (error) {
|
|
1199
|
+
results.push({
|
|
1200
|
+
success: false,
|
|
1201
|
+
id,
|
|
1414
1202
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1415
|
-
})
|
|
1416
|
-
|
|
1417
|
-
};
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1418
1205
|
}
|
|
1206
|
+
return results;
|
|
1419
1207
|
}
|
|
1420
1208
|
}
|
|
1421
1209
|
//# sourceMappingURL=assets_manager.js.map
|