@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.
Files changed (60) hide show
  1. package/README.md +131 -13
  2. package/cjs/config/storage-config.service.js +5 -0
  3. package/cjs/config/storage.constants.js +0 -8
  4. package/cjs/controllers/file-manager.controller.js +44 -1
  5. package/cjs/controllers/folder.controller.js +44 -1
  6. package/cjs/controllers/storage-config.controller.js +44 -1
  7. package/cjs/controllers/upload.controller.js +6 -12
  8. package/cjs/dtos/file-manager.dto.js +8 -5
  9. package/cjs/dtos/storage-config.dto.js +41 -1
  10. package/cjs/dtos/upload.dto.js +7 -0
  11. package/cjs/entities/storage-config-base.entity.js +31 -2
  12. package/cjs/interfaces/index.js +0 -1
  13. package/cjs/middlewares/file-serve.middleware.js +6 -0
  14. package/cjs/providers/local-provider.js +52 -2
  15. package/cjs/providers/storage-factory.service.js +2 -2
  16. package/cjs/services/file-manager.service.js +37 -24
  17. package/cjs/services/folder.service.js +5 -8
  18. package/cjs/services/storage-provider-config.service.js +18 -35
  19. package/cjs/services/upload.service.js +39 -27
  20. package/cjs/utils/file-validator.util.js +470 -0
  21. package/cjs/utils/image-compressor.util.js +1 -3
  22. package/config/storage-config.service.d.ts +5 -2
  23. package/config/storage.constants.d.ts +0 -2
  24. package/controllers/upload.controller.d.ts +2 -6
  25. package/dtos/file-manager.dto.d.ts +2 -4
  26. package/dtos/folder.dto.d.ts +2 -4
  27. package/dtos/storage-config.dto.d.ts +9 -6
  28. package/entities/storage-config-base.entity.d.ts +2 -0
  29. package/fesm/config/storage-config.service.js +5 -0
  30. package/fesm/config/storage.constants.js +0 -2
  31. package/fesm/controllers/file-manager.controller.js +45 -2
  32. package/fesm/controllers/folder.controller.js +45 -2
  33. package/fesm/controllers/storage-config.controller.js +45 -2
  34. package/fesm/controllers/upload.controller.js +7 -13
  35. package/fesm/dtos/file-manager.dto.js +8 -5
  36. package/fesm/dtos/storage-config.dto.js +45 -11
  37. package/fesm/dtos/upload.dto.js +8 -1
  38. package/fesm/entities/index.js +1 -5
  39. package/fesm/entities/storage-config-base.entity.js +33 -7
  40. package/fesm/interfaces/index.js +0 -1
  41. package/fesm/interfaces/storage-config.interface.js +1 -3
  42. package/fesm/middlewares/file-serve.middleware.js +7 -1
  43. package/fesm/providers/local-provider.js +52 -2
  44. package/fesm/providers/storage-factory.service.js +2 -2
  45. package/fesm/services/file-manager.service.js +38 -25
  46. package/fesm/services/folder.service.js +5 -8
  47. package/fesm/services/storage-provider-config.service.js +18 -35
  48. package/fesm/services/upload.service.js +40 -28
  49. package/fesm/utils/file-validator.util.js +463 -0
  50. package/fesm/utils/image-compressor.util.js +1 -3
  51. package/interfaces/file-manager.interface.d.ts +7 -4
  52. package/interfaces/index.d.ts +0 -1
  53. package/interfaces/storage-config.interface.d.ts +2 -20
  54. package/package.json +6 -6
  55. package/providers/local-provider.d.ts +2 -0
  56. package/services/file-manager.service.d.ts +2 -2
  57. package/utils/file-validator.util.d.ts +16 -0
  58. package/cjs/interfaces/file-upload-response.interface.js +0 -4
  59. package/fesm/interfaces/file-upload-response.interface.js +0 -1
  60. 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 (_error) {
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
- return _path.resolve(key);
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 originalMessage = error?.message || 'Unknown error';
134
- throw new Error(`Failed to initialize storage provider '${config.provider}': ${originalMessage}`);
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: dto.storageConfigId
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
- storageLocation = storageConfig.storage;
170
+ const typedConfig = storageConfig;
171
+ if (typedConfig.storage) {
172
+ storageLocation = typedConfig.storage;
173
+ }
169
174
  }
170
175
  } catch (error) {
171
- this.logger.warn(`Failed to get storage location from config: ${error}`);
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
- fileManager = {
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 they exist on the entity (when company feature is enabled)
183
- if ('companyId' in fileManager) {
184
- fileManager.companyId = user?.companyId ?? null;
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 fileManager;
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
- this.logger.warn(`Failed to fetch provider names: ${error}`);
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
- // If company feature enabled and user has companyId, filter by user's company
309
+ // Apply company filter using shared utility
302
310
  const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
303
- if (enableCompanyFeature && user?.companyId) {
304
- query.andWhere('file_manager.companyId = :companyId', {
305
- companyId: user.companyId
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
- if (config && config.config?.basePath) {
371
- const basePath = config.config.basePath.replace(/^\.\//, '');
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
- this.logger.warn(`Failed to get basePath for delete: ${error}`);
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
- this.logger.error(`Failed to generate URL for file ${file.id}: ${error?.message || 'Unknown error'}`);
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
- if (config && config.config?.basePath) {
443
- const basePath = config.config.basePath.replace(/^\.\//, ''); // Remove leading ./
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
- this.logger.warn(`Failed to get basePath for file ${file.id}: ${error}`);
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
- this.logger.error(`Failed to save updated file URLs: ${error?.message || 'Unknown error'}`);
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
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
83
- query.andWhere('folder.companyId = :companyId', {
84
- companyId: user.companyId
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
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
84
- query.andWhere('storageConfig.companyId = :companyId', {
85
- companyId: user.companyId
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
- // Filter by company only if company feature is enabled and user is provided
110
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
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
- name: 'default'
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
- // Filter by company only if company feature is enabled and user is provided
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 if company feature enabled
71
- if (this.storageConfigService.isCompanyFeatureEnabled() && user?.companyId) {
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 (err) {
182
- this.logger.error('Single file upload failed', err);
183
- throw new _common.InternalServerErrorException(err?.message || 'Single file upload failed');
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 (err) {
201
- this.logger.error('Multiple files upload failed', err);
202
- throw new _common.InternalServerErrorException(err?.message || 'Multiple files upload failed');
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 Error('No file path provided');
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 (err) {
212
- this.logger.error('Delete single file failed', err);
213
- throw new _common.InternalServerErrorException(err?.message || 'Delete single file failed');
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 Error('No file paths provided');
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 (err) {
223
- this.logger.error('Delete multiple files failed', err);
224
- throw new _common.InternalServerErrorException(err?.message || 'Delete multiple files failed');
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
- this.logger.warn(`Failed to get local storage basePath: ${error.message}`);
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 (err) {
272
- this.logger.error('Generate file URL failed', err);
273
- throw new _common.InternalServerErrorException(err?.message || 'Generate file URL failed');
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