@flusys/nestjs-storage 1.0.0-beta → 1.0.0-rc
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/README.md +131 -13
- package/cjs/config/storage-config.service.js +5 -0
- package/cjs/config/storage.constants.js +0 -8
- package/cjs/controllers/file-manager.controller.js +44 -1
- package/cjs/controllers/folder.controller.js +44 -1
- package/cjs/controllers/storage-config.controller.js +44 -1
- package/cjs/controllers/upload.controller.js +6 -12
- package/cjs/dtos/file-manager.dto.js +8 -5
- package/cjs/dtos/storage-config.dto.js +41 -1
- package/cjs/dtos/upload.dto.js +7 -0
- package/cjs/entities/storage-config-base.entity.js +31 -2
- package/cjs/interfaces/index.js +0 -1
- package/cjs/middlewares/file-serve.middleware.js +6 -0
- package/cjs/providers/local-provider.js +52 -2
- package/cjs/providers/storage-factory.service.js +2 -2
- package/cjs/services/file-manager.service.js +37 -24
- package/cjs/services/folder.service.js +5 -8
- package/cjs/services/storage-provider-config.service.js +18 -35
- package/cjs/services/upload.service.js +39 -27
- package/cjs/utils/file-validator.util.js +470 -0
- package/cjs/utils/image-compressor.util.js +1 -3
- package/config/storage-config.service.d.ts +5 -2
- package/config/storage.constants.d.ts +0 -2
- package/controllers/upload.controller.d.ts +2 -6
- package/dtos/file-manager.dto.d.ts +2 -4
- package/dtos/folder.dto.d.ts +2 -4
- package/dtos/storage-config.dto.d.ts +9 -6
- package/entities/storage-config-base.entity.d.ts +2 -0
- package/fesm/config/storage-config.service.js +5 -0
- package/fesm/config/storage.constants.js +0 -2
- package/fesm/controllers/file-manager.controller.js +45 -2
- package/fesm/controllers/folder.controller.js +45 -2
- package/fesm/controllers/storage-config.controller.js +45 -2
- package/fesm/controllers/upload.controller.js +7 -13
- package/fesm/dtos/file-manager.dto.js +8 -5
- package/fesm/dtos/storage-config.dto.js +45 -11
- package/fesm/dtos/upload.dto.js +8 -1
- package/fesm/entities/index.js +1 -5
- package/fesm/entities/storage-config-base.entity.js +33 -7
- package/fesm/interfaces/index.js +0 -1
- package/fesm/interfaces/storage-config.interface.js +1 -3
- package/fesm/middlewares/file-serve.middleware.js +7 -1
- package/fesm/providers/local-provider.js +52 -2
- package/fesm/providers/storage-factory.service.js +2 -2
- package/fesm/services/file-manager.service.js +38 -25
- package/fesm/services/folder.service.js +5 -8
- package/fesm/services/storage-provider-config.service.js +18 -35
- package/fesm/services/upload.service.js +40 -28
- package/fesm/utils/file-validator.util.js +463 -0
- package/fesm/utils/image-compressor.util.js +1 -3
- package/interfaces/file-manager.interface.d.ts +7 -4
- package/interfaces/index.d.ts +0 -1
- package/interfaces/storage-config.interface.d.ts +2 -20
- package/package.json +6 -6
- package/providers/local-provider.d.ts +2 -0
- package/services/file-manager.service.d.ts +2 -2
- package/utils/file-validator.util.d.ts +16 -0
- package/cjs/interfaces/file-upload-response.interface.js +0 -4
- package/fesm/interfaces/file-upload-response.interface.js +0 -1
- package/interfaces/file-upload-response.interface.d.ts +0 -6
|
@@ -76,6 +76,11 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
76
76
|
function _ts_metadata(k, v) {
|
|
77
77
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
78
78
|
}
|
|
79
|
+
function _ts_param(paramIndex, decorator) {
|
|
80
|
+
return function(target, key) {
|
|
81
|
+
decorator(target, key, paramIndex);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
79
84
|
let FileServeMiddleware = class FileServeMiddleware {
|
|
80
85
|
async use(req, res, next) {
|
|
81
86
|
try {
|
|
@@ -197,6 +202,7 @@ let FileServeMiddleware = class FileServeMiddleware {
|
|
|
197
202
|
};
|
|
198
203
|
FileServeMiddleware = _ts_decorate([
|
|
199
204
|
(0, _common.Injectable)(),
|
|
205
|
+
_ts_param(0, (0, _common.Inject)(_uploadservice.UploadService)),
|
|
200
206
|
_ts_metadata("design:type", Function),
|
|
201
207
|
_ts_metadata("design:paramtypes", [
|
|
202
208
|
typeof _uploadservice.UploadService === "undefined" ? Object : _uploadservice.UploadService
|
|
@@ -80,6 +80,29 @@ function _interop_require_wildcard(obj, nodeInterop) {
|
|
|
80
80
|
}
|
|
81
81
|
let LocalProvider = class LocalProvider {
|
|
82
82
|
/**
|
|
83
|
+
* SECURITY: Validates that a target path does not escape the base directory
|
|
84
|
+
* Prevents path traversal attacks using ../ sequences
|
|
85
|
+
* @throws Error if path traversal is detected
|
|
86
|
+
*/ validatePathWithinBase(targetPath) {
|
|
87
|
+
const normalizedBasePath = _path.resolve(this.basePath);
|
|
88
|
+
const normalizedTargetPath = _path.resolve(targetPath);
|
|
89
|
+
if (!normalizedTargetPath.startsWith(normalizedBasePath + _path.sep) && normalizedTargetPath !== normalizedBasePath) {
|
|
90
|
+
this.logger.warn(`Path traversal attempt detected: ${targetPath}`);
|
|
91
|
+
throw new Error('Invalid path: Path traversal attempt detected');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* SECURITY: Validates file key format to prevent malicious input
|
|
96
|
+
* @throws Error if key contains suspicious patterns
|
|
97
|
+
*/ validateKeyFormat(key) {
|
|
98
|
+
if (!key || typeof key !== 'string' || key.trim().length === 0) {
|
|
99
|
+
throw new Error('Invalid file key: empty or invalid');
|
|
100
|
+
}
|
|
101
|
+
if (key.includes('\0')) {
|
|
102
|
+
throw new Error('Invalid file key: contains null bytes');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
83
106
|
* Initialize Local File System provider with configuration
|
|
84
107
|
* @param config.basePath - Base path for file storage (default: './uploads')
|
|
85
108
|
* @param config.baseUrl - Optional base URL for generating file URLs
|
|
@@ -112,7 +135,11 @@ let LocalProvider = class LocalProvider {
|
|
|
112
135
|
}
|
|
113
136
|
// Build file path
|
|
114
137
|
const folderPath = options.folderPath ? _path.join(this.basePath, options.folderPath) : this.basePath;
|
|
138
|
+
// SECURITY: Validate path does not escape base directory
|
|
139
|
+
this.validatePathWithinBase(folderPath);
|
|
115
140
|
const filePath = _path.join(folderPath, fileName);
|
|
141
|
+
// SECURITY: Double-check final file path
|
|
142
|
+
this.validatePathWithinBase(filePath);
|
|
116
143
|
// Ensure directory exists
|
|
117
144
|
await _promises.mkdir(folderPath, {
|
|
118
145
|
recursive: true
|
|
@@ -135,14 +162,20 @@ let LocalProvider = class LocalProvider {
|
|
|
135
162
|
}
|
|
136
163
|
async deleteFile(key) {
|
|
137
164
|
try {
|
|
165
|
+
// SECURITY: Validate key format first
|
|
166
|
+
this.validateKeyFormat(key);
|
|
138
167
|
// Key now includes the basePath, resolve from cwd
|
|
139
168
|
let filePath = _path.resolve(key);
|
|
169
|
+
// SECURITY: Validate resolved path is within base directory
|
|
170
|
+
this.validatePathWithinBase(filePath);
|
|
140
171
|
// Check if file exists at the resolved path
|
|
141
172
|
try {
|
|
142
173
|
await _promises.access(filePath);
|
|
143
174
|
} catch {
|
|
144
175
|
// Fallback: try with basePath prefix (for old keys without basePath)
|
|
145
176
|
const fallbackPath = _path.join(this.basePath, key);
|
|
177
|
+
// SECURITY: Validate fallback path as well
|
|
178
|
+
this.validatePathWithinBase(fallbackPath);
|
|
146
179
|
try {
|
|
147
180
|
await _promises.access(fallbackPath);
|
|
148
181
|
filePath = fallbackPath;
|
|
@@ -155,7 +188,11 @@ let LocalProvider = class LocalProvider {
|
|
|
155
188
|
}
|
|
156
189
|
await _promises.unlink(filePath);
|
|
157
190
|
this.logger.log(`Deleted file from local storage: ${key}`);
|
|
158
|
-
} catch (
|
|
191
|
+
} catch (error) {
|
|
192
|
+
// Re-throw security errors
|
|
193
|
+
if (error instanceof Error && error.message.includes('Invalid')) {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
159
196
|
this.logger.warn(`Failed to delete file from local storage: ${key}`);
|
|
160
197
|
}
|
|
161
198
|
}
|
|
@@ -186,14 +223,23 @@ let LocalProvider = class LocalProvider {
|
|
|
186
223
|
* Get the absolute path for a file key
|
|
187
224
|
* Key now includes basePath, so resolve from cwd
|
|
188
225
|
*/ getAbsolutePath(key) {
|
|
189
|
-
|
|
226
|
+
// SECURITY: Validate key format
|
|
227
|
+
this.validateKeyFormat(key);
|
|
228
|
+
const filePath = _path.resolve(key);
|
|
229
|
+
// SECURITY: Validate path is within base directory
|
|
230
|
+
this.validatePathWithinBase(filePath);
|
|
231
|
+
return filePath;
|
|
190
232
|
}
|
|
191
233
|
/**
|
|
192
234
|
* Check if a file exists
|
|
193
235
|
*/ async fileExists(key) {
|
|
194
236
|
try {
|
|
237
|
+
// SECURITY: Validate key format
|
|
238
|
+
this.validateKeyFormat(key);
|
|
195
239
|
// Key now includes basePath, resolve from cwd
|
|
196
240
|
const filePath = _path.resolve(key);
|
|
241
|
+
// SECURITY: Validate path is within base directory
|
|
242
|
+
this.validatePathWithinBase(filePath);
|
|
197
243
|
await _promises.access(filePath);
|
|
198
244
|
return true;
|
|
199
245
|
} catch {
|
|
@@ -203,8 +249,12 @@ let LocalProvider = class LocalProvider {
|
|
|
203
249
|
/**
|
|
204
250
|
* Get file stats
|
|
205
251
|
*/ async getFileStats(key) {
|
|
252
|
+
// SECURITY: Validate key format
|
|
253
|
+
this.validateKeyFormat(key);
|
|
206
254
|
// Key now includes basePath, resolve from cwd
|
|
207
255
|
const filePath = _path.resolve(key);
|
|
256
|
+
// SECURITY: Validate path is within base directory
|
|
257
|
+
this.validatePathWithinBase(filePath);
|
|
208
258
|
const stats = await _promises.stat(filePath);
|
|
209
259
|
return {
|
|
210
260
|
size: stats.size,
|
|
@@ -130,8 +130,8 @@ let StorageFactoryService = class StorageFactoryService {
|
|
|
130
130
|
} catch (error) {
|
|
131
131
|
this.logger.error(`Failed to create provider ${config.provider}:`, error);
|
|
132
132
|
// Preserve original error message for better debugging
|
|
133
|
-
const
|
|
134
|
-
throw new Error(`Failed to initialize storage provider '${config.provider}': ${
|
|
133
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
134
|
+
throw new Error(`Failed to initialize storage provider '${config.provider}': ${errorMessage}`);
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
/**
|
|
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "FileManagerService", {
|
|
|
10
10
|
});
|
|
11
11
|
const _classes = require("@flusys/nestjs-shared/classes");
|
|
12
12
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
13
|
+
const _utils = require("@flusys/nestjs-shared/utils");
|
|
13
14
|
const _common = require("@nestjs/common");
|
|
14
15
|
const _typeorm = require("typeorm");
|
|
15
16
|
const _config = require("../config");
|
|
@@ -154,8 +155,9 @@ let FileManagerService = class FileManagerService extends _classes.RequestScoped
|
|
|
154
155
|
const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
|
|
155
156
|
const storageConfigEntity = enableCompanyFeature ? (await Promise.resolve().then(()=>/*#__PURE__*/ _interop_require_wildcard(require("../entities")))).StorageConfigWithCompany : (await Promise.resolve().then(()=>/*#__PURE__*/ _interop_require_wildcard(require("../entities")))).StorageConfig;
|
|
156
157
|
const storageConfigRepository = await this.dataSourceProvider.getRepository(storageConfigEntity);
|
|
158
|
+
const storageConfigId = dto.storageConfigId;
|
|
157
159
|
const whereCondition = {
|
|
158
|
-
id:
|
|
160
|
+
id: storageConfigId
|
|
159
161
|
};
|
|
160
162
|
// Filter by company if company feature is enabled
|
|
161
163
|
if (enableCompanyFeature && user?.companyId) {
|
|
@@ -165,25 +167,30 @@ let FileManagerService = class FileManagerService extends _classes.RequestScoped
|
|
|
165
167
|
where: whereCondition
|
|
166
168
|
});
|
|
167
169
|
if (storageConfig) {
|
|
168
|
-
|
|
170
|
+
const typedConfig = storageConfig;
|
|
171
|
+
if (typedConfig.storage) {
|
|
172
|
+
storageLocation = typedConfig.storage;
|
|
173
|
+
}
|
|
169
174
|
}
|
|
170
175
|
} catch (error) {
|
|
171
|
-
|
|
176
|
+
const errorMessage = _utils.ErrorHandler.getErrorMessage(error);
|
|
177
|
+
this.logger.warn(`Failed to get storage location from config ${dto.storageConfigId}: ${errorMessage}`);
|
|
172
178
|
// Fall back to DTO location or default
|
|
173
179
|
}
|
|
174
180
|
}
|
|
175
|
-
// Set basic fields
|
|
176
|
-
|
|
181
|
+
// Set basic fields - merge existing data with DTO
|
|
182
|
+
const mergedFileManager = {
|
|
177
183
|
...fileManager,
|
|
178
184
|
...dto,
|
|
179
185
|
location: storageLocation,
|
|
180
186
|
folder: validatedFolder
|
|
181
187
|
};
|
|
182
|
-
// Only set company fields if
|
|
183
|
-
|
|
184
|
-
|
|
188
|
+
// Only set company fields if company feature is enabled
|
|
189
|
+
const enableCompanyFeatureForEntity = this.storageConfig.isCompanyFeatureEnabled();
|
|
190
|
+
if (enableCompanyFeatureForEntity) {
|
|
191
|
+
mergedFileManager.companyId = user?.companyId ?? null;
|
|
185
192
|
}
|
|
186
|
-
return
|
|
193
|
+
return mergedFileManager;
|
|
187
194
|
}
|
|
188
195
|
async getSelectQuery(query, _user, select) {
|
|
189
196
|
if (!select || !select.length) {
|
|
@@ -260,7 +267,8 @@ let FileManagerService = class FileManagerService extends _classes.RequestScoped
|
|
|
260
267
|
this.logger.debug(`enrichWithProviderNames: First enriched item: ${JSON.stringify(enrichedItems[0])}`);
|
|
261
268
|
return enrichedItems;
|
|
262
269
|
} catch (error) {
|
|
263
|
-
|
|
270
|
+
const errorMessage = _utils.ErrorHandler.getErrorMessage(error);
|
|
271
|
+
this.logger.warn(`Failed to fetch provider names: ${errorMessage}`);
|
|
264
272
|
return items.map((item)=>({
|
|
265
273
|
...item,
|
|
266
274
|
providerName: undefined
|
|
@@ -298,13 +306,12 @@ let FileManagerService = class FileManagerService extends _classes.RequestScoped
|
|
|
298
306
|
* Override: Extra query manipulation - Auto-filter by user's company and private file permissions
|
|
299
307
|
*/ async getExtraManipulateQuery(query, filterDto, user) {
|
|
300
308
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
301
|
-
//
|
|
309
|
+
// Apply company filter using shared utility
|
|
302
310
|
const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
311
|
+
(0, _utils.applyCompanyFilter)(query, {
|
|
312
|
+
isCompanyFeatureEnabled: enableCompanyFeature,
|
|
313
|
+
entityAlias: 'file_manager'
|
|
314
|
+
}, user);
|
|
308
315
|
// Check if user has permission to see private files
|
|
309
316
|
if (user) {
|
|
310
317
|
const cacheKey = enableCompanyFeature ? `${USER_ACTION_PERMISSION_CACHE_KEY}_${user.id}_${user.companyId}` : `${USER_ACTION_PERMISSION_CACHE_KEY}_${user.id}`;
|
|
@@ -367,8 +374,9 @@ let FileManagerService = class FileManagerService extends _classes.RequestScoped
|
|
|
367
374
|
'config'
|
|
368
375
|
]
|
|
369
376
|
});
|
|
370
|
-
|
|
371
|
-
|
|
377
|
+
const typedConfig = config;
|
|
378
|
+
if (typedConfig?.config?.basePath) {
|
|
379
|
+
const basePath = typedConfig.config.basePath.replace(/^\.\//, '');
|
|
372
380
|
// Convert old keys to new format
|
|
373
381
|
deleteKeys = keys.map((key)=>{
|
|
374
382
|
if (!key.includes('/')) {
|
|
@@ -378,7 +386,8 @@ let FileManagerService = class FileManagerService extends _classes.RequestScoped
|
|
|
378
386
|
});
|
|
379
387
|
}
|
|
380
388
|
} catch (error) {
|
|
381
|
-
|
|
389
|
+
const errorMessage = _utils.ErrorHandler.getErrorMessage(error);
|
|
390
|
+
this.logger.warn(`Failed to get basePath for delete: ${errorMessage}`);
|
|
382
391
|
}
|
|
383
392
|
}
|
|
384
393
|
await this.uploadService.deleteMultipleFile(deleteKeys, configId === 'default' ? undefined : configId, user ?? undefined, location);
|
|
@@ -412,7 +421,8 @@ let FileManagerService = class FileManagerService extends _classes.RequestScoped
|
|
|
412
421
|
file.expiresAt = now + expiresIn * 1000;
|
|
413
422
|
shouldUpdate = true;
|
|
414
423
|
} catch (error) {
|
|
415
|
-
|
|
424
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
425
|
+
this.logger.error(`Failed to generate URL for file ${file.id}: ${errorMessage}`);
|
|
416
426
|
// Use fallback URL with appUrl from config
|
|
417
427
|
const baseUrl = this.getFileBaseUrl(protocol, host);
|
|
418
428
|
file.url = `${baseUrl}/storage/upload/file/${file.key}`;
|
|
@@ -439,13 +449,15 @@ let FileManagerService = class FileManagerService extends _classes.RequestScoped
|
|
|
439
449
|
'config'
|
|
440
450
|
]
|
|
441
451
|
});
|
|
442
|
-
|
|
443
|
-
|
|
452
|
+
const typedConfig = config;
|
|
453
|
+
if (typedConfig?.config?.basePath) {
|
|
454
|
+
const basePath = typedConfig.config.basePath.replace(/^\.\//, ''); // Remove leading ./
|
|
444
455
|
fileKey = `${basePath}/${file.key}`;
|
|
445
456
|
this.logger.debug(`Prefixed old key with basePath: ${fileKey}`);
|
|
446
457
|
}
|
|
447
458
|
} catch (error) {
|
|
448
|
-
|
|
459
|
+
const errorMessage = _utils.ErrorHandler.getErrorMessage(error);
|
|
460
|
+
this.logger.warn(`Failed to get basePath for file ${file.id}: ${errorMessage}`);
|
|
449
461
|
}
|
|
450
462
|
}
|
|
451
463
|
const baseUrl = this.getFileBaseUrl(protocol, host);
|
|
@@ -470,7 +482,8 @@ let FileManagerService = class FileManagerService extends _classes.RequestScoped
|
|
|
470
482
|
try {
|
|
471
483
|
await this.repository.save(updatedFiles);
|
|
472
484
|
} catch (error) {
|
|
473
|
-
|
|
485
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
486
|
+
this.logger.error(`Failed to save updated file URLs: ${errorMessage}`);
|
|
474
487
|
}
|
|
475
488
|
}
|
|
476
489
|
return responses;
|
|
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "FolderService", {
|
|
|
10
10
|
});
|
|
11
11
|
const _classes = require("@flusys/nestjs-shared/classes");
|
|
12
12
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
13
|
+
const _utils = require("@flusys/nestjs-shared/utils");
|
|
13
14
|
const _common = require("@nestjs/common");
|
|
14
15
|
const _config = require("../config");
|
|
15
16
|
const _entities = require("../entities");
|
|
@@ -48,16 +49,13 @@ let FolderService = class FolderService extends _classes.RequestScopedApiService
|
|
|
48
49
|
getDataSourceProvider() {
|
|
49
50
|
return this.dataSourceProvider;
|
|
50
51
|
}
|
|
51
|
-
// Entity Conversion
|
|
52
52
|
async convertSingleDtoToEntity(dto, user) {
|
|
53
53
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
54
|
-
// Set companyId from user context if company feature enabled
|
|
55
54
|
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
56
55
|
entity.companyId = user?.companyId ?? null;
|
|
57
56
|
}
|
|
58
57
|
return entity;
|
|
59
58
|
}
|
|
60
|
-
// Query Customization
|
|
61
59
|
async getSelectQuery(query, _user, select) {
|
|
62
60
|
if (!select?.length) {
|
|
63
61
|
select = [
|
|
@@ -79,11 +77,10 @@ let FolderService = class FolderService extends _classes.RequestScopedApiService
|
|
|
79
77
|
}
|
|
80
78
|
async getExtraManipulateQuery(query, filterDto, user) {
|
|
81
79
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
80
|
+
(0, _utils.applyCompanyFilter)(query, {
|
|
81
|
+
isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
|
|
82
|
+
entityAlias: 'folder'
|
|
83
|
+
}, user);
|
|
87
84
|
return result;
|
|
88
85
|
}
|
|
89
86
|
constructor(cacheManager, utilsService, storageConfig, dataSourceProvider){
|
|
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "StorageProviderConfigService", {
|
|
|
10
10
|
});
|
|
11
11
|
const _classes = require("@flusys/nestjs-shared/classes");
|
|
12
12
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
13
|
+
const _utils = require("@flusys/nestjs-shared/utils");
|
|
13
14
|
const _common = require("@nestjs/common");
|
|
14
15
|
const _config = require("../config");
|
|
15
16
|
const _entities = require("../entities");
|
|
@@ -48,16 +49,13 @@ let StorageProviderConfigService = class StorageProviderConfigService extends _c
|
|
|
48
49
|
getDataSourceProvider() {
|
|
49
50
|
return this.dataSourceProvider;
|
|
50
51
|
}
|
|
51
|
-
// Entity Conversion
|
|
52
52
|
async convertSingleDtoToEntity(dto, user) {
|
|
53
53
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
54
|
-
// Set companyId from user context if company feature enabled
|
|
55
54
|
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
56
55
|
entity.companyId = user?.companyId ?? null;
|
|
57
56
|
}
|
|
58
57
|
return entity;
|
|
59
58
|
}
|
|
60
|
-
// Query Customization
|
|
61
59
|
async getSelectQuery(query, _user, select) {
|
|
62
60
|
if (!select?.length) {
|
|
63
61
|
select = [
|
|
@@ -65,6 +63,8 @@ let StorageProviderConfigService = class StorageProviderConfigService extends _c
|
|
|
65
63
|
'name',
|
|
66
64
|
'storage',
|
|
67
65
|
'config',
|
|
66
|
+
'isActive',
|
|
67
|
+
'isDefault',
|
|
68
68
|
'createdAt',
|
|
69
69
|
'updatedAt'
|
|
70
70
|
];
|
|
@@ -80,19 +80,14 @@ let StorageProviderConfigService = class StorageProviderConfigService extends _c
|
|
|
80
80
|
}
|
|
81
81
|
async getExtraManipulateQuery(query, filterDto, user) {
|
|
82
82
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
83
|
+
(0, _utils.applyCompanyFilter)(query, {
|
|
84
|
+
isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
|
|
85
|
+
entityAlias: 'storageConfig'
|
|
86
|
+
}, user);
|
|
88
87
|
query.orderBy(`${this.entityName}.createdAt`, 'DESC');
|
|
89
88
|
return result;
|
|
90
89
|
}
|
|
91
|
-
|
|
92
|
-
* Find storage config by ID without throwing (returns null if not found)
|
|
93
|
-
* Uses direct repository query - bypasses company filtering
|
|
94
|
-
* Use for internal operations like file deletion where config ID is already known
|
|
95
|
-
*/ async findByIdDirect(id) {
|
|
90
|
+
async findByIdDirect(id) {
|
|
96
91
|
await this.ensureRepositoryInitialized();
|
|
97
92
|
return await this.repository.findOne({
|
|
98
93
|
where: {
|
|
@@ -100,21 +95,15 @@ let StorageProviderConfigService = class StorageProviderConfigService extends _c
|
|
|
100
95
|
}
|
|
101
96
|
});
|
|
102
97
|
}
|
|
103
|
-
|
|
104
|
-
* Get default storage configuration (scoped to user's company if enabled)
|
|
105
|
-
* Falls back to any available config if 'default' not found
|
|
106
|
-
*/ async getDefaultConfig(user) {
|
|
98
|
+
async getDefaultConfig(user) {
|
|
107
99
|
await this.ensureRepositoryInitialized();
|
|
108
|
-
const baseWhere = {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
baseWhere.companyId = user.companyId;
|
|
112
|
-
}
|
|
113
|
-
// First try to find config named 'default'
|
|
100
|
+
const baseWhere = (0, _utils.buildCompanyWhereCondition)({
|
|
101
|
+
isActive: true
|
|
102
|
+
}, this.storageConfig.isCompanyFeatureEnabled(), user);
|
|
114
103
|
const defaultConfig = await this.repository.findOne({
|
|
115
104
|
where: {
|
|
116
105
|
...baseWhere,
|
|
117
|
-
|
|
106
|
+
isDefault: true
|
|
118
107
|
},
|
|
119
108
|
order: {
|
|
120
109
|
createdAt: 'ASC'
|
|
@@ -123,7 +112,6 @@ let StorageProviderConfigService = class StorageProviderConfigService extends _c
|
|
|
123
112
|
if (defaultConfig) {
|
|
124
113
|
return defaultConfig;
|
|
125
114
|
}
|
|
126
|
-
// Fall back to any available config for this company/user
|
|
127
115
|
return await this.repository.findOne({
|
|
128
116
|
where: baseWhere,
|
|
129
117
|
order: {
|
|
@@ -131,17 +119,12 @@ let StorageProviderConfigService = class StorageProviderConfigService extends _c
|
|
|
131
119
|
}
|
|
132
120
|
});
|
|
133
121
|
}
|
|
134
|
-
|
|
135
|
-
* Get storage configuration by type (scoped to user's company if enabled)
|
|
136
|
-
*/ async getConfigByType(storage, user) {
|
|
122
|
+
async getConfigByType(storage, user) {
|
|
137
123
|
await this.ensureRepositoryInitialized();
|
|
138
|
-
const where = {
|
|
139
|
-
storage
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
|
|
143
|
-
where.companyId = user.companyId;
|
|
144
|
-
}
|
|
124
|
+
const where = (0, _utils.buildCompanyWhereCondition)({
|
|
125
|
+
storage,
|
|
126
|
+
isActive: true
|
|
127
|
+
}, this.storageConfig.isCompanyFeatureEnabled(), user);
|
|
145
128
|
return await this.repository.find({
|
|
146
129
|
where
|
|
147
130
|
});
|
|
@@ -8,11 +8,13 @@ Object.defineProperty(exports, "UploadService", {
|
|
|
8
8
|
return UploadService;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
+
const _utils = require("@flusys/nestjs-shared/utils");
|
|
11
12
|
const _common = require("@nestjs/common");
|
|
12
13
|
const _config = require("../config");
|
|
13
14
|
const _filelocationenum = require("../enums/file-location.enum");
|
|
14
15
|
const _storagefactoryservice = require("../providers/storage-factory.service");
|
|
15
16
|
const _storageproviderconfigservice = require("./storage-provider-config.service");
|
|
17
|
+
const _filevalidatorutil = require("../utils/file-validator.util");
|
|
16
18
|
function _define_property(obj, key, value) {
|
|
17
19
|
if (key in obj) {
|
|
18
20
|
Object.defineProperty(obj, key, {
|
|
@@ -42,18 +44,32 @@ function _ts_param(paramIndex, decorator) {
|
|
|
42
44
|
}
|
|
43
45
|
let UploadService = class UploadService {
|
|
44
46
|
/**
|
|
45
|
-
* Validate file before upload
|
|
47
|
+
* Validate file before upload - includes size, type, and content validation.
|
|
48
|
+
* Uses magic bytes to verify file content matches declared MIME type.
|
|
46
49
|
*/ validateFile(file) {
|
|
47
50
|
// Validate file size
|
|
48
51
|
const sizeValidation = this.storageConfigService.validateFileSize(file.size);
|
|
49
52
|
if (!sizeValidation.valid) {
|
|
50
53
|
throw new _common.BadRequestException(sizeValidation.message);
|
|
51
54
|
}
|
|
52
|
-
// Validate file type
|
|
55
|
+
// Validate declared file type (MIME)
|
|
53
56
|
const typeValidation = this.storageConfigService.validateFileType(file.mimetype);
|
|
54
57
|
if (!typeValidation.valid) {
|
|
55
58
|
throw new _common.BadRequestException(typeValidation.message);
|
|
56
59
|
}
|
|
60
|
+
// Validate file content matches declared type (magic bytes check)
|
|
61
|
+
// This prevents MIME type spoofing attacks
|
|
62
|
+
const allowedTypes = this.storageConfigService.getAllowedFileTypes();
|
|
63
|
+
const contentValidation = _filevalidatorutil.FileValidator.validateFileContent(file.buffer, file.mimetype, allowedTypes);
|
|
64
|
+
if (!contentValidation.valid) {
|
|
65
|
+
this.logger.warn(`File content validation failed: ${contentValidation.message}`, {
|
|
66
|
+
declaredType: file.mimetype,
|
|
67
|
+
detectedType: contentValidation.detectedType
|
|
68
|
+
});
|
|
69
|
+
throw new _common.BadRequestException(contentValidation.message || 'File content validation failed');
|
|
70
|
+
}
|
|
71
|
+
// Sanitize filename to prevent path traversal attacks
|
|
72
|
+
file.originalname = _filevalidatorutil.FileValidator.sanitizeFilename(file.originalname);
|
|
57
73
|
}
|
|
58
74
|
/**
|
|
59
75
|
* Get storage provider and config info based on storage config ID
|
|
@@ -67,13 +83,8 @@ let UploadService = class UploadService {
|
|
|
67
83
|
if (!config) {
|
|
68
84
|
throw new _common.NotFoundException('Storage configuration not found');
|
|
69
85
|
}
|
|
70
|
-
// Validate company ownership
|
|
71
|
-
|
|
72
|
-
const configWithCompany = config;
|
|
73
|
-
if (configWithCompany.companyId && configWithCompany.companyId !== user.companyId) {
|
|
74
|
-
throw new _common.BadRequestException('Storage configuration belongs to another company');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
86
|
+
// Validate company ownership using shared utility
|
|
87
|
+
(0, _utils.validateCompanyOwnership)(config, user, this.storageConfigService.isCompanyFeatureEnabled(), 'Storage configuration');
|
|
77
88
|
storageConfig = config;
|
|
78
89
|
} else {
|
|
79
90
|
// Use default config (scoped to user's company/branch)
|
|
@@ -178,9 +189,9 @@ let UploadService = class UploadService {
|
|
|
178
189
|
location,
|
|
179
190
|
storageConfigId: configId
|
|
180
191
|
};
|
|
181
|
-
} catch (
|
|
182
|
-
this.logger
|
|
183
|
-
|
|
192
|
+
} catch (error) {
|
|
193
|
+
_utils.ErrorHandler.logError(this.logger, error, 'uploadSingleFile');
|
|
194
|
+
_utils.ErrorHandler.rethrowError(error);
|
|
184
195
|
}
|
|
185
196
|
}
|
|
186
197
|
async uploadMultipleFiles(files, options, user) {
|
|
@@ -197,31 +208,31 @@ let UploadService = class UploadService {
|
|
|
197
208
|
location,
|
|
198
209
|
storageConfigId: configId
|
|
199
210
|
}));
|
|
200
|
-
} catch (
|
|
201
|
-
this.logger
|
|
202
|
-
|
|
211
|
+
} catch (error) {
|
|
212
|
+
_utils.ErrorHandler.logError(this.logger, error, 'uploadMultipleFiles');
|
|
213
|
+
_utils.ErrorHandler.rethrowError(error);
|
|
203
214
|
}
|
|
204
215
|
}
|
|
205
216
|
async deleteSingleFile(key, storageConfigId, user, locationHint) {
|
|
206
217
|
try {
|
|
207
|
-
if (!key) throw new
|
|
218
|
+
if (!key) throw new _common.BadRequestException('No file path provided');
|
|
208
219
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
209
220
|
await provider.deleteFile(key);
|
|
210
221
|
return true;
|
|
211
|
-
} catch (
|
|
212
|
-
this.logger
|
|
213
|
-
|
|
222
|
+
} catch (error) {
|
|
223
|
+
_utils.ErrorHandler.logError(this.logger, error, 'deleteSingleFile');
|
|
224
|
+
_utils.ErrorHandler.rethrowError(error);
|
|
214
225
|
}
|
|
215
226
|
}
|
|
216
227
|
async deleteMultipleFile(keys, storageConfigId, user, locationHint) {
|
|
217
228
|
try {
|
|
218
|
-
if (!keys || !keys.length) throw new
|
|
229
|
+
if (!keys || !keys.length) throw new _common.BadRequestException('No file paths provided');
|
|
219
230
|
const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
|
|
220
231
|
await provider.deleteMultipleFiles(keys);
|
|
221
232
|
return true;
|
|
222
|
-
} catch (
|
|
223
|
-
this.logger
|
|
224
|
-
|
|
233
|
+
} catch (error) {
|
|
234
|
+
_utils.ErrorHandler.logError(this.logger, error, 'deleteMultipleFiles');
|
|
235
|
+
_utils.ErrorHandler.rethrowError(error);
|
|
225
236
|
}
|
|
226
237
|
}
|
|
227
238
|
bytesToKb(bytes) {
|
|
@@ -251,7 +262,8 @@ let UploadService = class UploadService {
|
|
|
251
262
|
}
|
|
252
263
|
return null;
|
|
253
264
|
} catch (error) {
|
|
254
|
-
|
|
265
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
266
|
+
this.logger.warn(`Failed to get local storage basePath: ${errorMessage}`);
|
|
255
267
|
return null;
|
|
256
268
|
}
|
|
257
269
|
}
|
|
@@ -268,9 +280,9 @@ let UploadService = class UploadService {
|
|
|
268
280
|
}
|
|
269
281
|
// For SFTP or other providers without presigned URLs
|
|
270
282
|
return key;
|
|
271
|
-
} catch (
|
|
272
|
-
this.logger
|
|
273
|
-
|
|
283
|
+
} catch (error) {
|
|
284
|
+
_utils.ErrorHandler.logError(this.logger, error, 'makeFileUrl');
|
|
285
|
+
_utils.ErrorHandler.rethrowError(error);
|
|
274
286
|
}
|
|
275
287
|
}
|
|
276
288
|
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|