@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
|
@@ -32,6 +32,29 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
32
32
|
* Uses Node.js built-in fs module - no external dependencies
|
|
33
33
|
*/ export class LocalProvider {
|
|
34
34
|
/**
|
|
35
|
+
* SECURITY: Validates that a target path does not escape the base directory
|
|
36
|
+
* Prevents path traversal attacks using ../ sequences
|
|
37
|
+
* @throws Error if path traversal is detected
|
|
38
|
+
*/ validatePathWithinBase(targetPath) {
|
|
39
|
+
const normalizedBasePath = path.resolve(this.basePath);
|
|
40
|
+
const normalizedTargetPath = path.resolve(targetPath);
|
|
41
|
+
if (!normalizedTargetPath.startsWith(normalizedBasePath + path.sep) && normalizedTargetPath !== normalizedBasePath) {
|
|
42
|
+
this.logger.warn(`Path traversal attempt detected: ${targetPath}`);
|
|
43
|
+
throw new Error('Invalid path: Path traversal attempt detected');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* SECURITY: Validates file key format to prevent malicious input
|
|
48
|
+
* @throws Error if key contains suspicious patterns
|
|
49
|
+
*/ validateKeyFormat(key) {
|
|
50
|
+
if (!key || typeof key !== 'string' || key.trim().length === 0) {
|
|
51
|
+
throw new Error('Invalid file key: empty or invalid');
|
|
52
|
+
}
|
|
53
|
+
if (key.includes('\0')) {
|
|
54
|
+
throw new Error('Invalid file key: contains null bytes');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
35
58
|
* Initialize Local File System provider with configuration
|
|
36
59
|
* @param config.basePath - Base path for file storage (default: './uploads')
|
|
37
60
|
* @param config.baseUrl - Optional base URL for generating file URLs
|
|
@@ -64,7 +87,11 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
64
87
|
}
|
|
65
88
|
// Build file path
|
|
66
89
|
const folderPath = options.folderPath ? path.join(this.basePath, options.folderPath) : this.basePath;
|
|
90
|
+
// SECURITY: Validate path does not escape base directory
|
|
91
|
+
this.validatePathWithinBase(folderPath);
|
|
67
92
|
const filePath = path.join(folderPath, fileName);
|
|
93
|
+
// SECURITY: Double-check final file path
|
|
94
|
+
this.validatePathWithinBase(filePath);
|
|
68
95
|
// Ensure directory exists
|
|
69
96
|
await fs.mkdir(folderPath, {
|
|
70
97
|
recursive: true
|
|
@@ -87,14 +114,20 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
87
114
|
}
|
|
88
115
|
async deleteFile(key) {
|
|
89
116
|
try {
|
|
117
|
+
// SECURITY: Validate key format first
|
|
118
|
+
this.validateKeyFormat(key);
|
|
90
119
|
// Key now includes the basePath, resolve from cwd
|
|
91
120
|
let filePath = path.resolve(key);
|
|
121
|
+
// SECURITY: Validate resolved path is within base directory
|
|
122
|
+
this.validatePathWithinBase(filePath);
|
|
92
123
|
// Check if file exists at the resolved path
|
|
93
124
|
try {
|
|
94
125
|
await fs.access(filePath);
|
|
95
126
|
} catch {
|
|
96
127
|
// Fallback: try with basePath prefix (for old keys without basePath)
|
|
97
128
|
const fallbackPath = path.join(this.basePath, key);
|
|
129
|
+
// SECURITY: Validate fallback path as well
|
|
130
|
+
this.validatePathWithinBase(fallbackPath);
|
|
98
131
|
try {
|
|
99
132
|
await fs.access(fallbackPath);
|
|
100
133
|
filePath = fallbackPath;
|
|
@@ -107,7 +140,11 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
107
140
|
}
|
|
108
141
|
await fs.unlink(filePath);
|
|
109
142
|
this.logger.log(`Deleted file from local storage: ${key}`);
|
|
110
|
-
} catch (
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// Re-throw security errors
|
|
145
|
+
if (error instanceof Error && error.message.includes('Invalid')) {
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
111
148
|
this.logger.warn(`Failed to delete file from local storage: ${key}`);
|
|
112
149
|
}
|
|
113
150
|
}
|
|
@@ -138,14 +175,23 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
138
175
|
* Get the absolute path for a file key
|
|
139
176
|
* Key now includes basePath, so resolve from cwd
|
|
140
177
|
*/ getAbsolutePath(key) {
|
|
141
|
-
|
|
178
|
+
// SECURITY: Validate key format
|
|
179
|
+
this.validateKeyFormat(key);
|
|
180
|
+
const filePath = path.resolve(key);
|
|
181
|
+
// SECURITY: Validate path is within base directory
|
|
182
|
+
this.validatePathWithinBase(filePath);
|
|
183
|
+
return filePath;
|
|
142
184
|
}
|
|
143
185
|
/**
|
|
144
186
|
* Check if a file exists
|
|
145
187
|
*/ async fileExists(key) {
|
|
146
188
|
try {
|
|
189
|
+
// SECURITY: Validate key format
|
|
190
|
+
this.validateKeyFormat(key);
|
|
147
191
|
// Key now includes basePath, resolve from cwd
|
|
148
192
|
const filePath = path.resolve(key);
|
|
193
|
+
// SECURITY: Validate path is within base directory
|
|
194
|
+
this.validatePathWithinBase(filePath);
|
|
149
195
|
await fs.access(filePath);
|
|
150
196
|
return true;
|
|
151
197
|
} catch {
|
|
@@ -155,8 +201,12 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
155
201
|
/**
|
|
156
202
|
* Get file stats
|
|
157
203
|
*/ async getFileStats(key) {
|
|
204
|
+
// SECURITY: Validate key format
|
|
205
|
+
this.validateKeyFormat(key);
|
|
158
206
|
// Key now includes basePath, resolve from cwd
|
|
159
207
|
const filePath = path.resolve(key);
|
|
208
|
+
// SECURITY: Validate path is within base directory
|
|
209
|
+
this.validatePathWithinBase(filePath);
|
|
160
210
|
const stats = await fs.stat(filePath);
|
|
161
211
|
return {
|
|
162
212
|
size: stats.size,
|
|
@@ -79,8 +79,8 @@ export class StorageFactoryService {
|
|
|
79
79
|
} catch (error) {
|
|
80
80
|
this.logger.error(`Failed to create provider ${config.provider}:`, error);
|
|
81
81
|
// Preserve original error message for better debugging
|
|
82
|
-
const
|
|
83
|
-
throw new Error(`Failed to initialize storage provider '${config.provider}': ${
|
|
82
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
83
|
+
throw new Error(`Failed to initialize storage provider '${config.provider}': ${errorMessage}`);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
@@ -25,8 +25,9 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import {
|
|
28
|
+
import { HybridCache, RequestScopedApiService } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
+
import { applyCompanyFilter, ErrorHandler } from '@flusys/nestjs-shared/utils';
|
|
30
31
|
import { BadRequestException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
|
|
31
32
|
import { Brackets, In } from 'typeorm';
|
|
32
33
|
import { StorageConfigService } from '../config';
|
|
@@ -103,8 +104,9 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
103
104
|
const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
|
|
104
105
|
const storageConfigEntity = enableCompanyFeature ? (await import('../entities')).StorageConfigWithCompany : (await import('../entities')).StorageConfig;
|
|
105
106
|
const storageConfigRepository = await this.dataSourceProvider.getRepository(storageConfigEntity);
|
|
107
|
+
const storageConfigId = dto.storageConfigId;
|
|
106
108
|
const whereCondition = {
|
|
107
|
-
id:
|
|
109
|
+
id: storageConfigId
|
|
108
110
|
};
|
|
109
111
|
// Filter by company if company feature is enabled
|
|
110
112
|
if (enableCompanyFeature && user?.companyId) {
|
|
@@ -114,25 +116,30 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
114
116
|
where: whereCondition
|
|
115
117
|
});
|
|
116
118
|
if (storageConfig) {
|
|
117
|
-
|
|
119
|
+
const typedConfig = storageConfig;
|
|
120
|
+
if (typedConfig.storage) {
|
|
121
|
+
storageLocation = typedConfig.storage;
|
|
122
|
+
}
|
|
118
123
|
}
|
|
119
124
|
} catch (error) {
|
|
120
|
-
|
|
125
|
+
const errorMessage = ErrorHandler.getErrorMessage(error);
|
|
126
|
+
this.logger.warn(`Failed to get storage location from config ${dto.storageConfigId}: ${errorMessage}`);
|
|
121
127
|
// Fall back to DTO location or default
|
|
122
128
|
}
|
|
123
129
|
}
|
|
124
|
-
// Set basic fields
|
|
125
|
-
|
|
130
|
+
// Set basic fields - merge existing data with DTO
|
|
131
|
+
const mergedFileManager = {
|
|
126
132
|
...fileManager,
|
|
127
133
|
...dto,
|
|
128
134
|
location: storageLocation,
|
|
129
135
|
folder: validatedFolder
|
|
130
136
|
};
|
|
131
|
-
// Only set company fields if
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
// Only set company fields if company feature is enabled
|
|
138
|
+
const enableCompanyFeatureForEntity = this.storageConfig.isCompanyFeatureEnabled();
|
|
139
|
+
if (enableCompanyFeatureForEntity) {
|
|
140
|
+
mergedFileManager.companyId = user?.companyId ?? null;
|
|
134
141
|
}
|
|
135
|
-
return
|
|
142
|
+
return mergedFileManager;
|
|
136
143
|
}
|
|
137
144
|
async getSelectQuery(query, _user, select) {
|
|
138
145
|
if (!select || !select.length) {
|
|
@@ -209,7 +216,8 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
209
216
|
this.logger.debug(`enrichWithProviderNames: First enriched item: ${JSON.stringify(enrichedItems[0])}`);
|
|
210
217
|
return enrichedItems;
|
|
211
218
|
} catch (error) {
|
|
212
|
-
|
|
219
|
+
const errorMessage = ErrorHandler.getErrorMessage(error);
|
|
220
|
+
this.logger.warn(`Failed to fetch provider names: ${errorMessage}`);
|
|
213
221
|
return items.map((item)=>({
|
|
214
222
|
...item,
|
|
215
223
|
providerName: undefined
|
|
@@ -247,13 +255,12 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
247
255
|
* Override: Extra query manipulation - Auto-filter by user's company and private file permissions
|
|
248
256
|
*/ async getExtraManipulateQuery(query, filterDto, user) {
|
|
249
257
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
250
|
-
//
|
|
258
|
+
// Apply company filter using shared utility
|
|
251
259
|
const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
260
|
+
applyCompanyFilter(query, {
|
|
261
|
+
isCompanyFeatureEnabled: enableCompanyFeature,
|
|
262
|
+
entityAlias: 'file_manager'
|
|
263
|
+
}, user);
|
|
257
264
|
// Check if user has permission to see private files
|
|
258
265
|
if (user) {
|
|
259
266
|
const cacheKey = enableCompanyFeature ? `${USER_ACTION_PERMISSION_CACHE_KEY}_${user.id}_${user.companyId}` : `${USER_ACTION_PERMISSION_CACHE_KEY}_${user.id}`;
|
|
@@ -316,8 +323,9 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
316
323
|
'config'
|
|
317
324
|
]
|
|
318
325
|
});
|
|
319
|
-
|
|
320
|
-
|
|
326
|
+
const typedConfig = config;
|
|
327
|
+
if (typedConfig?.config?.basePath) {
|
|
328
|
+
const basePath = typedConfig.config.basePath.replace(/^\.\//, '');
|
|
321
329
|
// Convert old keys to new format
|
|
322
330
|
deleteKeys = keys.map((key)=>{
|
|
323
331
|
if (!key.includes('/')) {
|
|
@@ -327,7 +335,8 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
327
335
|
});
|
|
328
336
|
}
|
|
329
337
|
} catch (error) {
|
|
330
|
-
|
|
338
|
+
const errorMessage = ErrorHandler.getErrorMessage(error);
|
|
339
|
+
this.logger.warn(`Failed to get basePath for delete: ${errorMessage}`);
|
|
331
340
|
}
|
|
332
341
|
}
|
|
333
342
|
await this.uploadService.deleteMultipleFile(deleteKeys, configId === 'default' ? undefined : configId, user ?? undefined, location);
|
|
@@ -361,7 +370,8 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
361
370
|
file.expiresAt = now + expiresIn * 1000;
|
|
362
371
|
shouldUpdate = true;
|
|
363
372
|
} catch (error) {
|
|
364
|
-
|
|
373
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
374
|
+
this.logger.error(`Failed to generate URL for file ${file.id}: ${errorMessage}`);
|
|
365
375
|
// Use fallback URL with appUrl from config
|
|
366
376
|
const baseUrl = this.getFileBaseUrl(protocol, host);
|
|
367
377
|
file.url = `${baseUrl}/storage/upload/file/${file.key}`;
|
|
@@ -388,13 +398,15 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
388
398
|
'config'
|
|
389
399
|
]
|
|
390
400
|
});
|
|
391
|
-
|
|
392
|
-
|
|
401
|
+
const typedConfig = config;
|
|
402
|
+
if (typedConfig?.config?.basePath) {
|
|
403
|
+
const basePath = typedConfig.config.basePath.replace(/^\.\//, ''); // Remove leading ./
|
|
393
404
|
fileKey = `${basePath}/${file.key}`;
|
|
394
405
|
this.logger.debug(`Prefixed old key with basePath: ${fileKey}`);
|
|
395
406
|
}
|
|
396
407
|
} catch (error) {
|
|
397
|
-
|
|
408
|
+
const errorMessage = ErrorHandler.getErrorMessage(error);
|
|
409
|
+
this.logger.warn(`Failed to get basePath for file ${file.id}: ${errorMessage}`);
|
|
398
410
|
}
|
|
399
411
|
}
|
|
400
412
|
const baseUrl = this.getFileBaseUrl(protocol, host);
|
|
@@ -419,7 +431,8 @@ export class FileManagerService extends RequestScopedApiService {
|
|
|
419
431
|
try {
|
|
420
432
|
await this.repository.save(updatedFiles);
|
|
421
433
|
} catch (error) {
|
|
422
|
-
|
|
434
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
435
|
+
this.logger.error(`Failed to save updated file URLs: ${errorMessage}`);
|
|
423
436
|
}
|
|
424
437
|
}
|
|
425
438
|
return responses;
|
|
@@ -27,51 +27,27 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
-
import {
|
|
30
|
+
import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
|
|
31
|
+
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
32
|
import { StorageConfigService } from '../config';
|
|
32
33
|
import { Folder, FolderWithCompany } from '../entities';
|
|
33
34
|
import { StorageDataSourceProvider } from './storage-datasource.provider';
|
|
34
35
|
export class FolderService extends RequestScopedApiService {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* @returns Folder or FolderWithCompany based on configuration
|
|
38
|
-
*/ resolveEntity() {
|
|
39
|
-
const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
|
|
40
|
-
return enableCompanyFeature ? FolderWithCompany : Folder;
|
|
36
|
+
resolveEntity() {
|
|
37
|
+
return this.storageConfig.isCompanyFeatureEnabled() ? FolderWithCompany : Folder;
|
|
41
38
|
}
|
|
42
|
-
|
|
43
|
-
* Get DataSource provider for this service
|
|
44
|
-
* @returns StorageDataSourceProvider instance
|
|
45
|
-
*/ getDataSourceProvider() {
|
|
39
|
+
getDataSourceProvider() {
|
|
46
40
|
return this.dataSourceProvider;
|
|
47
41
|
}
|
|
48
42
|
async convertSingleDtoToEntity(dto, user) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const dbData = await this.repository.findOne({
|
|
53
|
-
where: {
|
|
54
|
-
id: dto.id
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
if (!dbData) {
|
|
58
|
-
throw new NotFoundException('No such entity data found for update! Please, Try Again.');
|
|
59
|
-
}
|
|
60
|
-
folder = dbData;
|
|
61
|
-
}
|
|
62
|
-
// Set company/branch IDs if company feature is enabled
|
|
63
|
-
folder = {
|
|
64
|
-
...folder,
|
|
65
|
-
...dto
|
|
66
|
-
};
|
|
67
|
-
// Only set company fields if they exist on the entity (when company feature is enabled)
|
|
68
|
-
if ('companyId' in folder) {
|
|
69
|
-
folder.companyId = user?.companyId ?? null;
|
|
43
|
+
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
44
|
+
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
45
|
+
entity.companyId = user?.companyId ?? null;
|
|
70
46
|
}
|
|
71
|
-
return
|
|
47
|
+
return entity;
|
|
72
48
|
}
|
|
73
49
|
async getSelectQuery(query, _user, select) {
|
|
74
|
-
if (!select
|
|
50
|
+
if (!select?.length) {
|
|
75
51
|
select = [
|
|
76
52
|
'id',
|
|
77
53
|
'name',
|
|
@@ -79,35 +55,25 @@ export class FolderService extends RequestScopedApiService {
|
|
|
79
55
|
'createdAt',
|
|
80
56
|
'deletedAt'
|
|
81
57
|
];
|
|
58
|
+
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
59
|
+
select.push('companyId');
|
|
60
|
+
}
|
|
82
61
|
}
|
|
83
|
-
|
|
84
|
-
// Add company context fields if company feature is enabled
|
|
85
|
-
// The entity will have these fields only if company feature is enabled
|
|
86
|
-
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
87
|
-
selectFields.push('folder.companyId');
|
|
88
|
-
}
|
|
89
|
-
query.select(selectFields);
|
|
62
|
+
query.select(select.map((f)=>`${this.entityName}.${f}`));
|
|
90
63
|
return {
|
|
91
64
|
query,
|
|
92
65
|
isRaw: false
|
|
93
66
|
};
|
|
94
67
|
}
|
|
95
|
-
|
|
96
|
-
* Override: Extra query manipulation - Auto-filter by user's company
|
|
97
|
-
*/ async getExtraManipulateQuery(query, filterDto, user) {
|
|
68
|
+
async getExtraManipulateQuery(query, filterDto, user) {
|
|
98
69
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
companyId: user.companyId
|
|
104
|
-
});
|
|
105
|
-
}
|
|
70
|
+
applyCompanyFilter(query, {
|
|
71
|
+
isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
|
|
72
|
+
entityAlias: 'folder'
|
|
73
|
+
}, user);
|
|
106
74
|
return result;
|
|
107
75
|
}
|
|
108
|
-
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|
|
109
76
|
constructor(cacheManager, utilsService, storageConfig, dataSourceProvider){
|
|
110
|
-
// Repository will be set asynchronously by RequestScopedApiService
|
|
111
77
|
super('folder', null, cacheManager, utilsService, FolderService.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;
|
|
112
78
|
}
|
|
113
79
|
}
|
|
@@ -32,10 +32,8 @@ import { Request } from 'express';
|
|
|
32
32
|
import { StorageModuleOptions } from '../interfaces';
|
|
33
33
|
import { STORAGE_MODULE_OPTIONS } from '../config/storage.constants';
|
|
34
34
|
export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
35
|
-
//
|
|
36
|
-
/**
|
|
37
|
-
* Build parent options from StorageModuleOptions
|
|
38
|
-
*/ static buildParentOptions(options) {
|
|
35
|
+
// Factory Methods
|
|
36
|
+
/** Build parent options from StorageModuleOptions */ static buildParentOptions(options) {
|
|
39
37
|
return {
|
|
40
38
|
bootstrapAppConfig: options.bootstrapAppConfig,
|
|
41
39
|
defaultDatabaseConfig: options.config?.defaultDatabaseConfig,
|
|
@@ -43,10 +41,8 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
43
41
|
tenants: options.config?.tenants
|
|
44
42
|
};
|
|
45
43
|
}
|
|
46
|
-
//
|
|
47
|
-
/**
|
|
48
|
-
* Get global enable company feature flag
|
|
49
|
-
*/ getEnableCompanyFeature() {
|
|
44
|
+
// Feature Flags
|
|
45
|
+
/** Get global enable company feature flag */ getEnableCompanyFeature() {
|
|
50
46
|
return this.storageOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
51
47
|
}
|
|
52
48
|
/**
|
|
@@ -60,11 +56,11 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
60
56
|
*/ getEnableCompanyFeatureForCurrentTenant() {
|
|
61
57
|
return this.getEnableCompanyFeatureForTenant(this.getCurrentTenant() ?? undefined);
|
|
62
58
|
}
|
|
63
|
-
//
|
|
59
|
+
// Entity Management
|
|
64
60
|
/**
|
|
65
|
-
* Get storage entities for migrations based on company feature flag
|
|
66
|
-
*
|
|
67
|
-
*
|
|
61
|
+
* Get storage entities for migrations based on company feature flag.
|
|
62
|
+
* For TypeORM repositories, we always use the base entities (FileManager, etc.)
|
|
63
|
+
* but for migrations, we need the correct entity based on the feature flag.
|
|
68
64
|
*/ async getStorageEntities(enableCompanyFeature) {
|
|
69
65
|
const enable = enableCompanyFeature ?? this.getEnableCompanyFeature();
|
|
70
66
|
const { FileManager, Folder, StorageConfig } = await import('../entities');
|
|
@@ -84,10 +80,8 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
84
80
|
StorageConfig
|
|
85
81
|
];
|
|
86
82
|
}
|
|
87
|
-
//
|
|
88
|
-
/**
|
|
89
|
-
* Override to dynamically set entities based on tenant config
|
|
90
|
-
*/ async createDataSourceFromConfig(config) {
|
|
83
|
+
// Overrides
|
|
84
|
+
/** Override to dynamically set entities based on tenant config */ async createDataSourceFromConfig(config) {
|
|
91
85
|
const currentTenant = this.getCurrentTenant();
|
|
92
86
|
const enableCompanyFeature = this.getEnableCompanyFeatureForTenant(currentTenant ?? undefined);
|
|
93
87
|
const entities = await this.getStorageEntities(enableCompanyFeature);
|
|
@@ -27,78 +27,57 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
+
import { applyCompanyFilter, buildCompanyWhereCondition } from '@flusys/nestjs-shared/utils';
|
|
30
31
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
32
|
import { StorageConfigService } from '../config';
|
|
32
33
|
import { StorageConfig, StorageConfigWithCompany } from '../entities';
|
|
33
34
|
import { StorageDataSourceProvider } from './storage-datasource.provider';
|
|
34
35
|
export class StorageProviderConfigService extends RequestScopedApiService {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* @returns StorageConfig or StorageConfigWithCompany based on configuration
|
|
38
|
-
*/ resolveEntity() {
|
|
39
|
-
const enableCompanyFeature = this.storageConfig.isCompanyFeatureEnabled();
|
|
40
|
-
return enableCompanyFeature ? StorageConfigWithCompany : StorageConfig;
|
|
36
|
+
resolveEntity() {
|
|
37
|
+
return this.storageConfig.isCompanyFeatureEnabled() ? StorageConfigWithCompany : StorageConfig;
|
|
41
38
|
}
|
|
42
|
-
|
|
43
|
-
* Get DataSource provider for this service
|
|
44
|
-
* @returns StorageDataSourceProvider instance
|
|
45
|
-
*/ getDataSourceProvider() {
|
|
39
|
+
getDataSourceProvider() {
|
|
46
40
|
return this.dataSourceProvider;
|
|
47
41
|
}
|
|
48
42
|
async convertSingleDtoToEntity(dto, user) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
...storageConfig,
|
|
53
|
-
...dto
|
|
54
|
-
};
|
|
55
|
-
// Only set company fields if they exist on the entity (when company feature is enabled)
|
|
56
|
-
if ('companyId' in storageConfig) {
|
|
57
|
-
storageConfig.companyId = user?.companyId ?? null;
|
|
43
|
+
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
44
|
+
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
45
|
+
entity.companyId = user?.companyId ?? null;
|
|
58
46
|
}
|
|
59
|
-
return
|
|
47
|
+
return entity;
|
|
60
48
|
}
|
|
61
49
|
async getSelectQuery(query, _user, select) {
|
|
62
|
-
if (!select
|
|
50
|
+
if (!select?.length) {
|
|
63
51
|
select = [
|
|
64
52
|
'id',
|
|
65
53
|
'name',
|
|
66
54
|
'storage',
|
|
67
55
|
'config',
|
|
56
|
+
'isActive',
|
|
57
|
+
'isDefault',
|
|
68
58
|
'createdAt',
|
|
69
59
|
'updatedAt'
|
|
70
60
|
];
|
|
71
|
-
// Add company fields if company feature is enabled
|
|
72
61
|
if (this.storageConfig.isCompanyFeatureEnabled()) {
|
|
73
62
|
select.push('companyId');
|
|
74
63
|
}
|
|
75
64
|
}
|
|
76
|
-
|
|
77
|
-
query.select(selectFields);
|
|
65
|
+
query.select(select.map((f)=>`${this.entityName}.${f}`));
|
|
78
66
|
return {
|
|
79
67
|
query,
|
|
80
68
|
isRaw: false
|
|
81
69
|
};
|
|
82
70
|
}
|
|
83
|
-
|
|
84
|
-
* Override: Extra query manipulation - Auto-filter by user's company
|
|
85
|
-
*/ async getExtraManipulateQuery(query, filterDto, user) {
|
|
71
|
+
async getExtraManipulateQuery(query, filterDto, user) {
|
|
86
72
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
companyId: user.companyId
|
|
92
|
-
});
|
|
93
|
-
}
|
|
73
|
+
applyCompanyFilter(query, {
|
|
74
|
+
isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
|
|
75
|
+
entityAlias: 'storageConfig'
|
|
76
|
+
}, user);
|
|
94
77
|
query.orderBy(`${this.entityName}.createdAt`, 'DESC');
|
|
95
78
|
return result;
|
|
96
79
|
}
|
|
97
|
-
|
|
98
|
-
* Find storage config by ID without throwing (returns null if not found)
|
|
99
|
-
* Uses direct repository query - bypasses company filtering
|
|
100
|
-
* Use for internal operations like file deletion where config ID is already known
|
|
101
|
-
*/ async findByIdDirect(id) {
|
|
80
|
+
async findByIdDirect(id) {
|
|
102
81
|
await this.ensureRepositoryInitialized();
|
|
103
82
|
return await this.repository.findOne({
|
|
104
83
|
where: {
|
|
@@ -106,21 +85,15 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
106
85
|
}
|
|
107
86
|
});
|
|
108
87
|
}
|
|
109
|
-
|
|
110
|
-
* Get default storage configuration (scoped to user's company if enabled)
|
|
111
|
-
* Falls back to any available config if 'default' not found
|
|
112
|
-
*/ async getDefaultConfig(user) {
|
|
88
|
+
async getDefaultConfig(user) {
|
|
113
89
|
await this.ensureRepositoryInitialized();
|
|
114
|
-
const baseWhere = {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
baseWhere.companyId = user.companyId;
|
|
118
|
-
}
|
|
119
|
-
// First try to find config named 'default'
|
|
90
|
+
const baseWhere = buildCompanyWhereCondition({
|
|
91
|
+
isActive: true
|
|
92
|
+
}, this.storageConfig.isCompanyFeatureEnabled(), user);
|
|
120
93
|
const defaultConfig = await this.repository.findOne({
|
|
121
94
|
where: {
|
|
122
95
|
...baseWhere,
|
|
123
|
-
|
|
96
|
+
isDefault: true
|
|
124
97
|
},
|
|
125
98
|
order: {
|
|
126
99
|
createdAt: 'ASC'
|
|
@@ -129,7 +102,6 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
129
102
|
if (defaultConfig) {
|
|
130
103
|
return defaultConfig;
|
|
131
104
|
}
|
|
132
|
-
// Fall back to any available config for this company/user
|
|
133
105
|
return await this.repository.findOne({
|
|
134
106
|
where: baseWhere,
|
|
135
107
|
order: {
|
|
@@ -137,24 +109,17 @@ export class StorageProviderConfigService extends RequestScopedApiService {
|
|
|
137
109
|
}
|
|
138
110
|
});
|
|
139
111
|
}
|
|
140
|
-
|
|
141
|
-
* Get storage configuration by type (scoped to user's company if enabled)
|
|
142
|
-
*/ async getConfigByType(storage, user) {
|
|
112
|
+
async getConfigByType(storage, user) {
|
|
143
113
|
await this.ensureRepositoryInitialized();
|
|
144
|
-
const where = {
|
|
145
|
-
storage
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
|
|
149
|
-
where.companyId = user.companyId;
|
|
150
|
-
}
|
|
114
|
+
const where = buildCompanyWhereCondition({
|
|
115
|
+
storage,
|
|
116
|
+
isActive: true
|
|
117
|
+
}, this.storageConfig.isCompanyFeatureEnabled(), user);
|
|
151
118
|
return await this.repository.find({
|
|
152
119
|
where
|
|
153
120
|
});
|
|
154
121
|
}
|
|
155
|
-
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|
|
156
122
|
constructor(cacheManager, utilsService, storageConfig, dataSourceProvider){
|
|
157
|
-
// Repository will be set asynchronously by RequestScopedApiService
|
|
158
123
|
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;
|
|
159
124
|
}
|
|
160
125
|
}
|