@flusys/nestjs-storage 1.1.0-beta → 2.0.0-rc.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 +148 -6
  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 +70 -34
  10. package/cjs/dtos/folder.dto.js +15 -9
  11. package/cjs/dtos/storage-config.dto.js +4 -85
  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 +74 -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 +15 -37
  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 +12 -5
  41. package/dtos/folder.dto.d.ts +5 -5
  42. package/dtos/storage-config.dto.d.ts +7 -13
  43. package/entities/file-manager-with-company.entity.d.ts +2 -2
  44. package/entities/file-manager.entity.d.ts +12 -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 +8 -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 +71 -35
  58. package/fesm/dtos/folder.dto.js +16 -10
  59. package/fesm/dtos/storage-config.dto.js +8 -95
  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 +75 -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 +13 -35
  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 +0 -20
  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 +4 -4
  104. package/services/upload.service.d.ts +3 -2
  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 -93
  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 -9
  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 -83
  117. package/fesm/interfaces/file-upload-response.interface.js +0 -1
  118. package/interfaces/file-upload-response.interface.d.ts +0 -6
@@ -27,8 +27,9 @@ 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 } from '@flusys/nestjs-shared/utils';
30
31
  import { Inject, Injectable, Scope } from '@nestjs/common';
31
- import { StorageConfigService } from '../config';
32
+ import { StorageConfigService } from './storage-config.service';
32
33
  import { Folder, FolderWithCompany } from '../entities';
33
34
  import { StorageDataSourceProvider } from './storage-datasource.provider';
34
35
  export class FolderService extends RequestScopedApiService {
@@ -38,16 +39,13 @@ export class FolderService extends RequestScopedApiService {
38
39
  getDataSourceProvider() {
39
40
  return this.dataSourceProvider;
40
41
  }
41
- // Entity Conversion
42
42
  async convertSingleDtoToEntity(dto, user) {
43
43
  const entity = await super.convertSingleDtoToEntity(dto, user);
44
- // Set companyId from user context if company feature enabled
45
44
  if (this.storageConfig.isCompanyFeatureEnabled()) {
46
45
  entity.companyId = user?.companyId ?? null;
47
46
  }
48
47
  return entity;
49
48
  }
50
- // Query Customization
51
49
  async getSelectQuery(query, _user, select) {
52
50
  if (!select?.length) {
53
51
  select = [
@@ -69,11 +67,10 @@ export class FolderService extends RequestScopedApiService {
69
67
  }
70
68
  async getExtraManipulateQuery(query, filterDto, user) {
71
69
  const result = await super.getExtraManipulateQuery(query, filterDto, user);
72
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
73
- query.andWhere('folder.companyId = :companyId', {
74
- companyId: user.companyId
75
- });
76
- }
70
+ applyCompanyFilter(query, {
71
+ isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
72
+ entityAlias: 'folder'
73
+ }, user);
77
74
  return result;
78
75
  }
79
76
  constructor(cacheManager, utilsService, storageConfig, dataSourceProvider){
@@ -1,5 +1,6 @@
1
1
  export * from './file-manager.service';
2
2
  export * from './folder.service';
3
+ export * from './storage-config.service';
3
4
  export * from './storage-provider-config.service';
4
5
  export * from './storage-datasource.provider';
5
6
  export * from './upload.service';
@@ -27,115 +27,71 @@ function _ts_param(paramIndex, decorator) {
27
27
  }
28
28
  import { Inject, Injectable } from '@nestjs/common';
29
29
  import { StorageModuleOptions } from '../interfaces';
30
- import { STORAGE_MODULE_OPTIONS, DEFAULT_MAX_FILE_SIZE, DEFAULT_ALLOWED_FILE_TYPES } from './storage.constants';
30
+ import { STORAGE_MODULE_OPTIONS, DEFAULT_MAX_FILE_SIZE, DEFAULT_ALLOWED_FILE_TYPES } from '../config/storage.constants';
31
+ const BYTES_PER_MB = 1024 * 1024;
32
+ const DEFAULT_LOCAL_PATH = './uploads';
33
+ const DEFAULT_PORT = '3000';
31
34
  export class StorageConfigService {
32
- /**
33
- * Check if company feature is enabled
34
- */ isCompanyFeatureEnabled() {
35
+ // ─── IModuleConfigService Implementation ────────────────────────────────────
36
+ isCompanyFeatureEnabled() {
35
37
  return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
36
38
  }
37
- /**
38
- * Get database mode
39
- */ getDatabaseMode() {
39
+ getDatabaseMode() {
40
40
  return this.options.bootstrapAppConfig?.databaseMode ?? 'single';
41
41
  }
42
- /**
43
- * Get maximum file size in bytes
44
- */ getMaxFileSize() {
42
+ isMultiTenant() {
43
+ return this.getDatabaseMode() === 'multi-tenant';
44
+ }
45
+ // ─── Config Getters ─────────────────────────────────────────────────────────
46
+ getMaxFileSize() {
45
47
  return this.options.config?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
46
48
  }
47
- /**
48
- * Get allowed file types (MIME types or patterns)
49
- */ getAllowedFileTypes() {
49
+ getAllowedFileTypes() {
50
50
  return this.options.config?.allowedFileTypes ?? DEFAULT_ALLOWED_FILE_TYPES;
51
51
  }
52
- /**
53
- * Check if file type is allowed
54
- */ isFileTypeAllowed(mimeType) {
52
+ getOptions() {
53
+ return this.options;
54
+ }
55
+ getDefaultLocalStoragePath() {
56
+ return this.options.config?.localStoragePath ?? DEFAULT_LOCAL_PATH;
57
+ }
58
+ getAppUrl() {
59
+ return this.options.config?.appUrl ?? process.env.APP_URL ?? `http://localhost:${process.env.PORT ?? DEFAULT_PORT}`;
60
+ }
61
+ // ─── Validation Methods ─────────────────────────────────────────────────────
62
+ isFileTypeAllowed(mimeType) {
55
63
  const allowedTypes = this.getAllowedFileTypes();
56
- // If wildcard, allow all
57
64
  if (allowedTypes.includes('*/*')) {
58
65
  return true;
59
66
  }
60
- // Check exact match or wildcard pattern
61
- return allowedTypes.some((allowedType)=>{
62
- if (allowedType.endsWith('/*')) {
63
- const prefix = allowedType.slice(0, -2);
64
- return mimeType.startsWith(prefix);
65
- }
66
- return allowedType === mimeType;
67
- });
67
+ return allowedTypes.some((type)=>type.endsWith('/*') ? mimeType.startsWith(type.slice(0, -2)) : type === mimeType);
68
68
  }
69
- /**
70
- * Validate file size
71
- */ validateFileSize(fileSize) {
69
+ validateFileSize(fileSize) {
72
70
  const maxSize = this.getMaxFileSize();
73
71
  if (fileSize > maxSize) {
74
- const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(2);
75
- const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
76
72
  return {
77
73
  valid: false,
78
- message: `File size (${fileSizeMB}MB) exceeds the maximum allowed size of ${maxSizeMB}MB`
74
+ message: `File size (${this.toMB(fileSize)}MB) exceeds maximum ${this.toMB(maxSize)}MB`
79
75
  };
80
76
  }
81
77
  return {
82
78
  valid: true
83
79
  };
84
80
  }
85
- /**
86
- * Validate file type
87
- */ validateFileType(mimeType) {
81
+ validateFileType(mimeType) {
88
82
  if (!this.isFileTypeAllowed(mimeType)) {
89
- const allowedTypes = this.getAllowedFileTypes();
90
83
  return {
91
84
  valid: false,
92
- message: `File type "${mimeType}" is not allowed. Allowed types: ${allowedTypes.join(', ')}`
85
+ message: `File type "${mimeType}" not allowed. Allowed: ${this.getAllowedFileTypes().join(', ')}`
93
86
  };
94
87
  }
95
88
  return {
96
89
  valid: true
97
90
  };
98
91
  }
99
- /**
100
- * Get default database config
101
- */ getDefaultDatabaseConfig() {
102
- return this.options.config?.defaultDatabaseConfig;
103
- }
104
- /**
105
- * Get all options
106
- */ getOptions() {
107
- return this.options;
108
- }
109
- /**
110
- * Get default local storage base path
111
- * Used for serving local files without database lookup
112
- * Falls back to './uploads' if not configured
113
- */ getDefaultLocalStoragePath() {
114
- // Check if localStoragePath is configured in module options
115
- const configuredPath = this.options.config?.localStoragePath;
116
- if (configuredPath) {
117
- return configuredPath;
118
- }
119
- // Default to ./uploads in the project root
120
- return './uploads';
121
- }
122
- /**
123
- * Get application base URL for generating file URLs
124
- * - First tries module config
125
- * - Falls back to APP_URL env var
126
- * - Finally constructs from PORT env var
127
- */ getAppUrl() {
128
- // Try from module config first
129
- if (this.options.config?.appUrl) {
130
- return this.options.config.appUrl;
131
- }
132
- // Fallback: read directly from environment
133
- if (process.env.APP_URL) {
134
- return process.env.APP_URL;
135
- }
136
- // Last resort: construct from PORT
137
- const port = process.env.PORT || '3000';
138
- return `http://localhost:${port}`;
92
+ // ─── Private Helpers ────────────────────────────────────────────────────────
93
+ toMB(bytes) {
94
+ return (bytes / BYTES_PER_MB).toFixed(2);
139
95
  }
140
96
  constructor(options){
141
97
  _define_property(this, "options", void 0);
@@ -29,8 +29,7 @@ import { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
29
29
  import { Inject, Injectable, Logger, Optional, Scope } from '@nestjs/common';
30
30
  import { REQUEST } from '@nestjs/core';
31
31
  import { Request } from 'express';
32
- import { StorageModuleOptions } from '../interfaces';
33
- import { STORAGE_MODULE_OPTIONS } from '../config/storage.constants';
32
+ import { StorageConfigService } from './storage-config.service';
34
33
  export class StorageDataSourceProvider extends MultiTenantDataSourceService {
35
34
  // Factory Methods
36
35
  /** Build parent options from StorageModuleOptions */ static buildParentOptions(options) {
@@ -42,14 +41,11 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
42
41
  };
43
42
  }
44
43
  // Feature Flags
45
- /** Get global enable company feature flag */ getEnableCompanyFeature() {
46
- return this.storageOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
47
- }
48
44
  /**
49
45
  * Get enable company feature for specific tenant
50
46
  * Falls back to global setting if not specified per-tenant
51
47
  */ getEnableCompanyFeatureForTenant(tenant) {
52
- return tenant?.enableCompanyFeature ?? this.getEnableCompanyFeature();
48
+ return tenant?.enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
53
49
  }
54
50
  /**
55
51
  * Get enable company feature for current request context
@@ -62,22 +58,16 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
62
58
  * For TypeORM repositories, we always use the base entities (FileManager, etc.)
63
59
  * but for migrations, we need the correct entity based on the feature flag.
64
60
  */ async getStorageEntities(enableCompanyFeature) {
65
- const enable = enableCompanyFeature ?? this.getEnableCompanyFeature();
66
- const { FileManager, Folder, StorageConfig } = await import('../entities');
67
- // For migrations and schema sync, return the appropriate entity
68
- // Both versions map to the same table, but with different columns
69
- if (enable) {
70
- const { FileManagerWithCompany, FolderWithCompany, StorageConfigWithCompany } = await import('../entities');
71
- return [
72
- FileManagerWithCompany,
73
- FolderWithCompany,
74
- StorageConfigWithCompany
75
- ];
76
- }
77
- return [
78
- FileManager,
79
- Folder,
80
- StorageConfig
61
+ const enable = enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
62
+ const entities = await import('../entities');
63
+ return enable ? [
64
+ entities.FileManagerWithCompany,
65
+ entities.FolderWithCompany,
66
+ entities.StorageConfigWithCompany
67
+ ] : [
68
+ entities.FileManager,
69
+ entities.Folder,
70
+ entities.StorageConfig
81
71
  ];
82
72
  }
83
73
  // Overrides
@@ -138,8 +128,8 @@ export class StorageDataSourceProvider extends MultiTenantDataSourceService {
138
128
  StorageDataSourceProvider.connectionLocks.delete(tenant.id);
139
129
  }
140
130
  }
141
- constructor(storageOptions, request){
142
- super(StorageDataSourceProvider.buildParentOptions(storageOptions), request), _define_property(this, "storageOptions", void 0), _define_property(this, "logger", void 0), this.storageOptions = storageOptions, this.logger = new Logger(StorageDataSourceProvider.name);
131
+ constructor(configService, request){
132
+ super(StorageDataSourceProvider.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0), _define_property(this, "logger", void 0), this.configService = configService, this.logger = new Logger(StorageDataSourceProvider.name);
143
133
  }
144
134
  }
145
135
  // Override parent's static properties to have Storage-specific cache
@@ -153,12 +143,12 @@ StorageDataSourceProvider = _ts_decorate([
153
143
  Injectable({
154
144
  scope: Scope.REQUEST
155
145
  }),
156
- _ts_param(0, Inject(STORAGE_MODULE_OPTIONS)),
146
+ _ts_param(0, Inject(StorageConfigService)),
157
147
  _ts_param(1, Optional()),
158
148
  _ts_param(1, Inject(REQUEST)),
159
149
  _ts_metadata("design:type", Function),
160
150
  _ts_metadata("design:paramtypes", [
161
- typeof StorageModuleOptions === "undefined" ? Object : StorageModuleOptions,
151
+ typeof StorageConfigService === "undefined" ? Object : StorageConfigService,
162
152
  typeof Request === "undefined" ? Object : Request
163
153
  ])
164
154
  ], StorageDataSourceProvider);
@@ -27,8 +27,9 @@ 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
- import { StorageConfigService } from '../config';
32
+ import { StorageConfigService } from './storage-config.service';
32
33
  import { StorageConfig, StorageConfigWithCompany } from '../entities';
33
34
  import { StorageDataSourceProvider } from './storage-datasource.provider';
34
35
  export class StorageProviderConfigService extends RequestScopedApiService {
@@ -38,16 +39,13 @@ export class StorageProviderConfigService extends RequestScopedApiService {
38
39
  getDataSourceProvider() {
39
40
  return this.dataSourceProvider;
40
41
  }
41
- // Entity Conversion
42
42
  async convertSingleDtoToEntity(dto, user) {
43
43
  const entity = await super.convertSingleDtoToEntity(dto, user);
44
- // Set companyId from user context if company feature enabled
45
44
  if (this.storageConfig.isCompanyFeatureEnabled()) {
46
45
  entity.companyId = user?.companyId ?? null;
47
46
  }
48
47
  return entity;
49
48
  }
50
- // Query Customization
51
49
  async getSelectQuery(query, _user, select) {
52
50
  if (!select?.length) {
53
51
  select = [
@@ -72,19 +70,14 @@ export class StorageProviderConfigService extends RequestScopedApiService {
72
70
  }
73
71
  async getExtraManipulateQuery(query, filterDto, user) {
74
72
  const result = await super.getExtraManipulateQuery(query, filterDto, user);
75
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
76
- query.andWhere('storageConfig.companyId = :companyId', {
77
- companyId: user.companyId
78
- });
79
- }
73
+ applyCompanyFilter(query, {
74
+ isCompanyFeatureEnabled: this.storageConfig.isCompanyFeatureEnabled(),
75
+ entityAlias: 'storageConfig'
76
+ }, user);
80
77
  query.orderBy(`${this.entityName}.createdAt`, 'DESC');
81
78
  return result;
82
79
  }
83
- /**
84
- * Find storage config by ID without throwing (returns null if not found)
85
- * Uses direct repository query - bypasses company filtering
86
- * Use for internal operations like file deletion where config ID is already known
87
- */ async findByIdDirect(id) {
80
+ async findByIdDirect(id) {
88
81
  await this.ensureRepositoryInitialized();
89
82
  return await this.repository.findOne({
90
83
  where: {
@@ -92,19 +85,11 @@ export class StorageProviderConfigService extends RequestScopedApiService {
92
85
  }
93
86
  });
94
87
  }
95
- /**
96
- * Get default storage configuration (scoped to user's company if enabled)
97
- * Priority: isDefault=true > any active config (oldest first)
98
- */ async getDefaultConfig(user) {
88
+ async getDefaultConfig(user) {
99
89
  await this.ensureRepositoryInitialized();
100
- const baseWhere = {
90
+ const baseWhere = buildCompanyWhereCondition({
101
91
  isActive: true
102
- };
103
- // Filter by company only if company feature is enabled and user is provided
104
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
105
- baseWhere.companyId = user.companyId;
106
- }
107
- // First try to find config marked as default
92
+ }, this.storageConfig.isCompanyFeatureEnabled(), user);
108
93
  const defaultConfig = await this.repository.findOne({
109
94
  where: {
110
95
  ...baseWhere,
@@ -117,7 +102,6 @@ export class StorageProviderConfigService extends RequestScopedApiService {
117
102
  if (defaultConfig) {
118
103
  return defaultConfig;
119
104
  }
120
- // Fall back to any available active config (oldest first for consistency)
121
105
  return await this.repository.findOne({
122
106
  where: baseWhere,
123
107
  order: {
@@ -125,18 +109,12 @@ export class StorageProviderConfigService extends RequestScopedApiService {
125
109
  }
126
110
  });
127
111
  }
128
- /**
129
- * Get storage configuration by type (scoped to user's company if enabled)
130
- */ async getConfigByType(storage, user) {
112
+ async getConfigByType(storage, user) {
131
113
  await this.ensureRepositoryInitialized();
132
- const where = {
114
+ const where = buildCompanyWhereCondition({
133
115
  storage,
134
116
  isActive: true
135
- };
136
- // Filter by company only if company feature is enabled and user is provided
137
- if (this.storageConfig.isCompanyFeatureEnabled() && user?.companyId) {
138
- where.companyId = user.companyId;
139
- }
117
+ }, this.storageConfig.isCompanyFeatureEnabled(), user);
140
118
  return await this.repository.find({
141
119
  where
142
120
  });
@@ -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