@flusys/nestjs-storage 0.1.0-beta.3 → 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 (67) hide show
  1. package/README.md +131 -19
  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 +50 -5
  5. package/cjs/controllers/folder.controller.js +46 -4
  6. package/cjs/controllers/storage-config.controller.js +46 -4
  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/modules/storage.module.js +2 -4
  15. package/cjs/providers/local-provider.js +52 -2
  16. package/cjs/providers/storage-factory.service.js +2 -2
  17. package/cjs/services/file-manager.service.js +37 -24
  18. package/cjs/services/folder.service.js +18 -52
  19. package/cjs/services/storage-datasource.provider.js +10 -16
  20. package/cjs/services/storage-provider-config.service.js +28 -63
  21. package/cjs/services/upload.service.js +39 -27
  22. package/cjs/utils/file-validator.util.js +470 -0
  23. package/cjs/utils/image-compressor.util.js +1 -3
  24. package/config/storage-config.service.d.ts +5 -2
  25. package/config/storage.constants.d.ts +0 -2
  26. package/controllers/file-manager.controller.d.ts +1 -1
  27. package/controllers/upload.controller.d.ts +2 -6
  28. package/dtos/file-manager.dto.d.ts +2 -4
  29. package/dtos/folder.dto.d.ts +2 -4
  30. package/dtos/storage-config.dto.d.ts +9 -6
  31. package/entities/storage-config-base.entity.d.ts +2 -0
  32. package/fesm/config/storage-config.service.js +5 -0
  33. package/fesm/config/storage.constants.js +0 -2
  34. package/fesm/controllers/file-manager.controller.js +51 -6
  35. package/fesm/controllers/folder.controller.js +49 -7
  36. package/fesm/controllers/storage-config.controller.js +49 -7
  37. package/fesm/controllers/upload.controller.js +7 -13
  38. package/fesm/dtos/file-manager.dto.js +8 -5
  39. package/fesm/dtos/storage-config.dto.js +45 -11
  40. package/fesm/dtos/upload.dto.js +8 -1
  41. package/fesm/entities/index.js +1 -5
  42. package/fesm/entities/storage-config-base.entity.js +33 -7
  43. package/fesm/interfaces/index.js +0 -1
  44. package/fesm/interfaces/storage-config.interface.js +1 -3
  45. package/fesm/middlewares/file-serve.middleware.js +7 -1
  46. package/fesm/modules/storage.module.js +2 -4
  47. package/fesm/providers/local-provider.js +52 -2
  48. package/fesm/providers/storage-factory.service.js +2 -2
  49. package/fesm/services/file-manager.service.js +38 -25
  50. package/fesm/services/folder.service.js +19 -53
  51. package/fesm/services/storage-datasource.provider.js +10 -16
  52. package/fesm/services/storage-provider-config.service.js +28 -63
  53. package/fesm/services/upload.service.js +40 -28
  54. package/fesm/utils/file-validator.util.js +463 -0
  55. package/fesm/utils/image-compressor.util.js +1 -3
  56. package/interfaces/file-manager.interface.d.ts +7 -4
  57. package/interfaces/index.d.ts +0 -1
  58. package/interfaces/storage-config.interface.d.ts +2 -20
  59. package/package.json +6 -6
  60. package/providers/local-provider.d.ts +2 -0
  61. package/services/file-manager.service.d.ts +2 -2
  62. package/services/folder.service.d.ts +1 -2
  63. package/services/storage-provider-config.service.d.ts +1 -2
  64. package/utils/file-validator.util.d.ts +16 -0
  65. package/cjs/interfaces/file-upload-response.interface.js +0 -4
  66. package/fesm/interfaces/file-upload-response.interface.js +0 -1
  67. package/interfaces/file-upload-response.interface.d.ts +0 -6
@@ -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");
@@ -42,73 +43,51 @@ function _ts_param(paramIndex, decorator) {
42
43
  };
43
44
  }
44
45
  let StorageProviderConfigService = class StorageProviderConfigService extends _classes.RequestScopedApiService {
45
- /**
46
- * Resolve entity class for this service
47
- * @returns StorageConfig or StorageConfigWithCompany based on configuration
48
- */ resolveEntity() {
49
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
50
- return enableCompanyFeature ? _entities.StorageConfigWithCompany : _entities.StorageConfig;
46
+ resolveEntity() {
47
+ return this.storageConfig.isCompanyFeatureEnabled() ? _entities.StorageConfigWithCompany : _entities.StorageConfig;
51
48
  }
52
- /**
53
- * Get DataSource provider for this service
54
- * @returns StorageDataSourceProvider instance
55
- */ getDataSourceProvider() {
49
+ getDataSourceProvider() {
56
50
  return this.dataSourceProvider;
57
51
  }
58
52
  async convertSingleDtoToEntity(dto, user) {
59
- let storageConfig = {};
60
- // Set basic fields
61
- storageConfig = {
62
- ...storageConfig,
63
- ...dto
64
- };
65
- // Only set company fields if they exist on the entity (when company feature is enabled)
66
- if ('companyId' in storageConfig) {
67
- storageConfig.companyId = user?.companyId ?? null;
53
+ const entity = await super.convertSingleDtoToEntity(dto, user);
54
+ if (this.storageConfig.isCompanyFeatureEnabled()) {
55
+ entity.companyId = user?.companyId ?? null;
68
56
  }
69
- return storageConfig;
57
+ return entity;
70
58
  }
71
59
  async getSelectQuery(query, _user, select) {
72
- if (!select || !select.length) {
60
+ if (!select?.length) {
73
61
  select = [
74
62
  'id',
75
63
  'name',
76
64
  'storage',
77
65
  'config',
66
+ 'isActive',
67
+ 'isDefault',
78
68
  'createdAt',
79
69
  'updatedAt'
80
70
  ];
81
- // Add company fields if company feature is enabled
82
71
  if (this.storageConfig.isCompanyFeatureEnabled()) {
83
72
  select.push('companyId');
84
73
  }
85
74
  }
86
- const selectFields = select.map((field)=>`${this.entityName}.${field}`);
87
- query.select(selectFields);
75
+ query.select(select.map((f)=>`${this.entityName}.${f}`));
88
76
  return {
89
77
  query,
90
78
  isRaw: false
91
79
  };
92
80
  }
93
- /**
94
- * Override: Extra query manipulation - Auto-filter by user's company
95
- */ async getExtraManipulateQuery(query, filterDto, user) {
81
+ async getExtraManipulateQuery(query, filterDto, user) {
96
82
  const result = await super.getExtraManipulateQuery(query, filterDto, user);
97
- // If company feature enabled and user has companyId, filter by user's company
98
- const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
99
- if (enableCompanyFeature && user?.companyId) {
100
- query.andWhere('storageConfig.companyId = :companyId', {
101
- companyId: user.companyId
102
- });
103
- }
83
+ (0, _utils.applyCompanyFilter)(query, {
84
+ isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
85
+ entityAlias: 'storageConfig'
86
+ }, user);
104
87
  query.orderBy(`${this.entityName}.createdAt`, 'DESC');
105
88
  return result;
106
89
  }
107
- /**
108
- * Find storage config by ID without throwing (returns null if not found)
109
- * Uses direct repository query - bypasses company filtering
110
- * Use for internal operations like file deletion where config ID is already known
111
- */ async findByIdDirect(id) {
90
+ async findByIdDirect(id) {
112
91
  await this.ensureRepositoryInitialized();
113
92
  return await this.repository.findOne({
114
93
  where: {
@@ -116,21 +95,15 @@ let StorageProviderConfigService = class StorageProviderConfigService extends _c
116
95
  }
117
96
  });
118
97
  }
119
- /**
120
- * Get default storage configuration (scoped to user's company if enabled)
121
- * Falls back to any available config if 'default' not found
122
- */ async getDefaultConfig(user) {
98
+ async getDefaultConfig(user) {
123
99
  await this.ensureRepositoryInitialized();
124
- const baseWhere = {};
125
- // Filter by company only if company feature is enabled and user is provided
126
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
127
- baseWhere.companyId = user.companyId;
128
- }
129
- // First try to find config named 'default'
100
+ const baseWhere = (0, _utils.buildCompanyWhereCondition)({
101
+ isActive: true
102
+ }, this.storageConfig.isCompanyFeatureEnabled(), user);
130
103
  const defaultConfig = await this.repository.findOne({
131
104
  where: {
132
105
  ...baseWhere,
133
- name: 'default'
106
+ isDefault: true
134
107
  },
135
108
  order: {
136
109
  createdAt: 'ASC'
@@ -139,7 +112,6 @@ let StorageProviderConfigService = class StorageProviderConfigService extends _c
139
112
  if (defaultConfig) {
140
113
  return defaultConfig;
141
114
  }
142
- // Fall back to any available config for this company/user
143
115
  return await this.repository.findOne({
144
116
  where: baseWhere,
145
117
  order: {
@@ -147,24 +119,17 @@ let StorageProviderConfigService = class StorageProviderConfigService extends _c
147
119
  }
148
120
  });
149
121
  }
150
- /**
151
- * Get storage configuration by type (scoped to user's company if enabled)
152
- */ async getConfigByType(storage, user) {
122
+ async getConfigByType(storage, user) {
153
123
  await this.ensureRepositoryInitialized();
154
- const where = {
155
- storage
156
- };
157
- // Filter by company only if company feature is enabled and user is provided
158
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
159
- where.companyId = user.companyId;
160
- }
124
+ const where = (0, _utils.buildCompanyWhereCondition)({
125
+ storage,
126
+ isActive: true
127
+ }, this.storageConfig.isCompanyFeatureEnabled(), user);
161
128
  return await this.repository.find({
162
129
  where
163
130
  });
164
131
  }
165
- // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
166
132
  constructor(cacheManager, utilsService, storageConfig, dataSourceProvider){
167
- // Repository will be set asynchronously by RequestScopedApiService
168
133
  super('storageConfig', null, cacheManager, utilsService, StorageProviderConfigService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "storageConfig", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.storageConfig = storageConfig, this.dataSourceProvider = dataSourceProvider;
169
134
  }
170
135
  };
@@ -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