digitaltwin-core 0.9.0 → 0.9.2

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