@flusys/nestjs-iam 1.1.0-beta → 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 (76) hide show
  1. package/README.md +285 -115
  2. package/cjs/controllers/action.controller.js +45 -2
  3. package/cjs/controllers/company-action-permission.controller.js +16 -10
  4. package/cjs/controllers/my-permission.controller.js +7 -3
  5. package/cjs/controllers/role-permission.controller.js +35 -17
  6. package/cjs/controllers/role.controller.js +46 -3
  7. package/cjs/controllers/user-action-permission.controller.js +26 -11
  8. package/cjs/dtos/action.dto.js +0 -27
  9. package/cjs/dtos/permission.dto.js +117 -27
  10. package/cjs/dtos/role.dto.js +0 -27
  11. package/cjs/entities/permission-base.entity.js +0 -12
  12. package/cjs/helpers/company-access.helper.js +19 -0
  13. package/cjs/helpers/index.js +1 -1
  14. package/cjs/interfaces/iam-module-options.interface.js +0 -14
  15. package/cjs/interfaces/index.js +0 -1
  16. package/cjs/modules/iam.module.js +50 -102
  17. package/cjs/services/action.service.js +30 -41
  18. package/cjs/services/iam-config.service.js +2 -5
  19. package/cjs/services/{iam-datasource.provider.js → iam-datasource.service.js} +33 -36
  20. package/cjs/services/index.js +1 -1
  21. package/cjs/services/permission-cache.service.js +31 -61
  22. package/cjs/services/permission.service.js +160 -188
  23. package/cjs/services/role.service.js +8 -8
  24. package/cjs/types/logic-node.type.js +0 -24
  25. package/controllers/company-action-permission.controller.d.ts +3 -3
  26. package/controllers/my-permission.controller.d.ts +2 -2
  27. package/controllers/role-permission.controller.d.ts +7 -5
  28. package/controllers/user-action-permission.controller.d.ts +6 -4
  29. package/dtos/action.dto.d.ts +0 -7
  30. package/dtos/permission.dto.d.ts +4 -0
  31. package/dtos/role.dto.d.ts +0 -7
  32. package/entities/permission-base.entity.d.ts +0 -4
  33. package/fesm/controllers/action.controller.js +47 -4
  34. package/fesm/controllers/company-action-permission.controller.js +18 -12
  35. package/fesm/controllers/index.js +1 -1
  36. package/fesm/controllers/my-permission.controller.js +7 -3
  37. package/fesm/controllers/role-permission.controller.js +37 -19
  38. package/fesm/controllers/role.controller.js +45 -2
  39. package/fesm/controllers/user-action-permission.controller.js +28 -13
  40. package/fesm/dtos/action.dto.js +0 -24
  41. package/fesm/dtos/permission.dto.js +117 -29
  42. package/fesm/dtos/role.dto.js +0 -24
  43. package/fesm/entities/permission-base.entity.js +0 -12
  44. package/fesm/helpers/company-access.helper.js +14 -0
  45. package/fesm/helpers/index.js +1 -1
  46. package/fesm/interfaces/iam-module-options.interface.js +3 -1
  47. package/fesm/interfaces/index.js +0 -1
  48. package/fesm/modules/iam.module.js +52 -104
  49. package/fesm/services/action.service.js +32 -43
  50. package/fesm/services/iam-config.service.js +2 -5
  51. package/fesm/services/{iam-datasource.provider.js → iam-datasource.service.js} +31 -34
  52. package/fesm/services/index.js +1 -1
  53. package/fesm/services/permission-cache.service.js +31 -61
  54. package/fesm/services/permission.service.js +161 -189
  55. package/fesm/services/role.service.js +8 -8
  56. package/fesm/types/logic-node.type.js +1 -10
  57. package/helpers/company-access.helper.d.ts +3 -0
  58. package/helpers/index.d.ts +1 -1
  59. package/interfaces/iam-module-options.interface.d.ts +9 -1
  60. package/interfaces/index.d.ts +0 -1
  61. package/modules/iam.module.d.ts +2 -2
  62. package/package.json +3 -3
  63. package/services/action.service.d.ts +6 -4
  64. package/services/iam-config.service.d.ts +2 -2
  65. package/services/{iam-datasource.provider.d.ts → iam-datasource.service.d.ts} +4 -5
  66. package/services/index.d.ts +1 -1
  67. package/services/permission-cache.service.d.ts +4 -6
  68. package/services/permission.service.d.ts +8 -4
  69. package/services/role.service.d.ts +3 -3
  70. package/types/logic-node.type.d.ts +0 -8
  71. package/cjs/helpers/permission-evaluator.helper.js +0 -175
  72. package/cjs/interfaces/iam-module-async-options.interface.js +0 -4
  73. package/fesm/helpers/permission-evaluator.helper.js +0 -165
  74. package/fesm/interfaces/iam-module-async-options.interface.js +0 -3
  75. package/helpers/permission-evaluator.helper.d.ts +0 -26
  76. package/interfaces/iam-module-async-options.interface.d.ts +0 -11
@@ -25,7 +25,7 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { 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';
@@ -35,9 +35,8 @@ import { Role } from '../entities/role.entity';
35
35
  import { IamEntityType, IamPermissionType, UserIamPermission } from '../entities/user-iam-permission.entity';
36
36
  import { ActionType } from '../enums/action-type.enum';
37
37
  import { IAMPermissionMode } from '../enums/permission-type.enum';
38
- import { PermissionEvaluatorHelper } from '../helpers/permission-evaluator.helper';
39
38
  import { IAMConfigService } from './iam-config.service';
40
- import { IAMDataSourceProvider } from './iam-datasource.provider';
39
+ import { IAMDataSourceService } from './iam-datasource.service';
41
40
  import { PermissionCacheService } from './permission-cache.service';
42
41
  export class PermissionService {
43
42
  // Repository Getters
@@ -56,16 +55,16 @@ export class PermissionService {
56
55
  }
57
56
  // User-Action Permissions
58
57
  async assignUserActions(dto) {
58
+ if (!this.iamConfigService.isDirectPermissionEnabled()) {
59
+ throw new BadRequestException('Direct permission assignment not available in RBAC-only mode. Use role-based permissions instead.');
60
+ }
59
61
  const permissionRepo = await this.getPermissionRepository();
60
62
  const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
61
63
  const branchId = dto.branchId ?? null;
62
64
  const companyId = dto.companyId ?? null;
63
- // Separate items into add and remove batches
64
- const itemsToAdd = dto.items.filter((item)=>item.action === PermissionAction.ADD);
65
- const itemsToRemove = dto.items.filter((item)=>item.action === PermissionAction.REMOVE);
65
+ const { toAdd: itemsToAdd, toRemove: itemsToRemove } = this.splitItemsByAction(dto.items);
66
66
  let added = 0;
67
67
  let removed = 0;
68
- // Batch add: Find existing permissions in one query
69
68
  if (itemsToAdd.length > 0) {
70
69
  const actionIdsToAdd = itemsToAdd.map((item)=>item.id);
71
70
  const whereFind = {
@@ -97,11 +96,17 @@ 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
- // Batch remove: Delete all at once using IN clause
105
110
  if (itemsToRemove.length > 0) {
106
111
  const actionIdsToRemove = itemsToRemove.map((item)=>item.id);
107
112
  const whereDelete = {
@@ -118,14 +123,8 @@ export class PermissionService {
118
123
  const result = await permissionRepo.delete(whereDelete);
119
124
  removed = result.affected || 0;
120
125
  }
121
- // Invalidate permission cache for this user
122
126
  await this.invalidateUserPermissionCache(dto.userId, branchId, companyId);
123
- return {
124
- success: true,
125
- added,
126
- removed,
127
- message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed`
128
- };
127
+ return this.buildOperationResult(dto.items.length, added, removed);
129
128
  }
130
129
  async getUserActions(userId, branchId, companyId) {
131
130
  const permissionRepo = await this.getPermissionRepository();
@@ -136,22 +135,9 @@ export class PermissionService {
136
135
  sourceType: IamEntityType.USER,
137
136
  sourceId: userId
138
137
  };
139
- // When company feature enabled:
140
- // - companyId provided → Filter by that company (optional additional filter)
141
- // - branchId provided → Filter by that branch (branch-specific actions)
142
- // - branchId not provided → Filter by IS NULL (company-wide actions)
143
138
  if (enableCompanyFeature) {
144
- if (companyId) {
145
- // Filter by companyId if provided
146
- where.companyId = companyId;
147
- }
148
- if (branchId) {
149
- // Branch-specific actions only
150
- where.branchId = branchId;
151
- } else {
152
- // Company-wide actions only (branchId IS NULL)
153
- where.branchId = null;
154
- }
139
+ if (companyId) where.companyId = companyId;
140
+ where.branchId = branchId ?? null;
155
141
  }
156
142
  const permissions = await permissionRepo.find({
157
143
  where
@@ -185,9 +171,11 @@ export class PermissionService {
185
171
  }
186
172
  // Role-Action Permissions
187
173
  async assignRoleActions(dto) {
174
+ if (!this.iamConfigService.isRbacEnabled()) {
175
+ throw new BadRequestException('Role-based permission assignment not available in DIRECT-only mode. Use direct user permissions instead.');
176
+ }
188
177
  const permissionRepo = await this.getPermissionRepository();
189
178
  const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
190
- // Fetch role companyId if company feature is enabled
191
179
  let roleCompanyId = null;
192
180
  if (enableCompanyFeature) {
193
181
  const roleRepo = await this.getRoleRepository();
@@ -202,12 +190,9 @@ export class PermissionService {
202
190
  });
203
191
  roleCompanyId = role?.companyId ?? null;
204
192
  }
205
- // Separate items into add and remove batches
206
- const itemsToAdd = dto.items.filter((item)=>item.action === PermissionAction.ADD);
207
- const itemsToRemove = dto.items.filter((item)=>item.action === PermissionAction.REMOVE);
193
+ const { toAdd: itemsToAdd, toRemove: itemsToRemove } = this.splitItemsByAction(dto.items);
208
194
  let added = 0;
209
195
  let removed = 0;
210
- // Batch add: Find existing permissions in one query
211
196
  if (itemsToAdd.length > 0) {
212
197
  const actionIdsToAdd = itemsToAdd.map((item)=>item.id);
213
198
  const existingPermissions = await permissionRepo.find({
@@ -234,11 +219,17 @@ export class PermissionService {
234
219
  branchId: null
235
220
  }));
236
221
  if (newPermissions.length > 0) {
237
- await permissionRepo.save(newPermissions);
238
- 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
+ }
239
231
  }
240
232
  }
241
- // Batch remove: Delete all at once using IN clause
242
233
  if (itemsToRemove.length > 0) {
243
234
  const actionIdsToRemove = itemsToRemove.map((item)=>item.id);
244
235
  const result = await permissionRepo.delete({
@@ -250,14 +241,8 @@ export class PermissionService {
250
241
  });
251
242
  removed = result.affected || 0;
252
243
  }
253
- // Invalidate cache for all users with this role
254
244
  const affectedUsers = await this.invalidateRoleMembersCache(dto.roleId);
255
- return {
256
- success: true,
257
- added,
258
- removed,
259
- message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed. Invalidated cache for ${affectedUsers} users.`
260
- };
245
+ return this.buildOperationResult(dto.items.length, added, removed, `. Invalidated cache for ${affectedUsers} users.`);
261
246
  }
262
247
  async getRoleActions(roleId) {
263
248
  const permissionRepo = await this.getPermissionRepository();
@@ -299,8 +284,7 @@ export class PermissionService {
299
284
  /** Assign or remove actions to/from a company (whitelist) */ async assignCompanyActions(dto) {
300
285
  const permissionRepo = await this.getPermissionRepository();
301
286
  const dataSource = permissionRepo.manager.connection;
302
- const itemsToAdd = dto.items.filter((item)=>item.action === PermissionAction.ADD);
303
- const itemsToRemove = dto.items.filter((item)=>item.action === PermissionAction.REMOVE);
287
+ const { toAdd: itemsToAdd, toRemove: itemsToRemove } = this.splitItemsByAction(dto.items);
304
288
  let added = 0;
305
289
  let removed = 0;
306
290
  let removedRoleActions = 0;
@@ -320,12 +304,7 @@ export class PermissionService {
320
304
  });
321
305
  const affectedCacheEntries = await this.invalidateCompanyMembersCache(dto.companyId);
322
306
  const cascadeInfo = removedRoleActions > 0 || removedUserActions > 0 ? ` Cascaded removal: ${removedRoleActions} role permissions, ${removedUserActions} user permissions.` : '';
323
- return {
324
- success: true,
325
- added,
326
- removed,
327
- message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed.${cascadeInfo} Invalidated ${affectedCacheEntries} cache entries.`
328
- };
307
+ return this.buildOperationResult(dto.items.length, added, removed, `.${cascadeInfo} Invalidated ${affectedCacheEntries} cache entries.`);
329
308
  }
330
309
  async addCompanyActions(permissionRepo, companyId, actionIds) {
331
310
  const existingPermissions = await permissionRepo.find({
@@ -459,16 +438,16 @@ export class PermissionService {
459
438
  }
460
439
  // User-Role Permissions
461
440
  /** Assign user to roles (branch-scoped when company feature is enabled) */ async assignUserRoles(dto) {
441
+ if (!this.iamConfigService.isRbacEnabled()) {
442
+ throw new BadRequestException('Role assignment not available in DIRECT-only mode. Use direct user permissions instead.');
443
+ }
462
444
  const permissionRepo = await this.getPermissionRepository();
463
445
  const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
464
446
  const branchId = dto.branchId ?? null;
465
447
  const companyId = dto.companyId ?? null;
466
- // Separate items into add and remove batches
467
- const itemsToAdd = dto.items.filter((item)=>item.action === PermissionAction.ADD);
468
- const itemsToRemove = dto.items.filter((item)=>item.action === PermissionAction.REMOVE);
448
+ const { toAdd: itemsToAdd, toRemove: itemsToRemove } = this.splitItemsByAction(dto.items);
469
449
  let added = 0;
470
450
  let removed = 0;
471
- // Batch add: Find existing permissions in one query
472
451
  if (itemsToAdd.length > 0) {
473
452
  const roleIdsToAdd = itemsToAdd.map((item)=>item.id);
474
453
  const whereFind = {
@@ -500,11 +479,17 @@ export class PermissionService {
500
479
  branchId: enableCompanyFeature ? branchId : null
501
480
  }));
502
481
  if (newPermissions.length > 0) {
503
- await permissionRepo.save(newPermissions);
504
- 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
+ }
505
491
  }
506
492
  }
507
- // Batch remove: Delete all at once using IN clause
508
493
  if (itemsToRemove.length > 0) {
509
494
  const roleIdsToRemove = itemsToRemove.map((item)=>item.id);
510
495
  const whereDelete = {
@@ -521,41 +506,21 @@ export class PermissionService {
521
506
  const result = await permissionRepo.delete(whereDelete);
522
507
  removed = result.affected || 0;
523
508
  }
524
- // Invalidate permission cache for this user
525
509
  await this.invalidateUserPermissionCache(dto.userId, branchId, companyId);
526
- return {
527
- success: true,
528
- added,
529
- removed,
530
- message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed`
531
- };
510
+ return this.buildOperationResult(dto.items.length, added, removed);
532
511
  }
533
512
  /** Get user's roles (branch-scoped, filtered by companyId and branchId if provided) */ async getUserRoles(userId, branchId, companyId) {
534
513
  const permissionRepo = await this.getPermissionRepository();
535
514
  const roleRepo = await this.getRoleRepository();
536
515
  const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
537
- // Build where clause with optional companyId and branchId filter
538
- // When company feature enabled:
539
- // - companyId provided → Filter by that company (optional additional filter)
540
- // - branchId provided → Filter by that branch (branch-specific roles)
541
- // - branchId not provided → Filter by IS NULL (company-wide roles)
542
516
  const where = {
543
517
  permissionType: IamPermissionType.USER_ROLE,
544
518
  sourceType: IamEntityType.USER,
545
519
  sourceId: userId
546
520
  };
547
521
  if (enableCompanyFeature) {
548
- if (companyId) {
549
- // Filter by companyId if provided
550
- where.companyId = companyId;
551
- }
552
- if (branchId) {
553
- // Branch-specific roles only
554
- where.branchId = branchId;
555
- } else {
556
- // Company-wide roles only (branchId IS NULL)
557
- where.branchId = null;
558
- }
522
+ if (companyId) where.companyId = companyId;
523
+ where.branchId = branchId ?? null;
559
524
  }
560
525
  const permissions = await permissionRepo.find({
561
526
  where
@@ -596,20 +561,15 @@ export class PermissionService {
596
561
  branchId,
597
562
  enableCompanyFeature
598
563
  };
599
- // Step 1: Check cache first
600
564
  const cachedData = await this.permissionCacheService.getMyPermissions(cacheOptions);
601
565
  if (cachedData) {
602
- // Cache hit - use cached data (even if empty)
603
566
  return this.buildResponseFromCache(cachedData, parentCodes);
604
567
  }
605
- // Step 2: Cache miss - fetch from DB and cache (even if empty)
606
568
  const freshData = await this.fetchAndCachePermissions(userId, branchId, companyId);
607
- // Step 3: Apply parentCodes filter and return
608
569
  return this.buildResponseFromCache(freshData, parentCodes);
609
570
  }
610
571
  /** Build response from cached data, applying optional parentCodes filter */ async buildResponseFromCache(cachedData, parentCodes) {
611
572
  let frontendActions = cachedData.frontendActions;
612
- // Apply parentCodes filter if provided
613
573
  if (parentCodes?.length) {
614
574
  const parentIds = await this.getParentIdsByCodesWithCache(parentCodes);
615
575
  if (parentIds.size > 0) {
@@ -628,13 +588,19 @@ export class PermissionService {
628
588
  cachedEndpoints: cachedData.backendCodes.length
629
589
  };
630
590
  }
631
- /** Get parent IDs by codes, using cache first */ async getParentIdsByCodesWithCache(codes) {
632
- // Try cache first
633
- const cachedMap = await this.permissionCacheService.getActionIdsByCodes(codes);
591
+ /** Get current tenant ID for multi-tenant cache keys */ getCurrentTenantId() {
592
+ if (!this.iamConfigService.isMultiTenant()) {
593
+ return undefined;
594
+ }
595
+ const tenant = this.dataSourceProvider.getCurrentTenant();
596
+ return tenant?.id;
597
+ }
598
+ /** Get parent IDs by codes, using cache first (tenant-aware) */ async getParentIdsByCodesWithCache(codes) {
599
+ const tenantId = this.getCurrentTenantId();
600
+ const cachedMap = await this.permissionCacheService.getActionIdsByCodes(codes, tenantId);
634
601
  if (cachedMap) {
635
602
  return new Set(Object.values(cachedMap));
636
603
  }
637
- // Cache miss - fetch from DB and cache
638
604
  const actionRepo = await this.getActionRepository();
639
605
  const allActions = await actionRepo.find({
640
606
  select: [
@@ -642,33 +608,42 @@ export class PermissionService {
642
608
  'code'
643
609
  ]
644
610
  });
645
- // Build full code → ID map and cache it
646
611
  const fullMap = {};
647
612
  for (const action of allActions){
648
613
  if (action.code) {
649
614
  fullMap[action.code] = action.id;
650
615
  }
651
616
  }
652
- await this.permissionCacheService.setActionCodeMap(fullMap);
653
- // Return only requested codes
617
+ await this.permissionCacheService.setActionCodeMap(fullMap, tenantId);
654
618
  return new Set(codes.map((code)=>fullMap[code]).filter(Boolean));
655
619
  }
656
620
  /** Fetch permissions from DB and cache them (empty permissions are also cached) */ async fetchAndCachePermissions(userId, branchId, companyId) {
657
621
  const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
658
- const permissionMode = this.iamConfigService.getPermissionMode();
659
622
  const cacheOptions = {
660
623
  userId,
661
624
  companyId,
662
625
  branchId,
663
626
  enableCompanyFeature
664
627
  };
665
- // Empty cache data for users with no permissions
666
628
  const emptyData = {
667
629
  frontendActions: [],
668
630
  backendCodes: []
669
631
  };
632
+ const allActionIds = await this.collectAllActionIds(userId, branchId, companyId);
633
+ if (allActionIds.size === 0) {
634
+ await this.permissionCacheService.setMyPermissions(cacheOptions, emptyData);
635
+ return emptyData;
636
+ }
637
+ await this.applyCompanyWhitelist(allActionIds, companyId);
638
+ if (allActionIds.size === 0) {
639
+ await this.permissionCacheService.setMyPermissions(cacheOptions, emptyData);
640
+ return emptyData;
641
+ }
642
+ return this.buildAndCachePermissionData(allActionIds, cacheOptions);
643
+ }
644
+ /** Collect all action IDs based on permission mode (RBAC, DIRECT, or FULL) */ async collectAllActionIds(userId, branchId, companyId) {
645
+ const permissionMode = this.iamConfigService.getPermissionMode();
670
646
  const allActionIds = new Set();
671
- // Collect action IDs based on permission mode
672
647
  if (permissionMode === IAMPermissionMode.RBAC || permissionMode === IAMPermissionMode.FULL) {
673
648
  const userRoleIds = await this.getUserRoleIds(userId, branchId, companyId);
674
649
  if (userRoleIds.length > 0) {
@@ -680,40 +655,33 @@ export class PermissionService {
680
655
  const userActionIds = await this.getUserActionIds(userId, branchId, companyId);
681
656
  userActionIds.forEach((id)=>allActionIds.add(id));
682
657
  }
683
- if (allActionIds.size === 0) {
684
- // Cache empty result to avoid repeated DB hits
685
- await this.permissionCacheService.setMyPermissions(cacheOptions, emptyData);
686
- return emptyData;
687
- }
688
- // Filter by company whitelist
689
- if (enableCompanyFeature && companyId) {
690
- const companyActionIds = await this.getCompanyActionIds(companyId);
691
- if (companyActionIds.length > 0) {
692
- const allowedActionIds = new Set(companyActionIds);
693
- for (const actionId of allActionIds){
694
- if (!allowedActionIds.has(actionId)) {
695
- allActionIds.delete(actionId);
696
- }
697
- }
658
+ return allActionIds;
659
+ }
660
+ /** Apply company whitelist filter to action IDs (mutates the set) */ async applyCompanyWhitelist(actionIds, companyId) {
661
+ if (!this.iamConfigService.isCompanyFeatureEnabled() || !companyId) {
662
+ return;
663
+ }
664
+ const companyActionIds = await this.getCompanyActionIds(companyId);
665
+ if (companyActionIds.length === 0) {
666
+ return;
667
+ }
668
+ const allowedActionIds = new Set(companyActionIds);
669
+ for (const actionId of actionIds){
670
+ if (!allowedActionIds.has(actionId)) {
671
+ actionIds.delete(actionId);
698
672
  }
699
673
  }
700
- if (allActionIds.size === 0) {
701
- // Cache empty result to avoid repeated DB hits
702
- await this.permissionCacheService.setMyPermissions(cacheOptions, emptyData);
703
- return emptyData;
704
- }
705
- // Fetch actions from DB
674
+ }
675
+ /** Fetch actions from DB, separate by type, build cache data, and cache it */ async buildAndCachePermissionData(actionIds, cacheOptions) {
706
676
  const actionRepo = await this.getActionRepository();
707
677
  const actions = await actionRepo.find({
708
678
  where: {
709
- id: In(Array.from(allActionIds))
679
+ id: In(Array.from(actionIds))
710
680
  }
711
681
  });
712
- // Separate by type
713
682
  const backendActions = actions.filter((a)=>a.actionType === ActionType.BACKEND || a.actionType === ActionType.BOTH);
714
683
  const frontendActions = actions.filter((a)=>a.actionType === ActionType.FRONTEND || a.actionType === ActionType.BOTH);
715
684
  const backendCodes = backendActions.map((a)=>a.code).filter((c)=>!!c);
716
- // Build cache data (includes parentId for filtering)
717
685
  const cacheData = {
718
686
  frontendActions: frontendActions.map((a)=>({
719
687
  id: a.id,
@@ -724,7 +692,6 @@ export class PermissionService {
724
692
  })),
725
693
  backendCodes
726
694
  };
727
- // Cache both: full permissions and backend codes for PermissionGuard
728
695
  await Promise.all([
729
696
  this.permissionCacheService.setMyPermissions(cacheOptions, cacheData),
730
697
  this.permissionCacheService.setPermissions(cacheOptions, backendCodes)
@@ -732,11 +699,24 @@ export class PermissionService {
732
699
  return cacheData;
733
700
  }
734
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
+ }
735
716
  /** Get role IDs assigned to a user (merges company-wide + branch-specific roles) */ async getUserRoleIds(userId, branchId, companyId) {
736
717
  const permissionRepo = await this.getPermissionRepository();
737
718
  const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
738
719
  if (!enableCompanyFeature) {
739
- // Simple case: no company feature, get all user roles
740
720
  const permissions = await permissionRepo.find({
741
721
  where: {
742
722
  permissionType: IamPermissionType.USER_ROLE,
@@ -744,33 +724,37 @@ export class PermissionService {
744
724
  sourceId: userId
745
725
  }
746
726
  });
747
- return permissions.map((p)=>p.targetId);
727
+ return permissions.filter((p)=>p.isValid()).map((p)=>p.targetId);
748
728
  }
749
- // Company feature enabled: merge company-wide + branch-specific roles
750
729
  const roleIds = new Set();
751
- // Step 1: Get company-wide roles (branchId = null)
752
- const companyWidePermissions = await permissionRepo.find({
753
- where: {
754
- permissionType: IamPermissionType.USER_ROLE,
755
- sourceType: IamEntityType.USER,
756
- sourceId: userId,
757
- branchId: IsNull(),
758
- companyId: companyId
759
- }
760
- });
761
- companyWidePermissions.forEach((p)=>roleIds.add(p.targetId));
762
- // Step 2: Get branch-specific roles (branchId = value) if branchId provided
730
+ const baseWhere = {
731
+ permissionType: IamPermissionType.USER_ROLE,
732
+ sourceType: IamEntityType.USER,
733
+ sourceId: userId,
734
+ companyId: companyId
735
+ };
763
736
  if (branchId) {
737
+ // Get company-wide + branch-specific roles
738
+ const companyWidePermissions = await permissionRepo.find({
739
+ where: {
740
+ ...baseWhere,
741
+ branchId: IsNull()
742
+ }
743
+ });
744
+ companyWidePermissions.filter((p)=>p.isValid()).forEach((p)=>roleIds.add(p.targetId));
764
745
  const branchPermissions = await permissionRepo.find({
765
746
  where: {
766
- permissionType: IamPermissionType.USER_ROLE,
767
- sourceType: IamEntityType.USER,
768
- sourceId: userId,
769
- branchId,
770
- companyId: companyId
747
+ ...baseWhere,
748
+ branchId
771
749
  }
772
750
  });
773
- branchPermissions.forEach((p)=>roleIds.add(p.targetId));
751
+ branchPermissions.filter((p)=>p.isValid()).forEach((p)=>roleIds.add(p.targetId));
752
+ } else {
753
+ // No branch context: get all roles for this company
754
+ const allPermissions = await permissionRepo.find({
755
+ where: baseWhere
756
+ });
757
+ allPermissions.filter((p)=>p.isValid()).forEach((p)=>roleIds.add(p.targetId));
774
758
  }
775
759
  return Array.from(roleIds);
776
760
  }
@@ -783,13 +767,12 @@ export class PermissionService {
783
767
  sourceId: In(roleIds)
784
768
  }
785
769
  });
786
- return permissions.map((p)=>p.targetId);
770
+ return permissions.filter((p)=>p.isValid()).map((p)=>p.targetId);
787
771
  }
788
772
  /** Get action IDs directly assigned to a user (merges company-wide + branch-specific actions) */ async getUserActionIds(userId, branchId, companyId) {
789
773
  const permissionRepo = await this.getPermissionRepository();
790
774
  const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
791
775
  if (!enableCompanyFeature) {
792
- // Simple case: no company feature, get all user actions
793
776
  const permissions = await permissionRepo.find({
794
777
  where: {
795
778
  permissionType: IamPermissionType.USER_ACTION,
@@ -797,44 +780,43 @@ export class PermissionService {
797
780
  sourceId: userId
798
781
  }
799
782
  });
800
- return permissions.map((p)=>p.targetId);
783
+ return permissions.filter((p)=>p.isValid()).map((p)=>p.targetId);
801
784
  }
802
- // Company feature enabled: merge company-wide + branch-specific actions
803
785
  const actionIds = new Set();
804
- // Step 1: Get company-wide actions (branchId = null, companyId = current)
805
- const companyWideWhere = {
786
+ const baseWhere = {
806
787
  permissionType: IamPermissionType.USER_ACTION,
807
788
  sourceType: IamEntityType.USER,
808
- sourceId: userId,
809
- branchId: IsNull()
789
+ sourceId: userId
810
790
  };
811
791
  if (companyId) {
812
- companyWideWhere.companyId = companyId;
792
+ baseWhere.companyId = companyId;
813
793
  }
814
- const companyWidePermissions = await permissionRepo.find({
815
- where: companyWideWhere
816
- });
817
- companyWidePermissions.forEach((p)=>actionIds.add(p.targetId));
818
- // Step 2: Get branch-specific actions (branchId = value, companyId = current) if branchId provided
819
794
  if (branchId) {
820
- const branchWhere = {
821
- permissionType: IamPermissionType.USER_ACTION,
822
- sourceType: IamEntityType.USER,
823
- sourceId: userId,
824
- branchId
825
- };
826
- if (companyId) {
827
- branchWhere.companyId = companyId;
828
- }
795
+ // Get company-wide + branch-specific actions
796
+ const companyWidePermissions = await permissionRepo.find({
797
+ where: {
798
+ ...baseWhere,
799
+ branchId: IsNull()
800
+ }
801
+ });
802
+ companyWidePermissions.filter((p)=>p.isValid()).forEach((p)=>actionIds.add(p.targetId));
829
803
  const branchPermissions = await permissionRepo.find({
830
- where: branchWhere
804
+ where: {
805
+ ...baseWhere,
806
+ branchId
807
+ }
808
+ });
809
+ branchPermissions.filter((p)=>p.isValid()).forEach((p)=>actionIds.add(p.targetId));
810
+ } else {
811
+ // No branch context: get all actions for this company
812
+ const allPermissions = await permissionRepo.find({
813
+ where: baseWhere
831
814
  });
832
- branchPermissions.forEach((p)=>actionIds.add(p.targetId));
815
+ allPermissions.filter((p)=>p.isValid()).forEach((p)=>actionIds.add(p.targetId));
833
816
  }
834
817
  return Array.from(actionIds);
835
818
  }
836
819
  /** Invalidate permission cache for a user */ async invalidateUserPermissionCache(userId, branchId, companyId) {
837
- // Wrap branchId in array if provided, otherwise invalidate null branch only
838
820
  const branchIds = branchId !== undefined ? [
839
821
  branchId
840
822
  ] : [
@@ -842,11 +824,10 @@ export class PermissionService {
842
824
  ];
843
825
  await this.permissionCacheService.invalidateUser(userId, companyId, branchIds);
844
826
  }
845
- /** Invalidate permission cache for all users with a specific role */ async invalidateRoleMembersCache(roleId) {
827
+ async invalidateRoleMembersCache(roleId) {
846
828
  const permissionRepo = await this.getPermissionRepository();
847
829
  const roleRepo = await this.getRoleRepository();
848
830
  const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
849
- // Find all users assigned to this role with their branch IDs
850
831
  const userRoles = await permissionRepo.find({
851
832
  where: {
852
833
  permissionType: IamPermissionType.USER_ROLE,
@@ -855,21 +836,18 @@ export class PermissionService {
855
836
  targetId: roleId
856
837
  }
857
838
  });
858
- // Get unique user IDs
859
839
  const userIds = [
860
840
  ...new Set(userRoles.map((ur)=>ur.sourceId))
861
841
  ];
862
842
  if (userIds.length === 0) {
863
843
  return 0;
864
844
  }
865
- // Get company ID from role (if company feature enabled)
866
845
  const role = await roleRepo.findOne({
867
846
  where: {
868
847
  id: roleId
869
848
  }
870
849
  });
871
850
  const companyId = role?.companyId || null;
872
- // Get all branch IDs for these users (to invalidate all their cached branches)
873
851
  let branchIds = [
874
852
  null
875
853
  ];
@@ -883,15 +861,13 @@ export class PermissionService {
883
861
  ...new Set(userBranches.map((p)=>p.branchId))
884
862
  ];
885
863
  }
886
- // Invalidate cache for all users with all their branches
887
- return await this.permissionCacheService.invalidateRole(roleId, userIds, companyId, branchIds);
864
+ return this.permissionCacheService.invalidateRole(roleId, userIds, companyId, branchIds);
888
865
  }
889
866
  /** Invalidate permission cache for all users in a company */ async invalidateCompanyMembersCache(companyId) {
890
867
  if (!this.iamConfigService.isCompanyFeatureEnabled()) {
891
868
  return 0;
892
869
  }
893
870
  const permissionRepo = await this.getPermissionRepository();
894
- // Find all unique user IDs and branch IDs that have permissions in this company
895
871
  const userPermissions = await permissionRepo.createQueryBuilder('p').select('DISTINCT p.user_id', 'userId').addSelect('p.branch_id', 'branchId').where('p.company_id = :companyId', {
896
872
  companyId
897
873
  }).andWhere('p.user_id IS NOT NULL').getRawMany();
@@ -907,13 +883,11 @@ export class PermissionService {
907
883
  return await this.permissionCacheService.invalidateUsers(userIds, companyId, branchIds);
908
884
  }
909
885
  // NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
910
- constructor(permissionEvaluator, permissionCacheService, iamConfigService, dataSourceProvider){
911
- _define_property(this, "permissionEvaluator", void 0);
886
+ constructor(permissionCacheService, iamConfigService, dataSourceProvider){
912
887
  _define_property(this, "permissionCacheService", void 0);
913
888
  _define_property(this, "iamConfigService", void 0);
914
889
  _define_property(this, "dataSourceProvider", void 0);
915
890
  _define_property(this, "logger", void 0);
916
- this.permissionEvaluator = permissionEvaluator;
917
891
  this.permissionCacheService = permissionCacheService;
918
892
  this.iamConfigService = iamConfigService;
919
893
  this.dataSourceProvider = dataSourceProvider;
@@ -924,15 +898,13 @@ PermissionService = _ts_decorate([
924
898
  Injectable({
925
899
  scope: Scope.REQUEST
926
900
  }),
927
- _ts_param(0, Inject(PermissionEvaluatorHelper)),
928
- _ts_param(1, Inject(PermissionCacheService)),
929
- _ts_param(2, Inject(IAMConfigService)),
930
- _ts_param(3, Inject(IAMDataSourceProvider)),
901
+ _ts_param(0, Inject(PermissionCacheService)),
902
+ _ts_param(1, Inject(IAMConfigService)),
903
+ _ts_param(2, Inject(IAMDataSourceService)),
931
904
  _ts_metadata("design:type", Function),
932
905
  _ts_metadata("design:paramtypes", [
933
- typeof PermissionEvaluatorHelper === "undefined" ? Object : PermissionEvaluatorHelper,
934
906
  typeof PermissionCacheService === "undefined" ? Object : PermissionCacheService,
935
907
  typeof IAMConfigService === "undefined" ? Object : IAMConfigService,
936
- typeof IAMDataSourceProvider === "undefined" ? Object : IAMDataSourceProvider
908
+ typeof IAMDataSourceService === "undefined" ? Object : IAMDataSourceService
937
909
  ])
938
910
  ], PermissionService);