digitaltwin-core 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { ApisixAuthParser } from '../auth/apisix_parser.js';
2
2
  import { UserService } from '../auth/user_service.js';
3
+ import { successResponse, errorResponse, badRequestResponse, unauthorizedResponse, forbiddenResponse, notFoundResponse, textResponse, fileResponse, multiStatusResponse, HttpStatus } from '../utils/http_responses.js';
3
4
  import fs from 'fs/promises';
4
5
  /**
5
6
  * Abstract base class for Assets Manager components with authentication and access control.
@@ -65,17 +66,23 @@ export class AssetsManager {
65
66
  *
66
67
  * @param {DatabaseAdapter} db - The database adapter for metadata storage
67
68
  * @param {StorageService} storage - The storage service for file persistence
69
+ * @param {UserService} [userService] - Optional user service for authentication (created automatically if not provided)
68
70
  *
69
71
  * @example
70
72
  * ```typescript
73
+ * // Standard usage (UserService created automatically)
71
74
  * const assetsManager = new MyAssetsManager()
72
75
  * assetsManager.setDependencies(databaseAdapter, storageService)
76
+ *
77
+ * // For testing (inject mock UserService)
78
+ * const mockUserService = new MockUserService()
79
+ * assetsManager.setDependencies(databaseAdapter, storageService, mockUserService)
73
80
  * ```
74
81
  */
75
- setDependencies(db, storage) {
82
+ setDependencies(db, storage, userService) {
76
83
  this.db = db;
77
84
  this.storage = storage;
78
- this.userService = new UserService(db);
85
+ this.userService = userService ?? new UserService(db);
79
86
  }
80
87
  /**
81
88
  * Validates that a source string is a valid URL.
@@ -177,6 +184,192 @@ export class AssetsManager {
177
184
  return false;
178
185
  }
179
186
  }
187
+ // ============================================================================
188
+ // Authentication & Request Processing Helpers
189
+ // ============================================================================
190
+ /**
191
+ * Authenticates a request and returns the user record.
192
+ *
193
+ * This method consolidates the authentication flow:
194
+ * 1. Validates APISIX headers are present
195
+ * 2. Parses authentication headers
196
+ * 3. Finds or creates user record in database
197
+ *
198
+ * @param req - HTTP request object
199
+ * @returns AuthResult with either userRecord on success or DataResponse on failure
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * const authResult = await this.authenticateRequest(req)
204
+ * if (!authResult.success) {
205
+ * return authResult.response
206
+ * }
207
+ * const userRecord = authResult.userRecord
208
+ * ```
209
+ */
210
+ async authenticateRequest(req) {
211
+ if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
212
+ return { success: false, response: unauthorizedResponse() };
213
+ }
214
+ const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
215
+ if (!authUser) {
216
+ return { success: false, response: unauthorizedResponse('Invalid authentication headers') };
217
+ }
218
+ const userRecord = await this.userService.findOrCreateUser(authUser);
219
+ if (!userRecord.id) {
220
+ return { success: false, response: errorResponse('Failed to retrieve user information') };
221
+ }
222
+ return { success: true, userRecord };
223
+ }
224
+ /**
225
+ * Extracts upload data from multipart form request.
226
+ *
227
+ * @param req - HTTP request object with body and file
228
+ * @returns UploadData object with extracted fields
229
+ */
230
+ extractUploadData(req) {
231
+ return {
232
+ description: req.body?.description,
233
+ source: req.body?.source,
234
+ is_public: req.body?.is_public,
235
+ filePath: req.file?.path,
236
+ filename: req.file?.originalname || req.body?.filename
237
+ };
238
+ }
239
+ /**
240
+ * Validates required fields for asset upload and returns validated data.
241
+ *
242
+ * @param data - Upload data to validate
243
+ * @returns UploadValidationResult with validated data on success or error response on failure
244
+ */
245
+ validateUploadFields(data) {
246
+ if (!data.filePath || !data.description || !data.source) {
247
+ return {
248
+ success: false,
249
+ response: badRequestResponse('Missing required fields: description, source, file')
250
+ };
251
+ }
252
+ if (!data.filename) {
253
+ return {
254
+ success: false,
255
+ response: badRequestResponse('Filename could not be determined from uploaded file')
256
+ };
257
+ }
258
+ if (!this.validateFileExtension(data.filename)) {
259
+ const config = this.getConfiguration();
260
+ return {
261
+ success: false,
262
+ response: badRequestResponse(`Invalid file extension. Expected: ${config.extension}`)
263
+ };
264
+ }
265
+ return {
266
+ success: true,
267
+ data: {
268
+ description: data.description,
269
+ source: data.source,
270
+ filePath: data.filePath,
271
+ filename: data.filename,
272
+ is_public: data.is_public !== undefined ? Boolean(data.is_public) : true
273
+ }
274
+ };
275
+ }
276
+ /**
277
+ * Reads file content from temporary upload path.
278
+ *
279
+ * @param filePath - Path to temporary file
280
+ * @returns Buffer with file content
281
+ * @throws Error if file cannot be read
282
+ */
283
+ async readTempFile(filePath) {
284
+ return fs.readFile(filePath);
285
+ }
286
+ /**
287
+ * Cleans up temporary file after processing.
288
+ * Silently ignores cleanup errors.
289
+ *
290
+ * @param filePath - Path to temporary file
291
+ */
292
+ async cleanupTempFile(filePath) {
293
+ await fs.unlink(filePath).catch(() => {
294
+ // Ignore cleanup errors
295
+ });
296
+ }
297
+ /**
298
+ * Validates ownership of an asset.
299
+ *
300
+ * @param asset - Asset record to check
301
+ * @param userId - User ID to validate against
302
+ * @returns DataResponse with error if not owner, undefined if valid
303
+ */
304
+ validateOwnership(asset, userId) {
305
+ if (asset.owner_id !== userId) {
306
+ return forbiddenResponse('You can only modify your own assets');
307
+ }
308
+ return undefined;
309
+ }
310
+ /**
311
+ * Checks if a user can access a private asset.
312
+ *
313
+ * @param asset - Asset record to check
314
+ * @param req - HTTP request for authentication context
315
+ * @returns DataResponse with error if access denied, undefined if allowed
316
+ */
317
+ async checkPrivateAssetAccess(asset, req) {
318
+ // Public assets are always accessible
319
+ if (asset.is_public) {
320
+ return undefined;
321
+ }
322
+ // Admins can access everything
323
+ if (ApisixAuthParser.isAdmin(req.headers || {})) {
324
+ return undefined;
325
+ }
326
+ // Private asset - require authentication
327
+ if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
328
+ return unauthorizedResponse('Authentication required for private assets');
329
+ }
330
+ const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
331
+ if (!authUser) {
332
+ return unauthorizedResponse('Invalid authentication headers');
333
+ }
334
+ const userRecord = await this.userService.findOrCreateUser(authUser);
335
+ if (!userRecord.id || asset.owner_id !== userRecord.id) {
336
+ return forbiddenResponse('This asset is private');
337
+ }
338
+ return undefined;
339
+ }
340
+ /**
341
+ * Fetches an asset by ID with full access control validation.
342
+ *
343
+ * This method consolidates the common logic for retrieving an asset:
344
+ * 1. Validates that ID is provided
345
+ * 2. Fetches the asset from database
346
+ * 3. Verifies the asset belongs to this component
347
+ * 4. Checks access permissions for private assets
348
+ *
349
+ * @param req - HTTP request with params.id
350
+ * @returns Object with asset on success, or DataResponse on failure
351
+ */
352
+ async fetchAssetWithAccessCheck(req) {
353
+ const { id } = req.params || {};
354
+ if (!id) {
355
+ return { success: false, response: badRequestResponse('Asset ID is required') };
356
+ }
357
+ const asset = await this.getAssetById(id);
358
+ if (!asset) {
359
+ return { success: false, response: textResponse(HttpStatus.NOT_FOUND, 'Asset not found') };
360
+ }
361
+ // Verify this asset belongs to our component
362
+ const config = this.getConfiguration();
363
+ if (asset.name !== config.name) {
364
+ return { success: false, response: textResponse(HttpStatus.NOT_FOUND, 'Asset not found') };
365
+ }
366
+ // Check access permissions for private assets
367
+ const accessError = await this.checkPrivateAssetAccess(asset, req);
368
+ if (accessError) {
369
+ return { success: false, response: accessError };
370
+ }
371
+ return { success: true, asset };
372
+ }
180
373
  /**
181
374
  * Upload a new asset file with metadata.
182
375
  *
@@ -240,80 +433,59 @@ export class AssetsManager {
240
433
  async retrieve(req) {
241
434
  try {
242
435
  const assets = await this.getAllAssets();
243
- const config = this.getConfiguration();
244
- // Check if user is admin (admins can see everything)
245
436
  const isAdmin = req && ApisixAuthParser.isAdmin(req.headers || {});
246
- // If admin, skip filtering and return all assets
437
+ // Admin can see everything
247
438
  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
- };
266
- }
267
- // For non-admin users, check authentication and ownership
268
- let authenticatedUserId = null;
269
- if (req && ApisixAuthParser.hasValidAuth(req.headers || {})) {
270
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
271
- if (authUser) {
272
- const userRecord = await this.userService.findOrCreateUser(authUser);
273
- authenticatedUserId = userRecord.id || null;
274
- }
439
+ return successResponse(this.formatAssetsForResponse(assets));
275
440
  }
276
- // 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
- };
441
+ // Get authenticated user ID if available
442
+ const authenticatedUserId = await this.getAuthenticatedUserId(req);
443
+ // Filter to visible assets only
444
+ const visibleAssets = assets.filter(asset => asset.is_public || (authenticatedUserId !== null && asset.owner_id === authenticatedUserId));
445
+ return successResponse(this.formatAssetsForResponse(visibleAssets));
307
446
  }
308
447
  catch (error) {
309
- return {
310
- status: 500,
311
- content: JSON.stringify({
312
- error: error instanceof Error ? error.message : 'Unknown error'
313
- }),
314
- headers: { 'Content-Type': 'application/json' }
315
- };
448
+ return errorResponse(error);
449
+ }
450
+ }
451
+ /**
452
+ * Gets the authenticated user's database ID from request headers.
453
+ *
454
+ * @param req - HTTP request object
455
+ * @returns User ID or null if not authenticated
456
+ */
457
+ async getAuthenticatedUserId(req) {
458
+ if (!req || !ApisixAuthParser.hasValidAuth(req.headers || {})) {
459
+ return null;
460
+ }
461
+ const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
462
+ if (!authUser) {
463
+ return null;
316
464
  }
465
+ const userRecord = await this.userService.findOrCreateUser(authUser);
466
+ return userRecord.id || null;
467
+ }
468
+ /**
469
+ * Formats assets for API response with metadata and URLs.
470
+ *
471
+ * @param assets - Array of asset records
472
+ * @returns Formatted assets array ready for JSON serialization
473
+ */
474
+ formatAssetsForResponse(assets) {
475
+ const config = this.getConfiguration();
476
+ return assets.map(asset => ({
477
+ id: asset.id,
478
+ name: asset.name,
479
+ date: asset.date,
480
+ contentType: asset.contentType,
481
+ description: asset.description || '',
482
+ source: asset.source || '',
483
+ owner_id: asset.owner_id || null,
484
+ filename: asset.filename || '',
485
+ is_public: asset.is_public ?? true,
486
+ url: `/${config.name}/${asset.id}`,
487
+ download_url: `/${config.name}/${asset.id}/download`
488
+ }));
317
489
  }
318
490
  /**
319
491
  * Get all assets for this component type.
@@ -619,128 +791,53 @@ export class AssetsManager {
619
791
  async handleUpload(req) {
620
792
  try {
621
793
  // Validate request structure
622
- if (!req || !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
- };
794
+ if (!req?.body) {
795
+ return badRequestResponse('Invalid request: missing request body');
640
796
  }
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
- };
797
+ // Authenticate user
798
+ const authResult = await this.authenticateRequest(req);
799
+ if (!authResult.success) {
800
+ return authResult.response;
651
801
  }
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
- };
802
+ const userId = authResult.userRecord.id;
803
+ if (!userId) {
804
+ return errorResponse('Failed to retrieve user information');
662
805
  }
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
- };
676
- }
677
- if (!filename) {
678
- return {
679
- status: 400,
680
- content: JSON.stringify({
681
- error: 'Filename could not be determined from uploaded file'
682
- }),
683
- headers: { 'Content-Type': 'application/json' }
684
- };
685
- }
686
- // Validate file extension early to avoid processing invalid files
687
- if (!this.validateFileExtension(filename)) {
688
- const config = this.getConfiguration();
689
- return {
690
- status: 400,
691
- content: JSON.stringify({
692
- error: `Invalid file extension. Expected: ${config.extension}`
693
- }),
694
- headers: { 'Content-Type': 'application/json' }
695
- };
806
+ // Extract and validate upload data
807
+ const uploadData = this.extractUploadData(req);
808
+ const validation = this.validateUploadFields(uploadData);
809
+ if (!validation.success) {
810
+ return validation.response;
696
811
  }
812
+ const validData = validation.data;
697
813
  // Read file from temporary location
698
814
  let fileBuffer;
699
815
  try {
700
- fileBuffer = await fs.readFile(filePath);
816
+ fileBuffer = await this.readTempFile(validData.filePath);
701
817
  }
702
818
  catch (error) {
703
- return {
704
- status: 500,
705
- content: JSON.stringify({
706
- error: `Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`
707
- }),
708
- headers: { 'Content-Type': 'application/json' }
709
- };
819
+ return errorResponse(`Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`);
710
820
  }
821
+ // Upload asset and cleanup
711
822
  try {
712
823
  await this.uploadAsset({
713
- description,
714
- source,
715
- owner_id: userRecord.id,
716
- filename,
824
+ description: validData.description,
825
+ source: validData.source,
826
+ owner_id: userId,
827
+ filename: validData.filename,
717
828
  file: fileBuffer,
718
- is_public: is_public !== 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
829
+ is_public: validData.is_public
723
830
  });
831
+ await this.cleanupTempFile(validData.filePath);
724
832
  }
725
833
  catch (error) {
726
- // Clean up temporary file even on error
727
- await fs.unlink(filePath).catch(() => { });
834
+ await this.cleanupTempFile(validData.filePath);
728
835
  throw error;
729
836
  }
730
- return {
731
- status: 200,
732
- content: JSON.stringify({ message: 'Asset uploaded successfully' }),
733
- headers: { 'Content-Type': 'application/json' }
734
- };
837
+ return successResponse({ message: 'Asset uploaded successfully' });
735
838
  }
736
839
  catch (error) {
737
- return {
738
- status: 500,
739
- content: JSON.stringify({
740
- error: error instanceof Error ? error.message : 'Unknown error'
741
- }),
742
- headers: { 'Content-Type': 'application/json' }
743
- };
840
+ return errorResponse(error);
744
841
  }
745
842
  }
746
843
  /**
@@ -760,87 +857,36 @@ export class AssetsManager {
760
857
  async handleUpdate(req) {
761
858
  try {
762
859
  if (!req) {
763
- return {
764
- status: 400,
765
- content: JSON.stringify({
766
- error: 'Invalid request: missing request object'
767
- }),
768
- headers: { 'Content-Type': 'application/json' }
769
- };
860
+ return badRequestResponse('Invalid request: missing request object');
770
861
  }
771
- // 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
- };
862
+ // Authenticate user
863
+ const authResult = await this.authenticateRequest(req);
864
+ if (!authResult.success) {
865
+ return authResult.response;
780
866
  }
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
- };
867
+ const userId = authResult.userRecord.id;
868
+ if (!userId) {
869
+ return errorResponse('Failed to retrieve user information');
791
870
  }
792
871
  const { id } = req.params || {};
793
872
  const { description, source, is_public } = req.body || {};
794
873
  if (!id) {
795
- return {
796
- status: 400,
797
- content: JSON.stringify({
798
- error: 'Asset ID is required'
799
- }),
800
- headers: { 'Content-Type': 'application/json' }
801
- };
874
+ return badRequestResponse('Asset ID is required');
802
875
  }
803
876
  if (!description && !source && is_public === undefined) {
804
- return {
805
- status: 400,
806
- content: JSON.stringify({
807
- error: 'At least one field (description, source, or is_public) must be provided for update'
808
- }),
809
- headers: { 'Content-Type': 'application/json' }
810
- };
877
+ return badRequestResponse('At least one field (description, source, or is_public) must be provided for update');
811
878
  }
812
- // 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
- };
822
- }
823
- // Check if asset exists and belongs to this user
879
+ // Check if asset exists
824
880
  const asset = await this.getAssetById(id);
825
881
  if (!asset) {
826
- return {
827
- status: 404,
828
- content: JSON.stringify({
829
- error: 'Asset not found'
830
- }),
831
- headers: { 'Content-Type': 'application/json' }
832
- };
882
+ return notFoundResponse('Asset not found');
833
883
  }
834
- // Check ownership (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
- };
884
+ // Check ownership
885
+ const ownershipError = this.validateOwnership(asset, userId);
886
+ if (ownershipError) {
887
+ return ownershipError;
843
888
  }
889
+ // Build and apply updates
844
890
  const updates = {};
845
891
  if (description !== undefined)
846
892
  updates.description = description;
@@ -849,20 +895,10 @@ export class AssetsManager {
849
895
  if (is_public !== undefined)
850
896
  updates.is_public = Boolean(is_public);
851
897
  await this.updateAssetMetadata(id, updates);
852
- return {
853
- status: 200,
854
- content: JSON.stringify({ message: 'Asset metadata updated successfully' }),
855
- headers: { 'Content-Type': 'application/json' }
856
- };
898
+ return successResponse({ message: 'Asset metadata updated successfully' });
857
899
  }
858
900
  catch (error) {
859
- return {
860
- status: 500,
861
- content: JSON.stringify({
862
- error: error instanceof Error ? error.message : 'Unknown error'
863
- }),
864
- headers: { 'Content-Type': 'application/json' }
865
- };
901
+ return errorResponse(error);
866
902
  }
867
903
  }
868
904
  /**
@@ -887,88 +923,15 @@ export class AssetsManager {
887
923
  */
888
924
  async handleGetAsset(req) {
889
925
  try {
890
- const { 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
- };
916
- }
917
- // Check if user is admin (admins can access everything)
918
- const isAdmin = ApisixAuthParser.isAdmin(req.headers || {});
919
- // Check visibility: if private and user is not admin, verify ownership
920
- if (!asset.is_public && !isAdmin) {
921
- // Asset is private, require authentication
922
- if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
923
- return {
924
- status: 401,
925
- content: JSON.stringify({
926
- error: 'Authentication required for private assets'
927
- }),
928
- headers: { 'Content-Type': 'application/json' }
929
- };
930
- }
931
- // Verify ownership
932
- const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
933
- if (!authUser) {
934
- return {
935
- status: 401,
936
- content: JSON.stringify({
937
- error: 'Invalid authentication headers'
938
- }),
939
- headers: { 'Content-Type': 'application/json' }
940
- };
941
- }
942
- const userRecord = await this.userService.findOrCreateUser(authUser);
943
- if (!userRecord.id || asset.owner_id !== userRecord.id) {
944
- return {
945
- status: 403,
946
- content: JSON.stringify({
947
- error: 'This asset is private'
948
- }),
949
- headers: { 'Content-Type': 'application/json' }
950
- };
951
- }
926
+ const result = await this.fetchAssetWithAccessCheck(req);
927
+ if (!result.success) {
928
+ return result.response;
952
929
  }
953
- // 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
- };
930
+ const fileContent = await result.asset.data();
931
+ return fileResponse(fileContent, this.getConfiguration().contentType);
963
932
  }
964
933
  catch (error) {
965
- return {
966
- status: 500,
967
- content: JSON.stringify({
968
- error: error instanceof Error ? error.message : 'Unknown error'
969
- }),
970
- headers: { 'Content-Type': 'application/json' }
971
- };
934
+ return errorResponse(error);
972
935
  }
973
936
  }
974
937
  /**
@@ -993,90 +956,16 @@ export class AssetsManager {
993
956
  */
994
957
  async handleDownload(req) {
995
958
  try {
996
- const { 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
- };
1005
- }
1006
- const asset = await this.getAssetById(id);
1007
- if (!asset) {
1008
- return {
1009
- status: 404,
1010
- content: 'Asset not found',
1011
- headers: { 'Content-Type': 'text/plain' }
1012
- };
959
+ const result = await this.fetchAssetWithAccessCheck(req);
960
+ if (!result.success) {
961
+ return result.response;
1013
962
  }
1014
- // 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
- };
963
+ const fileContent = await result.asset.data();
964
+ const filename = result.asset.filename || `asset_${req.params?.id}`;
965
+ return fileResponse(fileContent, this.getConfiguration().contentType, filename);
1071
966
  }
1072
967
  catch (error) {
1073
- return {
1074
- status: 500,
1075
- content: JSON.stringify({
1076
- error: error instanceof Error ? error.message : 'Unknown error'
1077
- }),
1078
- headers: { 'Content-Type': 'application/json' }
1079
- };
968
+ return errorResponse(error);
1080
969
  }
1081
970
  }
1082
971
  /**
@@ -1094,84 +983,34 @@ export class AssetsManager {
1094
983
  */
1095
984
  async handleDelete(req) {
1096
985
  try {
1097
- // 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
- };
986
+ // Authenticate user
987
+ const authResult = await this.authenticateRequest(req);
988
+ if (!authResult.success) {
989
+ return authResult.response;
1106
990
  }
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
- };
991
+ const userId = authResult.userRecord.id;
992
+ if (!userId) {
993
+ return errorResponse('Failed to retrieve user information');
1117
994
  }
1118
995
  const { id } = req.params || {};
1119
996
  if (!id) {
1120
- return {
1121
- status: 400,
1122
- content: JSON.stringify({
1123
- error: 'Asset ID is required'
1124
- }),
1125
- headers: { 'Content-Type': 'application/json' }
1126
- };
1127
- }
1128
- // Find or create user in database
1129
- const userRecord = await this.userService.findOrCreateUser(authUser);
1130
- if (!userRecord.id) {
1131
- return {
1132
- status: 500,
1133
- content: JSON.stringify({
1134
- error: 'Failed to retrieve user information'
1135
- }),
1136
- headers: { 'Content-Type': 'application/json' }
1137
- };
997
+ return badRequestResponse('Asset ID is required');
1138
998
  }
1139
- // Check if asset exists and belongs to this user
999
+ // Check if asset exists
1140
1000
  const asset = await this.getAssetById(id);
1141
1001
  if (!asset) {
1142
- return {
1143
- status: 404,
1144
- content: JSON.stringify({
1145
- error: 'Asset not found'
1146
- }),
1147
- headers: { 'Content-Type': 'application/json' }
1148
- };
1002
+ return notFoundResponse('Asset not found');
1149
1003
  }
1150
- // Check ownership (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
- };
1004
+ // Check ownership
1005
+ const ownershipError = this.validateOwnership(asset, userId);
1006
+ if (ownershipError) {
1007
+ return ownershipError;
1159
1008
  }
1160
1009
  await this.deleteAssetById(id);
1161
- return {
1162
- status: 200,
1163
- content: JSON.stringify({ message: 'Asset deleted successfully' }),
1164
- headers: { 'Content-Type': 'application/json' }
1165
- };
1010
+ return successResponse({ message: 'Asset deleted successfully' });
1166
1011
  }
1167
1012
  catch (error) {
1168
- return {
1169
- status: 500,
1170
- content: JSON.stringify({
1171
- error: error instanceof Error ? error.message : 'Unknown error'
1172
- }),
1173
- headers: { 'Content-Type': 'application/json' }
1174
- };
1013
+ return errorResponse(error);
1175
1014
  }
1176
1015
  }
1177
1016
  /**
@@ -1179,243 +1018,160 @@ export class AssetsManager {
1179
1018
  */
1180
1019
  async handleUploadBatch(req) {
1181
1020
  try {
1182
- if (!req || !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
- };
1021
+ if (!req?.body) {
1022
+ return badRequestResponse('Invalid request: missing request body');
1200
1023
  }
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
- };
1024
+ // Authenticate user
1025
+ const authResult = await this.authenticateRequest(req);
1026
+ if (!authResult.success) {
1027
+ return authResult.response;
1211
1028
  }
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
- };
1029
+ const userId = authResult.userRecord.id;
1030
+ if (!userId) {
1031
+ return errorResponse('Failed to retrieve user information');
1222
1032
  }
1223
1033
  const requests = req.body.requests;
1224
1034
  if (!Array.isArray(requests) || requests.length === 0) {
1225
- return {
1226
- status: 400,
1227
- content: JSON.stringify({
1228
- error: 'Requests array is required and must not be empty'
1229
- }),
1230
- headers: { 'Content-Type': 'application/json' }
1231
- };
1232
- }
1233
- // Validate all requests first (including extensions and base64 format)
1234
- for (const request of requests) {
1235
- if (!request.file || !request.description || !request.source || !request.filename) {
1236
- return {
1237
- status: 400,
1238
- content: JSON.stringify({
1239
- error: 'Each request must have description, source, filename, and file'
1240
- }),
1241
- headers: { 'Content-Type': 'application/json' }
1242
- };
1243
- }
1244
- // Validate that file is valid base64 string
1245
- if (!this.validateBase64(request.file)) {
1246
- return {
1247
- status: 400,
1248
- content: JSON.stringify({
1249
- error: `Invalid base64 data for file: ${request.filename}. File must be a valid base64-encoded string.`
1250
- }),
1251
- headers: { 'Content-Type': 'application/json' }
1252
- };
1253
- }
1254
- // Validate file extension for each request
1255
- if (!this.validateFileExtension(request.filename)) {
1256
- const config = this.getConfiguration();
1257
- return {
1258
- status: 400,
1259
- content: JSON.stringify({
1260
- error: `Invalid file extension for ${request.filename}. Expected: ${config.extension}`
1261
- }),
1262
- headers: { 'Content-Type': 'application/json' }
1263
- };
1264
- }
1035
+ return badRequestResponse('Requests array is required and must not be empty');
1265
1036
  }
1266
- // 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
- }
1037
+ // Validate all requests first
1038
+ const validationError = this.validateBatchRequests(requests);
1039
+ if (validationError) {
1040
+ return validationError;
1287
1041
  }
1042
+ // Process each request
1043
+ const results = await this.processBatchUploads(requests, userId);
1288
1044
  const successCount = results.filter(r => r.success).length;
1289
1045
  const failureCount = results.length - successCount;
1290
- 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
- };
1046
+ const message = `${successCount}/${requests.length} assets uploaded successfully`;
1047
+ if (failureCount > 0) {
1048
+ return multiStatusResponse(message, results);
1049
+ }
1050
+ return successResponse({ message, results });
1298
1051
  }
1299
1052
  catch (error) {
1300
- return {
1301
- status: 500,
1302
- content: JSON.stringify({
1053
+ return errorResponse(error);
1054
+ }
1055
+ }
1056
+ /**
1057
+ * Validates all requests in a batch upload.
1058
+ *
1059
+ * @param requests - Array of upload requests to validate
1060
+ * @returns DataResponse with error if validation fails, undefined if valid
1061
+ */
1062
+ validateBatchRequests(requests) {
1063
+ const config = this.getConfiguration();
1064
+ for (const request of requests) {
1065
+ if (!request.file || !request.description || !request.source || !request.filename) {
1066
+ return badRequestResponse('Each request must have description, source, filename, and file');
1067
+ }
1068
+ if (!this.validateBase64(request.file)) {
1069
+ return badRequestResponse(`Invalid base64 data for file: ${request.filename}. File must be a valid base64-encoded string.`);
1070
+ }
1071
+ if (!this.validateFileExtension(request.filename)) {
1072
+ return badRequestResponse(`Invalid file extension for ${request.filename}. Expected: ${config.extension}`);
1073
+ }
1074
+ }
1075
+ return undefined;
1076
+ }
1077
+ /**
1078
+ * Processes batch upload requests.
1079
+ *
1080
+ * @param requests - Array of upload requests
1081
+ * @param ownerId - Owner user ID
1082
+ * @returns Array of results for each upload
1083
+ */
1084
+ async processBatchUploads(requests, ownerId) {
1085
+ const results = [];
1086
+ for (const request of requests) {
1087
+ try {
1088
+ await this.uploadAsset({
1089
+ description: request.description,
1090
+ source: request.source,
1091
+ owner_id: ownerId,
1092
+ filename: request.filename,
1093
+ file: Buffer.from(request.file, 'base64'),
1094
+ is_public: request.is_public !== undefined ? Boolean(request.is_public) : true
1095
+ });
1096
+ results.push({ success: true, filename: request.filename });
1097
+ }
1098
+ catch (error) {
1099
+ results.push({
1100
+ success: false,
1101
+ filename: request.filename,
1303
1102
  error: error instanceof Error ? error.message : 'Unknown error'
1304
- }),
1305
- headers: { 'Content-Type': 'application/json' }
1306
- };
1103
+ });
1104
+ }
1307
1105
  }
1106
+ return results;
1308
1107
  }
1309
1108
  /**
1310
1109
  * Handle batch delete endpoint
1311
1110
  */
1312
1111
  async handleDeleteBatch(req) {
1313
1112
  try {
1314
- if (!req || !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
- };
1113
+ if (!req?.body) {
1114
+ return badRequestResponse('Invalid request: missing request body');
1332
1115
  }
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
- };
1116
+ // Authenticate user
1117
+ const authResult = await this.authenticateRequest(req);
1118
+ if (!authResult.success) {
1119
+ return authResult.response;
1343
1120
  }
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
- };
1121
+ const userId = authResult.userRecord.id;
1122
+ if (!userId) {
1123
+ return errorResponse('Failed to retrieve user information');
1354
1124
  }
1355
1125
  const { ids } = req.body;
1356
1126
  if (!Array.isArray(ids) || ids.length === 0) {
1357
- return {
1358
- status: 400,
1359
- content: JSON.stringify({
1360
- error: 'IDs array is required and must not be empty'
1361
- }),
1362
- headers: { 'Content-Type': 'application/json' }
1363
- };
1364
- }
1365
- // Process deletions individually with error handling and ownership checks
1366
- const results = [];
1367
- for (const id of ids) {
1368
- try {
1369
- // Check if asset exists and belongs to this user
1370
- const asset = await this.getAssetById(id);
1371
- if (!asset) {
1372
- results.push({
1373
- success: false,
1374
- id,
1375
- error: 'Asset not found'
1376
- });
1377
- continue;
1378
- }
1379
- // Check ownership (only owner can delete their assets)
1380
- if (asset.owner_id !== userRecord.id) {
1381
- results.push({
1382
- success: false,
1383
- id,
1384
- error: 'You can only delete your own assets'
1385
- });
1386
- continue;
1387
- }
1388
- await this.deleteAssetById(id);
1389
- results.push({ success: true, id });
1390
- }
1391
- catch (error) {
1392
- results.push({
1393
- success: false,
1394
- id,
1395
- error: error instanceof Error ? error.message : 'Unknown error'
1396
- });
1397
- }
1127
+ return badRequestResponse('IDs array is required and must not be empty');
1398
1128
  }
1129
+ // Process deletions
1130
+ const results = await this.processBatchDeletes(ids, userId);
1399
1131
  const successCount = results.filter(r => r.success).length;
1400
1132
  const failureCount = results.length - successCount;
1401
- 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
- };
1133
+ const message = `${successCount}/${ids.length} assets deleted successfully`;
1134
+ if (failureCount > 0) {
1135
+ return multiStatusResponse(message, results);
1136
+ }
1137
+ return successResponse({ message, results });
1409
1138
  }
1410
1139
  catch (error) {
1411
- return {
1412
- status: 500,
1413
- content: JSON.stringify({
1140
+ return errorResponse(error);
1141
+ }
1142
+ }
1143
+ /**
1144
+ * Processes batch delete requests.
1145
+ *
1146
+ * @param ids - Array of asset IDs to delete
1147
+ * @param userId - User ID for ownership validation
1148
+ * @returns Array of results for each deletion
1149
+ */
1150
+ async processBatchDeletes(ids, userId) {
1151
+ const results = [];
1152
+ for (const id of ids) {
1153
+ try {
1154
+ const asset = await this.getAssetById(id);
1155
+ if (!asset) {
1156
+ results.push({ success: false, id, error: 'Asset not found' });
1157
+ continue;
1158
+ }
1159
+ if (asset.owner_id !== userId) {
1160
+ results.push({ success: false, id, error: 'You can only delete your own assets' });
1161
+ continue;
1162
+ }
1163
+ await this.deleteAssetById(id);
1164
+ results.push({ success: true, id });
1165
+ }
1166
+ catch (error) {
1167
+ results.push({
1168
+ success: false,
1169
+ id,
1414
1170
  error: error instanceof Error ? error.message : 'Unknown error'
1415
- }),
1416
- headers: { 'Content-Type': 'application/json' }
1417
- };
1171
+ });
1172
+ }
1418
1173
  }
1174
+ return results;
1419
1175
  }
1420
1176
  }
1421
1177
  //# sourceMappingURL=assets_manager.js.map