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.
@@ -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
- // If admin, skip filtering and return all assets
454
+ // Admin can see everything
247
455
  if (isAdmin) {
248
- const assetsWithMetadata = assets.map(asset => ({
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
- // 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
- }
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
- status: 500,
311
- content: JSON.stringify({
312
- error: error instanceof Error ? error.message : 'Unknown error'
313
- }),
314
- headers: { 'Content-Type': 'application/json' }
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 || !req.body) {
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
- // Step 3: Find or create user record in database and sync roles from Keycloak
653
- const userRecord = await this.userService.findOrCreateUser(authUser);
654
- if (!userRecord.id) {
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
- // Step 4: Extract form data from multipart upload
664
- const { description, source, is_public } = req.body;
665
- const filePath = req.file?.path;
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
- 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
- };
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
- // 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
- };
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
- try {
700
- fileBuffer = await fs.readFile(filePath);
832
+ if (validData.fileBuffer) {
833
+ // Memory storage: buffer already available
834
+ fileBuffer = validData.fileBuffer;
701
835
  }
702
- 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
- };
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: userRecord.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 !== undefined ? Boolean(is_public) : true
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
- // Clean up temporary file even on error
727
- await fs.unlink(filePath).catch(() => { });
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
- // Check authentication
772
- if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
773
- return {
774
- status: 401,
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
- // Parse authenticated user
782
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
783
- if (!authUser) {
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 and belongs to this user
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 (only owner can modify their assets)
835
- if (asset.owner_id !== userRecord.id) {
836
- return {
837
- status: 403,
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 { id } = req.params || {};
891
- if (!id) {
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
- // 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
- }
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 { id } = req.params || {};
997
- if (!id) {
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 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
- };
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
- // Check authentication first
1098
- if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
1099
- return {
1100
- status: 401,
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
- // Parse authenticated user
1108
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
1109
- if (!authUser) {
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 and belongs to this user
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 (only owner can delete their assets)
1151
- if (asset.owner_id !== userRecord.id) {
1152
- return {
1153
- status: 403,
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 || !req.body) {
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
- // Parse authenticated user
1202
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
1203
- if (!authUser) {
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
- // Find or create user in database
1213
- const userRecord = await this.userService.findOrCreateUser(authUser);
1214
- if (!userRecord.id) {
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 (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
- }
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
- return {
1291
- status: failureCount > 0 ? 207 : 200, // 207 Multi-Status if some failed
1292
- content: JSON.stringify({
1293
- message: `${successCount}/${requests.length} assets uploaded successfully`,
1294
- results
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
- status: 500,
1302
- content: JSON.stringify({
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
- headers: { 'Content-Type': 'application/json' }
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 || !req.body) {
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
- // Parse authenticated user
1334
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
1335
- if (!authUser) {
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
- // Find or create user in database
1345
- const userRecord = await this.userService.findOrCreateUser(authUser);
1346
- if (!userRecord.id) {
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
- return {
1402
- status: failureCount > 0 ? 207 : 200, // 207 Multi-Status if some failed
1403
- content: JSON.stringify({
1404
- message: `${successCount}/${ids.length} assets deleted successfully`,
1405
- results
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
- status: 500,
1413
- content: JSON.stringify({
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
- headers: { 'Content-Type': 'application/json' }
1417
- };
1203
+ });
1204
+ }
1418
1205
  }
1206
+ return results;
1419
1207
  }
1420
1208
  }
1421
1209
  //# sourceMappingURL=assets_manager.js.map