@flusys/nestjs-storage 1.0.0-beta → 1.0.0

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 (118) hide show
  1. package/README.md +174 -13
  2. package/cjs/config/index.js +0 -1
  3. package/cjs/config/storage.constants.js +0 -17
  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 +18 -29
  8. package/cjs/docs/storage-swagger.config.js +24 -136
  9. package/cjs/dtos/file-manager.dto.js +71 -35
  10. package/cjs/dtos/folder.dto.js +15 -9
  11. package/cjs/dtos/storage-config.dto.js +25 -66
  12. package/cjs/dtos/upload.dto.js +24 -17
  13. package/cjs/entities/file-manager-with-company.entity.js +3 -4
  14. package/cjs/entities/file-manager.entity.js +71 -3
  15. package/cjs/entities/folder-with-company.entity.js +3 -4
  16. package/cjs/entities/folder.entity.js +19 -3
  17. package/cjs/entities/index.js +9 -10
  18. package/cjs/entities/storage-config-with-company.entity.js +3 -4
  19. package/cjs/entities/storage-config.entity.js +73 -3
  20. package/cjs/interfaces/index.js +0 -1
  21. package/cjs/middlewares/file-serve.middleware.js +113 -100
  22. package/cjs/modules/storage.module.js +82 -136
  23. package/cjs/providers/azure-provider.optional.js +10 -38
  24. package/cjs/providers/local-provider.js +38 -31
  25. package/cjs/providers/s3-provider.optional.js +19 -40
  26. package/cjs/providers/storage-factory.service.js +54 -99
  27. package/cjs/providers/storage-provider.registry.js +8 -18
  28. package/cjs/services/file-manager.service.js +238 -323
  29. package/cjs/services/folder.service.js +8 -11
  30. package/cjs/services/index.js +1 -0
  31. package/cjs/{config → services}/storage-config.service.js +32 -76
  32. package/cjs/services/storage-datasource.provider.js +16 -26
  33. package/cjs/services/storage-provider-config.service.js +21 -38
  34. package/cjs/services/upload.service.js +72 -88
  35. package/cjs/utils/file-validator.util.js +458 -0
  36. package/cjs/utils/image-compressor.util.js +3 -8
  37. package/config/index.d.ts +0 -1
  38. package/config/storage.constants.d.ts +0 -8
  39. package/controllers/upload.controller.d.ts +3 -6
  40. package/dtos/file-manager.dto.d.ts +13 -7
  41. package/dtos/folder.dto.d.ts +5 -5
  42. package/dtos/storage-config.dto.d.ts +13 -14
  43. package/entities/file-manager-with-company.entity.d.ts +2 -2
  44. package/entities/file-manager.entity.d.ts +11 -2
  45. package/entities/folder-with-company.entity.d.ts +2 -2
  46. package/entities/folder.entity.d.ts +4 -2
  47. package/entities/index.d.ts +3 -4
  48. package/entities/storage-config-with-company.entity.d.ts +2 -2
  49. package/entities/storage-config.entity.d.ts +7 -2
  50. package/fesm/config/index.js +0 -1
  51. package/fesm/config/storage.constants.js +0 -8
  52. package/fesm/controllers/file-manager.controller.js +45 -2
  53. package/fesm/controllers/folder.controller.js +45 -2
  54. package/fesm/controllers/storage-config.controller.js +45 -2
  55. package/fesm/controllers/upload.controller.js +19 -30
  56. package/fesm/docs/storage-swagger.config.js +27 -142
  57. package/fesm/dtos/file-manager.dto.js +72 -36
  58. package/fesm/dtos/folder.dto.js +16 -10
  59. package/fesm/dtos/storage-config.dto.js +29 -76
  60. package/fesm/dtos/upload.dto.js +25 -19
  61. package/fesm/entities/file-manager-with-company.entity.js +3 -4
  62. package/fesm/entities/file-manager.entity.js +72 -4
  63. package/fesm/entities/folder-with-company.entity.js +3 -4
  64. package/fesm/entities/folder.entity.js +20 -4
  65. package/fesm/entities/index.js +5 -13
  66. package/fesm/entities/storage-config-with-company.entity.js +3 -4
  67. package/fesm/entities/storage-config.entity.js +74 -4
  68. package/fesm/interfaces/index.js +0 -1
  69. package/fesm/interfaces/storage-config.interface.js +1 -3
  70. package/fesm/middlewares/file-serve.middleware.js +114 -101
  71. package/fesm/modules/storage.module.js +83 -136
  72. package/fesm/providers/azure-provider.optional.js +11 -42
  73. package/fesm/providers/local-provider.js +38 -31
  74. package/fesm/providers/s3-provider.optional.js +20 -44
  75. package/fesm/providers/storage-factory.service.js +52 -97
  76. package/fesm/providers/storage-provider.registry.js +10 -20
  77. package/fesm/services/file-manager.service.js +237 -322
  78. package/fesm/services/folder.service.js +6 -9
  79. package/fesm/services/index.js +1 -0
  80. package/fesm/{config → services}/storage-config.service.js +32 -76
  81. package/fesm/services/storage-datasource.provider.js +16 -26
  82. package/fesm/services/storage-provider-config.service.js +19 -36
  83. package/fesm/services/upload.service.js +71 -87
  84. package/fesm/utils/file-validator.util.js +451 -0
  85. package/fesm/utils/image-compressor.util.js +3 -8
  86. package/interfaces/file-manager.interface.d.ts +7 -4
  87. package/interfaces/index.d.ts +0 -1
  88. package/interfaces/storage-config.interface.d.ts +3 -22
  89. package/interfaces/storage-module-options.interface.d.ts +0 -5
  90. package/middlewares/file-serve.middleware.d.ts +9 -1
  91. package/modules/storage.module.d.ts +1 -2
  92. package/package.json +6 -6
  93. package/providers/azure-provider.optional.d.ts +8 -6
  94. package/providers/local-provider.d.ts +2 -7
  95. package/providers/s3-provider.optional.d.ts +9 -7
  96. package/providers/storage-factory.service.d.ts +8 -9
  97. package/providers/storage-provider.registry.d.ts +4 -4
  98. package/services/file-manager.service.d.ts +23 -16
  99. package/services/folder.service.d.ts +4 -4
  100. package/services/index.d.ts +1 -0
  101. package/services/storage-config.service.d.ts +24 -0
  102. package/services/storage-datasource.provider.d.ts +3 -4
  103. package/services/storage-provider-config.service.d.ts +5 -6
  104. package/services/upload.service.d.ts +5 -5
  105. package/utils/file-validator.util.d.ts +19 -0
  106. package/cjs/entities/file-manager-base.entity.js +0 -115
  107. package/cjs/entities/folder-base.entity.js +0 -55
  108. package/cjs/entities/storage-config-base.entity.js +0 -64
  109. package/cjs/interfaces/file-upload-response.interface.js +0 -4
  110. package/config/storage-config.service.d.ts +0 -22
  111. package/entities/file-manager-base.entity.d.ts +0 -13
  112. package/entities/folder-base.entity.d.ts +0 -5
  113. package/entities/storage-config-base.entity.d.ts +0 -7
  114. package/fesm/entities/file-manager-base.entity.js +0 -108
  115. package/fesm/entities/folder-base.entity.js +0 -48
  116. package/fesm/entities/storage-config-base.entity.js +0 -57
  117. package/fesm/interfaces/file-upload-response.interface.js +0 -1
  118. package/interfaces/file-upload-response.interface.d.ts +0 -6
@@ -25,25 +25,60 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { BadRequestException, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException, Scope } from '@nestjs/common';
29
- import { StorageConfigService } from '../config';
28
+ import { ErrorHandler, validateCompanyOwnership } from '@flusys/nestjs-shared/utils';
29
+ import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
30
+ import { StorageConfigService } from './storage-config.service';
30
31
  import { FileLocationEnum } from '../enums/file-location.enum';
31
32
  import { StorageFactoryService } from '../providers/storage-factory.service';
32
33
  import { StorageProviderConfigService } from './storage-provider-config.service';
34
+ import { FileValidator } from '../utils/file-validator.util';
33
35
  export class UploadService {
34
36
  /**
35
- * Validate file before upload
37
+ * Validate file before upload - includes size, type, and content validation.
38
+ * Uses magic bytes to verify file content matches declared MIME type.
36
39
  */ validateFile(file) {
37
40
  // Validate file size
38
41
  const sizeValidation = this.storageConfigService.validateFileSize(file.size);
39
42
  if (!sizeValidation.valid) {
40
43
  throw new BadRequestException(sizeValidation.message);
41
44
  }
42
- // Validate file type
45
+ // Validate declared file type (MIME)
43
46
  const typeValidation = this.storageConfigService.validateFileType(file.mimetype);
44
47
  if (!typeValidation.valid) {
45
48
  throw new BadRequestException(typeValidation.message);
46
49
  }
50
+ // Validate file content matches declared type (magic bytes check)
51
+ // This prevents MIME type spoofing attacks
52
+ const allowedTypes = this.storageConfigService.getAllowedFileTypes();
53
+ const contentValidation = FileValidator.validateFileContent(file.buffer, file.mimetype, allowedTypes);
54
+ if (!contentValidation.valid) {
55
+ this.logger.warn(`File content validation failed: ${contentValidation.message}`, {
56
+ declaredType: file.mimetype,
57
+ detectedType: contentValidation.detectedType
58
+ });
59
+ throw new BadRequestException(contentValidation.message || 'File content validation failed');
60
+ }
61
+ // Sanitize filename to prevent path traversal attacks
62
+ file.originalname = FileValidator.sanitizeFilename(file.originalname);
63
+ }
64
+ /**
65
+ * Create provider from storage config entity
66
+ */ async createProviderFromConfig(config) {
67
+ return this.storageFactory.createProvider({
68
+ provider: config.storage,
69
+ config: config.config
70
+ });
71
+ }
72
+ /**
73
+ * Create fallback local provider
74
+ */ async createFallbackLocalProvider() {
75
+ this.logger.warn('No storage config found, using fallback local provider');
76
+ return this.storageFactory.createProvider({
77
+ provider: FileLocationEnum.LOCAL,
78
+ config: {
79
+ basePath: this.storageConfigService.getDefaultLocalStoragePath()
80
+ }
81
+ });
47
82
  }
48
83
  /**
49
84
  * Get storage provider and config info based on storage config ID
@@ -57,13 +92,8 @@ export class UploadService {
57
92
  if (!config) {
58
93
  throw new NotFoundException('Storage configuration not found');
59
94
  }
60
- // Validate company ownership if company feature enabled
61
- if (this.storageConfigService.isCompanyFeatureEnabled() && user?.companyId) {
62
- const configWithCompany = config;
63
- if (configWithCompany.companyId && configWithCompany.companyId !== user.companyId) {
64
- throw new BadRequestException('Storage configuration belongs to another company');
65
- }
66
- }
95
+ // Validate company ownership using shared utility
96
+ validateCompanyOwnership(config, user, this.storageConfigService.isCompanyFeatureEnabled(), 'Storage configuration');
67
97
  storageConfig = config;
68
98
  } else {
69
99
  // Use default config (scoped to user's company/branch)
@@ -73,13 +103,7 @@ export class UploadService {
73
103
  }
74
104
  storageConfig = defaultConfig;
75
105
  }
76
- // Create provider config
77
- const providerConfig = {
78
- provider: storageConfig.storage,
79
- config: storageConfig.config
80
- };
81
- // Get or create provider instance
82
- const provider = await this.storageFactory.createProvider(providerConfig);
106
+ const provider = await this.createProviderFromConfig(storageConfig);
83
107
  return {
84
108
  provider,
85
109
  location: storageConfig.storage,
@@ -87,74 +111,33 @@ export class UploadService {
87
111
  };
88
112
  }
89
113
  /**
90
- * Get storage provider based on storage config ID (convenience method)
91
- */ async getStorageProvider(storageConfigId, user) {
92
- const { provider } = await this.getStorageProviderWithConfig(storageConfigId, user);
93
- return provider;
94
- }
95
- /**
96
114
  * Get storage provider for delete operations with fallback
97
115
  * If the original storage config doesn't exist, falls back based on locationHint
98
- * This ensures files can still be deleted even if storage config was removed
99
- * @param storageConfigId - The storage config ID to look up
100
- * @param user - User context for company filtering
101
- * @param locationHint - The file's original location type (used for fallback)
102
116
  */ async getStorageProviderForDelete(storageConfigId, user, locationHint) {
103
- // If storageConfigId provided, try to find it
117
+ // Try to find by storageConfigId
104
118
  if (storageConfigId) {
105
119
  const config = await this.storageProviderConfigService.findByIdDirect(storageConfigId);
106
120
  if (config) {
107
- const providerConfig = {
108
- provider: config.storage,
109
- config: config.config
110
- };
111
- return await this.storageFactory.createProvider(providerConfig);
121
+ return this.createProviderFromConfig(config);
112
122
  }
113
- // Config not found, log warning and try fallback
114
- this.logger.warn(`Storage config ${storageConfigId} not found, trying fallback for delete`);
123
+ this.logger.warn(`Storage config ${storageConfigId} not found, trying fallback`);
115
124
  }
116
- // Fallback: Use locationHint to find a matching config or create provider
125
+ // Fallback: Use locationHint to find a matching config
117
126
  if (locationHint) {
118
- // Try to find a config matching the file's original location type
119
127
  const matchingConfigs = await this.storageProviderConfigService.getConfigByType(locationHint, user);
120
128
  if (matchingConfigs.length > 0) {
121
- const providerConfig = {
122
- provider: matchingConfigs[0].storage,
123
- config: matchingConfigs[0].config
124
- };
125
- this.logger.debug(`Using matching ${locationHint} config for delete fallback`);
126
- return await this.storageFactory.createProvider(providerConfig);
129
+ return this.createProviderFromConfig(matchingConfigs[0]);
127
130
  }
128
- // No matching config found, create a basic provider based on locationHint
129
131
  if (locationHint === FileLocationEnum.LOCAL) {
130
- this.logger.warn('No local config found, using fallback local provider for delete');
131
- const localProviderConfig = {
132
- provider: FileLocationEnum.LOCAL,
133
- config: {
134
- basePath: this.storageConfigService.getDefaultLocalStoragePath()
135
- }
136
- };
137
- return await this.storageFactory.createProvider(localProviderConfig);
132
+ return this.createFallbackLocalProvider();
138
133
  }
139
134
  }
140
- // Last resort: Try default config (might be different provider type)
135
+ // Last resort: Try default config
141
136
  const defaultConfig = await this.storageProviderConfigService.getDefaultConfig(user);
142
137
  if (defaultConfig) {
143
- const providerConfig = {
144
- provider: defaultConfig.storage,
145
- config: defaultConfig.config
146
- };
147
- return await this.storageFactory.createProvider(providerConfig);
138
+ return this.createProviderFromConfig(defaultConfig);
148
139
  }
149
- // Final fallback: Create a basic local provider
150
- this.logger.warn('No storage config found, using fallback local provider for delete');
151
- const localProviderConfig = {
152
- provider: FileLocationEnum.LOCAL,
153
- config: {
154
- basePath: this.storageConfigService.getDefaultLocalStoragePath()
155
- }
156
- };
157
- return await this.storageFactory.createProvider(localProviderConfig);
140
+ return this.createFallbackLocalProvider();
158
141
  }
159
142
  async uploadSingleFile(file, options, user) {
160
143
  try {
@@ -168,9 +151,9 @@ export class UploadService {
168
151
  location,
169
152
  storageConfigId: configId
170
153
  };
171
- } catch (err) {
172
- this.logger.error('Single file upload failed', err);
173
- throw new InternalServerErrorException(err?.message || 'Single file upload failed');
154
+ } catch (error) {
155
+ ErrorHandler.logError(this.logger, error, 'uploadSingleFile');
156
+ ErrorHandler.rethrowError(error);
174
157
  }
175
158
  }
176
159
  async uploadMultipleFiles(files, options, user) {
@@ -187,31 +170,31 @@ export class UploadService {
187
170
  location,
188
171
  storageConfigId: configId
189
172
  }));
190
- } catch (err) {
191
- this.logger.error('Multiple files upload failed', err);
192
- throw new InternalServerErrorException(err?.message || 'Multiple files upload failed');
173
+ } catch (error) {
174
+ ErrorHandler.logError(this.logger, error, 'uploadMultipleFiles');
175
+ ErrorHandler.rethrowError(error);
193
176
  }
194
177
  }
195
178
  async deleteSingleFile(key, storageConfigId, user, locationHint) {
196
179
  try {
197
- if (!key) throw new Error('No file path provided');
180
+ if (!key) throw new BadRequestException('No file path provided');
198
181
  const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
199
182
  await provider.deleteFile(key);
200
183
  return true;
201
- } catch (err) {
202
- this.logger.error('Delete single file failed', err);
203
- throw new InternalServerErrorException(err?.message || 'Delete single file failed');
184
+ } catch (error) {
185
+ ErrorHandler.logError(this.logger, error, 'deleteSingleFile');
186
+ ErrorHandler.rethrowError(error);
204
187
  }
205
188
  }
206
189
  async deleteMultipleFile(keys, storageConfigId, user, locationHint) {
207
190
  try {
208
- if (!keys || !keys.length) throw new Error('No file paths provided');
191
+ if (!keys || !keys.length) throw new BadRequestException('No file paths provided');
209
192
  const provider = await this.getStorageProviderForDelete(storageConfigId, user, locationHint);
210
193
  await provider.deleteMultipleFiles(keys);
211
194
  return true;
212
- } catch (err) {
213
- this.logger.error('Delete multiple files failed', err);
214
- throw new InternalServerErrorException(err?.message || 'Delete multiple files failed');
195
+ } catch (error) {
196
+ ErrorHandler.logError(this.logger, error, 'deleteMultipleFiles');
197
+ ErrorHandler.rethrowError(error);
215
198
  }
216
199
  }
217
200
  bytesToKb(bytes) {
@@ -241,7 +224,8 @@ export class UploadService {
241
224
  }
242
225
  return null;
243
226
  } catch (error) {
244
- this.logger.warn(`Failed to get local storage basePath: ${error.message}`);
227
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
228
+ this.logger.warn(`Failed to get local storage basePath: ${errorMessage}`);
245
229
  return null;
246
230
  }
247
231
  }
@@ -251,16 +235,16 @@ export class UploadService {
251
235
  * For Local/SFTP: returns direct path
252
236
  */ async makeFileUrl(key, storageConfigId, expiresIn = 3600, user) {
253
237
  try {
254
- const provider = await this.getStorageProvider(storageConfigId, user);
238
+ const { provider } = await this.getStorageProviderWithConfig(storageConfigId, user);
255
239
  // Check if provider supports presigned URLs
256
240
  if (provider.generatePresignedUrl) {
257
241
  return await provider.generatePresignedUrl(key, expiresIn);
258
242
  }
259
243
  // For SFTP or other providers without presigned URLs
260
244
  return key;
261
- } catch (err) {
262
- this.logger.error('Generate file URL failed', err);
263
- throw new InternalServerErrorException(err?.message || 'Generate file URL failed');
245
+ } catch (error) {
246
+ ErrorHandler.logError(this.logger, error, 'makeFileUrl');
247
+ ErrorHandler.rethrowError(error);
264
248
  }
265
249
  }
266
250
  // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild