@flusys/nestjs-iam 1.0.0-rc → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +219 -118
  2. package/cjs/controllers/company-action-permission.controller.js +2 -17
  3. package/cjs/controllers/my-permission.controller.js +1 -2
  4. package/cjs/controllers/role-permission.controller.js +3 -9
  5. package/cjs/controllers/user-action-permission.controller.js +3 -9
  6. package/cjs/dtos/action.dto.js +0 -27
  7. package/cjs/dtos/permission.dto.js +81 -27
  8. package/cjs/dtos/role.dto.js +0 -27
  9. package/cjs/helpers/company-access.helper.js +19 -0
  10. package/cjs/helpers/index.js +1 -1
  11. package/cjs/interfaces/iam-module-options.interface.js +0 -14
  12. package/cjs/interfaces/index.js +0 -1
  13. package/cjs/modules/iam.module.js +38 -106
  14. package/cjs/services/action.service.js +30 -41
  15. package/cjs/services/iam-config.service.js +2 -5
  16. package/cjs/services/{iam-datasource.provider.js → iam-datasource.service.js} +33 -36
  17. package/cjs/services/index.js +1 -1
  18. package/cjs/services/permission-cache.service.js +6 -46
  19. package/cjs/services/permission.service.js +52 -41
  20. package/cjs/services/role.service.js +3 -3
  21. package/controllers/company-action-permission.controller.d.ts +2 -5
  22. package/controllers/role-permission.controller.d.ts +0 -1
  23. package/controllers/user-action-permission.controller.d.ts +0 -1
  24. package/dtos/action.dto.d.ts +0 -4
  25. package/dtos/role.dto.d.ts +0 -4
  26. package/fesm/controllers/company-action-permission.controller.js +4 -19
  27. package/fesm/controllers/my-permission.controller.js +1 -2
  28. package/fesm/controllers/role-permission.controller.js +4 -10
  29. package/fesm/controllers/user-action-permission.controller.js +4 -10
  30. package/fesm/dtos/action.dto.js +0 -24
  31. package/fesm/dtos/permission.dto.js +81 -27
  32. package/fesm/dtos/role.dto.js +0 -24
  33. package/fesm/helpers/company-access.helper.js +14 -0
  34. package/fesm/helpers/index.js +1 -1
  35. package/fesm/interfaces/iam-module-options.interface.js +3 -1
  36. package/fesm/interfaces/index.js +0 -1
  37. package/fesm/modules/iam.module.js +40 -108
  38. package/fesm/services/action.service.js +31 -42
  39. package/fesm/services/iam-config.service.js +2 -5
  40. package/fesm/services/{iam-datasource.provider.js → iam-datasource.service.js} +31 -34
  41. package/fesm/services/index.js +1 -1
  42. package/fesm/services/permission-cache.service.js +6 -46
  43. package/fesm/services/permission.service.js +53 -42
  44. package/fesm/services/role.service.js +3 -3
  45. package/helpers/company-access.helper.d.ts +3 -0
  46. package/helpers/index.d.ts +1 -1
  47. package/interfaces/iam-module-options.interface.d.ts +9 -1
  48. package/interfaces/index.d.ts +0 -1
  49. package/modules/iam.module.d.ts +1 -2
  50. package/package.json +3 -3
  51. package/services/action.service.d.ts +6 -4
  52. package/services/iam-config.service.d.ts +0 -1
  53. package/services/{iam-datasource.provider.d.ts → iam-datasource.service.d.ts} +4 -5
  54. package/services/index.d.ts +1 -1
  55. package/services/permission-cache.service.d.ts +1 -4
  56. package/services/permission.service.d.ts +4 -2
  57. package/services/role.service.d.ts +3 -3
  58. package/cjs/helpers/permission-evaluator.helper.js +0 -175
  59. package/cjs/interfaces/iam-module-async-options.interface.js +0 -4
  60. package/fesm/helpers/permission-evaluator.helper.js +0 -165
  61. package/fesm/interfaces/iam-module-async-options.interface.js +0 -3
  62. package/helpers/permission-evaluator.helper.d.ts +0 -26
  63. package/interfaces/iam-module-async-options.interface.d.ts +0 -11
@@ -25,13 +25,13 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
28
+ import { HybridCache, RequestScopedApiService } from '@flusys/nestjs-shared/classes';
29
29
  import { UtilsService } from '@flusys/nestjs-shared/modules';
30
30
  import { BadRequestException, Inject, Injectable, Scope } from '@nestjs/common';
31
31
  import { In } from 'typeorm';
32
32
  import { Action } from '../entities/action.entity';
33
33
  import { IAMConfigService } from './iam-config.service';
34
- import { IAMDataSourceProvider } from './iam-datasource.provider';
34
+ import { IAMDataSourceService } from './iam-datasource.service';
35
35
  import { PermissionService } from './permission.service';
36
36
  export class ActionService extends RequestScopedApiService {
37
37
  resolveEntity() {
@@ -93,55 +93,33 @@ export class ActionService extends RequestScopedApiService {
93
93
  deletedById: entity.deletedById
94
94
  };
95
95
  }
96
- // Custom Methods
97
- /** Get actions available for permission assignment (filtered by company whitelist) */ async getActionsForPermission(user) {
98
- await this.ensureRepositoryInitialized();
96
+ requireUser(user, methodName) {
99
97
  if (!user) {
100
- throw new BadRequestException('User is required for getActionsForPermission');
98
+ throw new BadRequestException(`User is required for ${methodName}`);
101
99
  }
102
- const selectFields = [
103
- 'id',
104
- 'code',
105
- 'name',
106
- 'description',
107
- 'actionType',
108
- 'permissionLogic',
109
- 'isActive',
110
- 'parentId',
111
- 'serial'
112
- ];
113
- const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
114
- if (enableCompanyFeature && user.companyId) {
100
+ }
101
+ /** Get actions available for permission assignment (filtered by company whitelist) */ async getActionsForPermission(user) {
102
+ await this.ensureRepositoryInitialized();
103
+ this.requireUser(user, 'getActionsForPermission');
104
+ let whereClause = {};
105
+ if (this.iamConfigService.isCompanyFeatureEnabled() && user.companyId) {
115
106
  const companyActionIds = await this.permissionService.getCompanyActionIds(user.companyId);
116
107
  if (companyActionIds.length === 0) {
117
108
  return [];
118
109
  }
119
- const actions = await this.repository.find({
120
- where: {
121
- id: In(companyActionIds)
122
- },
123
- select: selectFields
124
- });
125
- return actions.map((action)=>this.convertEntityToResponseDto(action, false));
110
+ whereClause = {
111
+ id: In(companyActionIds)
112
+ };
126
113
  }
127
114
  const actions = await this.repository.find({
128
- select: selectFields
115
+ where: whereClause,
116
+ select: this.actionSelectFields
129
117
  });
130
118
  return actions.map((action)=>this.convertEntityToResponseDto(action, false));
131
119
  }
132
- /**
133
- * Get actions in hierarchical tree structure
134
- *
135
- * @param user - Logged in user info for company filtering
136
- * @param search - Optional search term (name or code)
137
- * @param isActive - Optional filter by active status
138
- * @param withDeleted - Include deleted actions (default: false)
139
- * @returns Array of root actions with nested children
140
- */ async getActionTree(user, search, isActive, withDeleted = false) {
120
+ /** Get actions in hierarchical tree structure */ async getActionTree(user, search, isActive, withDeleted = false) {
141
121
  await this.ensureRepositoryInitialized();
142
- if (!user) {
143
- throw new BadRequestException('User is required for getActionTree');
144
- }
122
+ this.requireUser(user, 'getActionTree');
145
123
  const query = this.repository.createQueryBuilder('action');
146
124
  if (!withDeleted) {
147
125
  query.andWhere('action.deletedAt IS NULL');
@@ -189,7 +167,18 @@ export class ActionService extends RequestScopedApiService {
189
167
  return rootNodes;
190
168
  }
191
169
  constructor(cacheManager, utilsService, iamConfigService, dataSourceProvider, permissionService){
192
- super('action', null, cacheManager, utilsService, ActionService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "iamConfigService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "permissionService", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.iamConfigService = iamConfigService, this.dataSourceProvider = dataSourceProvider, this.permissionService = permissionService;
170
+ super('action', null, cacheManager, utilsService, ActionService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "iamConfigService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "permissionService", void 0), // Custom Methods
171
+ _define_property(this, "actionSelectFields", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.iamConfigService = iamConfigService, this.dataSourceProvider = dataSourceProvider, this.permissionService = permissionService, this.actionSelectFields = [
172
+ 'id',
173
+ 'code',
174
+ 'name',
175
+ 'description',
176
+ 'actionType',
177
+ 'permissionLogic',
178
+ 'isActive',
179
+ 'parentId',
180
+ 'serial'
181
+ ];
193
182
  }
194
183
  }
195
184
  ActionService = _ts_decorate([
@@ -199,14 +188,14 @@ ActionService = _ts_decorate([
199
188
  _ts_param(0, Inject('CACHE_INSTANCE')),
200
189
  _ts_param(1, Inject(UtilsService)),
201
190
  _ts_param(2, Inject(IAMConfigService)),
202
- _ts_param(3, Inject(IAMDataSourceProvider)),
191
+ _ts_param(3, Inject(IAMDataSourceService)),
203
192
  _ts_param(4, Inject(PermissionService)),
204
193
  _ts_metadata("design:type", Function),
205
194
  _ts_metadata("design:paramtypes", [
206
195
  typeof HybridCache === "undefined" ? Object : HybridCache,
207
196
  typeof UtilsService === "undefined" ? Object : UtilsService,
208
197
  typeof IAMConfigService === "undefined" ? Object : IAMConfigService,
209
- typeof IAMDataSourceProvider === "undefined" ? Object : IAMDataSourceProvider,
198
+ typeof IAMDataSourceService === "undefined" ? Object : IAMDataSourceService,
210
199
  typeof PermissionService === "undefined" ? Object : PermissionService
211
200
  ])
212
201
  ], ActionService);
@@ -38,12 +38,9 @@ export class IAMConfigService {
38
38
  isMultiTenant() {
39
39
  return this.getDatabaseMode() === 'multi-tenant';
40
40
  }
41
- // Company Feature
42
- getEnableCompanyFeature() {
43
- return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
44
- }
41
+ // Feature Flags
45
42
  isCompanyFeatureEnabled() {
46
- return this.getEnableCompanyFeature();
43
+ return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
47
44
  }
48
45
  // Permission Mode
49
46
  getPermissionMode() {
@@ -29,9 +29,9 @@ 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 { IAM_MODULE_OPTIONS } from '../config/iam.constants';
33
- import { IAMModuleOptions } from '../interfaces/iam-module-options.interface';
34
- export class IAMDataSourceProvider extends MultiTenantDataSourceService {
32
+ import { PermissionModeHelper } from '../helpers';
33
+ import { IAMConfigService } from './iam-config.service';
34
+ export class IAMDataSourceService extends MultiTenantDataSourceService {
35
35
  // Factory Methods
36
36
  static buildParentOptions(options) {
37
37
  return {
@@ -42,20 +42,17 @@ export class IAMDataSourceProvider extends MultiTenantDataSourceService {
42
42
  };
43
43
  }
44
44
  // Feature Flags
45
- getEnableCompanyFeature() {
46
- return this.iamOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
47
- }
48
45
  getEnableCompanyFeatureForTenant(tenant) {
49
- return tenant?.enableCompanyFeature ?? this.getEnableCompanyFeature();
46
+ return tenant?.enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
50
47
  }
51
48
  getEnableCompanyFeatureForCurrentTenant() {
52
49
  return this.getEnableCompanyFeatureForTenant(this.getCurrentTenant() ?? undefined);
53
50
  }
54
51
  // Entity Management
55
52
  async getIAMEntities() {
56
- const { Action, Role, RoleWithCompany, UserIamPermission, UserIamPermissionWithCompany, getIAMEntitiesByConfig } = await import('../entities');
53
+ const { getIAMEntitiesByConfig } = await import('../entities');
57
54
  const enableCompanyFeature = this.getEnableCompanyFeatureForCurrentTenant();
58
- const permissionMode = this.iamOptions.bootstrapAppConfig?.permissionMode || 'FULL';
55
+ const permissionMode = PermissionModeHelper.toString(this.configService.getPermissionMode());
59
56
  return getIAMEntitiesByConfig(enableCompanyFeature, permissionMode);
60
57
  }
61
58
  // Overrides
@@ -64,9 +61,9 @@ export class IAMDataSourceProvider extends MultiTenantDataSourceService {
64
61
  return super.createDataSourceFromConfig(config, entities);
65
62
  }
66
63
  async getSingleDataSource() {
67
- if (!IAMDataSourceProvider.singleDataSource) {
68
- if (IAMDataSourceProvider.singleConnectionLock) {
69
- return IAMDataSourceProvider.singleConnectionLock;
64
+ if (!IAMDataSourceService.singleDataSource) {
65
+ if (IAMDataSourceService.singleConnectionLock) {
66
+ return IAMDataSourceService.singleConnectionLock;
70
67
  }
71
68
  const lockPromise = (async ()=>{
72
69
  const config = this.getDefaultDatabaseConfig();
@@ -74,63 +71,63 @@ export class IAMDataSourceProvider extends MultiTenantDataSourceService {
74
71
  throw new Error('Default database config is not available');
75
72
  }
76
73
  const ds = await this.createDataSourceFromConfig(config);
77
- IAMDataSourceProvider.singleDataSource = ds;
78
- IAMDataSourceProvider.initialized = true;
74
+ IAMDataSourceService.singleDataSource = ds;
75
+ IAMDataSourceService.initialized = true;
79
76
  return ds;
80
77
  })();
81
- IAMDataSourceProvider.singleConnectionLock = lockPromise;
78
+ IAMDataSourceService.singleConnectionLock = lockPromise;
82
79
  try {
83
80
  return await lockPromise;
84
81
  } finally{
85
- IAMDataSourceProvider.singleConnectionLock = null;
82
+ IAMDataSourceService.singleConnectionLock = null;
86
83
  }
87
84
  }
88
- return IAMDataSourceProvider.singleDataSource;
85
+ return IAMDataSourceService.singleDataSource;
89
86
  }
90
87
  async getOrCreateTenantConnection(tenant) {
91
88
  // Return existing initialized connection from IAM-specific cache
92
- const existing = IAMDataSourceProvider.tenantConnections.get(tenant.id);
89
+ const existing = IAMDataSourceService.tenantConnections.get(tenant.id);
93
90
  if (existing?.isInitialized) {
94
91
  return existing;
95
92
  }
96
93
  // If another request is creating this tenant's connection, wait for it
97
- const pendingConnection = IAMDataSourceProvider.connectionLocks.get(tenant.id);
94
+ const pendingConnection = IAMDataSourceService.connectionLocks.get(tenant.id);
98
95
  if (pendingConnection) {
99
96
  return pendingConnection;
100
97
  }
101
98
  // Create connection with lock to prevent race conditions
102
99
  const config = this.buildTenantDatabaseConfig(tenant);
103
100
  const connectionPromise = this.createDataSourceFromConfig(config);
104
- IAMDataSourceProvider.connectionLocks.set(tenant.id, connectionPromise);
101
+ IAMDataSourceService.connectionLocks.set(tenant.id, connectionPromise);
105
102
  try {
106
103
  const dataSource = await connectionPromise;
107
- IAMDataSourceProvider.tenantConnections.set(tenant.id, dataSource);
104
+ IAMDataSourceService.tenantConnections.set(tenant.id, dataSource);
108
105
  return dataSource;
109
106
  } finally{
110
- IAMDataSourceProvider.connectionLocks.delete(tenant.id);
107
+ IAMDataSourceService.connectionLocks.delete(tenant.id);
111
108
  }
112
109
  }
113
- constructor(iamOptions, request){
114
- super(IAMDataSourceProvider.buildParentOptions(iamOptions), request), _define_property(this, "iamOptions", void 0), _define_property(this, "logger", void 0), this.iamOptions = iamOptions, this.logger = new Logger(IAMDataSourceProvider.name);
110
+ constructor(configService, request){
111
+ super(IAMDataSourceService.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0), _define_property(this, "logger", void 0), this.configService = configService, this.logger = new Logger(IAMDataSourceService.name);
115
112
  }
116
113
  }
117
114
  // Override parent's static properties to have IAM-specific cache
118
- _define_property(IAMDataSourceProvider, "tenantConnections", new Map());
119
- _define_property(IAMDataSourceProvider, "singleDataSource", null);
120
- _define_property(IAMDataSourceProvider, "tenantsRegistry", new Map());
121
- _define_property(IAMDataSourceProvider, "initialized", false);
122
- _define_property(IAMDataSourceProvider, "connectionLocks", new Map());
123
- _define_property(IAMDataSourceProvider, "singleConnectionLock", null);
124
- IAMDataSourceProvider = _ts_decorate([
115
+ _define_property(IAMDataSourceService, "tenantConnections", new Map());
116
+ _define_property(IAMDataSourceService, "singleDataSource", null);
117
+ _define_property(IAMDataSourceService, "tenantsRegistry", new Map());
118
+ _define_property(IAMDataSourceService, "initialized", false);
119
+ _define_property(IAMDataSourceService, "connectionLocks", new Map());
120
+ _define_property(IAMDataSourceService, "singleConnectionLock", null);
121
+ IAMDataSourceService = _ts_decorate([
125
122
  Injectable({
126
123
  scope: Scope.REQUEST
127
124
  }),
128
- _ts_param(0, Inject(IAM_MODULE_OPTIONS)),
125
+ _ts_param(0, Inject(IAMConfigService)),
129
126
  _ts_param(1, Optional()),
130
127
  _ts_param(1, Inject(REQUEST)),
131
128
  _ts_metadata("design:type", Function),
132
129
  _ts_metadata("design:paramtypes", [
133
- typeof IAMModuleOptions === "undefined" ? Object : IAMModuleOptions,
130
+ typeof IAMConfigService === "undefined" ? Object : IAMConfigService,
134
131
  typeof Request === "undefined" ? Object : Request
135
132
  ])
136
- ], IAMDataSourceProvider);
133
+ ], IAMDataSourceService);
@@ -1,6 +1,6 @@
1
1
  export * from './action.service';
2
2
  export * from './iam-config.service';
3
- export * from './iam-datasource.provider';
3
+ export * from './iam-datasource.service';
4
4
  export * from './permission-cache.service';
5
5
  export * from './permission.service';
6
6
  export * from './role.service';
@@ -31,18 +31,17 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
31
31
  export class PermissionCacheService {
32
32
  // Cache Key Generation
33
33
  generateCacheKey(options) {
34
- const { userId, companyId, branchId, enableCompanyFeature } = options;
35
- if (enableCompanyFeature && companyId) {
36
- return `${this.CACHE_PREFIX}:company:${companyId}:branch:${branchId || 'null'}:user:${userId}`;
37
- }
38
- return `${this.CACHE_PREFIX}:user:${userId}`;
34
+ return this.buildCacheKey(this.CACHE_PREFIX, options);
39
35
  }
40
36
  generateMyPermissionsCacheKey(options) {
37
+ return this.buildCacheKey(this.MY_PERMISSIONS_PREFIX, options);
38
+ }
39
+ buildCacheKey(prefix, options) {
41
40
  const { userId, companyId, branchId, enableCompanyFeature } = options;
42
41
  if (enableCompanyFeature && companyId) {
43
- return `${this.MY_PERMISSIONS_PREFIX}:company:${companyId}:branch:${branchId || 'null'}:user:${userId}`;
42
+ return `${prefix}:company:${companyId}:branch:${branchId || 'null'}:user:${userId}`;
44
43
  }
45
- return `${this.MY_PERMISSIONS_PREFIX}:user:${userId}`;
44
+ return `${prefix}:user:${userId}`;
46
45
  }
47
46
  // Cache Operations
48
47
  async setPermissions(options, permissions) {
@@ -56,17 +55,6 @@ export class PermissionCacheService {
56
55
  // Don't throw - cache failure shouldn't break the operation
57
56
  }
58
57
  }
59
- async getPermissions(options) {
60
- try {
61
- const key = this.generateCacheKey(options);
62
- const result = await this.cacheManager.get(key);
63
- return result || null;
64
- } catch (error) {
65
- const errorMessage = ErrorHandler.getErrorMessage(error);
66
- this.logger.error(`Failed to get permissions from cache: ${errorMessage}`);
67
- return null;
68
- }
69
- }
70
58
  // My-Permissions Cache Operations
71
59
  async setMyPermissions(options, data) {
72
60
  try {
@@ -129,16 +117,6 @@ export class PermissionCacheService {
129
117
  return null;
130
118
  }
131
119
  }
132
- async invalidateActionCodeCache(tenantId) {
133
- try {
134
- const key = this.generateActionCodeCacheKey(tenantId);
135
- await this.cacheManager.del(key);
136
- this.logger.debug(`Invalidated action code cache${tenantId ? ` for tenant ${tenantId}` : ''}`);
137
- } catch (error) {
138
- const errorMessage = ErrorHandler.getErrorMessage(error);
139
- this.logger.warn(`Failed to invalidate action code cache: ${errorMessage}`);
140
- }
141
- }
142
120
  // Cache Invalidation
143
121
  async invalidateUser(userId, companyId, branchIds) {
144
122
  try {
@@ -178,13 +156,6 @@ export class PermissionCacheService {
178
156
  }
179
157
  return successCount;
180
158
  }
181
- /** Invalidate cache for all users in company (requires userIds via invalidateUsers) */ async invalidateCompany(companyId) {
182
- // Note: HybridCache doesn't support pattern matching (keys() method)
183
- // Company-wide invalidation requires passing individual user IDs
184
- // This is a placeholder that logs a warning
185
- this.logger.warn(`invalidateCompany called for ${companyId}, but pattern matching is not supported. ` + `Use invalidateUsers() with specific user IDs instead.`);
186
- return 0;
187
- }
188
159
  async invalidateRole(roleId, userIds, companyId, branchIds) {
189
160
  if (userIds.length === 0) {
190
161
  this.logger.debug(`No users found for role ${roleId}`);
@@ -196,17 +167,6 @@ export class PermissionCacheService {
196
167
  }
197
168
  return count;
198
169
  }
199
- // Administrative Operations
200
- /** Clear all permission caches (memory and redis) */ async clearAll() {
201
- try {
202
- await this.cacheManager.reset(); // Clear memory cache
203
- await this.cacheManager.resetL2(); // Clear redis cache
204
- this.logger.warn('Cleared all cache entries (memory and redis)');
205
- } catch (error) {
206
- const errorMessage = ErrorHandler.getErrorMessage(error);
207
- this.logger.error(`Failed to clear all caches: ${errorMessage}`);
208
- }
209
- }
210
170
  constructor(cacheManager){
211
171
  _define_property(this, "cacheManager", void 0);
212
172
  _define_property(this, "logger", void 0);
@@ -25,7 +25,7 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { BadRequestException, Inject, Injectable, Logger, Scope } from '@nestjs/common';
28
+ import { BadRequestException, ConflictException, Inject, Injectable, Logger, Scope } from '@nestjs/common';
29
29
  import { In, IsNull } from 'typeorm';
30
30
  import { PermissionAction } from '../dtos/permission.dto';
31
31
  import { Action } from '../entities/action.entity';
@@ -36,7 +36,7 @@ import { IamEntityType, IamPermissionType, UserIamPermission } from '../entities
36
36
  import { ActionType } from '../enums/action-type.enum';
37
37
  import { IAMPermissionMode } from '../enums/permission-type.enum';
38
38
  import { IAMConfigService } from './iam-config.service';
39
- import { IAMDataSourceProvider } from './iam-datasource.provider';
39
+ import { IAMDataSourceService } from './iam-datasource.service';
40
40
  import { PermissionCacheService } from './permission-cache.service';
41
41
  export class PermissionService {
42
42
  // Repository Getters
@@ -62,8 +62,7 @@ export class PermissionService {
62
62
  const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
63
63
  const branchId = dto.branchId ?? null;
64
64
  const companyId = dto.companyId ?? null;
65
- const itemsToAdd = dto.items.filter((item)=>item.action === PermissionAction.ADD);
66
- const itemsToRemove = dto.items.filter((item)=>item.action === PermissionAction.REMOVE);
65
+ const { toAdd: itemsToAdd, toRemove: itemsToRemove } = this.splitItemsByAction(dto.items);
67
66
  let added = 0;
68
67
  let removed = 0;
69
68
  if (itemsToAdd.length > 0) {
@@ -97,8 +96,15 @@ export class PermissionService {
97
96
  branchId: enableCompanyFeature ? branchId : null
98
97
  }));
99
98
  if (newPermissions.length > 0) {
100
- await permissionRepo.save(newPermissions);
101
- added = newPermissions.length;
99
+ try {
100
+ await permissionRepo.save(newPermissions);
101
+ added = newPermissions.length;
102
+ } catch (error) {
103
+ if (error?.code === 'ER_DUP_ENTRY' || error?.message?.includes('Duplicate entry')) {
104
+ throw new ConflictException('Some permissions already exist for this user. Please refresh and try again.');
105
+ }
106
+ throw error;
107
+ }
102
108
  }
103
109
  }
104
110
  if (itemsToRemove.length > 0) {
@@ -118,12 +124,7 @@ export class PermissionService {
118
124
  removed = result.affected || 0;
119
125
  }
120
126
  await this.invalidateUserPermissionCache(dto.userId, branchId, companyId);
121
- return {
122
- success: true,
123
- added,
124
- removed,
125
- message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed`
126
- };
127
+ return this.buildOperationResult(dto.items.length, added, removed);
127
128
  }
128
129
  async getUserActions(userId, branchId, companyId) {
129
130
  const permissionRepo = await this.getPermissionRepository();
@@ -189,8 +190,7 @@ export class PermissionService {
189
190
  });
190
191
  roleCompanyId = role?.companyId ?? null;
191
192
  }
192
- const itemsToAdd = dto.items.filter((item)=>item.action === PermissionAction.ADD);
193
- const itemsToRemove = dto.items.filter((item)=>item.action === PermissionAction.REMOVE);
193
+ const { toAdd: itemsToAdd, toRemove: itemsToRemove } = this.splitItemsByAction(dto.items);
194
194
  let added = 0;
195
195
  let removed = 0;
196
196
  if (itemsToAdd.length > 0) {
@@ -219,8 +219,15 @@ export class PermissionService {
219
219
  branchId: null
220
220
  }));
221
221
  if (newPermissions.length > 0) {
222
- await permissionRepo.save(newPermissions);
223
- added = newPermissions.length;
222
+ try {
223
+ await permissionRepo.save(newPermissions);
224
+ added = newPermissions.length;
225
+ } catch (error) {
226
+ if (error?.code === 'ER_DUP_ENTRY' || error?.message?.includes('Duplicate entry')) {
227
+ throw new ConflictException('Some role-action permissions already exist. Please refresh and try again.');
228
+ }
229
+ throw error;
230
+ }
224
231
  }
225
232
  }
226
233
  if (itemsToRemove.length > 0) {
@@ -235,12 +242,7 @@ export class PermissionService {
235
242
  removed = result.affected || 0;
236
243
  }
237
244
  const affectedUsers = await this.invalidateRoleMembersCache(dto.roleId);
238
- return {
239
- success: true,
240
- added,
241
- removed,
242
- message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed. Invalidated cache for ${affectedUsers} users.`
243
- };
245
+ return this.buildOperationResult(dto.items.length, added, removed, `. Invalidated cache for ${affectedUsers} users.`);
244
246
  }
245
247
  async getRoleActions(roleId) {
246
248
  const permissionRepo = await this.getPermissionRepository();
@@ -282,8 +284,7 @@ export class PermissionService {
282
284
  /** Assign or remove actions to/from a company (whitelist) */ async assignCompanyActions(dto) {
283
285
  const permissionRepo = await this.getPermissionRepository();
284
286
  const dataSource = permissionRepo.manager.connection;
285
- const itemsToAdd = dto.items.filter((item)=>item.action === PermissionAction.ADD);
286
- const itemsToRemove = dto.items.filter((item)=>item.action === PermissionAction.REMOVE);
287
+ const { toAdd: itemsToAdd, toRemove: itemsToRemove } = this.splitItemsByAction(dto.items);
287
288
  let added = 0;
288
289
  let removed = 0;
289
290
  let removedRoleActions = 0;
@@ -303,12 +304,7 @@ export class PermissionService {
303
304
  });
304
305
  const affectedCacheEntries = await this.invalidateCompanyMembersCache(dto.companyId);
305
306
  const cascadeInfo = removedRoleActions > 0 || removedUserActions > 0 ? ` Cascaded removal: ${removedRoleActions} role permissions, ${removedUserActions} user permissions.` : '';
306
- return {
307
- success: true,
308
- added,
309
- removed,
310
- message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed.${cascadeInfo} Invalidated ${affectedCacheEntries} cache entries.`
311
- };
307
+ return this.buildOperationResult(dto.items.length, added, removed, `.${cascadeInfo} Invalidated ${affectedCacheEntries} cache entries.`);
312
308
  }
313
309
  async addCompanyActions(permissionRepo, companyId, actionIds) {
314
310
  const existingPermissions = await permissionRepo.find({
@@ -449,8 +445,7 @@ export class PermissionService {
449
445
  const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
450
446
  const branchId = dto.branchId ?? null;
451
447
  const companyId = dto.companyId ?? null;
452
- const itemsToAdd = dto.items.filter((item)=>item.action === PermissionAction.ADD);
453
- const itemsToRemove = dto.items.filter((item)=>item.action === PermissionAction.REMOVE);
448
+ const { toAdd: itemsToAdd, toRemove: itemsToRemove } = this.splitItemsByAction(dto.items);
454
449
  let added = 0;
455
450
  let removed = 0;
456
451
  if (itemsToAdd.length > 0) {
@@ -484,8 +479,15 @@ export class PermissionService {
484
479
  branchId: enableCompanyFeature ? branchId : null
485
480
  }));
486
481
  if (newPermissions.length > 0) {
487
- await permissionRepo.save(newPermissions);
488
- added = newPermissions.length;
482
+ try {
483
+ await permissionRepo.save(newPermissions);
484
+ added = newPermissions.length;
485
+ } catch (error) {
486
+ if (error?.code === 'ER_DUP_ENTRY' || error?.message?.includes('Duplicate entry')) {
487
+ throw new ConflictException('Some user-role permissions already exist. Please refresh and try again.');
488
+ }
489
+ throw error;
490
+ }
489
491
  }
490
492
  }
491
493
  if (itemsToRemove.length > 0) {
@@ -505,12 +507,7 @@ export class PermissionService {
505
507
  removed = result.affected || 0;
506
508
  }
507
509
  await this.invalidateUserPermissionCache(dto.userId, branchId, companyId);
508
- return {
509
- success: true,
510
- added,
511
- removed,
512
- message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed`
513
- };
510
+ return this.buildOperationResult(dto.items.length, added, removed);
514
511
  }
515
512
  /** Get user's roles (branch-scoped, filtered by companyId and branchId if provided) */ async getUserRoles(userId, branchId, companyId) {
516
513
  const permissionRepo = await this.getPermissionRepository();
@@ -702,6 +699,20 @@ export class PermissionService {
702
699
  return cacheData;
703
700
  }
704
701
  // Helper Methods
702
+ /** Split permission items into add and remove arrays */ splitItemsByAction(items) {
703
+ return {
704
+ toAdd: items.filter((item)=>item.action === PermissionAction.ADD),
705
+ toRemove: items.filter((item)=>item.action === PermissionAction.REMOVE)
706
+ };
707
+ }
708
+ /** Build standard operation result DTO */ buildOperationResult(totalItems, added, removed, additionalMessage = '') {
709
+ return {
710
+ success: true,
711
+ added,
712
+ removed,
713
+ message: `Successfully processed ${totalItems} items: ${added} added, ${removed} removed${additionalMessage}`
714
+ };
715
+ }
705
716
  /** Get role IDs assigned to a user (merges company-wide + branch-specific roles) */ async getUserRoleIds(userId, branchId, companyId) {
706
717
  const permissionRepo = await this.getPermissionRepository();
707
718
  const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
@@ -889,11 +900,11 @@ PermissionService = _ts_decorate([
889
900
  }),
890
901
  _ts_param(0, Inject(PermissionCacheService)),
891
902
  _ts_param(1, Inject(IAMConfigService)),
892
- _ts_param(2, Inject(IAMDataSourceProvider)),
903
+ _ts_param(2, Inject(IAMDataSourceService)),
893
904
  _ts_metadata("design:type", Function),
894
905
  _ts_metadata("design:paramtypes", [
895
906
  typeof PermissionCacheService === "undefined" ? Object : PermissionCacheService,
896
907
  typeof IAMConfigService === "undefined" ? Object : IAMConfigService,
897
- typeof IAMDataSourceProvider === "undefined" ? Object : IAMDataSourceProvider
908
+ typeof IAMDataSourceService === "undefined" ? Object : IAMDataSourceService
898
909
  ])
899
910
  ], PermissionService);
@@ -32,7 +32,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
32
32
  import { RoleWithCompany } from '../entities/role-with-company.entity';
33
33
  import { Role } from '../entities/role.entity';
34
34
  import { IAMConfigService } from './iam-config.service';
35
- import { IAMDataSourceProvider } from './iam-datasource.provider';
35
+ import { IAMDataSourceService } from './iam-datasource.service';
36
36
  export class RoleService extends RequestScopedApiService {
37
37
  resolveEntity() {
38
38
  return this.iamConfigService.isCompanyFeatureEnabled() ? RoleWithCompany : Role;
@@ -122,12 +122,12 @@ RoleService = _ts_decorate([
122
122
  _ts_param(0, Inject('CACHE_INSTANCE')),
123
123
  _ts_param(1, Inject(UtilsService)),
124
124
  _ts_param(2, Inject(IAMConfigService)),
125
- _ts_param(3, Inject(IAMDataSourceProvider)),
125
+ _ts_param(3, Inject(IAMDataSourceService)),
126
126
  _ts_metadata("design:type", Function),
127
127
  _ts_metadata("design:paramtypes", [
128
128
  typeof HybridCache === "undefined" ? Object : HybridCache,
129
129
  typeof UtilsService === "undefined" ? Object : UtilsService,
130
130
  typeof IAMConfigService === "undefined" ? Object : IAMConfigService,
131
- typeof IAMDataSourceProvider === "undefined" ? Object : IAMDataSourceProvider
131
+ typeof IAMDataSourceService === "undefined" ? Object : IAMDataSourceService
132
132
  ])
133
133
  ], RoleService);
@@ -0,0 +1,3 @@
1
+ import { ILoggedUserInfo } from '@flusys/nestjs-shared';
2
+ import { IAMConfigService } from '../services/iam-config.service';
3
+ export declare function validateCompanyAccess(config: IAMConfigService, companyId: string | undefined, user: ILoggedUserInfo, errorMessage?: string): void;
@@ -1,2 +1,2 @@
1
- export * from './permission-evaluator.helper';
1
+ export * from './company-access.helper';
2
2
  export * from './permission-mode.helper';
@@ -1,4 +1,5 @@
1
1
  import { IBootstrapAppConfig, IDataSourceServiceOptions, IDynamicModuleConfig, IModuleOptionsFactory } from '@flusys/nestjs-core';
2
+ import { ModuleMetadata, Type } from '@nestjs/common';
2
3
  export interface IIAMModuleConfig extends IDataSourceServiceOptions {
3
4
  }
4
5
  export interface IAMModuleOptions extends IDynamicModuleConfig {
@@ -9,4 +10,11 @@ export interface IAMOptionsFactory extends IModuleOptionsFactory<IIAMModuleConfi
9
10
  createIAMOptions(): Promise<IIAMModuleConfig> | IIAMModuleConfig;
10
11
  createOptions(): Promise<IIAMModuleConfig> | IIAMModuleConfig;
11
12
  }
12
- export * from './iam-module-async-options.interface';
13
+ export interface IAMModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'>, IDynamicModuleConfig {
14
+ bootstrapAppConfig?: IBootstrapAppConfig;
15
+ config?: IIAMModuleConfig;
16
+ useFactory?: (...args: any[]) => Promise<IIAMModuleConfig> | IIAMModuleConfig;
17
+ inject?: any[];
18
+ useClass?: Type<IAMOptionsFactory>;
19
+ useExisting?: Type<IAMOptionsFactory>;
20
+ }
@@ -1,4 +1,3 @@
1
1
  export * from './action.interface';
2
2
  export * from './role.interface';
3
3
  export * from './iam-module-options.interface';
4
- export * from './iam-module-async-options.interface';
@@ -2,10 +2,9 @@ import { DynamicModule } from '@nestjs/common';
2
2
  import { IAMModuleAsyncOptions, IAMModuleOptions } from '../interfaces/iam-module-options.interface';
3
3
  export declare class IAMModule {
4
4
  private static getControllers;
5
- private static getEntities;
6
5
  private static getServices;
7
6
  private static getPermissionGuardConfigProvider;
8
- private static getRepositoryProviders;
7
+ private static getExports;
9
8
  static forRoot(options?: IAMModuleOptions): DynamicModule;
10
9
  static forRootAsync(asyncOptions: IAMModuleAsyncOptions): DynamicModule;
11
10
  private static createAsyncProviders;