digitaltwin-core 0.8.3 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/apisix_parser.d.ts +24 -0
- package/dist/auth/apisix_parser.d.ts.map +1 -1
- package/dist/auth/apisix_parser.js +28 -0
- package/dist/auth/apisix_parser.js.map +1 -1
- package/dist/auth/auth_config.d.ts +31 -0
- package/dist/auth/auth_config.d.ts.map +1 -1
- package/dist/auth/auth_config.js +48 -4
- package/dist/auth/auth_config.js.map +1 -1
- package/dist/auth/user_service.d.ts.map +1 -1
- package/dist/auth/user_service.js +4 -1
- package/dist/auth/user_service.js.map +1 -1
- package/dist/components/assets_manager.d.ts +79 -8
- package/dist/components/assets_manager.d.ts.map +1 -1
- package/dist/components/assets_manager.js +353 -21
- package/dist/components/assets_manager.js.map +1 -1
- package/dist/components/custom_table_manager.d.ts +142 -1
- package/dist/components/custom_table_manager.d.ts.map +1 -1
- package/dist/components/custom_table_manager.js +313 -3
- package/dist/components/custom_table_manager.js.map +1 -1
- package/dist/components/map_manager.d.ts.map +1 -1
- package/dist/components/map_manager.js +9 -0
- package/dist/components/map_manager.js.map +1 -1
- package/dist/components/tileset_manager.d.ts.map +1 -1
- package/dist/components/tileset_manager.js +9 -0
- package/dist/components/tileset_manager.js.map +1 -1
- package/dist/components/types.d.ts +169 -19
- package/dist/components/types.d.ts.map +1 -1
- package/dist/components/types.js +6 -0
- package/dist/components/types.js.map +1 -1
- package/dist/database/adapters/knex_database_adapter.d.ts +10 -0
- package/dist/database/adapters/knex_database_adapter.d.ts.map +1 -1
- package/dist/database/adapters/knex_database_adapter.js +104 -9
- package/dist/database/adapters/knex_database_adapter.js.map +1 -1
- package/dist/database/database_adapter.d.ts +116 -7
- package/dist/database/database_adapter.d.ts.map +1 -1
- package/dist/database/database_adapter.js +41 -1
- package/dist/database/database_adapter.js.map +1 -1
- package/dist/engine/digital_twin_engine.d.ts +12 -0
- package/dist/engine/digital_twin_engine.d.ts.map +1 -1
- package/dist/engine/digital_twin_engine.js +15 -9
- package/dist/engine/digital_twin_engine.js.map +1 -1
- package/dist/engine/initializer.d.ts +8 -5
- package/dist/engine/initializer.d.ts.map +1 -1
- package/dist/engine/initializer.js +32 -13
- package/dist/engine/initializer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/data_record.d.ts +8 -0
- package/dist/types/data_record.d.ts.map +1 -1
- package/dist/utils/map_to_data_record.d.ts.map +1 -1
- package/dist/utils/map_to_data_record.js +4 -1
- package/dist/utils/map_to_data_record.js.map +1 -1
- package/package.json +1 -1
|
@@ -132,6 +132,51 @@ export class AssetsManager {
|
|
|
132
132
|
// Check if the filename ends with the required extension (case-insensitive)
|
|
133
133
|
return filename.toLowerCase().endsWith(requiredExtension.toLowerCase());
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Validates that a string is valid base64-encoded data.
|
|
137
|
+
*
|
|
138
|
+
* Used internally to ensure file data in batch uploads is properly base64-encoded
|
|
139
|
+
* before attempting to decode it.
|
|
140
|
+
*
|
|
141
|
+
* @private
|
|
142
|
+
* @param {any} data - Data to validate as base64
|
|
143
|
+
* @returns {boolean} True if data is a valid base64 string, false otherwise
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* this.validateBase64('SGVsbG8gV29ybGQ=') // returns true
|
|
148
|
+
* this.validateBase64('not-base64!@#') // returns false
|
|
149
|
+
* this.validateBase64(123) // returns false (not a string)
|
|
150
|
+
* this.validateBase64('') // returns false (empty string)
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
validateBase64(data) {
|
|
154
|
+
// Must be a non-empty string
|
|
155
|
+
if (typeof data !== 'string' || data.length === 0) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
// Base64 regex: only A-Z, a-z, 0-9, +, /, and = for padding
|
|
159
|
+
// Padding (=) can only appear at the end, max 2 times
|
|
160
|
+
const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
161
|
+
const trimmed = data.trim();
|
|
162
|
+
// Must match regex
|
|
163
|
+
if (!base64Regex.test(trimmed)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
// Length must be multiple of 4
|
|
167
|
+
if (trimmed.length % 4 !== 0) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
// Try to decode to verify it's valid base64
|
|
171
|
+
try {
|
|
172
|
+
const decoded = Buffer.from(trimmed, 'base64').toString('base64');
|
|
173
|
+
// Re-encode and compare to ensure no data loss (valid base64)
|
|
174
|
+
return decoded === trimmed;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
135
180
|
/**
|
|
136
181
|
* Upload a new asset file with metadata.
|
|
137
182
|
*
|
|
@@ -148,7 +193,8 @@ export class AssetsManager {
|
|
|
148
193
|
* source: 'https://city-data.example.com/buildings',
|
|
149
194
|
* owner_id: 'user123',
|
|
150
195
|
* filename: 'building.glb',
|
|
151
|
-
* file: fileBuffer
|
|
196
|
+
* file: fileBuffer,
|
|
197
|
+
* is_public: true
|
|
152
198
|
* })
|
|
153
199
|
* ```
|
|
154
200
|
*/
|
|
@@ -173,7 +219,8 @@ export class AssetsManager {
|
|
|
173
219
|
description: request.description,
|
|
174
220
|
source: request.source,
|
|
175
221
|
owner_id: request.owner_id,
|
|
176
|
-
filename: request.filename
|
|
222
|
+
filename: request.filename,
|
|
223
|
+
is_public: request.is_public ?? true // Default to public if not specified
|
|
177
224
|
};
|
|
178
225
|
await this.db.save(metadata);
|
|
179
226
|
}
|
|
@@ -183,15 +230,61 @@ export class AssetsManager {
|
|
|
183
230
|
* Returns a JSON list of all assets with their metadata, following the
|
|
184
231
|
* framework pattern but adapted for assets management.
|
|
185
232
|
*
|
|
233
|
+
* Access control:
|
|
234
|
+
* - Unauthenticated users: Can only see public assets
|
|
235
|
+
* - Authenticated users: Can see public assets + their own private assets
|
|
236
|
+
* - Admin users: Can see all assets (public and private from all users)
|
|
237
|
+
*
|
|
186
238
|
* @returns {Promise<DataResponse>} JSON response with all assets
|
|
187
239
|
*/
|
|
188
|
-
async retrieve() {
|
|
240
|
+
async retrieve(req) {
|
|
189
241
|
try {
|
|
190
242
|
const assets = await this.getAllAssets();
|
|
191
243
|
const config = this.getConfiguration();
|
|
244
|
+
// Check if user is admin (admins can see everything)
|
|
245
|
+
const isAdmin = req && ApisixAuthParser.isAdmin(req.headers || {});
|
|
246
|
+
// If admin, skip filtering and return all assets
|
|
247
|
+
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
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Filter assets based on visibility and ownership
|
|
277
|
+
const visibleAssets = assets.filter(asset => {
|
|
278
|
+
// Public assets are visible to everyone
|
|
279
|
+
if (asset.is_public) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
// Private assets are only visible to the owner
|
|
283
|
+
return authenticatedUserId !== null && asset.owner_id === authenticatedUserId;
|
|
284
|
+
});
|
|
192
285
|
// Transform to include asset metadata and user-friendly URLs
|
|
193
286
|
// Note: assets are already sorted by date desc from DB query
|
|
194
|
-
const assetsWithMetadata =
|
|
287
|
+
const assetsWithMetadata = visibleAssets.map(asset => ({
|
|
195
288
|
id: asset.id,
|
|
196
289
|
name: asset.name,
|
|
197
290
|
date: asset.date,
|
|
@@ -201,6 +294,7 @@ export class AssetsManager {
|
|
|
201
294
|
source: asset.source || '',
|
|
202
295
|
owner_id: asset.owner_id || null,
|
|
203
296
|
filename: asset.filename || '',
|
|
297
|
+
is_public: asset.is_public ?? true,
|
|
204
298
|
// Add URLs that the front-end can actually use
|
|
205
299
|
url: `/${config.name}/${asset.id}`, // For display/use
|
|
206
300
|
download_url: `/${config.name}/${asset.id}/download` // For download
|
|
@@ -264,7 +358,7 @@ export class AssetsManager {
|
|
|
264
358
|
/**
|
|
265
359
|
* Update asset metadata by ID.
|
|
266
360
|
*
|
|
267
|
-
* Updates the metadata (description and/or
|
|
361
|
+
* Updates the metadata (description, source, and/or visibility) of a specific asset.
|
|
268
362
|
* Asset metadata is stored as dedicated columns in the database.
|
|
269
363
|
*
|
|
270
364
|
* @param {string} id - The ID of the asset to update
|
|
@@ -275,7 +369,8 @@ export class AssetsManager {
|
|
|
275
369
|
* ```typescript
|
|
276
370
|
* await assetsManager.updateAssetMetadata('123', {
|
|
277
371
|
* description: 'Updated building model with new textures',
|
|
278
|
-
* source: 'https://updated-source.example.com'
|
|
372
|
+
* source: 'https://updated-source.example.com',
|
|
373
|
+
* is_public: false
|
|
279
374
|
* })
|
|
280
375
|
* ```
|
|
281
376
|
*/
|
|
@@ -302,7 +397,8 @@ export class AssetsManager {
|
|
|
302
397
|
description: updates.description ?? record.description ?? '',
|
|
303
398
|
source: updates.source ?? record.source ?? '',
|
|
304
399
|
owner_id: record.owner_id ?? null,
|
|
305
|
-
filename: record.filename ?? ''
|
|
400
|
+
filename: record.filename ?? '',
|
|
401
|
+
is_public: updates.is_public !== undefined ? updates.is_public : (record.is_public ?? true)
|
|
306
402
|
};
|
|
307
403
|
// Update the record - delete and re-create with updated metadata
|
|
308
404
|
await this.db.delete(id, this.getConfiguration().name);
|
|
@@ -393,7 +489,8 @@ export class AssetsManager {
|
|
|
393
489
|
description: request.description,
|
|
394
490
|
source: request.source,
|
|
395
491
|
owner_id: request.owner_id,
|
|
396
|
-
filename: request.filename
|
|
492
|
+
filename: request.filename,
|
|
493
|
+
is_public: request.is_public ?? true
|
|
397
494
|
};
|
|
398
495
|
metadataList.push(metadata);
|
|
399
496
|
}
|
|
@@ -491,10 +588,37 @@ export class AssetsManager {
|
|
|
491
588
|
];
|
|
492
589
|
}
|
|
493
590
|
/**
|
|
494
|
-
* Handle upload
|
|
591
|
+
* Handle single asset upload via HTTP POST.
|
|
592
|
+
*
|
|
593
|
+
* Flow:
|
|
594
|
+
* 1. Validate request structure and authentication
|
|
595
|
+
* 2. Extract user identity from Apache APISIX headers
|
|
596
|
+
* 3. Validate file extension and read uploaded file
|
|
597
|
+
* 4. Store file via storage service and metadata in database
|
|
598
|
+
* 5. Set owner_id to authenticated user (prevents ownership spoofing)
|
|
599
|
+
* 6. Apply is_public setting (defaults to true if not specified)
|
|
600
|
+
*
|
|
601
|
+
* Authentication: Required
|
|
602
|
+
* Ownership: Automatically set to authenticated user
|
|
603
|
+
*
|
|
604
|
+
* @param req - HTTP request with multipart/form-data file upload
|
|
605
|
+
* @returns HTTP response with success/error status
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* POST /assets
|
|
609
|
+
* Content-Type: multipart/form-data
|
|
610
|
+
* x-user-id: user-uuid
|
|
611
|
+
* x-user-roles: user,premium
|
|
612
|
+
*
|
|
613
|
+
* Form data:
|
|
614
|
+
* - file: <binary file>
|
|
615
|
+
* - description: "3D model of building"
|
|
616
|
+
* - source: "https://source.com"
|
|
617
|
+
* - is_public: true
|
|
495
618
|
*/
|
|
496
619
|
async handleUpload(req) {
|
|
497
620
|
try {
|
|
621
|
+
// Validate request structure
|
|
498
622
|
if (!req || !req.body) {
|
|
499
623
|
return {
|
|
500
624
|
status: 400,
|
|
@@ -504,7 +628,7 @@ export class AssetsManager {
|
|
|
504
628
|
headers: { 'Content-Type': 'application/json' }
|
|
505
629
|
};
|
|
506
630
|
}
|
|
507
|
-
//
|
|
631
|
+
// Step 1: Verify user is authenticated via Apache APISIX headers
|
|
508
632
|
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
509
633
|
return {
|
|
510
634
|
status: 401,
|
|
@@ -514,7 +638,7 @@ export class AssetsManager {
|
|
|
514
638
|
headers: { 'Content-Type': 'application/json' }
|
|
515
639
|
};
|
|
516
640
|
}
|
|
517
|
-
//
|
|
641
|
+
// Step 2: Extract user identity from APISIX headers (x-user-id, x-user-roles)
|
|
518
642
|
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
519
643
|
if (!authUser) {
|
|
520
644
|
return {
|
|
@@ -525,11 +649,22 @@ export class AssetsManager {
|
|
|
525
649
|
headers: { 'Content-Type': 'application/json' }
|
|
526
650
|
};
|
|
527
651
|
}
|
|
528
|
-
// Find or create user in database
|
|
652
|
+
// Step 3: Find or create user record in database and sync roles from Keycloak
|
|
529
653
|
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
530
|
-
|
|
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
|
+
};
|
|
662
|
+
}
|
|
663
|
+
// Step 4: Extract form data from multipart upload
|
|
664
|
+
const { description, source, is_public } = req.body;
|
|
531
665
|
const filePath = req.file?.path;
|
|
532
666
|
const filename = req.file?.originalname || req.body.filename;
|
|
667
|
+
// Step 5: Validate required fields are present
|
|
533
668
|
if (!filePath || !description || !source) {
|
|
534
669
|
return {
|
|
535
670
|
status: 400,
|
|
@@ -579,7 +714,8 @@ export class AssetsManager {
|
|
|
579
714
|
source,
|
|
580
715
|
owner_id: userRecord.id,
|
|
581
716
|
filename,
|
|
582
|
-
file: fileBuffer
|
|
717
|
+
file: fileBuffer,
|
|
718
|
+
is_public: is_public !== undefined ? Boolean(is_public) : true
|
|
583
719
|
});
|
|
584
720
|
// Clean up temporary file after successful upload
|
|
585
721
|
await fs.unlink(filePath).catch(() => {
|
|
@@ -654,7 +790,7 @@ export class AssetsManager {
|
|
|
654
790
|
};
|
|
655
791
|
}
|
|
656
792
|
const { id } = req.params || {};
|
|
657
|
-
const { description, source } = req.body || {};
|
|
793
|
+
const { description, source, is_public } = req.body || {};
|
|
658
794
|
if (!id) {
|
|
659
795
|
return {
|
|
660
796
|
status: 400,
|
|
@@ -664,17 +800,26 @@ export class AssetsManager {
|
|
|
664
800
|
headers: { 'Content-Type': 'application/json' }
|
|
665
801
|
};
|
|
666
802
|
}
|
|
667
|
-
if (!description && !source) {
|
|
803
|
+
if (!description && !source && is_public === undefined) {
|
|
668
804
|
return {
|
|
669
805
|
status: 400,
|
|
670
806
|
content: JSON.stringify({
|
|
671
|
-
error: 'At least one field (description or
|
|
807
|
+
error: 'At least one field (description, source, or is_public) must be provided for update'
|
|
672
808
|
}),
|
|
673
809
|
headers: { 'Content-Type': 'application/json' }
|
|
674
810
|
};
|
|
675
811
|
}
|
|
676
812
|
// Find or create user in database
|
|
677
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
|
+
}
|
|
678
823
|
// Check if asset exists and belongs to this user
|
|
679
824
|
const asset = await this.getAssetById(id);
|
|
680
825
|
if (!asset) {
|
|
@@ -701,6 +846,8 @@ export class AssetsManager {
|
|
|
701
846
|
updates.description = description;
|
|
702
847
|
if (source !== undefined)
|
|
703
848
|
updates.source = source;
|
|
849
|
+
if (is_public !== undefined)
|
|
850
|
+
updates.is_public = Boolean(is_public);
|
|
704
851
|
await this.updateAssetMetadata(id, updates);
|
|
705
852
|
return {
|
|
706
853
|
status: 200,
|
|
@@ -724,6 +871,11 @@ export class AssetsManager {
|
|
|
724
871
|
* Returns the file content of a specific asset by ID for display/use in front-end.
|
|
725
872
|
* No download headers - just the raw file content.
|
|
726
873
|
*
|
|
874
|
+
* Access control:
|
|
875
|
+
* - Public assets: Accessible to everyone
|
|
876
|
+
* - Private assets: Accessible only to owner
|
|
877
|
+
* - Admin users: Can access all assets (public and private)
|
|
878
|
+
*
|
|
727
879
|
* @param {any} req - HTTP request object with params.id
|
|
728
880
|
* @returns {Promise<DataResponse>} HTTP response with file content
|
|
729
881
|
*
|
|
@@ -762,6 +914,42 @@ export class AssetsManager {
|
|
|
762
914
|
headers: { 'Content-Type': 'text/plain' }
|
|
763
915
|
};
|
|
764
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
|
+
}
|
|
952
|
+
}
|
|
765
953
|
// Get the file content
|
|
766
954
|
const fileContent = await asset.data();
|
|
767
955
|
return {
|
|
@@ -789,6 +977,11 @@ export class AssetsManager {
|
|
|
789
977
|
* Downloads the file content of a specific asset by ID with download headers.
|
|
790
978
|
* Forces browser to download the file rather than display it.
|
|
791
979
|
*
|
|
980
|
+
* Access control:
|
|
981
|
+
* - Public assets: Accessible to everyone
|
|
982
|
+
* - Private assets: Accessible only to owner
|
|
983
|
+
* - Admin users: Can download all assets (public and private)
|
|
984
|
+
*
|
|
792
985
|
* @param {any} req - HTTP request object with params.id
|
|
793
986
|
* @returns {Promise<DataResponse>} HTTP response with file content and download headers
|
|
794
987
|
*
|
|
@@ -827,6 +1020,42 @@ export class AssetsManager {
|
|
|
827
1020
|
headers: { 'Content-Type': 'text/plain' }
|
|
828
1021
|
};
|
|
829
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
|
+
}
|
|
830
1059
|
// Get the file content
|
|
831
1060
|
const fileContent = await asset.data();
|
|
832
1061
|
// Get original filename from asset metadata
|
|
@@ -898,6 +1127,15 @@ export class AssetsManager {
|
|
|
898
1127
|
}
|
|
899
1128
|
// Find or create user in database
|
|
900
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
|
+
};
|
|
1138
|
+
}
|
|
901
1139
|
// Check if asset exists and belongs to this user
|
|
902
1140
|
const asset = await this.getAssetById(id);
|
|
903
1141
|
if (!asset) {
|
|
@@ -950,6 +1188,38 @@ export class AssetsManager {
|
|
|
950
1188
|
headers: { 'Content-Type': 'application/json' }
|
|
951
1189
|
};
|
|
952
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
|
+
};
|
|
1200
|
+
}
|
|
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
|
+
};
|
|
1211
|
+
}
|
|
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
|
+
};
|
|
1222
|
+
}
|
|
953
1223
|
const requests = req.body.requests;
|
|
954
1224
|
if (!Array.isArray(requests) || requests.length === 0) {
|
|
955
1225
|
return {
|
|
@@ -960,7 +1230,7 @@ export class AssetsManager {
|
|
|
960
1230
|
headers: { 'Content-Type': 'application/json' }
|
|
961
1231
|
};
|
|
962
1232
|
}
|
|
963
|
-
// Validate all requests first (including extensions)
|
|
1233
|
+
// Validate all requests first (including extensions and base64 format)
|
|
964
1234
|
for (const request of requests) {
|
|
965
1235
|
if (!request.file || !request.description || !request.source || !request.filename) {
|
|
966
1236
|
return {
|
|
@@ -971,6 +1241,16 @@ export class AssetsManager {
|
|
|
971
1241
|
headers: { 'Content-Type': 'application/json' }
|
|
972
1242
|
};
|
|
973
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
|
+
}
|
|
974
1254
|
// Validate file extension for each request
|
|
975
1255
|
if (!this.validateFileExtension(request.filename)) {
|
|
976
1256
|
const config = this.getConfiguration();
|
|
@@ -990,9 +1270,10 @@ export class AssetsManager {
|
|
|
990
1270
|
await this.uploadAsset({
|
|
991
1271
|
description: request.description,
|
|
992
1272
|
source: request.source,
|
|
993
|
-
owner_id:
|
|
1273
|
+
owner_id: userRecord.id,
|
|
994
1274
|
filename: request.filename,
|
|
995
|
-
file: Buffer.from(request.file, 'base64') //
|
|
1275
|
+
file: Buffer.from(request.file, 'base64'), // Already validated as base64 above
|
|
1276
|
+
is_public: request.is_public !== undefined ? Boolean(request.is_public) : true
|
|
996
1277
|
});
|
|
997
1278
|
results.push({ success: true, filename: request.filename });
|
|
998
1279
|
}
|
|
@@ -1039,6 +1320,38 @@ export class AssetsManager {
|
|
|
1039
1320
|
headers: { 'Content-Type': 'application/json' }
|
|
1040
1321
|
};
|
|
1041
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
|
+
};
|
|
1332
|
+
}
|
|
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
|
+
};
|
|
1343
|
+
}
|
|
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
|
+
};
|
|
1354
|
+
}
|
|
1042
1355
|
const { ids } = req.body;
|
|
1043
1356
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
1044
1357
|
return {
|
|
@@ -1049,10 +1362,29 @@ export class AssetsManager {
|
|
|
1049
1362
|
headers: { 'Content-Type': 'application/json' }
|
|
1050
1363
|
};
|
|
1051
1364
|
}
|
|
1052
|
-
// Process deletions individually with error handling
|
|
1365
|
+
// Process deletions individually with error handling and ownership checks
|
|
1053
1366
|
const results = [];
|
|
1054
1367
|
for (const id of ids) {
|
|
1055
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
|
+
}
|
|
1056
1388
|
await this.deleteAssetById(id);
|
|
1057
1389
|
results.push({ success: true, id });
|
|
1058
1390
|
}
|