digitaltwin-core 0.9.0 → 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 +26 -0
- package/dist/auth/auth_config.d.ts.map +1 -1
- package/dist/auth/auth_config.js +35 -0
- package/dist/auth/auth_config.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 +326 -21
- package/dist/components/assets_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 +86 -0
- 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 +4 -2
- 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/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,7 +649,7 @@ 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) {
|
|
531
655
|
return {
|
|
@@ -536,9 +660,11 @@ export class AssetsManager {
|
|
|
536
660
|
headers: { 'Content-Type': 'application/json' }
|
|
537
661
|
};
|
|
538
662
|
}
|
|
539
|
-
|
|
663
|
+
// Step 4: Extract form data from multipart upload
|
|
664
|
+
const { description, source, is_public } = req.body;
|
|
540
665
|
const filePath = req.file?.path;
|
|
541
666
|
const filename = req.file?.originalname || req.body.filename;
|
|
667
|
+
// Step 5: Validate required fields are present
|
|
542
668
|
if (!filePath || !description || !source) {
|
|
543
669
|
return {
|
|
544
670
|
status: 400,
|
|
@@ -588,7 +714,8 @@ export class AssetsManager {
|
|
|
588
714
|
source,
|
|
589
715
|
owner_id: userRecord.id,
|
|
590
716
|
filename,
|
|
591
|
-
file: fileBuffer
|
|
717
|
+
file: fileBuffer,
|
|
718
|
+
is_public: is_public !== undefined ? Boolean(is_public) : true
|
|
592
719
|
});
|
|
593
720
|
// Clean up temporary file after successful upload
|
|
594
721
|
await fs.unlink(filePath).catch(() => {
|
|
@@ -663,7 +790,7 @@ export class AssetsManager {
|
|
|
663
790
|
};
|
|
664
791
|
}
|
|
665
792
|
const { id } = req.params || {};
|
|
666
|
-
const { description, source } = req.body || {};
|
|
793
|
+
const { description, source, is_public } = req.body || {};
|
|
667
794
|
if (!id) {
|
|
668
795
|
return {
|
|
669
796
|
status: 400,
|
|
@@ -673,11 +800,11 @@ export class AssetsManager {
|
|
|
673
800
|
headers: { 'Content-Type': 'application/json' }
|
|
674
801
|
};
|
|
675
802
|
}
|
|
676
|
-
if (!description && !source) {
|
|
803
|
+
if (!description && !source && is_public === undefined) {
|
|
677
804
|
return {
|
|
678
805
|
status: 400,
|
|
679
806
|
content: JSON.stringify({
|
|
680
|
-
error: 'At least one field (description or
|
|
807
|
+
error: 'At least one field (description, source, or is_public) must be provided for update'
|
|
681
808
|
}),
|
|
682
809
|
headers: { 'Content-Type': 'application/json' }
|
|
683
810
|
};
|
|
@@ -719,6 +846,8 @@ export class AssetsManager {
|
|
|
719
846
|
updates.description = description;
|
|
720
847
|
if (source !== undefined)
|
|
721
848
|
updates.source = source;
|
|
849
|
+
if (is_public !== undefined)
|
|
850
|
+
updates.is_public = Boolean(is_public);
|
|
722
851
|
await this.updateAssetMetadata(id, updates);
|
|
723
852
|
return {
|
|
724
853
|
status: 200,
|
|
@@ -742,6 +871,11 @@ export class AssetsManager {
|
|
|
742
871
|
* Returns the file content of a specific asset by ID for display/use in front-end.
|
|
743
872
|
* No download headers - just the raw file content.
|
|
744
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
|
+
*
|
|
745
879
|
* @param {any} req - HTTP request object with params.id
|
|
746
880
|
* @returns {Promise<DataResponse>} HTTP response with file content
|
|
747
881
|
*
|
|
@@ -780,6 +914,42 @@ export class AssetsManager {
|
|
|
780
914
|
headers: { 'Content-Type': 'text/plain' }
|
|
781
915
|
};
|
|
782
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
|
+
}
|
|
783
953
|
// Get the file content
|
|
784
954
|
const fileContent = await asset.data();
|
|
785
955
|
return {
|
|
@@ -807,6 +977,11 @@ export class AssetsManager {
|
|
|
807
977
|
* Downloads the file content of a specific asset by ID with download headers.
|
|
808
978
|
* Forces browser to download the file rather than display it.
|
|
809
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
|
+
*
|
|
810
985
|
* @param {any} req - HTTP request object with params.id
|
|
811
986
|
* @returns {Promise<DataResponse>} HTTP response with file content and download headers
|
|
812
987
|
*
|
|
@@ -845,6 +1020,42 @@ export class AssetsManager {
|
|
|
845
1020
|
headers: { 'Content-Type': 'text/plain' }
|
|
846
1021
|
};
|
|
847
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
|
+
}
|
|
848
1059
|
// Get the file content
|
|
849
1060
|
const fileContent = await asset.data();
|
|
850
1061
|
// Get original filename from asset metadata
|
|
@@ -977,6 +1188,38 @@ export class AssetsManager {
|
|
|
977
1188
|
headers: { 'Content-Type': 'application/json' }
|
|
978
1189
|
};
|
|
979
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
|
+
}
|
|
980
1223
|
const requests = req.body.requests;
|
|
981
1224
|
if (!Array.isArray(requests) || requests.length === 0) {
|
|
982
1225
|
return {
|
|
@@ -987,7 +1230,7 @@ export class AssetsManager {
|
|
|
987
1230
|
headers: { 'Content-Type': 'application/json' }
|
|
988
1231
|
};
|
|
989
1232
|
}
|
|
990
|
-
// Validate all requests first (including extensions)
|
|
1233
|
+
// Validate all requests first (including extensions and base64 format)
|
|
991
1234
|
for (const request of requests) {
|
|
992
1235
|
if (!request.file || !request.description || !request.source || !request.filename) {
|
|
993
1236
|
return {
|
|
@@ -998,6 +1241,16 @@ export class AssetsManager {
|
|
|
998
1241
|
headers: { 'Content-Type': 'application/json' }
|
|
999
1242
|
};
|
|
1000
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
|
+
}
|
|
1001
1254
|
// Validate file extension for each request
|
|
1002
1255
|
if (!this.validateFileExtension(request.filename)) {
|
|
1003
1256
|
const config = this.getConfiguration();
|
|
@@ -1017,9 +1270,10 @@ export class AssetsManager {
|
|
|
1017
1270
|
await this.uploadAsset({
|
|
1018
1271
|
description: request.description,
|
|
1019
1272
|
source: request.source,
|
|
1020
|
-
owner_id:
|
|
1273
|
+
owner_id: userRecord.id,
|
|
1021
1274
|
filename: request.filename,
|
|
1022
|
-
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
|
|
1023
1277
|
});
|
|
1024
1278
|
results.push({ success: true, filename: request.filename });
|
|
1025
1279
|
}
|
|
@@ -1066,6 +1320,38 @@ export class AssetsManager {
|
|
|
1066
1320
|
headers: { 'Content-Type': 'application/json' }
|
|
1067
1321
|
};
|
|
1068
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
|
+
}
|
|
1069
1355
|
const { ids } = req.body;
|
|
1070
1356
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
1071
1357
|
return {
|
|
@@ -1076,10 +1362,29 @@ export class AssetsManager {
|
|
|
1076
1362
|
headers: { 'Content-Type': 'application/json' }
|
|
1077
1363
|
};
|
|
1078
1364
|
}
|
|
1079
|
-
// Process deletions individually with error handling
|
|
1365
|
+
// Process deletions individually with error handling and ownership checks
|
|
1080
1366
|
const results = [];
|
|
1081
1367
|
for (const id of ids) {
|
|
1082
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
|
+
}
|
|
1083
1388
|
await this.deleteAssetById(id);
|
|
1084
1389
|
results.push({ success: true, id });
|
|
1085
1390
|
}
|