@flusys/nestjs-iam 6.2.2 → 6.4.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/cjs/config/message-keys.js +3 -1
- package/cjs/controllers/my-permission.controller.js +54 -0
- package/cjs/controllers/my-permission.controller.spec.js +73 -1
- package/cjs/services/permission.service.js +98 -0
- package/cjs/services/permission.service.spec.js +146 -0
- package/config/message-keys.d.ts +2 -0
- package/controllers/my-permission.controller.d.ts +3 -1
- package/fesm/config/message-keys.js +3 -1
- package/fesm/controllers/my-permission.controller.js +55 -1
- package/fesm/controllers/my-permission.controller.spec.js +73 -1
- package/fesm/services/permission.service.js +98 -0
- package/fesm/services/permission.service.spec.js +146 -0
- package/package.json +3 -3
- package/services/permission.service.d.ts +2 -0
|
@@ -51,7 +51,9 @@ const COMPANY_ACTION_PERMISSION_MESSAGES = {
|
|
|
51
51
|
GET_SUCCESS: 'company.action.permission.get.success'
|
|
52
52
|
};
|
|
53
53
|
const MY_PERMISSION_MESSAGES = {
|
|
54
|
-
GET_SUCCESS: 'my.permission.get.success'
|
|
54
|
+
GET_SUCCESS: 'my.permission.get.success',
|
|
55
|
+
MY_ROLES_SUCCESS: 'my.permission.roles.success',
|
|
56
|
+
MY_ACTIONS_SUCCESS: 'my.permission.actions.success'
|
|
55
57
|
};
|
|
56
58
|
const IAM_MODE_MESSAGES = {
|
|
57
59
|
DIRECT_MODE_UNAVAILABLE: 'iam.direct.mode.unavailable',
|
|
@@ -51,6 +51,24 @@ let MyPermissionController = class MyPermissionController {
|
|
|
51
51
|
data
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
async getMyRoles(user) {
|
|
55
|
+
const data = await this.permissionService.getUserEffectiveRoles(user.id, user.companyId ?? null, user.branchId ?? null);
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
message: 'Roles loaded successfully',
|
|
59
|
+
messageKey: _config.MY_PERMISSION_MESSAGES.MY_ROLES_SUCCESS,
|
|
60
|
+
data
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async getMyActions(user) {
|
|
64
|
+
const data = await this.permissionService.getUserEffectiveActions(user.id, user.companyId ?? null, user.branchId ?? null);
|
|
65
|
+
return {
|
|
66
|
+
success: true,
|
|
67
|
+
message: 'Actions loaded successfully',
|
|
68
|
+
messageKey: _config.MY_PERMISSION_MESSAGES.MY_ACTIONS_SUCCESS,
|
|
69
|
+
data
|
|
70
|
+
};
|
|
71
|
+
}
|
|
54
72
|
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|
|
55
73
|
constructor(permissionService){
|
|
56
74
|
_define_property(this, "permissionService", void 0);
|
|
@@ -80,6 +98,42 @@ _ts_decorate([
|
|
|
80
98
|
]),
|
|
81
99
|
_ts_metadata("design:returntype", Promise)
|
|
82
100
|
], MyPermissionController.prototype, "getMyPermissions", null);
|
|
101
|
+
_ts_decorate([
|
|
102
|
+
(0, _common.Post)('my-roles'),
|
|
103
|
+
(0, _swagger.ApiOperation)({
|
|
104
|
+
summary: 'Get current user roles',
|
|
105
|
+
description: 'Returns effective roles for the authenticated user, merging company-wide (branch-independent) and login-branch-specific role assignments.'
|
|
106
|
+
}),
|
|
107
|
+
(0, _nestjsshared.ApiResponseDto)(_permissiondto.UserRoleResponseDto, true, 'single'),
|
|
108
|
+
(0, _swagger.ApiResponse)({
|
|
109
|
+
status: 401,
|
|
110
|
+
description: 'Unauthorized'
|
|
111
|
+
}),
|
|
112
|
+
_ts_param(0, (0, _nestjsshared.CurrentUser)()),
|
|
113
|
+
_ts_metadata("design:type", Function),
|
|
114
|
+
_ts_metadata("design:paramtypes", [
|
|
115
|
+
typeof _nestjsshared.ILoggedUserInfo === "undefined" ? Object : _nestjsshared.ILoggedUserInfo
|
|
116
|
+
]),
|
|
117
|
+
_ts_metadata("design:returntype", Promise)
|
|
118
|
+
], MyPermissionController.prototype, "getMyRoles", null);
|
|
119
|
+
_ts_decorate([
|
|
120
|
+
(0, _common.Post)('my-actions'),
|
|
121
|
+
(0, _swagger.ApiOperation)({
|
|
122
|
+
summary: 'Get current user direct actions',
|
|
123
|
+
description: 'Returns effective direct action assignments for the authenticated user, merging company-wide (branch-independent) and login-branch-specific assignments.'
|
|
124
|
+
}),
|
|
125
|
+
(0, _nestjsshared.ApiResponseDto)(_permissiondto.UserActionResponseDto, true, 'single'),
|
|
126
|
+
(0, _swagger.ApiResponse)({
|
|
127
|
+
status: 401,
|
|
128
|
+
description: 'Unauthorized'
|
|
129
|
+
}),
|
|
130
|
+
_ts_param(0, (0, _nestjsshared.CurrentUser)()),
|
|
131
|
+
_ts_metadata("design:type", Function),
|
|
132
|
+
_ts_metadata("design:paramtypes", [
|
|
133
|
+
typeof _nestjsshared.ILoggedUserInfo === "undefined" ? Object : _nestjsshared.ILoggedUserInfo
|
|
134
|
+
]),
|
|
135
|
+
_ts_metadata("design:returntype", Promise)
|
|
136
|
+
], MyPermissionController.prototype, "getMyActions", null);
|
|
83
137
|
MyPermissionController = _ts_decorate([
|
|
84
138
|
(0, _swagger.ApiTags)('IAM - My Permissions'),
|
|
85
139
|
(0, _common.Controller)('iam/permissions'),
|
|
@@ -9,7 +9,9 @@ describe('MyPermissionController', ()=>{
|
|
|
9
9
|
let mockPermissionService;
|
|
10
10
|
beforeEach(()=>{
|
|
11
11
|
mockPermissionService = {
|
|
12
|
-
getMyPermissions: jest.fn()
|
|
12
|
+
getMyPermissions: jest.fn(),
|
|
13
|
+
getUserEffectiveRoles: jest.fn(),
|
|
14
|
+
getUserEffectiveActions: jest.fn()
|
|
13
15
|
};
|
|
14
16
|
controller = new _mypermissioncontroller.MyPermissionController(mockPermissionService);
|
|
15
17
|
});
|
|
@@ -53,4 +55,74 @@ describe('MyPermissionController', ()=>{
|
|
|
53
55
|
expect(mockPermissionService.getMyPermissions).toHaveBeenCalledWith('user-1', null, null, undefined);
|
|
54
56
|
});
|
|
55
57
|
});
|
|
58
|
+
describe('getMyRoles', ()=>{
|
|
59
|
+
it('resolves effective roles for the current user, company, and branch', async ()=>{
|
|
60
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
61
|
+
id: 'user-1',
|
|
62
|
+
companyId: 'company-1',
|
|
63
|
+
branchId: 'branch-1'
|
|
64
|
+
});
|
|
65
|
+
const roles = [
|
|
66
|
+
{
|
|
67
|
+
id: 'perm-1',
|
|
68
|
+
roleId: 'role-1',
|
|
69
|
+
roleName: 'Manager'
|
|
70
|
+
}
|
|
71
|
+
];
|
|
72
|
+
mockPermissionService.getUserEffectiveRoles.mockResolvedValue(roles);
|
|
73
|
+
const result = await controller.getMyRoles(user);
|
|
74
|
+
expect(mockPermissionService.getUserEffectiveRoles).toHaveBeenCalledWith('user-1', 'company-1', 'branch-1');
|
|
75
|
+
expect(result).toEqual({
|
|
76
|
+
success: true,
|
|
77
|
+
message: 'Roles loaded successfully',
|
|
78
|
+
messageKey: expect.any(String),
|
|
79
|
+
data: roles
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
it('passes null for missing company and branch context', async ()=>{
|
|
83
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
84
|
+
id: 'user-1',
|
|
85
|
+
companyId: undefined,
|
|
86
|
+
branchId: undefined
|
|
87
|
+
});
|
|
88
|
+
mockPermissionService.getUserEffectiveRoles.mockResolvedValue([]);
|
|
89
|
+
await controller.getMyRoles(user);
|
|
90
|
+
expect(mockPermissionService.getUserEffectiveRoles).toHaveBeenCalledWith('user-1', null, null);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('getMyActions', ()=>{
|
|
94
|
+
it('resolves effective actions for the current user, company, and branch', async ()=>{
|
|
95
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
96
|
+
id: 'user-1',
|
|
97
|
+
companyId: 'company-1',
|
|
98
|
+
branchId: 'branch-1'
|
|
99
|
+
});
|
|
100
|
+
const actions = [
|
|
101
|
+
{
|
|
102
|
+
id: 'perm-1',
|
|
103
|
+
actionId: 'action-1',
|
|
104
|
+
actionCode: 'user.view'
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
mockPermissionService.getUserEffectiveActions.mockResolvedValue(actions);
|
|
108
|
+
const result = await controller.getMyActions(user);
|
|
109
|
+
expect(mockPermissionService.getUserEffectiveActions).toHaveBeenCalledWith('user-1', 'company-1', 'branch-1');
|
|
110
|
+
expect(result).toEqual({
|
|
111
|
+
success: true,
|
|
112
|
+
message: 'Actions loaded successfully',
|
|
113
|
+
messageKey: expect.any(String),
|
|
114
|
+
data: actions
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
it('passes null for missing company and branch context', async ()=>{
|
|
118
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
119
|
+
id: 'user-1',
|
|
120
|
+
companyId: undefined,
|
|
121
|
+
branchId: undefined
|
|
122
|
+
});
|
|
123
|
+
mockPermissionService.getUserEffectiveActions.mockResolvedValue([]);
|
|
124
|
+
await controller.getMyActions(user);
|
|
125
|
+
expect(mockPermissionService.getUserEffectiveActions).toHaveBeenCalledWith('user-1', null, null);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
56
128
|
});
|
|
@@ -531,6 +531,104 @@ let PermissionService = class PermissionService {
|
|
|
531
531
|
};
|
|
532
532
|
});
|
|
533
533
|
}
|
|
534
|
+
// Effective (Merged) Permissions — self-scoped, company-wide + branch-specific
|
|
535
|
+
/** Get user's effective roles: company-wide (branchId null) merged with branch-specific */ async getUserEffectiveRoles(userId, companyId, branchId) {
|
|
536
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
537
|
+
const roleRepo = await this.getRoleRepository();
|
|
538
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
539
|
+
const baseWhere = {
|
|
540
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ROLE,
|
|
541
|
+
sourceType: _iamentitytypeenum.IamEntityType.USER,
|
|
542
|
+
sourceId: userId
|
|
543
|
+
};
|
|
544
|
+
if (enableCompanyFeature && companyId) baseWhere.companyId = companyId;
|
|
545
|
+
const permissions = enableCompanyFeature && branchId ? await permissionRepo.find({
|
|
546
|
+
where: [
|
|
547
|
+
{
|
|
548
|
+
...baseWhere,
|
|
549
|
+
branchId: (0, _typeorm.IsNull)()
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
...baseWhere,
|
|
553
|
+
branchId
|
|
554
|
+
}
|
|
555
|
+
]
|
|
556
|
+
}) : await permissionRepo.find({
|
|
557
|
+
where: baseWhere
|
|
558
|
+
});
|
|
559
|
+
const validPermissions = permissions.filter((p)=>p.isValid());
|
|
560
|
+
if (validPermissions.length === 0) {
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
const roleIds = [
|
|
564
|
+
...new Set(validPermissions.map((p)=>p.targetId))
|
|
565
|
+
];
|
|
566
|
+
const roles = await roleRepo.find({
|
|
567
|
+
where: {
|
|
568
|
+
id: (0, _typeorm.In)(roleIds)
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
const roleMap = new Map(roles.map((r)=>[
|
|
572
|
+
r.id,
|
|
573
|
+
r
|
|
574
|
+
]));
|
|
575
|
+
return validPermissions.filter((p)=>roleMap.has(p.targetId)).map((p)=>{
|
|
576
|
+
const role = roleMap.get(p.targetId);
|
|
577
|
+
return {
|
|
578
|
+
id: p.id,
|
|
579
|
+
userId: p.sourceId,
|
|
580
|
+
roleId: role.id,
|
|
581
|
+
roleName: role.name,
|
|
582
|
+
branchId: enableCompanyFeature ? p.branchId ?? null : null,
|
|
583
|
+
createdAt: p.createdAt
|
|
584
|
+
};
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
/** Get user's effective actions: company-wide (branchId null) merged with branch-specific */ async getUserEffectiveActions(userId, companyId, branchId) {
|
|
588
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
589
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
590
|
+
const baseWhere = {
|
|
591
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ACTION,
|
|
592
|
+
sourceType: _iamentitytypeenum.IamEntityType.USER,
|
|
593
|
+
sourceId: userId
|
|
594
|
+
};
|
|
595
|
+
if (enableCompanyFeature && companyId) baseWhere.companyId = companyId;
|
|
596
|
+
const permissions = enableCompanyFeature && branchId ? await permissionRepo.find({
|
|
597
|
+
where: [
|
|
598
|
+
{
|
|
599
|
+
...baseWhere,
|
|
600
|
+
branchId: (0, _typeorm.IsNull)()
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
...baseWhere,
|
|
604
|
+
branchId
|
|
605
|
+
}
|
|
606
|
+
]
|
|
607
|
+
}) : await permissionRepo.find({
|
|
608
|
+
where: baseWhere
|
|
609
|
+
});
|
|
610
|
+
const validPermissions = permissions.filter((p)=>p.isValid());
|
|
611
|
+
if (validPermissions.length === 0) {
|
|
612
|
+
return [];
|
|
613
|
+
}
|
|
614
|
+
const actionIds = [
|
|
615
|
+
...new Set(validPermissions.map((p)=>p.targetId))
|
|
616
|
+
];
|
|
617
|
+
const actionMap = await this.getActionsMap(actionIds);
|
|
618
|
+
return validPermissions.filter((p)=>actionMap.has(p.targetId)).map((p)=>{
|
|
619
|
+
const action = actionMap.get(p.targetId);
|
|
620
|
+
return {
|
|
621
|
+
id: p.id,
|
|
622
|
+
userId: p.sourceId,
|
|
623
|
+
actionId: action.id,
|
|
624
|
+
actionCode: action.code ?? '',
|
|
625
|
+
actionName: action.name,
|
|
626
|
+
companyId: ('companyId' in p ? p.companyId : null) ?? null,
|
|
627
|
+
branchId: ('branchId' in p ? p.branchId : null) ?? null,
|
|
628
|
+
createdAt: p.createdAt
|
|
629
|
+
};
|
|
630
|
+
});
|
|
631
|
+
}
|
|
534
632
|
// My Permissions
|
|
535
633
|
/** Get user's effective permissions (cache-first approach) */ async getMyPermissions(userId, branchId, companyId, parentCodes) {
|
|
536
634
|
return this.permissionCacheService.getMyPermissionsResponse(userId, branchId, companyId, parentCodes);
|
|
@@ -573,6 +573,152 @@ describe('PermissionService', ()=>{
|
|
|
573
573
|
}));
|
|
574
574
|
});
|
|
575
575
|
});
|
|
576
|
+
// ==================== Effective (Merged) Permissions ====================
|
|
577
|
+
describe('getUserEffectiveRoles', ()=>{
|
|
578
|
+
it('returns an empty array when the user has no role assignments', async ()=>{
|
|
579
|
+
mockPermissionRepo.find.mockResolvedValue([]);
|
|
580
|
+
expect(await service.getUserEffectiveRoles('user-1')).toEqual([]);
|
|
581
|
+
expect(mockRoleRepo.find).not.toHaveBeenCalled();
|
|
582
|
+
});
|
|
583
|
+
it('queries only by userId when the company feature is disabled, ignoring companyId/branchId', async ()=>{
|
|
584
|
+
mockPermissionRepo.find.mockResolvedValue([]);
|
|
585
|
+
await service.getUserEffectiveRoles('user-1', 'company-1', 'branch-1');
|
|
586
|
+
expect(mockPermissionRepo.find).toHaveBeenCalledWith({
|
|
587
|
+
where: {
|
|
588
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ROLE,
|
|
589
|
+
sourceType: _iamentitytypeenum.IamEntityType.USER,
|
|
590
|
+
sourceId: 'user-1'
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
it('merges company-wide (branchId null) and branch-specific role assignments via an OR query', async ()=>{
|
|
595
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
596
|
+
mockPermissionRepo.find.mockResolvedValue([
|
|
597
|
+
buildPermissionRow({
|
|
598
|
+
id: 'perm-1',
|
|
599
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ROLE,
|
|
600
|
+
targetType: _iamentitytypeenum.IamEntityType.ROLE,
|
|
601
|
+
targetId: 'role-1',
|
|
602
|
+
companyId: 'company-1',
|
|
603
|
+
branchId: null
|
|
604
|
+
}),
|
|
605
|
+
buildPermissionRow({
|
|
606
|
+
id: 'perm-2',
|
|
607
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ROLE,
|
|
608
|
+
targetType: _iamentitytypeenum.IamEntityType.ROLE,
|
|
609
|
+
targetId: 'role-2',
|
|
610
|
+
companyId: 'company-1',
|
|
611
|
+
branchId: 'branch-1'
|
|
612
|
+
})
|
|
613
|
+
]);
|
|
614
|
+
mockRoleRepo.find.mockResolvedValue([
|
|
615
|
+
{
|
|
616
|
+
id: 'role-1',
|
|
617
|
+
name: 'Company Admin'
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
id: 'role-2',
|
|
621
|
+
name: 'Branch Manager'
|
|
622
|
+
}
|
|
623
|
+
]);
|
|
624
|
+
const result = await service.getUserEffectiveRoles('user-1', 'company-1', 'branch-1');
|
|
625
|
+
expect(mockPermissionRepo.find).toHaveBeenCalledWith({
|
|
626
|
+
where: [
|
|
627
|
+
expect.objectContaining({
|
|
628
|
+
sourceId: 'user-1',
|
|
629
|
+
companyId: 'company-1',
|
|
630
|
+
branchId: expect.anything()
|
|
631
|
+
}),
|
|
632
|
+
expect.objectContaining({
|
|
633
|
+
sourceId: 'user-1',
|
|
634
|
+
companyId: 'company-1',
|
|
635
|
+
branchId: 'branch-1'
|
|
636
|
+
})
|
|
637
|
+
]
|
|
638
|
+
});
|
|
639
|
+
expect(result.map((r)=>r.roleId).sort()).toEqual([
|
|
640
|
+
'role-1',
|
|
641
|
+
'role-2'
|
|
642
|
+
]);
|
|
643
|
+
});
|
|
644
|
+
it('excludes expired/not-yet-valid permission rows', async ()=>{
|
|
645
|
+
mockPermissionRepo.find.mockResolvedValue([
|
|
646
|
+
buildPermissionRow({
|
|
647
|
+
permissionType: _iampermissiontypeenum.IamPermissionType.USER_ROLE,
|
|
648
|
+
targetType: _iamentitytypeenum.IamEntityType.ROLE,
|
|
649
|
+
targetId: 'role-1',
|
|
650
|
+
validUntil: new Date('2000-01-01')
|
|
651
|
+
})
|
|
652
|
+
]);
|
|
653
|
+
const result = await service.getUserEffectiveRoles('user-1');
|
|
654
|
+
expect(result).toEqual([]);
|
|
655
|
+
expect(mockRoleRepo.find).not.toHaveBeenCalled();
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
describe('getUserEffectiveActions', ()=>{
|
|
659
|
+
it('returns an empty array when the user has no action assignments', async ()=>{
|
|
660
|
+
mockPermissionRepo.find.mockResolvedValue([]);
|
|
661
|
+
expect(await service.getUserEffectiveActions('user-1')).toEqual([]);
|
|
662
|
+
expect(mockActionRepo.find).not.toHaveBeenCalled();
|
|
663
|
+
});
|
|
664
|
+
it('merges company-wide (branchId null) and branch-specific action assignments via an OR query', async ()=>{
|
|
665
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
666
|
+
mockPermissionRepo.find.mockResolvedValue([
|
|
667
|
+
buildPermissionRow({
|
|
668
|
+
id: 'perm-1',
|
|
669
|
+
targetId: 'action-1',
|
|
670
|
+
companyId: 'company-1',
|
|
671
|
+
branchId: null
|
|
672
|
+
}),
|
|
673
|
+
buildPermissionRow({
|
|
674
|
+
id: 'perm-2',
|
|
675
|
+
targetId: 'action-2',
|
|
676
|
+
companyId: 'company-1',
|
|
677
|
+
branchId: 'branch-1'
|
|
678
|
+
})
|
|
679
|
+
]);
|
|
680
|
+
mockActionRepo.find.mockResolvedValue([
|
|
681
|
+
buildAction({
|
|
682
|
+
id: 'action-1',
|
|
683
|
+
code: 'user.view'
|
|
684
|
+
}),
|
|
685
|
+
buildAction({
|
|
686
|
+
id: 'action-2',
|
|
687
|
+
code: 'user.create'
|
|
688
|
+
})
|
|
689
|
+
]);
|
|
690
|
+
const result = await service.getUserEffectiveActions('user-1', 'company-1', 'branch-1');
|
|
691
|
+
expect(mockPermissionRepo.find).toHaveBeenCalledWith({
|
|
692
|
+
where: [
|
|
693
|
+
expect.objectContaining({
|
|
694
|
+
sourceId: 'user-1',
|
|
695
|
+
companyId: 'company-1',
|
|
696
|
+
branchId: expect.anything()
|
|
697
|
+
}),
|
|
698
|
+
expect.objectContaining({
|
|
699
|
+
sourceId: 'user-1',
|
|
700
|
+
companyId: 'company-1',
|
|
701
|
+
branchId: 'branch-1'
|
|
702
|
+
})
|
|
703
|
+
]
|
|
704
|
+
});
|
|
705
|
+
expect(result.map((a)=>a.actionId).sort()).toEqual([
|
|
706
|
+
'action-1',
|
|
707
|
+
'action-2'
|
|
708
|
+
]);
|
|
709
|
+
});
|
|
710
|
+
it('excludes expired/not-yet-valid permission rows', async ()=>{
|
|
711
|
+
mockPermissionRepo.find.mockResolvedValue([
|
|
712
|
+
buildPermissionRow({
|
|
713
|
+
targetId: 'action-1',
|
|
714
|
+
validUntil: new Date('2000-01-01')
|
|
715
|
+
})
|
|
716
|
+
]);
|
|
717
|
+
const result = await service.getUserEffectiveActions('user-1');
|
|
718
|
+
expect(result).toEqual([]);
|
|
719
|
+
expect(mockActionRepo.find).not.toHaveBeenCalled();
|
|
720
|
+
});
|
|
721
|
+
});
|
|
576
722
|
// ==================== My Permissions / permission-mode branching ====================
|
|
577
723
|
describe('getMyPermissions', ()=>{
|
|
578
724
|
it('delegates to the cache service and returns its response', async ()=>{
|
package/config/message-keys.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export declare const COMPANY_ACTION_PERMISSION_MESSAGES: {
|
|
|
18
18
|
};
|
|
19
19
|
export declare const MY_PERMISSION_MESSAGES: {
|
|
20
20
|
readonly GET_SUCCESS: "my.permission.get.success";
|
|
21
|
+
readonly MY_ROLES_SUCCESS: "my.permission.roles.success";
|
|
22
|
+
readonly MY_ACTIONS_SUCCESS: "my.permission.actions.success";
|
|
21
23
|
};
|
|
22
24
|
export declare const IAM_MODE_MESSAGES: {
|
|
23
25
|
readonly DIRECT_MODE_UNAVAILABLE: "iam.direct.mode.unavailable";
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ILoggedUserInfo, SingleResponseDto } from '@flusys/nestjs-shared';
|
|
2
|
-
import { MyPermissionsQueryDto, MyPermissionsResponseDto } from '../dtos/permission.dto';
|
|
2
|
+
import { MyPermissionsQueryDto, MyPermissionsResponseDto, UserActionResponseDto, UserRoleResponseDto } from '../dtos/permission.dto';
|
|
3
3
|
import { PermissionService } from '../services/permission.service';
|
|
4
4
|
export declare class MyPermissionController {
|
|
5
5
|
private readonly permissionService;
|
|
6
6
|
constructor(permissionService: PermissionService);
|
|
7
7
|
getMyPermissions(query: MyPermissionsQueryDto, user: ILoggedUserInfo): Promise<SingleResponseDto<MyPermissionsResponseDto>>;
|
|
8
|
+
getMyRoles(user: ILoggedUserInfo): Promise<SingleResponseDto<UserRoleResponseDto[]>>;
|
|
9
|
+
getMyActions(user: ILoggedUserInfo): Promise<SingleResponseDto<UserActionResponseDto[]>>;
|
|
8
10
|
}
|
|
@@ -18,7 +18,9 @@ export const COMPANY_ACTION_PERMISSION_MESSAGES = {
|
|
|
18
18
|
GET_SUCCESS: 'company.action.permission.get.success'
|
|
19
19
|
};
|
|
20
20
|
export const MY_PERMISSION_MESSAGES = {
|
|
21
|
-
GET_SUCCESS: 'my.permission.get.success'
|
|
21
|
+
GET_SUCCESS: 'my.permission.get.success',
|
|
22
|
+
MY_ROLES_SUCCESS: 'my.permission.roles.success',
|
|
23
|
+
MY_ACTIONS_SUCCESS: 'my.permission.actions.success'
|
|
22
24
|
};
|
|
23
25
|
export const IAM_MODE_MESSAGES = {
|
|
24
26
|
DIRECT_MODE_UNAVAILABLE: 'iam.direct.mode.unavailable',
|
|
@@ -29,7 +29,7 @@ import { ApiResponseDto, CurrentUser, ILoggedUserInfo, JwtAuthGuard } from '@flu
|
|
|
29
29
|
import { MY_PERMISSION_MESSAGES } from '../config';
|
|
30
30
|
import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
|
31
31
|
import { ApiBearerAuth, ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
|
32
|
-
import { MyPermissionsQueryDto, MyPermissionsResponseDto } from '../dtos/permission.dto';
|
|
32
|
+
import { MyPermissionsQueryDto, MyPermissionsResponseDto, UserActionResponseDto, UserRoleResponseDto } from '../dtos/permission.dto';
|
|
33
33
|
import { PermissionService } from '../services/permission.service';
|
|
34
34
|
export class MyPermissionController {
|
|
35
35
|
async getMyPermissions(query, user) {
|
|
@@ -41,6 +41,24 @@ export class MyPermissionController {
|
|
|
41
41
|
data
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
async getMyRoles(user) {
|
|
45
|
+
const data = await this.permissionService.getUserEffectiveRoles(user.id, user.companyId ?? null, user.branchId ?? null);
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
message: 'Roles loaded successfully',
|
|
49
|
+
messageKey: MY_PERMISSION_MESSAGES.MY_ROLES_SUCCESS,
|
|
50
|
+
data
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async getMyActions(user) {
|
|
54
|
+
const data = await this.permissionService.getUserEffectiveActions(user.id, user.companyId ?? null, user.branchId ?? null);
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
message: 'Actions loaded successfully',
|
|
58
|
+
messageKey: MY_PERMISSION_MESSAGES.MY_ACTIONS_SUCCESS,
|
|
59
|
+
data
|
|
60
|
+
};
|
|
61
|
+
}
|
|
44
62
|
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|
|
45
63
|
constructor(permissionService){
|
|
46
64
|
_define_property(this, "permissionService", void 0);
|
|
@@ -70,6 +88,42 @@ _ts_decorate([
|
|
|
70
88
|
]),
|
|
71
89
|
_ts_metadata("design:returntype", Promise)
|
|
72
90
|
], MyPermissionController.prototype, "getMyPermissions", null);
|
|
91
|
+
_ts_decorate([
|
|
92
|
+
Post('my-roles'),
|
|
93
|
+
ApiOperation({
|
|
94
|
+
summary: 'Get current user roles',
|
|
95
|
+
description: 'Returns effective roles for the authenticated user, merging company-wide (branch-independent) and login-branch-specific role assignments.'
|
|
96
|
+
}),
|
|
97
|
+
ApiResponseDto(UserRoleResponseDto, true, 'single'),
|
|
98
|
+
ApiResponse({
|
|
99
|
+
status: 401,
|
|
100
|
+
description: 'Unauthorized'
|
|
101
|
+
}),
|
|
102
|
+
_ts_param(0, CurrentUser()),
|
|
103
|
+
_ts_metadata("design:type", Function),
|
|
104
|
+
_ts_metadata("design:paramtypes", [
|
|
105
|
+
typeof ILoggedUserInfo === "undefined" ? Object : ILoggedUserInfo
|
|
106
|
+
]),
|
|
107
|
+
_ts_metadata("design:returntype", Promise)
|
|
108
|
+
], MyPermissionController.prototype, "getMyRoles", null);
|
|
109
|
+
_ts_decorate([
|
|
110
|
+
Post('my-actions'),
|
|
111
|
+
ApiOperation({
|
|
112
|
+
summary: 'Get current user direct actions',
|
|
113
|
+
description: 'Returns effective direct action assignments for the authenticated user, merging company-wide (branch-independent) and login-branch-specific assignments.'
|
|
114
|
+
}),
|
|
115
|
+
ApiResponseDto(UserActionResponseDto, true, 'single'),
|
|
116
|
+
ApiResponse({
|
|
117
|
+
status: 401,
|
|
118
|
+
description: 'Unauthorized'
|
|
119
|
+
}),
|
|
120
|
+
_ts_param(0, CurrentUser()),
|
|
121
|
+
_ts_metadata("design:type", Function),
|
|
122
|
+
_ts_metadata("design:paramtypes", [
|
|
123
|
+
typeof ILoggedUserInfo === "undefined" ? Object : ILoggedUserInfo
|
|
124
|
+
]),
|
|
125
|
+
_ts_metadata("design:returntype", Promise)
|
|
126
|
+
], MyPermissionController.prototype, "getMyActions", null);
|
|
73
127
|
MyPermissionController = _ts_decorate([
|
|
74
128
|
ApiTags('IAM - My Permissions'),
|
|
75
129
|
Controller('iam/permissions'),
|
|
@@ -5,7 +5,9 @@ describe('MyPermissionController', ()=>{
|
|
|
5
5
|
let mockPermissionService;
|
|
6
6
|
beforeEach(()=>{
|
|
7
7
|
mockPermissionService = {
|
|
8
|
-
getMyPermissions: jest.fn()
|
|
8
|
+
getMyPermissions: jest.fn(),
|
|
9
|
+
getUserEffectiveRoles: jest.fn(),
|
|
10
|
+
getUserEffectiveActions: jest.fn()
|
|
9
11
|
};
|
|
10
12
|
controller = new MyPermissionController(mockPermissionService);
|
|
11
13
|
});
|
|
@@ -49,4 +51,74 @@ describe('MyPermissionController', ()=>{
|
|
|
49
51
|
expect(mockPermissionService.getMyPermissions).toHaveBeenCalledWith('user-1', null, null, undefined);
|
|
50
52
|
});
|
|
51
53
|
});
|
|
54
|
+
describe('getMyRoles', ()=>{
|
|
55
|
+
it('resolves effective roles for the current user, company, and branch', async ()=>{
|
|
56
|
+
const user = buildMockUser({
|
|
57
|
+
id: 'user-1',
|
|
58
|
+
companyId: 'company-1',
|
|
59
|
+
branchId: 'branch-1'
|
|
60
|
+
});
|
|
61
|
+
const roles = [
|
|
62
|
+
{
|
|
63
|
+
id: 'perm-1',
|
|
64
|
+
roleId: 'role-1',
|
|
65
|
+
roleName: 'Manager'
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
mockPermissionService.getUserEffectiveRoles.mockResolvedValue(roles);
|
|
69
|
+
const result = await controller.getMyRoles(user);
|
|
70
|
+
expect(mockPermissionService.getUserEffectiveRoles).toHaveBeenCalledWith('user-1', 'company-1', 'branch-1');
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
success: true,
|
|
73
|
+
message: 'Roles loaded successfully',
|
|
74
|
+
messageKey: expect.any(String),
|
|
75
|
+
data: roles
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
it('passes null for missing company and branch context', async ()=>{
|
|
79
|
+
const user = buildMockUser({
|
|
80
|
+
id: 'user-1',
|
|
81
|
+
companyId: undefined,
|
|
82
|
+
branchId: undefined
|
|
83
|
+
});
|
|
84
|
+
mockPermissionService.getUserEffectiveRoles.mockResolvedValue([]);
|
|
85
|
+
await controller.getMyRoles(user);
|
|
86
|
+
expect(mockPermissionService.getUserEffectiveRoles).toHaveBeenCalledWith('user-1', null, null);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('getMyActions', ()=>{
|
|
90
|
+
it('resolves effective actions for the current user, company, and branch', async ()=>{
|
|
91
|
+
const user = buildMockUser({
|
|
92
|
+
id: 'user-1',
|
|
93
|
+
companyId: 'company-1',
|
|
94
|
+
branchId: 'branch-1'
|
|
95
|
+
});
|
|
96
|
+
const actions = [
|
|
97
|
+
{
|
|
98
|
+
id: 'perm-1',
|
|
99
|
+
actionId: 'action-1',
|
|
100
|
+
actionCode: 'user.view'
|
|
101
|
+
}
|
|
102
|
+
];
|
|
103
|
+
mockPermissionService.getUserEffectiveActions.mockResolvedValue(actions);
|
|
104
|
+
const result = await controller.getMyActions(user);
|
|
105
|
+
expect(mockPermissionService.getUserEffectiveActions).toHaveBeenCalledWith('user-1', 'company-1', 'branch-1');
|
|
106
|
+
expect(result).toEqual({
|
|
107
|
+
success: true,
|
|
108
|
+
message: 'Actions loaded successfully',
|
|
109
|
+
messageKey: expect.any(String),
|
|
110
|
+
data: actions
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
it('passes null for missing company and branch context', async ()=>{
|
|
114
|
+
const user = buildMockUser({
|
|
115
|
+
id: 'user-1',
|
|
116
|
+
companyId: undefined,
|
|
117
|
+
branchId: undefined
|
|
118
|
+
});
|
|
119
|
+
mockPermissionService.getUserEffectiveActions.mockResolvedValue([]);
|
|
120
|
+
await controller.getMyActions(user);
|
|
121
|
+
expect(mockPermissionService.getUserEffectiveActions).toHaveBeenCalledWith('user-1', null, null);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
52
124
|
});
|
|
@@ -521,6 +521,104 @@ export class PermissionService {
|
|
|
521
521
|
};
|
|
522
522
|
});
|
|
523
523
|
}
|
|
524
|
+
// Effective (Merged) Permissions — self-scoped, company-wide + branch-specific
|
|
525
|
+
/** Get user's effective roles: company-wide (branchId null) merged with branch-specific */ async getUserEffectiveRoles(userId, companyId, branchId) {
|
|
526
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
527
|
+
const roleRepo = await this.getRoleRepository();
|
|
528
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
529
|
+
const baseWhere = {
|
|
530
|
+
permissionType: IamPermissionType.USER_ROLE,
|
|
531
|
+
sourceType: IamEntityType.USER,
|
|
532
|
+
sourceId: userId
|
|
533
|
+
};
|
|
534
|
+
if (enableCompanyFeature && companyId) baseWhere.companyId = companyId;
|
|
535
|
+
const permissions = enableCompanyFeature && branchId ? await permissionRepo.find({
|
|
536
|
+
where: [
|
|
537
|
+
{
|
|
538
|
+
...baseWhere,
|
|
539
|
+
branchId: IsNull()
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
...baseWhere,
|
|
543
|
+
branchId
|
|
544
|
+
}
|
|
545
|
+
]
|
|
546
|
+
}) : await permissionRepo.find({
|
|
547
|
+
where: baseWhere
|
|
548
|
+
});
|
|
549
|
+
const validPermissions = permissions.filter((p)=>p.isValid());
|
|
550
|
+
if (validPermissions.length === 0) {
|
|
551
|
+
return [];
|
|
552
|
+
}
|
|
553
|
+
const roleIds = [
|
|
554
|
+
...new Set(validPermissions.map((p)=>p.targetId))
|
|
555
|
+
];
|
|
556
|
+
const roles = await roleRepo.find({
|
|
557
|
+
where: {
|
|
558
|
+
id: In(roleIds)
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
const roleMap = new Map(roles.map((r)=>[
|
|
562
|
+
r.id,
|
|
563
|
+
r
|
|
564
|
+
]));
|
|
565
|
+
return validPermissions.filter((p)=>roleMap.has(p.targetId)).map((p)=>{
|
|
566
|
+
const role = roleMap.get(p.targetId);
|
|
567
|
+
return {
|
|
568
|
+
id: p.id,
|
|
569
|
+
userId: p.sourceId,
|
|
570
|
+
roleId: role.id,
|
|
571
|
+
roleName: role.name,
|
|
572
|
+
branchId: enableCompanyFeature ? p.branchId ?? null : null,
|
|
573
|
+
createdAt: p.createdAt
|
|
574
|
+
};
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/** Get user's effective actions: company-wide (branchId null) merged with branch-specific */ async getUserEffectiveActions(userId, companyId, branchId) {
|
|
578
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
579
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
580
|
+
const baseWhere = {
|
|
581
|
+
permissionType: IamPermissionType.USER_ACTION,
|
|
582
|
+
sourceType: IamEntityType.USER,
|
|
583
|
+
sourceId: userId
|
|
584
|
+
};
|
|
585
|
+
if (enableCompanyFeature && companyId) baseWhere.companyId = companyId;
|
|
586
|
+
const permissions = enableCompanyFeature && branchId ? await permissionRepo.find({
|
|
587
|
+
where: [
|
|
588
|
+
{
|
|
589
|
+
...baseWhere,
|
|
590
|
+
branchId: IsNull()
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
...baseWhere,
|
|
594
|
+
branchId
|
|
595
|
+
}
|
|
596
|
+
]
|
|
597
|
+
}) : await permissionRepo.find({
|
|
598
|
+
where: baseWhere
|
|
599
|
+
});
|
|
600
|
+
const validPermissions = permissions.filter((p)=>p.isValid());
|
|
601
|
+
if (validPermissions.length === 0) {
|
|
602
|
+
return [];
|
|
603
|
+
}
|
|
604
|
+
const actionIds = [
|
|
605
|
+
...new Set(validPermissions.map((p)=>p.targetId))
|
|
606
|
+
];
|
|
607
|
+
const actionMap = await this.getActionsMap(actionIds);
|
|
608
|
+
return validPermissions.filter((p)=>actionMap.has(p.targetId)).map((p)=>{
|
|
609
|
+
const action = actionMap.get(p.targetId);
|
|
610
|
+
return {
|
|
611
|
+
id: p.id,
|
|
612
|
+
userId: p.sourceId,
|
|
613
|
+
actionId: action.id,
|
|
614
|
+
actionCode: action.code ?? '',
|
|
615
|
+
actionName: action.name,
|
|
616
|
+
companyId: ('companyId' in p ? p.companyId : null) ?? null,
|
|
617
|
+
branchId: ('branchId' in p ? p.branchId : null) ?? null,
|
|
618
|
+
createdAt: p.createdAt
|
|
619
|
+
};
|
|
620
|
+
});
|
|
621
|
+
}
|
|
524
622
|
// My Permissions
|
|
525
623
|
/** Get user's effective permissions (cache-first approach) */ async getMyPermissions(userId, branchId, companyId, parentCodes) {
|
|
526
624
|
return this.permissionCacheService.getMyPermissionsResponse(userId, branchId, companyId, parentCodes);
|
|
@@ -569,6 +569,152 @@ describe('PermissionService', ()=>{
|
|
|
569
569
|
}));
|
|
570
570
|
});
|
|
571
571
|
});
|
|
572
|
+
// ==================== Effective (Merged) Permissions ====================
|
|
573
|
+
describe('getUserEffectiveRoles', ()=>{
|
|
574
|
+
it('returns an empty array when the user has no role assignments', async ()=>{
|
|
575
|
+
mockPermissionRepo.find.mockResolvedValue([]);
|
|
576
|
+
expect(await service.getUserEffectiveRoles('user-1')).toEqual([]);
|
|
577
|
+
expect(mockRoleRepo.find).not.toHaveBeenCalled();
|
|
578
|
+
});
|
|
579
|
+
it('queries only by userId when the company feature is disabled, ignoring companyId/branchId', async ()=>{
|
|
580
|
+
mockPermissionRepo.find.mockResolvedValue([]);
|
|
581
|
+
await service.getUserEffectiveRoles('user-1', 'company-1', 'branch-1');
|
|
582
|
+
expect(mockPermissionRepo.find).toHaveBeenCalledWith({
|
|
583
|
+
where: {
|
|
584
|
+
permissionType: IamPermissionType.USER_ROLE,
|
|
585
|
+
sourceType: IamEntityType.USER,
|
|
586
|
+
sourceId: 'user-1'
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
it('merges company-wide (branchId null) and branch-specific role assignments via an OR query', async ()=>{
|
|
591
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
592
|
+
mockPermissionRepo.find.mockResolvedValue([
|
|
593
|
+
buildPermissionRow({
|
|
594
|
+
id: 'perm-1',
|
|
595
|
+
permissionType: IamPermissionType.USER_ROLE,
|
|
596
|
+
targetType: IamEntityType.ROLE,
|
|
597
|
+
targetId: 'role-1',
|
|
598
|
+
companyId: 'company-1',
|
|
599
|
+
branchId: null
|
|
600
|
+
}),
|
|
601
|
+
buildPermissionRow({
|
|
602
|
+
id: 'perm-2',
|
|
603
|
+
permissionType: IamPermissionType.USER_ROLE,
|
|
604
|
+
targetType: IamEntityType.ROLE,
|
|
605
|
+
targetId: 'role-2',
|
|
606
|
+
companyId: 'company-1',
|
|
607
|
+
branchId: 'branch-1'
|
|
608
|
+
})
|
|
609
|
+
]);
|
|
610
|
+
mockRoleRepo.find.mockResolvedValue([
|
|
611
|
+
{
|
|
612
|
+
id: 'role-1',
|
|
613
|
+
name: 'Company Admin'
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
id: 'role-2',
|
|
617
|
+
name: 'Branch Manager'
|
|
618
|
+
}
|
|
619
|
+
]);
|
|
620
|
+
const result = await service.getUserEffectiveRoles('user-1', 'company-1', 'branch-1');
|
|
621
|
+
expect(mockPermissionRepo.find).toHaveBeenCalledWith({
|
|
622
|
+
where: [
|
|
623
|
+
expect.objectContaining({
|
|
624
|
+
sourceId: 'user-1',
|
|
625
|
+
companyId: 'company-1',
|
|
626
|
+
branchId: expect.anything()
|
|
627
|
+
}),
|
|
628
|
+
expect.objectContaining({
|
|
629
|
+
sourceId: 'user-1',
|
|
630
|
+
companyId: 'company-1',
|
|
631
|
+
branchId: 'branch-1'
|
|
632
|
+
})
|
|
633
|
+
]
|
|
634
|
+
});
|
|
635
|
+
expect(result.map((r)=>r.roleId).sort()).toEqual([
|
|
636
|
+
'role-1',
|
|
637
|
+
'role-2'
|
|
638
|
+
]);
|
|
639
|
+
});
|
|
640
|
+
it('excludes expired/not-yet-valid permission rows', async ()=>{
|
|
641
|
+
mockPermissionRepo.find.mockResolvedValue([
|
|
642
|
+
buildPermissionRow({
|
|
643
|
+
permissionType: IamPermissionType.USER_ROLE,
|
|
644
|
+
targetType: IamEntityType.ROLE,
|
|
645
|
+
targetId: 'role-1',
|
|
646
|
+
validUntil: new Date('2000-01-01')
|
|
647
|
+
})
|
|
648
|
+
]);
|
|
649
|
+
const result = await service.getUserEffectiveRoles('user-1');
|
|
650
|
+
expect(result).toEqual([]);
|
|
651
|
+
expect(mockRoleRepo.find).not.toHaveBeenCalled();
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
describe('getUserEffectiveActions', ()=>{
|
|
655
|
+
it('returns an empty array when the user has no action assignments', async ()=>{
|
|
656
|
+
mockPermissionRepo.find.mockResolvedValue([]);
|
|
657
|
+
expect(await service.getUserEffectiveActions('user-1')).toEqual([]);
|
|
658
|
+
expect(mockActionRepo.find).not.toHaveBeenCalled();
|
|
659
|
+
});
|
|
660
|
+
it('merges company-wide (branchId null) and branch-specific action assignments via an OR query', async ()=>{
|
|
661
|
+
mockIamConfigService.isCompanyFeatureEnabled.mockReturnValue(true);
|
|
662
|
+
mockPermissionRepo.find.mockResolvedValue([
|
|
663
|
+
buildPermissionRow({
|
|
664
|
+
id: 'perm-1',
|
|
665
|
+
targetId: 'action-1',
|
|
666
|
+
companyId: 'company-1',
|
|
667
|
+
branchId: null
|
|
668
|
+
}),
|
|
669
|
+
buildPermissionRow({
|
|
670
|
+
id: 'perm-2',
|
|
671
|
+
targetId: 'action-2',
|
|
672
|
+
companyId: 'company-1',
|
|
673
|
+
branchId: 'branch-1'
|
|
674
|
+
})
|
|
675
|
+
]);
|
|
676
|
+
mockActionRepo.find.mockResolvedValue([
|
|
677
|
+
buildAction({
|
|
678
|
+
id: 'action-1',
|
|
679
|
+
code: 'user.view'
|
|
680
|
+
}),
|
|
681
|
+
buildAction({
|
|
682
|
+
id: 'action-2',
|
|
683
|
+
code: 'user.create'
|
|
684
|
+
})
|
|
685
|
+
]);
|
|
686
|
+
const result = await service.getUserEffectiveActions('user-1', 'company-1', 'branch-1');
|
|
687
|
+
expect(mockPermissionRepo.find).toHaveBeenCalledWith({
|
|
688
|
+
where: [
|
|
689
|
+
expect.objectContaining({
|
|
690
|
+
sourceId: 'user-1',
|
|
691
|
+
companyId: 'company-1',
|
|
692
|
+
branchId: expect.anything()
|
|
693
|
+
}),
|
|
694
|
+
expect.objectContaining({
|
|
695
|
+
sourceId: 'user-1',
|
|
696
|
+
companyId: 'company-1',
|
|
697
|
+
branchId: 'branch-1'
|
|
698
|
+
})
|
|
699
|
+
]
|
|
700
|
+
});
|
|
701
|
+
expect(result.map((a)=>a.actionId).sort()).toEqual([
|
|
702
|
+
'action-1',
|
|
703
|
+
'action-2'
|
|
704
|
+
]);
|
|
705
|
+
});
|
|
706
|
+
it('excludes expired/not-yet-valid permission rows', async ()=>{
|
|
707
|
+
mockPermissionRepo.find.mockResolvedValue([
|
|
708
|
+
buildPermissionRow({
|
|
709
|
+
targetId: 'action-1',
|
|
710
|
+
validUntil: new Date('2000-01-01')
|
|
711
|
+
})
|
|
712
|
+
]);
|
|
713
|
+
const result = await service.getUserEffectiveActions('user-1');
|
|
714
|
+
expect(result).toEqual([]);
|
|
715
|
+
expect(mockActionRepo.find).not.toHaveBeenCalled();
|
|
716
|
+
});
|
|
717
|
+
});
|
|
572
718
|
// ==================== My Permissions / permission-mode branching ====================
|
|
573
719
|
describe('getMyPermissions', ()=>{
|
|
574
720
|
it('delegates to the cache service and returns its response', async ()=>{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-iam",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"description": "Identity and Access Management (IAM) module for NestJS applications",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"express": "^5.0.0"
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
|
-
"@flusys/nestjs-core": "6.
|
|
94
|
-
"@flusys/nestjs-shared": "6.
|
|
93
|
+
"@flusys/nestjs-core": "6.4.0",
|
|
94
|
+
"@flusys/nestjs-shared": "6.4.0"
|
|
95
95
|
}
|
|
96
96
|
}
|
|
@@ -19,6 +19,8 @@ export declare class PermissionService {
|
|
|
19
19
|
getCompanyActions(companyId: string, isOnlyId?: boolean): Promise<CompanyActionResponseDto[] | string[]>;
|
|
20
20
|
assignUserRoles(dto: AssignUserRolesDto): Promise<PermissionOperationResultDto>;
|
|
21
21
|
getUserRoles(userId: string, branchId?: string | undefined, companyId?: string | undefined): Promise<UserRoleResponseDto[]>;
|
|
22
|
+
getUserEffectiveRoles(userId: string, companyId?: string | null, branchId?: string | null): Promise<UserRoleResponseDto[]>;
|
|
23
|
+
getUserEffectiveActions(userId: string, companyId?: string | null, branchId?: string | null): Promise<UserActionResponseDto[]>;
|
|
22
24
|
getMyPermissions(userId: string, branchId: string | null, companyId?: string | null, parentCodes?: string[]): Promise<MyPermissionsResponseDto>;
|
|
23
25
|
private splitItemsByAction;
|
|
24
26
|
private buildOperationResult;
|