@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.
- package/README.md +131 -19
- package/cjs/config/storage-config.service.js +5 -0
- package/cjs/config/storage.constants.js +0 -8
- package/cjs/controllers/file-manager.controller.js +50 -5
- package/cjs/controllers/folder.controller.js +46 -4
- package/cjs/controllers/storage-config.controller.js +46 -4
- 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/modules/storage.module.js +2 -4
- 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 +18 -52
- package/cjs/services/storage-datasource.provider.js +10 -16
- package/cjs/services/storage-provider-config.service.js +28 -63
- 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/file-manager.controller.d.ts +1 -1
- 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 +51 -6
- package/fesm/controllers/folder.controller.js +49 -7
- package/fesm/controllers/storage-config.controller.js +49 -7
- 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/modules/storage.module.js +2 -4
- 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 +19 -53
- package/fesm/services/storage-datasource.provider.js +10 -16
- package/fesm/services/storage-provider-config.service.js +28 -63
- 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/services/folder.service.d.ts +1 -2
- package/services/storage-provider-config.service.d.ts +1 -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
|
@@ -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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
57
|
+
return entity;
|
|
70
58
|
}
|
|
71
59
|
async getSelectQuery(query, _user, select) {
|
|
72
|
-
if (!select
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|