digitaltwin-core 0.13.3 → 0.14.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/package.json +101 -106
- package/dist/auth/apisix_parser.d.ts +0 -146
- package/dist/auth/apisix_parser.d.ts.map +0 -1
- package/dist/auth/apisix_parser.js +0 -185
- package/dist/auth/apisix_parser.js.map +0 -1
- package/dist/auth/auth_config.d.ts +0 -126
- package/dist/auth/auth_config.d.ts.map +0 -1
- package/dist/auth/auth_config.js +0 -169
- package/dist/auth/auth_config.js.map +0 -1
- package/dist/auth/index.d.ts +0 -5
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js +0 -4
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/types.d.ts +0 -100
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/auth/types.js +0 -2
- package/dist/auth/types.js.map +0 -1
- package/dist/auth/user_service.d.ts +0 -86
- package/dist/auth/user_service.d.ts.map +0 -1
- package/dist/auth/user_service.js +0 -237
- package/dist/auth/user_service.js.map +0 -1
- package/dist/components/assets_manager.d.ts +0 -662
- package/dist/components/assets_manager.d.ts.map +0 -1
- package/dist/components/assets_manager.js +0 -1529
- package/dist/components/assets_manager.js.map +0 -1
- package/dist/components/async_upload.d.ts +0 -20
- package/dist/components/async_upload.d.ts.map +0 -1
- package/dist/components/async_upload.js +0 -10
- package/dist/components/async_upload.js.map +0 -1
- package/dist/components/collector.d.ts +0 -203
- package/dist/components/collector.d.ts.map +0 -1
- package/dist/components/collector.js +0 -202
- package/dist/components/collector.js.map +0 -1
- package/dist/components/custom_table_manager.d.ts +0 -503
- package/dist/components/custom_table_manager.d.ts.map +0 -1
- package/dist/components/custom_table_manager.js +0 -1052
- package/dist/components/custom_table_manager.js.map +0 -1
- package/dist/components/global_assets_handler.d.ts +0 -63
- package/dist/components/global_assets_handler.d.ts.map +0 -1
- package/dist/components/global_assets_handler.js +0 -127
- package/dist/components/global_assets_handler.js.map +0 -1
- package/dist/components/handler.d.ts +0 -104
- package/dist/components/handler.d.ts.map +0 -1
- package/dist/components/handler.js +0 -110
- package/dist/components/handler.js.map +0 -1
- package/dist/components/harvester.d.ts +0 -182
- package/dist/components/harvester.d.ts.map +0 -1
- package/dist/components/harvester.js +0 -393
- package/dist/components/harvester.js.map +0 -1
- package/dist/components/index.d.ts +0 -11
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/index.js +0 -9
- package/dist/components/index.js.map +0 -1
- package/dist/components/interfaces.d.ts +0 -126
- package/dist/components/interfaces.d.ts.map +0 -1
- package/dist/components/interfaces.js +0 -8
- package/dist/components/interfaces.js.map +0 -1
- package/dist/components/map_manager.d.ts +0 -61
- package/dist/components/map_manager.d.ts.map +0 -1
- package/dist/components/map_manager.js +0 -242
- package/dist/components/map_manager.js.map +0 -1
- package/dist/components/tileset_manager.d.ts +0 -125
- package/dist/components/tileset_manager.d.ts.map +0 -1
- package/dist/components/tileset_manager.js +0 -618
- package/dist/components/tileset_manager.js.map +0 -1
- package/dist/components/types.d.ts +0 -226
- package/dist/components/types.d.ts.map +0 -1
- package/dist/components/types.js +0 -8
- package/dist/components/types.js.map +0 -1
- package/dist/database/adapters/knex_database_adapter.d.ts +0 -92
- package/dist/database/adapters/knex_database_adapter.d.ts.map +0 -1
- package/dist/database/adapters/knex_database_adapter.js +0 -647
- package/dist/database/adapters/knex_database_adapter.js.map +0 -1
- package/dist/database/database_adapter.d.ts +0 -251
- package/dist/database/database_adapter.d.ts.map +0 -1
- package/dist/database/database_adapter.js +0 -46
- package/dist/database/database_adapter.js.map +0 -1
- package/dist/engine/digital_twin_engine.d.ts +0 -253
- package/dist/engine/digital_twin_engine.d.ts.map +0 -1
- package/dist/engine/digital_twin_engine.js +0 -790
- package/dist/engine/digital_twin_engine.js.map +0 -1
- package/dist/engine/endpoints.d.ts +0 -47
- package/dist/engine/endpoints.d.ts.map +0 -1
- package/dist/engine/endpoints.js +0 -56
- package/dist/engine/endpoints.js.map +0 -1
- package/dist/engine/events.d.ts +0 -93
- package/dist/engine/events.d.ts.map +0 -1
- package/dist/engine/events.js +0 -71
- package/dist/engine/events.js.map +0 -1
- package/dist/engine/initializer.d.ts +0 -62
- package/dist/engine/initializer.d.ts.map +0 -1
- package/dist/engine/initializer.js +0 -108
- package/dist/engine/initializer.js.map +0 -1
- package/dist/engine/queue_manager.d.ts +0 -87
- package/dist/engine/queue_manager.d.ts.map +0 -1
- package/dist/engine/queue_manager.js +0 -196
- package/dist/engine/queue_manager.js.map +0 -1
- package/dist/engine/scheduler.d.ts +0 -30
- package/dist/engine/scheduler.d.ts.map +0 -1
- package/dist/engine/scheduler.js +0 -370
- package/dist/engine/scheduler.js.map +0 -1
- package/dist/engine/upload_processor.d.ts +0 -36
- package/dist/engine/upload_processor.d.ts.map +0 -1
- package/dist/engine/upload_processor.js +0 -101
- package/dist/engine/upload_processor.js.map +0 -1
- package/dist/env/env.d.ts +0 -134
- package/dist/env/env.d.ts.map +0 -1
- package/dist/env/env.js +0 -177
- package/dist/env/env.js.map +0 -1
- package/dist/index.d.ts +0 -49
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -57
- package/dist/index.js.map +0 -1
- package/dist/openapi/generator.d.ts +0 -93
- package/dist/openapi/generator.d.ts.map +0 -1
- package/dist/openapi/generator.js +0 -293
- package/dist/openapi/generator.js.map +0 -1
- package/dist/openapi/index.d.ts +0 -9
- package/dist/openapi/index.d.ts.map +0 -1
- package/dist/openapi/index.js +0 -9
- package/dist/openapi/index.js.map +0 -1
- package/dist/openapi/types.d.ts +0 -182
- package/dist/openapi/types.d.ts.map +0 -1
- package/dist/openapi/types.js +0 -16
- package/dist/openapi/types.js.map +0 -1
- package/dist/storage/adapters/local_storage_service.d.ts +0 -51
- package/dist/storage/adapters/local_storage_service.d.ts.map +0 -1
- package/dist/storage/adapters/local_storage_service.js +0 -110
- package/dist/storage/adapters/local_storage_service.js.map +0 -1
- package/dist/storage/adapters/ovh_storage_service.d.ts +0 -61
- package/dist/storage/adapters/ovh_storage_service.d.ts.map +0 -1
- package/dist/storage/adapters/ovh_storage_service.js +0 -172
- package/dist/storage/adapters/ovh_storage_service.js.map +0 -1
- package/dist/storage/storage_factory.d.ts +0 -14
- package/dist/storage/storage_factory.d.ts.map +0 -1
- package/dist/storage/storage_factory.js +0 -36
- package/dist/storage/storage_factory.js.map +0 -1
- package/dist/storage/storage_service.d.ts +0 -163
- package/dist/storage/storage_service.d.ts.map +0 -1
- package/dist/storage/storage_service.js +0 -54
- package/dist/storage/storage_service.js.map +0 -1
- package/dist/types/data_record.d.ts +0 -123
- package/dist/types/data_record.d.ts.map +0 -1
- package/dist/types/data_record.js +0 -8
- package/dist/types/data_record.js.map +0 -1
- package/dist/utils/http_responses.d.ts +0 -155
- package/dist/utils/http_responses.d.ts.map +0 -1
- package/dist/utils/http_responses.js +0 -190
- package/dist/utils/http_responses.js.map +0 -1
- package/dist/utils/index.d.ts +0 -8
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -6
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -74
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -92
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/map_to_data_record.d.ts +0 -10
- package/dist/utils/map_to_data_record.d.ts.map +0 -1
- package/dist/utils/map_to_data_record.js +0 -36
- package/dist/utils/map_to_data_record.js.map +0 -1
- package/dist/utils/servable_endpoint.d.ts +0 -63
- package/dist/utils/servable_endpoint.d.ts.map +0 -1
- package/dist/utils/servable_endpoint.js +0 -67
- package/dist/utils/servable_endpoint.js.map +0 -1
- package/dist/utils/zip_utils.d.ts +0 -66
- package/dist/utils/zip_utils.d.ts.map +0 -1
- package/dist/utils/zip_utils.js +0 -169
- package/dist/utils/zip_utils.js.map +0 -1
|
@@ -1,1529 +0,0 @@
|
|
|
1
|
-
import { ApisixAuthParser } from '../auth/apisix_parser.js';
|
|
2
|
-
import { UserService } from '../auth/user_service.js';
|
|
3
|
-
import { AuthConfig } from '../auth/auth_config.js';
|
|
4
|
-
import { successResponse, errorResponse, badRequestResponse, unauthorizedResponse, forbiddenResponse, notFoundResponse, textResponse, fileResponse, multiStatusResponse, HttpStatus } from '../utils/http_responses.js';
|
|
5
|
-
import fs from 'fs/promises';
|
|
6
|
-
/**
|
|
7
|
-
* Abstract base class for Assets Manager components with authentication and access control.
|
|
8
|
-
*
|
|
9
|
-
* Provides secure file upload, storage, and retrieval capabilities following the Digital Twin framework patterns.
|
|
10
|
-
* Each concrete implementation manages a specific type of asset and creates its own database table.
|
|
11
|
-
*
|
|
12
|
-
* ## Authentication & Authorization
|
|
13
|
-
*
|
|
14
|
-
* - **Write Operations** (POST, PUT, DELETE): Require authentication via Apache APISIX headers
|
|
15
|
-
* - **User Management**: Automatically creates/updates user records from Keycloak data
|
|
16
|
-
* - **Access Control**: Users can only modify/delete their own assets (ownership-based)
|
|
17
|
-
* - **Resource Linking**: Assets are automatically linked to their owners via user_id foreign key
|
|
18
|
-
*
|
|
19
|
-
* ## Required Headers for Authenticated Endpoints
|
|
20
|
-
*
|
|
21
|
-
* - `x-user-id`: Keycloak user UUID (required)
|
|
22
|
-
* - `x-user-roles`: Comma-separated list of user roles (optional)
|
|
23
|
-
*
|
|
24
|
-
* These headers are automatically added by Apache APISIX after successful Keycloak authentication.
|
|
25
|
-
*
|
|
26
|
-
* @abstract
|
|
27
|
-
* @class AssetsManager
|
|
28
|
-
* @implements {Component}
|
|
29
|
-
* @implements {Servable}
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* ```typescript
|
|
33
|
-
* // Create concrete implementations for different asset types
|
|
34
|
-
* class GLTFAssetsManager extends AssetsManager {
|
|
35
|
-
* getConfiguration() {
|
|
36
|
-
* return { name: 'gltf', description: 'GLTF 3D models manager', ... }
|
|
37
|
-
* }
|
|
38
|
-
* }
|
|
39
|
-
*
|
|
40
|
-
* class PointCloudAssetsManager extends AssetsManager {
|
|
41
|
-
* getConfiguration() {
|
|
42
|
-
* return { name: 'pointcloud', description: 'Point cloud data manager', ... }
|
|
43
|
-
* }
|
|
44
|
-
* }
|
|
45
|
-
*
|
|
46
|
-
* // Usage in engine
|
|
47
|
-
* const gltfManager = new GLTFAssetsManager()
|
|
48
|
-
* gltfManager.setDependencies(database, storage)
|
|
49
|
-
*
|
|
50
|
-
* // Each creates its own table and endpoints:
|
|
51
|
-
* // - GLTFAssetsManager → table 'gltf', endpoints /gltf/*
|
|
52
|
-
* // - PointCloudAssetsManager → table 'pointcloud', endpoints /pointcloud/*
|
|
53
|
-
* ```
|
|
54
|
-
*
|
|
55
|
-
* @remarks
|
|
56
|
-
* Asset metadata is stored as dedicated columns in the database table:
|
|
57
|
-
* - id, name, url, date (standard columns)
|
|
58
|
-
* - description, source, owner_id, filename (asset-specific columns)
|
|
59
|
-
*
|
|
60
|
-
* Each concrete AssetsManager creates its own table based on the configuration name.
|
|
61
|
-
*/
|
|
62
|
-
export class AssetsManager {
|
|
63
|
-
/**
|
|
64
|
-
* Injects dependencies into the assets manager.
|
|
65
|
-
*
|
|
66
|
-
* Called by the framework during component initialization.
|
|
67
|
-
*
|
|
68
|
-
* @param {DatabaseAdapter} db - The database adapter for metadata storage
|
|
69
|
-
* @param {StorageService} storage - The storage service for file persistence
|
|
70
|
-
* @param {UserService} [userService] - Optional user service for authentication (created automatically if not provided)
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```typescript
|
|
74
|
-
* // Standard usage (UserService created automatically)
|
|
75
|
-
* const assetsManager = new MyAssetsManager()
|
|
76
|
-
* assetsManager.setDependencies(databaseAdapter, storageService)
|
|
77
|
-
*
|
|
78
|
-
* // For testing (inject mock UserService)
|
|
79
|
-
* const mockUserService = new MockUserService()
|
|
80
|
-
* assetsManager.setDependencies(databaseAdapter, storageService, mockUserService)
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
|
-
setDependencies(db, storage, userService) {
|
|
84
|
-
this.db = db;
|
|
85
|
-
this.storage = storage;
|
|
86
|
-
this.userService = userService ?? new UserService(db);
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Validates that a source string is a valid URL.
|
|
90
|
-
*
|
|
91
|
-
* Used internally to ensure data provenance URLs are properly formatted.
|
|
92
|
-
*
|
|
93
|
-
* @private
|
|
94
|
-
* @param {string} source - The source URL to validate
|
|
95
|
-
* @returns {boolean} True if the source is a valid URL, false otherwise
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* ```typescript
|
|
99
|
-
* this.validateSourceURL('https://example.com/data') // returns true
|
|
100
|
-
* this.validateSourceURL('not-a-url') // returns false
|
|
101
|
-
* ```
|
|
102
|
-
*/
|
|
103
|
-
validateSourceURL(source) {
|
|
104
|
-
try {
|
|
105
|
-
new URL(source);
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Validates that a filename has the correct extension as configured.
|
|
114
|
-
*
|
|
115
|
-
* Used internally to ensure uploaded files match the expected extension.
|
|
116
|
-
*
|
|
117
|
-
* @private
|
|
118
|
-
* @param {string} filename - The filename to validate
|
|
119
|
-
* @returns {boolean} True if the filename has the correct extension or no extension is configured, false otherwise
|
|
120
|
-
*
|
|
121
|
-
* @example
|
|
122
|
-
* ```typescript
|
|
123
|
-
* // If config.extension = '.glb'
|
|
124
|
-
* this.validateFileExtension('model.glb') // returns true
|
|
125
|
-
* this.validateFileExtension('model.json') // returns false
|
|
126
|
-
* this.validateFileExtension('model') // returns false
|
|
127
|
-
*
|
|
128
|
-
* // If config.extension is undefined
|
|
129
|
-
* this.validateFileExtension('any-file.ext') // returns true
|
|
130
|
-
* ```
|
|
131
|
-
*/
|
|
132
|
-
validateFileExtension(filename) {
|
|
133
|
-
const config = this.getConfiguration();
|
|
134
|
-
// If no extension is configured, allow any file
|
|
135
|
-
if (!config.extension) {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
// Ensure the configured extension starts with a dot
|
|
139
|
-
const requiredExtension = config.extension.startsWith('.') ? config.extension : `.${config.extension}`;
|
|
140
|
-
// Check if the filename ends with the required extension (case-insensitive)
|
|
141
|
-
return filename.toLowerCase().endsWith(requiredExtension.toLowerCase());
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Validates that a string is valid base64-encoded data.
|
|
145
|
-
*
|
|
146
|
-
* Used internally to ensure file data in batch uploads is properly base64-encoded
|
|
147
|
-
* before attempting to decode it.
|
|
148
|
-
*
|
|
149
|
-
* @private
|
|
150
|
-
* @param {any} data - Data to validate as base64
|
|
151
|
-
* @returns {boolean} True if data is a valid base64 string, false otherwise
|
|
152
|
-
*
|
|
153
|
-
* @example
|
|
154
|
-
* ```typescript
|
|
155
|
-
* this.validateBase64('SGVsbG8gV29ybGQ=') // returns true
|
|
156
|
-
* this.validateBase64('not-base64!@#') // returns false
|
|
157
|
-
* this.validateBase64(123) // returns false (not a string)
|
|
158
|
-
* this.validateBase64('') // returns false (empty string)
|
|
159
|
-
* ```
|
|
160
|
-
*/
|
|
161
|
-
validateBase64(data) {
|
|
162
|
-
// Must be a non-empty string
|
|
163
|
-
if (typeof data !== 'string' || data.length === 0) {
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
// Base64 regex: only A-Z, a-z, 0-9, +, /, and = for padding
|
|
167
|
-
// Padding (=) can only appear at the end, max 2 times
|
|
168
|
-
const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
169
|
-
const trimmed = data.trim();
|
|
170
|
-
// Must match regex
|
|
171
|
-
if (!base64Regex.test(trimmed)) {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
// Length must be multiple of 4
|
|
175
|
-
if (trimmed.length % 4 !== 0) {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
// Try to decode to verify it's valid base64
|
|
179
|
-
try {
|
|
180
|
-
const decoded = Buffer.from(trimmed, 'base64').toString('base64');
|
|
181
|
-
// Re-encode and compare to ensure no data loss (valid base64)
|
|
182
|
-
return decoded === trimmed;
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
// ============================================================================
|
|
189
|
-
// Authentication & Request Processing Helpers
|
|
190
|
-
// ============================================================================
|
|
191
|
-
/**
|
|
192
|
-
* Authenticates a request and returns the user record.
|
|
193
|
-
*
|
|
194
|
-
* This method consolidates the authentication flow:
|
|
195
|
-
* 1. Validates APISIX headers are present
|
|
196
|
-
* 2. Parses authentication headers
|
|
197
|
-
* 3. Finds or creates user record in database
|
|
198
|
-
*
|
|
199
|
-
* @param req - HTTP request object
|
|
200
|
-
* @returns AuthResult with either userRecord on success or DataResponse on failure
|
|
201
|
-
*
|
|
202
|
-
* @example
|
|
203
|
-
* ```typescript
|
|
204
|
-
* const authResult = await this.authenticateRequest(req)
|
|
205
|
-
* if (!authResult.success) {
|
|
206
|
-
* return authResult.response
|
|
207
|
-
* }
|
|
208
|
-
* const userRecord = authResult.userRecord
|
|
209
|
-
* ```
|
|
210
|
-
*/
|
|
211
|
-
async authenticateRequest(req) {
|
|
212
|
-
// If auth is disabled, create an anonymous user
|
|
213
|
-
if (AuthConfig.isAuthDisabled()) {
|
|
214
|
-
const anonymousUser = {
|
|
215
|
-
id: AuthConfig.getAnonymousUserId(),
|
|
216
|
-
roles: []
|
|
217
|
-
};
|
|
218
|
-
const userRecord = await this.userService.findOrCreateUser(anonymousUser);
|
|
219
|
-
return { success: true, userRecord };
|
|
220
|
-
}
|
|
221
|
-
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
222
|
-
return { success: false, response: unauthorizedResponse() };
|
|
223
|
-
}
|
|
224
|
-
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
225
|
-
if (!authUser) {
|
|
226
|
-
return { success: false, response: unauthorizedResponse('Invalid authentication headers') };
|
|
227
|
-
}
|
|
228
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
229
|
-
if (!userRecord.id) {
|
|
230
|
-
return { success: false, response: errorResponse('Failed to retrieve user information') };
|
|
231
|
-
}
|
|
232
|
-
return { success: true, userRecord };
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Extracts upload data from multipart form request.
|
|
236
|
-
*
|
|
237
|
-
* @param req - HTTP request object with body and file
|
|
238
|
-
* @returns UploadData object with extracted fields
|
|
239
|
-
*/
|
|
240
|
-
extractUploadData(req) {
|
|
241
|
-
return {
|
|
242
|
-
description: req.body?.description,
|
|
243
|
-
source: req.body?.source,
|
|
244
|
-
is_public: req.body?.is_public,
|
|
245
|
-
filePath: req.file?.path,
|
|
246
|
-
fileBuffer: req.file?.buffer,
|
|
247
|
-
filename: req.file?.originalname || req.body?.filename
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Validates required fields for asset upload and returns validated data.
|
|
252
|
-
*
|
|
253
|
-
* @param data - Upload data to validate
|
|
254
|
-
* @returns UploadValidationResult with validated data on success or error response on failure
|
|
255
|
-
*/
|
|
256
|
-
validateUploadFields(data) {
|
|
257
|
-
const hasFile = data.filePath || data.fileBuffer;
|
|
258
|
-
if (!hasFile || !data.description || !data.source) {
|
|
259
|
-
return {
|
|
260
|
-
success: false,
|
|
261
|
-
response: badRequestResponse('Missing required fields: description, source, file')
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
if (!data.filename) {
|
|
265
|
-
return {
|
|
266
|
-
success: false,
|
|
267
|
-
response: badRequestResponse('Filename could not be determined from uploaded file')
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
if (!this.validateFileExtension(data.filename)) {
|
|
271
|
-
const config = this.getConfiguration();
|
|
272
|
-
return {
|
|
273
|
-
success: false,
|
|
274
|
-
response: badRequestResponse(`Invalid file extension. Expected: ${config.extension}`)
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
return {
|
|
278
|
-
success: true,
|
|
279
|
-
data: {
|
|
280
|
-
description: data.description,
|
|
281
|
-
source: data.source,
|
|
282
|
-
filePath: data.filePath,
|
|
283
|
-
fileBuffer: data.fileBuffer,
|
|
284
|
-
filename: data.filename,
|
|
285
|
-
is_public: data.is_public !== undefined ? Boolean(data.is_public) : true
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Reads file content from temporary upload path.
|
|
291
|
-
*
|
|
292
|
-
* @param filePath - Path to temporary file
|
|
293
|
-
* @returns Buffer with file content
|
|
294
|
-
* @throws Error if file cannot be read
|
|
295
|
-
*/
|
|
296
|
-
async readTempFile(filePath) {
|
|
297
|
-
return fs.readFile(filePath);
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Cleans up temporary file after processing.
|
|
301
|
-
* Silently ignores cleanup errors.
|
|
302
|
-
*
|
|
303
|
-
* @param filePath - Path to temporary file
|
|
304
|
-
*/
|
|
305
|
-
async cleanupTempFile(filePath) {
|
|
306
|
-
await fs.unlink(filePath).catch(() => {
|
|
307
|
-
// Ignore cleanup errors
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Validates ownership of an asset.
|
|
312
|
-
*
|
|
313
|
-
* Admins can modify any asset. Regular users can only modify their own assets
|
|
314
|
-
* or assets with no owner (owner_id = null).
|
|
315
|
-
*
|
|
316
|
-
* @param asset - Asset record to check
|
|
317
|
-
* @param userId - User ID to validate against
|
|
318
|
-
* @param headers - HTTP request headers (optional, for admin check)
|
|
319
|
-
* @returns DataResponse with error if not owner/admin, undefined if valid
|
|
320
|
-
*/
|
|
321
|
-
validateOwnership(asset, userId, headers) {
|
|
322
|
-
// Admins can modify any asset
|
|
323
|
-
if (headers && ApisixAuthParser.isAdmin(headers)) {
|
|
324
|
-
return undefined;
|
|
325
|
-
}
|
|
326
|
-
// Assets with no owner (null) can be modified by anyone
|
|
327
|
-
if (asset.owner_id === null) {
|
|
328
|
-
return undefined;
|
|
329
|
-
}
|
|
330
|
-
if (asset.owner_id !== userId) {
|
|
331
|
-
return forbiddenResponse('You can only modify your own assets');
|
|
332
|
-
}
|
|
333
|
-
return undefined;
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Checks if a user can access a private asset.
|
|
337
|
-
*
|
|
338
|
-
* @param asset - Asset record to check
|
|
339
|
-
* @param req - HTTP request for authentication context
|
|
340
|
-
* @returns DataResponse with error if access denied, undefined if allowed
|
|
341
|
-
*/
|
|
342
|
-
async checkPrivateAssetAccess(asset, req) {
|
|
343
|
-
// Public assets are always accessible
|
|
344
|
-
if (asset.is_public) {
|
|
345
|
-
return undefined;
|
|
346
|
-
}
|
|
347
|
-
// Admins can access everything
|
|
348
|
-
if (ApisixAuthParser.isAdmin(req.headers || {})) {
|
|
349
|
-
return undefined;
|
|
350
|
-
}
|
|
351
|
-
// Private asset - require authentication
|
|
352
|
-
if (!ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
353
|
-
return unauthorizedResponse('Authentication required for private assets');
|
|
354
|
-
}
|
|
355
|
-
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
356
|
-
if (!authUser) {
|
|
357
|
-
return unauthorizedResponse('Invalid authentication headers');
|
|
358
|
-
}
|
|
359
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
360
|
-
if (!userRecord.id || asset.owner_id !== userRecord.id) {
|
|
361
|
-
return forbiddenResponse('This asset is private');
|
|
362
|
-
}
|
|
363
|
-
return undefined;
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* Fetches an asset by ID with full access control validation.
|
|
367
|
-
*
|
|
368
|
-
* This method consolidates the common logic for retrieving an asset:
|
|
369
|
-
* 1. Validates that ID is provided
|
|
370
|
-
* 2. Fetches the asset from database
|
|
371
|
-
* 3. Verifies the asset belongs to this component
|
|
372
|
-
* 4. Checks access permissions for private assets
|
|
373
|
-
*
|
|
374
|
-
* @param req - HTTP request with params.id
|
|
375
|
-
* @returns Object with asset on success, or DataResponse on failure
|
|
376
|
-
*/
|
|
377
|
-
async fetchAssetWithAccessCheck(req) {
|
|
378
|
-
const { id } = req.params || {};
|
|
379
|
-
if (!id) {
|
|
380
|
-
return { success: false, response: badRequestResponse('Asset ID is required') };
|
|
381
|
-
}
|
|
382
|
-
const asset = await this.getAssetById(id);
|
|
383
|
-
if (!asset) {
|
|
384
|
-
return { success: false, response: textResponse(HttpStatus.NOT_FOUND, 'Asset not found') };
|
|
385
|
-
}
|
|
386
|
-
// Verify this asset belongs to our component
|
|
387
|
-
const config = this.getConfiguration();
|
|
388
|
-
if (asset.name !== config.name) {
|
|
389
|
-
return { success: false, response: textResponse(HttpStatus.NOT_FOUND, 'Asset not found') };
|
|
390
|
-
}
|
|
391
|
-
// Check access permissions for private assets
|
|
392
|
-
const accessError = await this.checkPrivateAssetAccess(asset, req);
|
|
393
|
-
if (accessError) {
|
|
394
|
-
return { success: false, response: accessError };
|
|
395
|
-
}
|
|
396
|
-
return { success: true, asset };
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Upload a new asset file with metadata.
|
|
400
|
-
*
|
|
401
|
-
* Stores the file using the storage service and saves metadata to the database.
|
|
402
|
-
* Asset metadata is stored as dedicated columns in the database table.
|
|
403
|
-
*
|
|
404
|
-
* @param {CreateAssetRequest} request - The asset upload request
|
|
405
|
-
* @throws {Error} If source URL is invalid
|
|
406
|
-
*
|
|
407
|
-
* @example
|
|
408
|
-
* ```typescript
|
|
409
|
-
* await assetsManager.uploadAsset({
|
|
410
|
-
* description: '3D building model',
|
|
411
|
-
* source: 'https://city-data.example.com/buildings',
|
|
412
|
-
* owner_id: 'user123',
|
|
413
|
-
* filename: 'building.glb',
|
|
414
|
-
* file: fileBuffer,
|
|
415
|
-
* is_public: true
|
|
416
|
-
* })
|
|
417
|
-
* ```
|
|
418
|
-
*/
|
|
419
|
-
async uploadAsset(request) {
|
|
420
|
-
if (!this.validateSourceURL(request.source)) {
|
|
421
|
-
throw new Error('Invalid source URL');
|
|
422
|
-
}
|
|
423
|
-
if (!this.validateFileExtension(request.filename)) {
|
|
424
|
-
const config = this.getConfiguration();
|
|
425
|
-
throw new Error(`Invalid file extension. Expected: ${config.extension}`);
|
|
426
|
-
}
|
|
427
|
-
const config = this.getConfiguration();
|
|
428
|
-
const now = new Date();
|
|
429
|
-
// Store file using framework pattern
|
|
430
|
-
const url = await this.storage.save(request.file, config.name, request.filename);
|
|
431
|
-
// Create metadata with all asset-specific fields
|
|
432
|
-
const metadata = {
|
|
433
|
-
name: config.name,
|
|
434
|
-
type: config.contentType,
|
|
435
|
-
url,
|
|
436
|
-
date: now,
|
|
437
|
-
description: request.description,
|
|
438
|
-
source: request.source,
|
|
439
|
-
owner_id: request.owner_id,
|
|
440
|
-
filename: request.filename,
|
|
441
|
-
is_public: request.is_public ?? true // Default to public if not specified
|
|
442
|
-
};
|
|
443
|
-
await this.db.save(metadata);
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Retrieve all assets for this component (like other components).
|
|
447
|
-
*
|
|
448
|
-
* Returns a JSON list of all assets with their metadata, following the
|
|
449
|
-
* framework pattern but adapted for assets management.
|
|
450
|
-
*
|
|
451
|
-
* Access control:
|
|
452
|
-
* - Unauthenticated users: Can only see public assets
|
|
453
|
-
* - Authenticated users: Can see public assets + their own private assets
|
|
454
|
-
* - Admin users: Can see all assets (public and private from all users)
|
|
455
|
-
*
|
|
456
|
-
* @returns {Promise<DataResponse>} JSON response with all assets
|
|
457
|
-
*/
|
|
458
|
-
async retrieve(req) {
|
|
459
|
-
try {
|
|
460
|
-
const assets = await this.getAllAssets();
|
|
461
|
-
const isAdmin = req && ApisixAuthParser.isAdmin(req.headers || {});
|
|
462
|
-
// Admin can see everything
|
|
463
|
-
if (isAdmin) {
|
|
464
|
-
return successResponse(this.formatAssetsForResponse(assets));
|
|
465
|
-
}
|
|
466
|
-
// Get authenticated user ID if available
|
|
467
|
-
const authenticatedUserId = await this.getAuthenticatedUserId(req);
|
|
468
|
-
// Filter to visible assets only
|
|
469
|
-
const visibleAssets = assets.filter(asset => asset.is_public || (authenticatedUserId !== null && asset.owner_id === authenticatedUserId));
|
|
470
|
-
return successResponse(this.formatAssetsForResponse(visibleAssets));
|
|
471
|
-
}
|
|
472
|
-
catch (error) {
|
|
473
|
-
return errorResponse(error);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Gets the authenticated user's database ID from request headers.
|
|
478
|
-
*
|
|
479
|
-
* @param req - HTTP request object
|
|
480
|
-
* @returns User ID or null if not authenticated
|
|
481
|
-
*/
|
|
482
|
-
async getAuthenticatedUserId(req) {
|
|
483
|
-
if (!req || !ApisixAuthParser.hasValidAuth(req.headers || {})) {
|
|
484
|
-
return null;
|
|
485
|
-
}
|
|
486
|
-
const authUser = ApisixAuthParser.parseAuthHeaders(req.headers || {});
|
|
487
|
-
if (!authUser) {
|
|
488
|
-
return null;
|
|
489
|
-
}
|
|
490
|
-
const userRecord = await this.userService.findOrCreateUser(authUser);
|
|
491
|
-
return userRecord.id || null;
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* Formats assets for API response with metadata and URLs.
|
|
495
|
-
*
|
|
496
|
-
* @param assets - Array of asset records
|
|
497
|
-
* @returns Formatted assets array ready for JSON serialization
|
|
498
|
-
*/
|
|
499
|
-
formatAssetsForResponse(assets) {
|
|
500
|
-
const config = this.getConfiguration();
|
|
501
|
-
return assets.map(asset => ({
|
|
502
|
-
id: asset.id,
|
|
503
|
-
name: asset.name,
|
|
504
|
-
date: asset.date,
|
|
505
|
-
contentType: asset.contentType,
|
|
506
|
-
description: asset.description || '',
|
|
507
|
-
source: asset.source || '',
|
|
508
|
-
owner_id: asset.owner_id || null,
|
|
509
|
-
filename: asset.filename || '',
|
|
510
|
-
is_public: asset.is_public ?? true,
|
|
511
|
-
url: `/${config.endpoint}/${asset.id}`,
|
|
512
|
-
download_url: `/${config.endpoint}/${asset.id}/download`
|
|
513
|
-
}));
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* Get all assets for this component type.
|
|
517
|
-
*
|
|
518
|
-
* Retrieves all assets managed by this component, with their metadata.
|
|
519
|
-
* Uses a very old start date to get all records.
|
|
520
|
-
*
|
|
521
|
-
* @returns {Promise<DataRecord[]>} Array of all asset records
|
|
522
|
-
*
|
|
523
|
-
* @example
|
|
524
|
-
* ```typescript
|
|
525
|
-
* const allAssets = await assetsManager.getAllAssets()
|
|
526
|
-
* // Returns: [{ id, name, type, url, date, contentType }, ...]
|
|
527
|
-
* ```
|
|
528
|
-
*/
|
|
529
|
-
async getAllAssets() {
|
|
530
|
-
const config = this.getConfiguration();
|
|
531
|
-
// Get all assets and sort by date descending (newest first)
|
|
532
|
-
const veryOldDate = new Date('1970-01-01');
|
|
533
|
-
const farFutureDate = new Date('2099-12-31');
|
|
534
|
-
const assets = await this.db.getByDateRange(config.name, veryOldDate, farFutureDate, 1000); // Max 1000 assets
|
|
535
|
-
// Sort by date descending (newest first) since getByDateRange returns ascending
|
|
536
|
-
return assets.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
537
|
-
}
|
|
538
|
-
/**
|
|
539
|
-
* Get asset by specific ID.
|
|
540
|
-
*
|
|
541
|
-
* @param {string} id - The asset ID to retrieve
|
|
542
|
-
* @returns {Promise<DataRecord | undefined>} The asset record or undefined if not found
|
|
543
|
-
*
|
|
544
|
-
* @example
|
|
545
|
-
* ```typescript
|
|
546
|
-
* const asset = await assetsManager.getAssetById('123')
|
|
547
|
-
* if (asset) {
|
|
548
|
-
* const fileData = await asset.data()
|
|
549
|
-
* }
|
|
550
|
-
* ```
|
|
551
|
-
*/
|
|
552
|
-
async getAssetById(id) {
|
|
553
|
-
return await this.db.getById(id, this.getConfiguration().name);
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* Update asset metadata by ID.
|
|
557
|
-
*
|
|
558
|
-
* Updates the metadata (description, source, and/or visibility) of a specific asset.
|
|
559
|
-
* Asset metadata is stored as dedicated columns in the database.
|
|
560
|
-
*
|
|
561
|
-
* @param {string} id - The ID of the asset to update
|
|
562
|
-
* @param {UpdateAssetRequest} updates - The metadata updates to apply
|
|
563
|
-
* @throws {Error} If source URL is invalid or asset not found
|
|
564
|
-
*
|
|
565
|
-
* @example
|
|
566
|
-
* ```typescript
|
|
567
|
-
* await assetsManager.updateAssetMetadata('123', {
|
|
568
|
-
* description: 'Updated building model with new textures',
|
|
569
|
-
* source: 'https://updated-source.example.com',
|
|
570
|
-
* is_public: false
|
|
571
|
-
* })
|
|
572
|
-
* ```
|
|
573
|
-
*/
|
|
574
|
-
async updateAssetMetadata(id, updates) {
|
|
575
|
-
if (updates.source && !this.validateSourceURL(updates.source)) {
|
|
576
|
-
throw new Error('Invalid source URL');
|
|
577
|
-
}
|
|
578
|
-
const record = await this.db.getById(id, this.getConfiguration().name);
|
|
579
|
-
if (!record) {
|
|
580
|
-
throw new Error(`Asset with ID ${id} not found`);
|
|
581
|
-
}
|
|
582
|
-
// Verify this asset belongs to our component
|
|
583
|
-
const config = this.getConfiguration();
|
|
584
|
-
if (record.name !== config.name) {
|
|
585
|
-
throw new Error(`Asset ${id} does not belong to component ${config.name}`);
|
|
586
|
-
}
|
|
587
|
-
// Apply updates, keeping existing values for non-updated fields
|
|
588
|
-
const updatedMetadata = {
|
|
589
|
-
id: parseInt(id),
|
|
590
|
-
name: config.name,
|
|
591
|
-
type: record.contentType,
|
|
592
|
-
url: record.url,
|
|
593
|
-
date: record.date, // Keep original date
|
|
594
|
-
description: updates.description ?? record.description ?? '',
|
|
595
|
-
source: updates.source ?? record.source ?? '',
|
|
596
|
-
owner_id: record.owner_id ?? null,
|
|
597
|
-
filename: record.filename ?? '',
|
|
598
|
-
is_public: updates.is_public !== undefined ? updates.is_public : (record.is_public ?? true)
|
|
599
|
-
};
|
|
600
|
-
// Update the record - delete and re-create with updated metadata
|
|
601
|
-
await this.db.delete(id, this.getConfiguration().name);
|
|
602
|
-
await this.db.save(updatedMetadata);
|
|
603
|
-
}
|
|
604
|
-
/**
|
|
605
|
-
* Delete asset by ID.
|
|
606
|
-
*
|
|
607
|
-
* Removes a specific asset.
|
|
608
|
-
*
|
|
609
|
-
* @param {string} id - The ID of the asset to delete
|
|
610
|
-
* @throws {Error} If asset not found or doesn't belong to this component
|
|
611
|
-
*
|
|
612
|
-
* @example
|
|
613
|
-
* ```typescript
|
|
614
|
-
* await assetsManager.deleteAssetById('123')
|
|
615
|
-
* ```
|
|
616
|
-
*/
|
|
617
|
-
async deleteAssetById(id) {
|
|
618
|
-
const record = await this.db.getById(id, this.getConfiguration().name);
|
|
619
|
-
if (!record) {
|
|
620
|
-
throw new Error(`Asset with ID ${id} not found`);
|
|
621
|
-
}
|
|
622
|
-
// Verify this asset belongs to our component
|
|
623
|
-
const config = this.getConfiguration();
|
|
624
|
-
if (record.name !== config.name) {
|
|
625
|
-
throw new Error(`Asset ${id} does not belong to component ${config.name}`);
|
|
626
|
-
}
|
|
627
|
-
await this.db.delete(id, this.getConfiguration().name);
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Delete latest asset (simplified)
|
|
631
|
-
*
|
|
632
|
-
* Removes the most recently uploaded asset for this component type.
|
|
633
|
-
*
|
|
634
|
-
* @throws {Error} If no assets exist to delete
|
|
635
|
-
*
|
|
636
|
-
* @example
|
|
637
|
-
* ```typescript
|
|
638
|
-
* await assetsManager.deleteLatestAsset()
|
|
639
|
-
* ```
|
|
640
|
-
*/
|
|
641
|
-
async deleteLatestAsset() {
|
|
642
|
-
const config = this.getConfiguration();
|
|
643
|
-
const record = await this.db.getLatestByName(config.name);
|
|
644
|
-
if (record) {
|
|
645
|
-
await this.db.delete(record.id.toString(), this.getConfiguration().name);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* Upload multiple assets in batch for better performance
|
|
650
|
-
*
|
|
651
|
-
* @param {CreateAssetRequest[]} requests - Array of asset upload requests
|
|
652
|
-
* @throws {Error} If any source URL is invalid
|
|
653
|
-
*
|
|
654
|
-
* @example
|
|
655
|
-
* ```typescript
|
|
656
|
-
* await assetsManager.uploadAssetsBatch([
|
|
657
|
-
* { description: 'Model 1', source: 'https://example.com/1', file: buffer1, ... },
|
|
658
|
-
* { description: 'Model 2', source: 'https://example.com/2', file: buffer2, ... }
|
|
659
|
-
* ])
|
|
660
|
-
* ```
|
|
661
|
-
*/
|
|
662
|
-
async uploadAssetsBatch(requests) {
|
|
663
|
-
if (requests.length === 0)
|
|
664
|
-
return;
|
|
665
|
-
// Validate all URLs and extensions first
|
|
666
|
-
for (const request of requests) {
|
|
667
|
-
if (!this.validateSourceURL(request.source)) {
|
|
668
|
-
throw new Error(`Invalid source URL: ${request.source}`);
|
|
669
|
-
}
|
|
670
|
-
if (!this.validateFileExtension(request.filename)) {
|
|
671
|
-
const config = this.getConfiguration();
|
|
672
|
-
throw new Error(`Invalid file extension for ${request.filename}. Expected: ${config.extension}`);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
const config = this.getConfiguration();
|
|
676
|
-
const now = new Date();
|
|
677
|
-
const metadataList = [];
|
|
678
|
-
// Store files and prepare metadata
|
|
679
|
-
for (const request of requests) {
|
|
680
|
-
const url = await this.storage.save(request.file, config.name, request.filename);
|
|
681
|
-
const metadata = {
|
|
682
|
-
name: config.name,
|
|
683
|
-
type: config.contentType,
|
|
684
|
-
url,
|
|
685
|
-
date: now,
|
|
686
|
-
description: request.description,
|
|
687
|
-
source: request.source,
|
|
688
|
-
owner_id: request.owner_id,
|
|
689
|
-
filename: request.filename,
|
|
690
|
-
is_public: request.is_public ?? true
|
|
691
|
-
};
|
|
692
|
-
metadataList.push(metadata);
|
|
693
|
-
}
|
|
694
|
-
// Save all metadata individually (compatible with all adapters)
|
|
695
|
-
for (const metadata of metadataList) {
|
|
696
|
-
await this.db.save(metadata);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
/**
|
|
700
|
-
* Delete multiple assets by IDs in batch
|
|
701
|
-
*
|
|
702
|
-
* @param {string[]} ids - Array of asset IDs to delete
|
|
703
|
-
* @throws {Error} If any asset not found or doesn't belong to this component
|
|
704
|
-
*/
|
|
705
|
-
async deleteAssetsBatch(ids) {
|
|
706
|
-
if (ids.length === 0)
|
|
707
|
-
return;
|
|
708
|
-
// Delete assets individually (compatible with all adapters)
|
|
709
|
-
for (const id of ids) {
|
|
710
|
-
await this.deleteAssetById(id);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Get endpoints following the framework pattern
|
|
715
|
-
*/
|
|
716
|
-
/**
|
|
717
|
-
* Get HTTP endpoints exposed by this assets manager.
|
|
718
|
-
*
|
|
719
|
-
* Returns the standard CRUD endpoints following the framework pattern.
|
|
720
|
-
*
|
|
721
|
-
* @returns {Array} Array of endpoint descriptors with methods, paths, and handlers
|
|
722
|
-
*
|
|
723
|
-
* @example
|
|
724
|
-
* ```typescript
|
|
725
|
-
* // For a manager with assetType: 'gltf', provides:
|
|
726
|
-
* GET /gltf - Get all assets
|
|
727
|
-
* POST /gltf/upload - Upload new asset
|
|
728
|
-
* GET /gltf/123 - Get specific asset
|
|
729
|
-
* PUT /gltf/123 - Update asset metadata
|
|
730
|
-
* GET /gltf/123/download - Download asset
|
|
731
|
-
* DELETE /gltf/123 - Delete asset
|
|
732
|
-
* ```
|
|
733
|
-
*/
|
|
734
|
-
getEndpoints() {
|
|
735
|
-
const config = this.getConfiguration();
|
|
736
|
-
return [
|
|
737
|
-
{
|
|
738
|
-
method: 'get',
|
|
739
|
-
path: `/${config.endpoint}`,
|
|
740
|
-
handler: this.retrieve.bind(this),
|
|
741
|
-
responseType: 'application/json'
|
|
742
|
-
},
|
|
743
|
-
{
|
|
744
|
-
method: 'post',
|
|
745
|
-
path: `/${config.endpoint}`,
|
|
746
|
-
handler: this.handleUpload.bind(this),
|
|
747
|
-
responseType: 'application/json'
|
|
748
|
-
},
|
|
749
|
-
{
|
|
750
|
-
method: 'get',
|
|
751
|
-
path: `/${config.endpoint}/:id`,
|
|
752
|
-
handler: this.handleGetAsset.bind(this),
|
|
753
|
-
responseType: config.contentType
|
|
754
|
-
},
|
|
755
|
-
{
|
|
756
|
-
method: 'put',
|
|
757
|
-
path: `/${config.endpoint}/:id`,
|
|
758
|
-
handler: this.handleUpdate.bind(this),
|
|
759
|
-
responseType: 'application/json'
|
|
760
|
-
},
|
|
761
|
-
{
|
|
762
|
-
method: 'get',
|
|
763
|
-
path: `/${config.endpoint}/:id/download`,
|
|
764
|
-
handler: this.handleDownload.bind(this),
|
|
765
|
-
responseType: config.contentType
|
|
766
|
-
},
|
|
767
|
-
{
|
|
768
|
-
method: 'delete',
|
|
769
|
-
path: `/${config.endpoint}/:id`,
|
|
770
|
-
handler: this.handleDelete.bind(this),
|
|
771
|
-
responseType: 'application/json'
|
|
772
|
-
},
|
|
773
|
-
{
|
|
774
|
-
method: 'post',
|
|
775
|
-
path: `/${config.endpoint}/batch`,
|
|
776
|
-
handler: this.handleUploadBatch.bind(this),
|
|
777
|
-
responseType: 'application/json'
|
|
778
|
-
},
|
|
779
|
-
{
|
|
780
|
-
method: 'delete',
|
|
781
|
-
path: `/${config.endpoint}/batch`,
|
|
782
|
-
handler: this.handleDeleteBatch.bind(this),
|
|
783
|
-
responseType: 'application/json'
|
|
784
|
-
}
|
|
785
|
-
];
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* Returns the OpenAPI specification for this assets manager's endpoints.
|
|
789
|
-
*
|
|
790
|
-
* Generates documentation for all CRUD endpoints including batch operations.
|
|
791
|
-
* Can be overridden by subclasses for more detailed specifications.
|
|
792
|
-
*
|
|
793
|
-
* @returns {OpenAPIComponentSpec} OpenAPI paths, tags, and schemas for this assets manager
|
|
794
|
-
*/
|
|
795
|
-
getOpenAPISpec() {
|
|
796
|
-
const config = this.getConfiguration();
|
|
797
|
-
const basePath = `/${config.endpoint}`;
|
|
798
|
-
const tagName = config.tags?.[0] || config.name;
|
|
799
|
-
return {
|
|
800
|
-
paths: {
|
|
801
|
-
[basePath]: {
|
|
802
|
-
get: {
|
|
803
|
-
summary: `List all ${config.name} assets`,
|
|
804
|
-
description: config.description,
|
|
805
|
-
tags: [tagName],
|
|
806
|
-
responses: {
|
|
807
|
-
'200': {
|
|
808
|
-
description: 'List of assets',
|
|
809
|
-
content: {
|
|
810
|
-
'application/json': {
|
|
811
|
-
schema: {
|
|
812
|
-
type: 'array',
|
|
813
|
-
items: { $ref: '#/components/schemas/AssetResponse' }
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
},
|
|
820
|
-
post: {
|
|
821
|
-
summary: `Upload a new ${config.name} asset`,
|
|
822
|
-
description: 'Upload a new asset file with metadata. Requires authentication.',
|
|
823
|
-
tags: [tagName],
|
|
824
|
-
security: [{ ApiKeyAuth: [] }],
|
|
825
|
-
requestBody: {
|
|
826
|
-
required: true,
|
|
827
|
-
content: {
|
|
828
|
-
'multipart/form-data': {
|
|
829
|
-
schema: {
|
|
830
|
-
type: 'object',
|
|
831
|
-
required: ['file', 'description', 'source'],
|
|
832
|
-
properties: {
|
|
833
|
-
file: {
|
|
834
|
-
type: 'string',
|
|
835
|
-
format: 'binary',
|
|
836
|
-
description: 'The file to upload'
|
|
837
|
-
},
|
|
838
|
-
description: { type: 'string', description: 'Asset description' },
|
|
839
|
-
source: {
|
|
840
|
-
type: 'string',
|
|
841
|
-
format: 'uri',
|
|
842
|
-
description: 'Source URL for provenance'
|
|
843
|
-
},
|
|
844
|
-
is_public: {
|
|
845
|
-
type: 'boolean',
|
|
846
|
-
description: 'Whether asset is public (default: true)'
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
},
|
|
853
|
-
responses: {
|
|
854
|
-
'200': {
|
|
855
|
-
description: 'Asset uploaded successfully',
|
|
856
|
-
content: {
|
|
857
|
-
'application/json': {
|
|
858
|
-
schema: { $ref: '#/components/schemas/SuccessResponse' }
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
},
|
|
862
|
-
'400': { description: 'Bad request - missing or invalid fields' },
|
|
863
|
-
'401': { description: 'Unauthorized - authentication required' }
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
},
|
|
867
|
-
[`${basePath}/{id}`]: {
|
|
868
|
-
get: {
|
|
869
|
-
summary: `Get ${config.name} asset by ID`,
|
|
870
|
-
description: 'Returns the asset file content',
|
|
871
|
-
tags: [tagName],
|
|
872
|
-
parameters: [
|
|
873
|
-
{
|
|
874
|
-
name: 'id',
|
|
875
|
-
in: 'path',
|
|
876
|
-
required: true,
|
|
877
|
-
schema: { type: 'string' },
|
|
878
|
-
description: 'Asset ID'
|
|
879
|
-
}
|
|
880
|
-
],
|
|
881
|
-
responses: {
|
|
882
|
-
'200': {
|
|
883
|
-
description: 'Asset file content',
|
|
884
|
-
content: {
|
|
885
|
-
[config.contentType]: {
|
|
886
|
-
schema: { type: 'string', format: 'binary' }
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
},
|
|
890
|
-
'404': { description: 'Asset not found' }
|
|
891
|
-
}
|
|
892
|
-
},
|
|
893
|
-
put: {
|
|
894
|
-
summary: `Update ${config.name} asset metadata`,
|
|
895
|
-
description: 'Update asset description, source, or visibility. Requires authentication and ownership.',
|
|
896
|
-
tags: [tagName],
|
|
897
|
-
security: [{ ApiKeyAuth: [] }],
|
|
898
|
-
parameters: [
|
|
899
|
-
{
|
|
900
|
-
name: 'id',
|
|
901
|
-
in: 'path',
|
|
902
|
-
required: true,
|
|
903
|
-
schema: { type: 'string' },
|
|
904
|
-
description: 'Asset ID'
|
|
905
|
-
}
|
|
906
|
-
],
|
|
907
|
-
requestBody: {
|
|
908
|
-
required: true,
|
|
909
|
-
content: {
|
|
910
|
-
'application/json': {
|
|
911
|
-
schema: {
|
|
912
|
-
type: 'object',
|
|
913
|
-
properties: {
|
|
914
|
-
description: { type: 'string' },
|
|
915
|
-
source: { type: 'string', format: 'uri' },
|
|
916
|
-
is_public: { type: 'boolean' }
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
},
|
|
922
|
-
responses: {
|
|
923
|
-
'200': {
|
|
924
|
-
description: 'Asset updated successfully',
|
|
925
|
-
content: {
|
|
926
|
-
'application/json': {
|
|
927
|
-
schema: { $ref: '#/components/schemas/SuccessResponse' }
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
},
|
|
931
|
-
'400': { description: 'Bad request' },
|
|
932
|
-
'401': { description: 'Unauthorized' },
|
|
933
|
-
'403': { description: 'Forbidden - not owner' },
|
|
934
|
-
'404': { description: 'Asset not found' }
|
|
935
|
-
}
|
|
936
|
-
},
|
|
937
|
-
delete: {
|
|
938
|
-
summary: `Delete ${config.name} asset`,
|
|
939
|
-
description: 'Delete an asset. Requires authentication and ownership.',
|
|
940
|
-
tags: [tagName],
|
|
941
|
-
security: [{ ApiKeyAuth: [] }],
|
|
942
|
-
parameters: [
|
|
943
|
-
{
|
|
944
|
-
name: 'id',
|
|
945
|
-
in: 'path',
|
|
946
|
-
required: true,
|
|
947
|
-
schema: { type: 'string' },
|
|
948
|
-
description: 'Asset ID'
|
|
949
|
-
}
|
|
950
|
-
],
|
|
951
|
-
responses: {
|
|
952
|
-
'200': {
|
|
953
|
-
description: 'Asset deleted successfully',
|
|
954
|
-
content: {
|
|
955
|
-
'application/json': {
|
|
956
|
-
schema: { $ref: '#/components/schemas/SuccessResponse' }
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
},
|
|
960
|
-
'401': { description: 'Unauthorized' },
|
|
961
|
-
'403': { description: 'Forbidden - not owner' },
|
|
962
|
-
'404': { description: 'Asset not found' }
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
},
|
|
966
|
-
[`${basePath}/{id}/download`]: {
|
|
967
|
-
get: {
|
|
968
|
-
summary: `Download ${config.name} asset`,
|
|
969
|
-
description: 'Download the asset file with Content-Disposition header',
|
|
970
|
-
tags: [tagName],
|
|
971
|
-
parameters: [
|
|
972
|
-
{
|
|
973
|
-
name: 'id',
|
|
974
|
-
in: 'path',
|
|
975
|
-
required: true,
|
|
976
|
-
schema: { type: 'string' },
|
|
977
|
-
description: 'Asset ID'
|
|
978
|
-
}
|
|
979
|
-
],
|
|
980
|
-
responses: {
|
|
981
|
-
'200': {
|
|
982
|
-
description: 'Asset file download',
|
|
983
|
-
content: {
|
|
984
|
-
[config.contentType]: {
|
|
985
|
-
schema: { type: 'string', format: 'binary' }
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
},
|
|
989
|
-
'404': { description: 'Asset not found' }
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
},
|
|
993
|
-
[`${basePath}/batch`]: {
|
|
994
|
-
post: {
|
|
995
|
-
summary: `Batch upload ${config.name} assets`,
|
|
996
|
-
description: 'Upload multiple assets in one request. Files must be base64 encoded.',
|
|
997
|
-
tags: [tagName],
|
|
998
|
-
security: [{ ApiKeyAuth: [] }],
|
|
999
|
-
requestBody: {
|
|
1000
|
-
required: true,
|
|
1001
|
-
content: {
|
|
1002
|
-
'application/json': {
|
|
1003
|
-
schema: {
|
|
1004
|
-
type: 'object',
|
|
1005
|
-
required: ['requests'],
|
|
1006
|
-
properties: {
|
|
1007
|
-
requests: {
|
|
1008
|
-
type: 'array',
|
|
1009
|
-
items: {
|
|
1010
|
-
type: 'object',
|
|
1011
|
-
required: ['file', 'description', 'source', 'filename'],
|
|
1012
|
-
properties: {
|
|
1013
|
-
file: {
|
|
1014
|
-
type: 'string',
|
|
1015
|
-
format: 'byte',
|
|
1016
|
-
description: 'Base64 encoded file'
|
|
1017
|
-
},
|
|
1018
|
-
filename: { type: 'string' },
|
|
1019
|
-
description: { type: 'string' },
|
|
1020
|
-
source: { type: 'string', format: 'uri' },
|
|
1021
|
-
is_public: { type: 'boolean' }
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
},
|
|
1030
|
-
responses: {
|
|
1031
|
-
'200': { description: 'All assets uploaded successfully' },
|
|
1032
|
-
'207': { description: 'Partial success - some uploads failed' },
|
|
1033
|
-
'400': { description: 'Bad request' },
|
|
1034
|
-
'401': { description: 'Unauthorized' }
|
|
1035
|
-
}
|
|
1036
|
-
},
|
|
1037
|
-
delete: {
|
|
1038
|
-
summary: `Batch delete ${config.name} assets`,
|
|
1039
|
-
description: 'Delete multiple assets by IDs. Requires authentication and ownership. Pass IDs as comma-separated query parameter.',
|
|
1040
|
-
tags: [tagName],
|
|
1041
|
-
security: [{ ApiKeyAuth: [] }],
|
|
1042
|
-
parameters: [
|
|
1043
|
-
{
|
|
1044
|
-
name: 'ids',
|
|
1045
|
-
in: 'query',
|
|
1046
|
-
required: true,
|
|
1047
|
-
schema: {
|
|
1048
|
-
type: 'string'
|
|
1049
|
-
},
|
|
1050
|
-
description: 'Comma-separated list of asset IDs to delete (e.g., 1,2,3)'
|
|
1051
|
-
}
|
|
1052
|
-
],
|
|
1053
|
-
responses: {
|
|
1054
|
-
'200': { description: 'All assets deleted successfully' },
|
|
1055
|
-
'207': { description: 'Partial success - some deletions failed' },
|
|
1056
|
-
'400': { description: 'Bad request' },
|
|
1057
|
-
'401': { description: 'Unauthorized' }
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
},
|
|
1062
|
-
tags: [
|
|
1063
|
-
{
|
|
1064
|
-
name: tagName,
|
|
1065
|
-
description: config.description
|
|
1066
|
-
}
|
|
1067
|
-
],
|
|
1068
|
-
schemas: {
|
|
1069
|
-
AssetResponse: {
|
|
1070
|
-
type: 'object',
|
|
1071
|
-
properties: {
|
|
1072
|
-
id: { type: 'integer' },
|
|
1073
|
-
name: { type: 'string' },
|
|
1074
|
-
date: { type: 'string', format: 'date-time' },
|
|
1075
|
-
contentType: { type: 'string' },
|
|
1076
|
-
description: { type: 'string' },
|
|
1077
|
-
source: { type: 'string' },
|
|
1078
|
-
owner_id: { type: 'integer', nullable: true },
|
|
1079
|
-
filename: { type: 'string' },
|
|
1080
|
-
is_public: { type: 'boolean' },
|
|
1081
|
-
url: { type: 'string' },
|
|
1082
|
-
download_url: { type: 'string' }
|
|
1083
|
-
}
|
|
1084
|
-
},
|
|
1085
|
-
SuccessResponse: {
|
|
1086
|
-
type: 'object',
|
|
1087
|
-
properties: {
|
|
1088
|
-
message: { type: 'string' }
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
/**
|
|
1095
|
-
* Handle single asset upload via HTTP POST.
|
|
1096
|
-
*
|
|
1097
|
-
* Flow:
|
|
1098
|
-
* 1. Validate request structure and authentication
|
|
1099
|
-
* 2. Extract user identity from Apache APISIX headers
|
|
1100
|
-
* 3. Validate file extension and read uploaded file
|
|
1101
|
-
* 4. Store file via storage service and metadata in database
|
|
1102
|
-
* 5. Set owner_id to authenticated user (prevents ownership spoofing)
|
|
1103
|
-
* 6. Apply is_public setting (defaults to true if not specified)
|
|
1104
|
-
*
|
|
1105
|
-
* Authentication: Required
|
|
1106
|
-
* Ownership: Automatically set to authenticated user
|
|
1107
|
-
*
|
|
1108
|
-
* @param req - HTTP request with multipart/form-data file upload
|
|
1109
|
-
* @returns HTTP response with success/error status
|
|
1110
|
-
*
|
|
1111
|
-
* @example
|
|
1112
|
-
* POST /assets
|
|
1113
|
-
* Content-Type: multipart/form-data
|
|
1114
|
-
* x-user-id: user-uuid
|
|
1115
|
-
* x-user-roles: user,premium
|
|
1116
|
-
*
|
|
1117
|
-
* Form data:
|
|
1118
|
-
* - file: <binary file>
|
|
1119
|
-
* - description: "3D model of building"
|
|
1120
|
-
* - source: "https://source.com"
|
|
1121
|
-
* - is_public: true
|
|
1122
|
-
*/
|
|
1123
|
-
async handleUpload(req) {
|
|
1124
|
-
try {
|
|
1125
|
-
// Validate request structure
|
|
1126
|
-
if (!req?.body) {
|
|
1127
|
-
return badRequestResponse('Invalid request: missing request body');
|
|
1128
|
-
}
|
|
1129
|
-
// Authenticate user
|
|
1130
|
-
const authResult = await this.authenticateRequest(req);
|
|
1131
|
-
if (!authResult.success) {
|
|
1132
|
-
return authResult.response;
|
|
1133
|
-
}
|
|
1134
|
-
const userId = authResult.userRecord.id;
|
|
1135
|
-
if (!userId) {
|
|
1136
|
-
return errorResponse('Failed to retrieve user information');
|
|
1137
|
-
}
|
|
1138
|
-
// Extract and validate upload data
|
|
1139
|
-
const uploadData = this.extractUploadData(req);
|
|
1140
|
-
const validation = this.validateUploadFields(uploadData);
|
|
1141
|
-
if (!validation.success) {
|
|
1142
|
-
return validation.response;
|
|
1143
|
-
}
|
|
1144
|
-
const validData = validation.data;
|
|
1145
|
-
// Get file buffer from memory or read from temporary location
|
|
1146
|
-
let fileBuffer;
|
|
1147
|
-
if (validData.fileBuffer) {
|
|
1148
|
-
// Memory storage: buffer already available
|
|
1149
|
-
fileBuffer = validData.fileBuffer;
|
|
1150
|
-
}
|
|
1151
|
-
else if (validData.filePath) {
|
|
1152
|
-
// Disk storage: read from temp file
|
|
1153
|
-
try {
|
|
1154
|
-
fileBuffer = await this.readTempFile(validData.filePath);
|
|
1155
|
-
}
|
|
1156
|
-
catch (error) {
|
|
1157
|
-
return errorResponse(`Failed to read uploaded file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
else {
|
|
1161
|
-
return badRequestResponse('No file data available');
|
|
1162
|
-
}
|
|
1163
|
-
// Upload asset and cleanup
|
|
1164
|
-
try {
|
|
1165
|
-
await this.uploadAsset({
|
|
1166
|
-
description: validData.description,
|
|
1167
|
-
source: validData.source,
|
|
1168
|
-
owner_id: userId,
|
|
1169
|
-
filename: validData.filename,
|
|
1170
|
-
file: fileBuffer,
|
|
1171
|
-
is_public: validData.is_public
|
|
1172
|
-
});
|
|
1173
|
-
if (validData.filePath) {
|
|
1174
|
-
await this.cleanupTempFile(validData.filePath);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
catch (error) {
|
|
1178
|
-
if (validData.filePath) {
|
|
1179
|
-
await this.cleanupTempFile(validData.filePath);
|
|
1180
|
-
}
|
|
1181
|
-
throw error;
|
|
1182
|
-
}
|
|
1183
|
-
return successResponse({ message: 'Asset uploaded successfully' });
|
|
1184
|
-
}
|
|
1185
|
-
catch (error) {
|
|
1186
|
-
return errorResponse(error);
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
/**
|
|
1190
|
-
* Handle update endpoint (PUT).
|
|
1191
|
-
*
|
|
1192
|
-
* Updates metadata for a specific asset by ID.
|
|
1193
|
-
*
|
|
1194
|
-
* @param {any} req - HTTP request object with params.id and body containing updates
|
|
1195
|
-
* @returns {Promise<DataResponse>} HTTP response
|
|
1196
|
-
*
|
|
1197
|
-
* @example
|
|
1198
|
-
* ```typescript
|
|
1199
|
-
* // PUT /gltf/123
|
|
1200
|
-
* // Body: { "description": "Updated model", "source": "https://new-source.com" }
|
|
1201
|
-
* ```
|
|
1202
|
-
*/
|
|
1203
|
-
async handleUpdate(req) {
|
|
1204
|
-
try {
|
|
1205
|
-
if (!req) {
|
|
1206
|
-
return badRequestResponse('Invalid request: missing request object');
|
|
1207
|
-
}
|
|
1208
|
-
// Authenticate user
|
|
1209
|
-
const authResult = await this.authenticateRequest(req);
|
|
1210
|
-
if (!authResult.success) {
|
|
1211
|
-
return authResult.response;
|
|
1212
|
-
}
|
|
1213
|
-
const userId = authResult.userRecord.id;
|
|
1214
|
-
if (!userId) {
|
|
1215
|
-
return errorResponse('Failed to retrieve user information');
|
|
1216
|
-
}
|
|
1217
|
-
const { id } = req.params || {};
|
|
1218
|
-
const { description, source, is_public } = req.body || {};
|
|
1219
|
-
if (!id) {
|
|
1220
|
-
return badRequestResponse('Asset ID is required');
|
|
1221
|
-
}
|
|
1222
|
-
if (!description && !source && is_public === undefined) {
|
|
1223
|
-
return badRequestResponse('At least one field (description, source, or is_public) must be provided for update');
|
|
1224
|
-
}
|
|
1225
|
-
// Check if asset exists
|
|
1226
|
-
const asset = await this.getAssetById(id);
|
|
1227
|
-
if (!asset) {
|
|
1228
|
-
return notFoundResponse('Asset not found');
|
|
1229
|
-
}
|
|
1230
|
-
// Check ownership (admins can modify any asset)
|
|
1231
|
-
const ownershipError = this.validateOwnership(asset, userId, req.headers);
|
|
1232
|
-
if (ownershipError) {
|
|
1233
|
-
return ownershipError;
|
|
1234
|
-
}
|
|
1235
|
-
// Build and apply updates
|
|
1236
|
-
const updates = {};
|
|
1237
|
-
if (description !== undefined)
|
|
1238
|
-
updates.description = description;
|
|
1239
|
-
if (source !== undefined)
|
|
1240
|
-
updates.source = source;
|
|
1241
|
-
if (is_public !== undefined)
|
|
1242
|
-
updates.is_public = Boolean(is_public);
|
|
1243
|
-
await this.updateAssetMetadata(id, updates);
|
|
1244
|
-
return successResponse({ message: 'Asset metadata updated successfully' });
|
|
1245
|
-
}
|
|
1246
|
-
catch (error) {
|
|
1247
|
-
return errorResponse(error);
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
/**
|
|
1251
|
-
* Handle get asset endpoint (GET).
|
|
1252
|
-
*
|
|
1253
|
-
* Returns the file content of a specific asset by ID for display/use in front-end.
|
|
1254
|
-
* No download headers - just the raw file content.
|
|
1255
|
-
*
|
|
1256
|
-
* Access control:
|
|
1257
|
-
* - Public assets: Accessible to everyone
|
|
1258
|
-
* - Private assets: Accessible only to owner
|
|
1259
|
-
* - Admin users: Can access all assets (public and private)
|
|
1260
|
-
*
|
|
1261
|
-
* @param {any} req - HTTP request object with params.id
|
|
1262
|
-
* @returns {Promise<DataResponse>} HTTP response with file content
|
|
1263
|
-
*
|
|
1264
|
-
* @example
|
|
1265
|
-
* ```typescript
|
|
1266
|
-
* // GET /gltf/123
|
|
1267
|
-
* // Returns the .glb file content for display in 3D viewer
|
|
1268
|
-
* ```
|
|
1269
|
-
*/
|
|
1270
|
-
async handleGetAsset(req) {
|
|
1271
|
-
try {
|
|
1272
|
-
const result = await this.fetchAssetWithAccessCheck(req);
|
|
1273
|
-
if (!result.success) {
|
|
1274
|
-
return result.response;
|
|
1275
|
-
}
|
|
1276
|
-
const fileContent = await result.asset.data();
|
|
1277
|
-
return fileResponse(fileContent, this.getConfiguration().contentType);
|
|
1278
|
-
}
|
|
1279
|
-
catch (error) {
|
|
1280
|
-
return errorResponse(error);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
/**
|
|
1284
|
-
* Handle download endpoint (GET).
|
|
1285
|
-
*
|
|
1286
|
-
* Downloads the file content of a specific asset by ID with download headers.
|
|
1287
|
-
* Forces browser to download the file rather than display it.
|
|
1288
|
-
*
|
|
1289
|
-
* Access control:
|
|
1290
|
-
* - Public assets: Accessible to everyone
|
|
1291
|
-
* - Private assets: Accessible only to owner
|
|
1292
|
-
* - Admin users: Can download all assets (public and private)
|
|
1293
|
-
*
|
|
1294
|
-
* @param {any} req - HTTP request object with params.id
|
|
1295
|
-
* @returns {Promise<DataResponse>} HTTP response with file content and download headers
|
|
1296
|
-
*
|
|
1297
|
-
* @example
|
|
1298
|
-
* ```typescript
|
|
1299
|
-
* // GET /gltf/123/download
|
|
1300
|
-
* // Returns the .glb file with download headers - browser will save it
|
|
1301
|
-
* ```
|
|
1302
|
-
*/
|
|
1303
|
-
async handleDownload(req) {
|
|
1304
|
-
try {
|
|
1305
|
-
const result = await this.fetchAssetWithAccessCheck(req);
|
|
1306
|
-
if (!result.success) {
|
|
1307
|
-
return result.response;
|
|
1308
|
-
}
|
|
1309
|
-
const fileContent = await result.asset.data();
|
|
1310
|
-
const filename = result.asset.filename || `asset_${req.params?.id}`;
|
|
1311
|
-
return fileResponse(fileContent, this.getConfiguration().contentType, filename);
|
|
1312
|
-
}
|
|
1313
|
-
catch (error) {
|
|
1314
|
-
return errorResponse(error);
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
/**
|
|
1318
|
-
* Handle delete endpoint (DELETE).
|
|
1319
|
-
*
|
|
1320
|
-
* Deletes a specific asset by ID.
|
|
1321
|
-
*
|
|
1322
|
-
* @param {any} req - HTTP request object with params.id
|
|
1323
|
-
* @returns {Promise<DataResponse>} HTTP response
|
|
1324
|
-
*
|
|
1325
|
-
* @example
|
|
1326
|
-
* ```typescript
|
|
1327
|
-
* // DELETE /gltf/123
|
|
1328
|
-
* ```
|
|
1329
|
-
*/
|
|
1330
|
-
async handleDelete(req) {
|
|
1331
|
-
try {
|
|
1332
|
-
// Authenticate user
|
|
1333
|
-
const authResult = await this.authenticateRequest(req);
|
|
1334
|
-
if (!authResult.success) {
|
|
1335
|
-
return authResult.response;
|
|
1336
|
-
}
|
|
1337
|
-
const userId = authResult.userRecord.id;
|
|
1338
|
-
if (!userId) {
|
|
1339
|
-
return errorResponse('Failed to retrieve user information');
|
|
1340
|
-
}
|
|
1341
|
-
const { id } = req.params || {};
|
|
1342
|
-
if (!id) {
|
|
1343
|
-
return badRequestResponse('Asset ID is required');
|
|
1344
|
-
}
|
|
1345
|
-
// Check if asset exists
|
|
1346
|
-
const asset = await this.getAssetById(id);
|
|
1347
|
-
if (!asset) {
|
|
1348
|
-
return notFoundResponse('Asset not found');
|
|
1349
|
-
}
|
|
1350
|
-
// Check ownership (admins can delete any asset)
|
|
1351
|
-
const ownershipError = this.validateOwnership(asset, userId, req.headers);
|
|
1352
|
-
if (ownershipError) {
|
|
1353
|
-
return ownershipError;
|
|
1354
|
-
}
|
|
1355
|
-
await this.deleteAssetById(id);
|
|
1356
|
-
return successResponse({ message: 'Asset deleted successfully' });
|
|
1357
|
-
}
|
|
1358
|
-
catch (error) {
|
|
1359
|
-
return errorResponse(error);
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
/**
|
|
1363
|
-
* Handle batch upload endpoint
|
|
1364
|
-
*/
|
|
1365
|
-
async handleUploadBatch(req) {
|
|
1366
|
-
try {
|
|
1367
|
-
if (!req?.body) {
|
|
1368
|
-
return badRequestResponse('Invalid request: missing request body');
|
|
1369
|
-
}
|
|
1370
|
-
// Authenticate user
|
|
1371
|
-
const authResult = await this.authenticateRequest(req);
|
|
1372
|
-
if (!authResult.success) {
|
|
1373
|
-
return authResult.response;
|
|
1374
|
-
}
|
|
1375
|
-
const userId = authResult.userRecord.id;
|
|
1376
|
-
if (!userId) {
|
|
1377
|
-
return errorResponse('Failed to retrieve user information');
|
|
1378
|
-
}
|
|
1379
|
-
const requests = req.body.requests;
|
|
1380
|
-
if (!Array.isArray(requests) || requests.length === 0) {
|
|
1381
|
-
return badRequestResponse('Requests array is required and must not be empty');
|
|
1382
|
-
}
|
|
1383
|
-
// Validate all requests first
|
|
1384
|
-
const validationError = this.validateBatchRequests(requests);
|
|
1385
|
-
if (validationError) {
|
|
1386
|
-
return validationError;
|
|
1387
|
-
}
|
|
1388
|
-
// Process each request
|
|
1389
|
-
const results = await this.processBatchUploads(requests, userId);
|
|
1390
|
-
const successCount = results.filter(r => r.success).length;
|
|
1391
|
-
const failureCount = results.length - successCount;
|
|
1392
|
-
const message = `${successCount}/${requests.length} assets uploaded successfully`;
|
|
1393
|
-
if (failureCount > 0) {
|
|
1394
|
-
return multiStatusResponse(message, results);
|
|
1395
|
-
}
|
|
1396
|
-
return successResponse({ message, results });
|
|
1397
|
-
}
|
|
1398
|
-
catch (error) {
|
|
1399
|
-
return errorResponse(error);
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
/**
|
|
1403
|
-
* Validates all requests in a batch upload.
|
|
1404
|
-
*
|
|
1405
|
-
* @param requests - Array of upload requests to validate
|
|
1406
|
-
* @returns DataResponse with error if validation fails, undefined if valid
|
|
1407
|
-
*/
|
|
1408
|
-
validateBatchRequests(requests) {
|
|
1409
|
-
const config = this.getConfiguration();
|
|
1410
|
-
for (const request of requests) {
|
|
1411
|
-
if (!request.file || !request.description || !request.source || !request.filename) {
|
|
1412
|
-
return badRequestResponse('Each request must have description, source, filename, and file');
|
|
1413
|
-
}
|
|
1414
|
-
if (!this.validateBase64(request.file)) {
|
|
1415
|
-
return badRequestResponse(`Invalid base64 data for file: ${request.filename}. File must be a valid base64-encoded string.`);
|
|
1416
|
-
}
|
|
1417
|
-
if (!this.validateFileExtension(request.filename)) {
|
|
1418
|
-
return badRequestResponse(`Invalid file extension for ${request.filename}. Expected: ${config.extension}`);
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
return undefined;
|
|
1422
|
-
}
|
|
1423
|
-
/**
|
|
1424
|
-
* Processes batch upload requests.
|
|
1425
|
-
*
|
|
1426
|
-
* @param requests - Array of upload requests
|
|
1427
|
-
* @param ownerId - Owner user ID
|
|
1428
|
-
* @returns Array of results for each upload
|
|
1429
|
-
*/
|
|
1430
|
-
async processBatchUploads(requests, ownerId) {
|
|
1431
|
-
const results = [];
|
|
1432
|
-
for (const request of requests) {
|
|
1433
|
-
try {
|
|
1434
|
-
await this.uploadAsset({
|
|
1435
|
-
description: request.description,
|
|
1436
|
-
source: request.source,
|
|
1437
|
-
owner_id: ownerId,
|
|
1438
|
-
filename: request.filename,
|
|
1439
|
-
file: Buffer.from(request.file, 'base64'),
|
|
1440
|
-
is_public: request.is_public !== undefined ? Boolean(request.is_public) : true
|
|
1441
|
-
});
|
|
1442
|
-
results.push({ success: true, filename: request.filename });
|
|
1443
|
-
}
|
|
1444
|
-
catch (error) {
|
|
1445
|
-
results.push({
|
|
1446
|
-
success: false,
|
|
1447
|
-
filename: request.filename,
|
|
1448
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
return results;
|
|
1453
|
-
}
|
|
1454
|
-
/**
|
|
1455
|
-
* Handle batch delete endpoint
|
|
1456
|
-
*/
|
|
1457
|
-
async handleDeleteBatch(req) {
|
|
1458
|
-
try {
|
|
1459
|
-
if (!req?.body) {
|
|
1460
|
-
return badRequestResponse('Invalid request: missing request body');
|
|
1461
|
-
}
|
|
1462
|
-
// Authenticate user
|
|
1463
|
-
const authResult = await this.authenticateRequest(req);
|
|
1464
|
-
if (!authResult.success) {
|
|
1465
|
-
return authResult.response;
|
|
1466
|
-
}
|
|
1467
|
-
const userId = authResult.userRecord.id;
|
|
1468
|
-
if (!userId) {
|
|
1469
|
-
return errorResponse('Failed to retrieve user information');
|
|
1470
|
-
}
|
|
1471
|
-
const { ids } = req.body;
|
|
1472
|
-
if (!Array.isArray(ids) || ids.length === 0) {
|
|
1473
|
-
return badRequestResponse('IDs array is required and must not be empty');
|
|
1474
|
-
}
|
|
1475
|
-
// Process deletions (admins can delete any asset)
|
|
1476
|
-
const results = await this.processBatchDeletes(ids, userId, req.headers);
|
|
1477
|
-
const successCount = results.filter(r => r.success).length;
|
|
1478
|
-
const failureCount = results.length - successCount;
|
|
1479
|
-
const message = `${successCount}/${ids.length} assets deleted successfully`;
|
|
1480
|
-
if (failureCount > 0) {
|
|
1481
|
-
return multiStatusResponse(message, results);
|
|
1482
|
-
}
|
|
1483
|
-
return successResponse({ message, results });
|
|
1484
|
-
}
|
|
1485
|
-
catch (error) {
|
|
1486
|
-
return errorResponse(error);
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Processes batch delete requests.
|
|
1491
|
-
*
|
|
1492
|
-
* Admins can delete any asset. Regular users can only delete their own assets
|
|
1493
|
-
* or assets with no owner.
|
|
1494
|
-
*
|
|
1495
|
-
* @param ids - Array of asset IDs to delete
|
|
1496
|
-
* @param userId - User ID for ownership validation
|
|
1497
|
-
* @param headers - HTTP request headers (for admin check)
|
|
1498
|
-
* @returns Array of results for each deletion
|
|
1499
|
-
*/
|
|
1500
|
-
async processBatchDeletes(ids, userId, headers) {
|
|
1501
|
-
const results = [];
|
|
1502
|
-
const isAdmin = headers && ApisixAuthParser.isAdmin(headers);
|
|
1503
|
-
for (const id of ids) {
|
|
1504
|
-
try {
|
|
1505
|
-
const asset = await this.getAssetById(id);
|
|
1506
|
-
if (!asset) {
|
|
1507
|
-
results.push({ success: false, id, error: 'Asset not found' });
|
|
1508
|
-
continue;
|
|
1509
|
-
}
|
|
1510
|
-
// Allow deletion if: admin OR owner is the current user OR asset has no owner
|
|
1511
|
-
if (!isAdmin && asset.owner_id !== null && asset.owner_id !== userId) {
|
|
1512
|
-
results.push({ success: false, id, error: 'You can only delete your own assets' });
|
|
1513
|
-
continue;
|
|
1514
|
-
}
|
|
1515
|
-
await this.deleteAssetById(id);
|
|
1516
|
-
results.push({ success: true, id });
|
|
1517
|
-
}
|
|
1518
|
-
catch (error) {
|
|
1519
|
-
results.push({
|
|
1520
|
-
success: false,
|
|
1521
|
-
id,
|
|
1522
|
-
error: error instanceof Error ? error.message : 'Unknown error'
|
|
1523
|
-
});
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
return results;
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
//# sourceMappingURL=assets_manager.js.map
|