@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.
- package/README.md +285 -115
- package/cjs/controllers/action.controller.js +45 -2
- package/cjs/controllers/company-action-permission.controller.js +16 -10
- package/cjs/controllers/my-permission.controller.js +7 -3
- package/cjs/controllers/role-permission.controller.js +35 -17
- package/cjs/controllers/role.controller.js +46 -3
- package/cjs/controllers/user-action-permission.controller.js +26 -11
- package/cjs/dtos/action.dto.js +0 -27
- package/cjs/dtos/permission.dto.js +117 -27
- package/cjs/dtos/role.dto.js +0 -27
- package/cjs/entities/permission-base.entity.js +0 -12
- package/cjs/helpers/company-access.helper.js +19 -0
- package/cjs/helpers/index.js +1 -1
- package/cjs/interfaces/iam-module-options.interface.js +0 -14
- package/cjs/interfaces/index.js +0 -1
- package/cjs/modules/iam.module.js +50 -102
- package/cjs/services/action.service.js +30 -41
- package/cjs/services/iam-config.service.js +2 -5
- package/cjs/services/{iam-datasource.provider.js → iam-datasource.service.js} +33 -36
- package/cjs/services/index.js +1 -1
- package/cjs/services/permission-cache.service.js +31 -61
- package/cjs/services/permission.service.js +160 -188
- package/cjs/services/role.service.js +8 -8
- package/cjs/types/logic-node.type.js +0 -24
- package/controllers/company-action-permission.controller.d.ts +3 -3
- package/controllers/my-permission.controller.d.ts +2 -2
- package/controllers/role-permission.controller.d.ts +7 -5
- package/controllers/user-action-permission.controller.d.ts +6 -4
- package/dtos/action.dto.d.ts +0 -7
- package/dtos/permission.dto.d.ts +4 -0
- package/dtos/role.dto.d.ts +0 -7
- package/entities/permission-base.entity.d.ts +0 -4
- package/fesm/controllers/action.controller.js +47 -4
- package/fesm/controllers/company-action-permission.controller.js +18 -12
- package/fesm/controllers/index.js +1 -1
- package/fesm/controllers/my-permission.controller.js +7 -3
- package/fesm/controllers/role-permission.controller.js +37 -19
- package/fesm/controllers/role.controller.js +45 -2
- package/fesm/controllers/user-action-permission.controller.js +28 -13
- package/fesm/dtos/action.dto.js +0 -24
- package/fesm/dtos/permission.dto.js +117 -29
- package/fesm/dtos/role.dto.js +0 -24
- package/fesm/entities/permission-base.entity.js +0 -12
- package/fesm/helpers/company-access.helper.js +14 -0
- package/fesm/helpers/index.js +1 -1
- package/fesm/interfaces/iam-module-options.interface.js +3 -1
- package/fesm/interfaces/index.js +0 -1
- package/fesm/modules/iam.module.js +52 -104
- package/fesm/services/action.service.js +32 -43
- package/fesm/services/iam-config.service.js +2 -5
- package/fesm/services/{iam-datasource.provider.js → iam-datasource.service.js} +31 -34
- package/fesm/services/index.js +1 -1
- package/fesm/services/permission-cache.service.js +31 -61
- package/fesm/services/permission.service.js +161 -189
- package/fesm/services/role.service.js +8 -8
- package/fesm/types/logic-node.type.js +1 -10
- package/helpers/company-access.helper.d.ts +3 -0
- package/helpers/index.d.ts +1 -1
- package/interfaces/iam-module-options.interface.d.ts +9 -1
- package/interfaces/index.d.ts +0 -1
- package/modules/iam.module.d.ts +2 -2
- package/package.json +3 -3
- package/services/action.service.d.ts +6 -4
- package/services/iam-config.service.d.ts +2 -2
- package/services/{iam-datasource.provider.d.ts → iam-datasource.service.d.ts} +4 -5
- package/services/index.d.ts +1 -1
- package/services/permission-cache.service.d.ts +4 -6
- package/services/permission.service.d.ts +8 -4
- package/services/role.service.d.ts +3 -3
- package/types/logic-node.type.d.ts +0 -8
- package/cjs/helpers/permission-evaluator.helper.js +0 -175
- package/cjs/interfaces/iam-module-async-options.interface.js +0 -4
- package/fesm/helpers/permission-evaluator.helper.js +0 -165
- package/fesm/interfaces/iam-module-async-options.interface.js +0 -3
- package/helpers/permission-evaluator.helper.d.ts +0 -26
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
701
|
-
|
|
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(
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
767
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
928
|
-
_ts_param(1, Inject(
|
|
929
|
-
_ts_param(2, Inject(
|
|
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
|
|
908
|
+
typeof IAMDataSourceService === "undefined" ? Object : IAMDataSourceService
|
|
937
909
|
])
|
|
938
910
|
], PermissionService);
|