@flusys/nestjs-iam 0.1.0-beta.1 → 0.1.0-beta.2
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 +665 -0
- package/cjs/config/iam.constants.js +11 -0
- package/cjs/config/index.js +18 -0
- package/cjs/controllers/action.controller.js +117 -0
- package/cjs/controllers/company-action-permission.controller.js +110 -0
- package/cjs/controllers/index.js +23 -0
- package/cjs/controllers/my-permission.controller.js +90 -0
- package/cjs/controllers/role-permission.controller.js +160 -0
- package/cjs/controllers/role.controller.js +58 -0
- package/cjs/controllers/user-action-permission.controller.js +110 -0
- package/cjs/docs/iam-swagger.config.js +202 -0
- package/cjs/docs/index.js +18 -0
- package/cjs/dtos/action.dto.js +347 -0
- package/cjs/dtos/index.js +21 -0
- package/cjs/dtos/permission.dto.js +554 -0
- package/cjs/dtos/role.dto.js +238 -0
- package/cjs/entities/action-base.entity.js +135 -0
- package/cjs/entities/action.entity.js +28 -0
- package/cjs/entities/index.js +81 -0
- package/cjs/entities/permission-base.entity.js +156 -0
- package/cjs/entities/permission-with-company.entity.js +99 -0
- package/cjs/entities/role-base.entity.js +86 -0
- package/cjs/entities/role-with-company.entity.js +55 -0
- package/cjs/entities/role.entity.js +25 -0
- package/cjs/entities/user-iam-permission.entity.js +57 -0
- package/cjs/enums/action-type.enum.js +22 -0
- package/cjs/enums/index.js +19 -0
- package/cjs/enums/permission-type.enum.js +16 -0
- package/cjs/helpers/index.js +19 -0
- package/cjs/helpers/permission-evaluator.helper.js +175 -0
- package/cjs/helpers/permission-mode.helper.js +49 -0
- package/cjs/index.js +28 -79
- package/cjs/interfaces/action.interface.js +4 -0
- package/cjs/interfaces/iam-module-async-options.interface.js +4 -0
- package/cjs/interfaces/iam-module-options.interface.js +18 -0
- package/cjs/interfaces/index.js +21 -0
- package/cjs/interfaces/role.interface.js +7 -0
- package/cjs/modules/iam.module.js +237 -0
- package/cjs/modules/index.js +18 -0
- package/cjs/services/action.service.js +253 -0
- package/cjs/services/iam-config.service.js +107 -0
- package/cjs/services/iam-datasource.provider.js +205 -0
- package/cjs/services/index.js +23 -0
- package/cjs/services/permission-cache.service.js +308 -0
- package/cjs/services/permission.service.js +1020 -0
- package/cjs/services/role.service.js +181 -0
- package/cjs/types/index.js +18 -0
- package/cjs/types/logic-node.type.js +54 -0
- package/fesm/config/iam.constants.js +1 -0
- package/fesm/config/index.js +1 -0
- package/fesm/controllers/action.controller.js +107 -0
- package/fesm/controllers/company-action-permission.controller.js +100 -0
- package/fesm/controllers/index.js +7 -0
- package/fesm/controllers/my-permission.controller.js +80 -0
- package/fesm/controllers/role-permission.controller.js +150 -0
- package/fesm/controllers/role.controller.js +48 -0
- package/fesm/controllers/user-action-permission.controller.js +100 -0
- package/fesm/docs/iam-swagger.config.js +192 -0
- package/fesm/docs/index.js +1 -0
- package/fesm/dtos/action.dto.js +317 -0
- package/fesm/dtos/index.js +4 -0
- package/fesm/dtos/permission.dto.js +490 -0
- package/fesm/dtos/role.dto.js +214 -0
- package/fesm/entities/action-base.entity.js +128 -0
- package/fesm/entities/action.entity.js +18 -0
- package/fesm/entities/index.js +56 -0
- package/fesm/entities/permission-base.entity.js +138 -0
- package/fesm/entities/permission-with-company.entity.js +89 -0
- package/fesm/entities/role-base.entity.js +79 -0
- package/fesm/entities/role-with-company.entity.js +45 -0
- package/fesm/entities/role.entity.js +15 -0
- package/fesm/entities/user-iam-permission.entity.js +38 -0
- package/fesm/enums/action-type.enum.js +12 -0
- package/fesm/enums/index.js +2 -0
- package/fesm/enums/permission-type.enum.js +6 -0
- package/fesm/helpers/index.js +2 -0
- package/fesm/helpers/permission-evaluator.helper.js +165 -0
- package/fesm/helpers/permission-mode.helper.js +49 -0
- package/fesm/index.js +11 -79
- package/fesm/interfaces/action.interface.js +3 -0
- package/fesm/interfaces/iam-module-async-options.interface.js +3 -0
- package/fesm/interfaces/iam-module-options.interface.js +1 -0
- package/fesm/interfaces/index.js +4 -0
- package/fesm/interfaces/role.interface.js +4 -0
- package/fesm/modules/iam.module.js +227 -0
- package/fesm/modules/index.js +1 -0
- package/fesm/services/action.service.js +243 -0
- package/fesm/services/iam-config.service.js +97 -0
- package/fesm/services/iam-datasource.provider.js +154 -0
- package/fesm/services/index.js +6 -0
- package/fesm/services/permission-cache.service.js +298 -0
- package/fesm/services/permission.service.js +1010 -0
- package/fesm/services/role.service.js +171 -0
- package/fesm/types/index.js +1 -0
- package/fesm/types/logic-node.type.js +36 -0
- package/package.json +25 -25
- package/cjs/config-index.js +0 -1
- package/cjs/controllers-index.js +0 -1
- package/cjs/docs-index.js +0 -79
- package/cjs/dtos-index.js +0 -1
- package/cjs/entities-index.js +0 -1
- package/cjs/enums-index.js +0 -1
- package/cjs/helpers-index.js +0 -1
- package/cjs/interfaces-index.js +0 -1
- package/cjs/modules-index.js +0 -1
- package/cjs/services-index.js +0 -1
- package/cjs/types-index.js +0 -1
- package/fesm/config-index.js +0 -1
- package/fesm/controllers-index.js +0 -1
- package/fesm/docs-index.js +0 -79
- package/fesm/dtos-index.js +0 -1
- package/fesm/entities-index.js +0 -1
- package/fesm/enums-index.js +0 -1
- package/fesm/helpers-index.js +0 -1
- package/fesm/interfaces-index.js +0 -0
- package/fesm/modules-index.js +0 -1
- package/fesm/services-index.js +0 -1
- package/fesm/types-index.js +0 -1
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "PermissionService", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return PermissionService;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _common = require("@nestjs/common");
|
|
12
|
+
const _typeorm = require("typeorm");
|
|
13
|
+
const _permissiondto = require("../dtos/permission.dto");
|
|
14
|
+
const _actionentity = require("../entities/action.entity");
|
|
15
|
+
const _permissionwithcompanyentity = require("../entities/permission-with-company.entity");
|
|
16
|
+
const _rolewithcompanyentity = require("../entities/role-with-company.entity");
|
|
17
|
+
const _roleentity = require("../entities/role.entity");
|
|
18
|
+
const _useriampermissionentity = require("../entities/user-iam-permission.entity");
|
|
19
|
+
const _actiontypeenum = require("../enums/action-type.enum");
|
|
20
|
+
const _permissiontypeenum = require("../enums/permission-type.enum");
|
|
21
|
+
const _permissionevaluatorhelper = require("../helpers/permission-evaluator.helper");
|
|
22
|
+
const _iamconfigservice = require("./iam-config.service");
|
|
23
|
+
const _iamdatasourceprovider = require("./iam-datasource.provider");
|
|
24
|
+
const _permissioncacheservice = require("./permission-cache.service");
|
|
25
|
+
function _define_property(obj, key, value) {
|
|
26
|
+
if (key in obj) {
|
|
27
|
+
Object.defineProperty(obj, key, {
|
|
28
|
+
value: value,
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true
|
|
32
|
+
});
|
|
33
|
+
} else {
|
|
34
|
+
obj[key] = value;
|
|
35
|
+
}
|
|
36
|
+
return obj;
|
|
37
|
+
}
|
|
38
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
39
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
40
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
41
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
42
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
43
|
+
}
|
|
44
|
+
function _ts_metadata(k, v) {
|
|
45
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
46
|
+
}
|
|
47
|
+
function _ts_param(paramIndex, decorator) {
|
|
48
|
+
return function(target, key) {
|
|
49
|
+
decorator(target, key, paramIndex);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
let PermissionService = class PermissionService {
|
|
53
|
+
// ==================== Repository Getters ====================
|
|
54
|
+
async getPermissionRepository() {
|
|
55
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
56
|
+
const entity = enableCompanyFeature ? _permissionwithcompanyentity.UserIamPermissionWithCompany : _useriampermissionentity.UserIamPermission;
|
|
57
|
+
return this.dataSourceProvider.getRepository(entity);
|
|
58
|
+
}
|
|
59
|
+
async getActionRepository() {
|
|
60
|
+
return this.dataSourceProvider.getRepository(_actionentity.Action);
|
|
61
|
+
}
|
|
62
|
+
async getRoleRepository() {
|
|
63
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
64
|
+
const entity = enableCompanyFeature ? _rolewithcompanyentity.RoleWithCompany : _roleentity.Role;
|
|
65
|
+
return this.dataSourceProvider.getRepository(entity);
|
|
66
|
+
}
|
|
67
|
+
// ==================== User-Action Permissions ====================
|
|
68
|
+
async assignUserActions(dto) {
|
|
69
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
70
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
71
|
+
const branchId = dto.branchId ?? null;
|
|
72
|
+
const companyId = dto.companyId ?? null;
|
|
73
|
+
// Separate items into add and remove batches
|
|
74
|
+
const itemsToAdd = dto.items.filter((item)=>item.action === _permissiondto.PermissionAction.ADD);
|
|
75
|
+
const itemsToRemove = dto.items.filter((item)=>item.action === _permissiondto.PermissionAction.REMOVE);
|
|
76
|
+
let added = 0;
|
|
77
|
+
let removed = 0;
|
|
78
|
+
// Batch add: Find existing permissions in one query
|
|
79
|
+
if (itemsToAdd.length > 0) {
|
|
80
|
+
const actionIdsToAdd = itemsToAdd.map((item)=>item.id);
|
|
81
|
+
const whereFind = {
|
|
82
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ACTION,
|
|
83
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
84
|
+
sourceId: dto.userId,
|
|
85
|
+
targetType: _useriampermissionentity.IamEntityType.ACTION,
|
|
86
|
+
targetId: (0, _typeorm.In)(actionIdsToAdd)
|
|
87
|
+
};
|
|
88
|
+
if (enableCompanyFeature) {
|
|
89
|
+
if (companyId) whereFind.companyId = companyId;
|
|
90
|
+
if (branchId) whereFind.branchId = branchId;
|
|
91
|
+
}
|
|
92
|
+
const existingPermissions = await permissionRepo.find({
|
|
93
|
+
where: whereFind,
|
|
94
|
+
select: [
|
|
95
|
+
'targetId'
|
|
96
|
+
]
|
|
97
|
+
});
|
|
98
|
+
const existingActionIds = new Set(existingPermissions.map((p)=>p.targetId));
|
|
99
|
+
const newPermissions = itemsToAdd.filter((item)=>!existingActionIds.has(item.id)).map((item)=>({
|
|
100
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ACTION,
|
|
101
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
102
|
+
sourceId: dto.userId,
|
|
103
|
+
targetType: _useriampermissionentity.IamEntityType.ACTION,
|
|
104
|
+
targetId: item.id,
|
|
105
|
+
userId: dto.userId,
|
|
106
|
+
companyId: enableCompanyFeature ? companyId : null,
|
|
107
|
+
branchId: enableCompanyFeature ? branchId : null
|
|
108
|
+
}));
|
|
109
|
+
if (newPermissions.length > 0) {
|
|
110
|
+
await permissionRepo.save(newPermissions);
|
|
111
|
+
added = newPermissions.length;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Batch remove: Delete all at once using IN clause
|
|
115
|
+
if (itemsToRemove.length > 0) {
|
|
116
|
+
const actionIdsToRemove = itemsToRemove.map((item)=>item.id);
|
|
117
|
+
const whereDelete = {
|
|
118
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ACTION,
|
|
119
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
120
|
+
sourceId: dto.userId,
|
|
121
|
+
targetType: _useriampermissionentity.IamEntityType.ACTION,
|
|
122
|
+
targetId: (0, _typeorm.In)(actionIdsToRemove)
|
|
123
|
+
};
|
|
124
|
+
if (enableCompanyFeature) {
|
|
125
|
+
if (companyId) whereDelete.companyId = companyId;
|
|
126
|
+
if (branchId) whereDelete.branchId = branchId;
|
|
127
|
+
}
|
|
128
|
+
const result = await permissionRepo.delete(whereDelete);
|
|
129
|
+
removed = result.affected || 0;
|
|
130
|
+
}
|
|
131
|
+
// Invalidate permission cache for this user
|
|
132
|
+
await this.invalidateUserPermissionCache(dto.userId, branchId, companyId);
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
added,
|
|
136
|
+
removed,
|
|
137
|
+
message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async getUserActions(userId, branchId, companyId) {
|
|
141
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
142
|
+
const actionRepo = await this.getActionRepository();
|
|
143
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
144
|
+
const where = {
|
|
145
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ACTION,
|
|
146
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
147
|
+
sourceId: userId
|
|
148
|
+
};
|
|
149
|
+
// When company feature enabled:
|
|
150
|
+
// - companyId provided → Filter by that company (optional additional filter)
|
|
151
|
+
// - branchId provided → Filter by that branch (branch-specific actions)
|
|
152
|
+
// - branchId not provided → Filter by IS NULL (company-wide actions)
|
|
153
|
+
if (enableCompanyFeature) {
|
|
154
|
+
if (companyId) {
|
|
155
|
+
// Filter by companyId if provided
|
|
156
|
+
where.companyId = companyId;
|
|
157
|
+
}
|
|
158
|
+
if (branchId) {
|
|
159
|
+
// Branch-specific actions only
|
|
160
|
+
where.branchId = branchId;
|
|
161
|
+
} else {
|
|
162
|
+
// Company-wide actions only (branchId IS NULL)
|
|
163
|
+
where.branchId = null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const permissions = await permissionRepo.find({
|
|
167
|
+
where
|
|
168
|
+
});
|
|
169
|
+
if (permissions.length === 0) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
const actionIds = permissions.map((p)=>p.targetId);
|
|
173
|
+
const actionWhere = {
|
|
174
|
+
id: (0, _typeorm.In)(actionIds)
|
|
175
|
+
};
|
|
176
|
+
const actions = await actionRepo.find({
|
|
177
|
+
where: actionWhere
|
|
178
|
+
});
|
|
179
|
+
const actionMap = new Map(actions.map((a)=>[
|
|
180
|
+
a.id,
|
|
181
|
+
a
|
|
182
|
+
]));
|
|
183
|
+
return permissions.filter((p)=>actionMap.has(p.targetId)).map((p)=>{
|
|
184
|
+
const action = actionMap.get(p.targetId);
|
|
185
|
+
return {
|
|
186
|
+
id: p.id,
|
|
187
|
+
userId: p.userId,
|
|
188
|
+
actionId: action.id,
|
|
189
|
+
actionCode: action.code ?? '',
|
|
190
|
+
actionName: action.name,
|
|
191
|
+
branchId: ('branchId' in p ? p.branchId : null) ?? null,
|
|
192
|
+
createdAt: p.createdAt
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// ==================== Role-Action Permissions ====================
|
|
197
|
+
async assignRoleActions(dto) {
|
|
198
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
199
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
200
|
+
// Fetch role companyId if company feature is enabled
|
|
201
|
+
let roleCompanyId = null;
|
|
202
|
+
if (enableCompanyFeature) {
|
|
203
|
+
const roleRepo = await this.getRoleRepository();
|
|
204
|
+
const role = await roleRepo.findOne({
|
|
205
|
+
where: {
|
|
206
|
+
id: dto.roleId
|
|
207
|
+
},
|
|
208
|
+
select: [
|
|
209
|
+
'id',
|
|
210
|
+
'companyId'
|
|
211
|
+
]
|
|
212
|
+
});
|
|
213
|
+
roleCompanyId = role?.companyId ?? null;
|
|
214
|
+
}
|
|
215
|
+
// Separate items into add and remove batches
|
|
216
|
+
const itemsToAdd = dto.items.filter((item)=>item.action === _permissiondto.PermissionAction.ADD);
|
|
217
|
+
const itemsToRemove = dto.items.filter((item)=>item.action === _permissiondto.PermissionAction.REMOVE);
|
|
218
|
+
let added = 0;
|
|
219
|
+
let removed = 0;
|
|
220
|
+
// Batch add: Find existing permissions in one query
|
|
221
|
+
if (itemsToAdd.length > 0) {
|
|
222
|
+
const actionIdsToAdd = itemsToAdd.map((item)=>item.id);
|
|
223
|
+
const existingPermissions = await permissionRepo.find({
|
|
224
|
+
where: {
|
|
225
|
+
permissionType: _useriampermissionentity.IamPermissionType.ROLE_ACTION,
|
|
226
|
+
sourceType: _useriampermissionentity.IamEntityType.ROLE,
|
|
227
|
+
sourceId: dto.roleId,
|
|
228
|
+
targetType: _useriampermissionentity.IamEntityType.ACTION,
|
|
229
|
+
targetId: (0, _typeorm.In)(actionIdsToAdd)
|
|
230
|
+
},
|
|
231
|
+
select: [
|
|
232
|
+
'targetId'
|
|
233
|
+
]
|
|
234
|
+
});
|
|
235
|
+
const existingActionIds = new Set(existingPermissions.map((p)=>p.targetId));
|
|
236
|
+
const newPermissions = itemsToAdd.filter((item)=>!existingActionIds.has(item.id)).map((item)=>({
|
|
237
|
+
permissionType: _useriampermissionentity.IamPermissionType.ROLE_ACTION,
|
|
238
|
+
sourceType: _useriampermissionentity.IamEntityType.ROLE,
|
|
239
|
+
sourceId: dto.roleId,
|
|
240
|
+
targetType: _useriampermissionentity.IamEntityType.ACTION,
|
|
241
|
+
targetId: item.id,
|
|
242
|
+
userId: null,
|
|
243
|
+
companyId: enableCompanyFeature ? roleCompanyId : null,
|
|
244
|
+
branchId: null
|
|
245
|
+
}));
|
|
246
|
+
if (newPermissions.length > 0) {
|
|
247
|
+
await permissionRepo.save(newPermissions);
|
|
248
|
+
added = newPermissions.length;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Batch remove: Delete all at once using IN clause
|
|
252
|
+
if (itemsToRemove.length > 0) {
|
|
253
|
+
const actionIdsToRemove = itemsToRemove.map((item)=>item.id);
|
|
254
|
+
const result = await permissionRepo.delete({
|
|
255
|
+
permissionType: _useriampermissionentity.IamPermissionType.ROLE_ACTION,
|
|
256
|
+
sourceType: _useriampermissionentity.IamEntityType.ROLE,
|
|
257
|
+
sourceId: dto.roleId,
|
|
258
|
+
targetType: _useriampermissionentity.IamEntityType.ACTION,
|
|
259
|
+
targetId: (0, _typeorm.In)(actionIdsToRemove)
|
|
260
|
+
});
|
|
261
|
+
removed = result.affected || 0;
|
|
262
|
+
}
|
|
263
|
+
// Invalidate cache for all users with this role
|
|
264
|
+
const affectedUsers = await this.invalidateRoleMembersCache(dto.roleId);
|
|
265
|
+
return {
|
|
266
|
+
success: true,
|
|
267
|
+
added,
|
|
268
|
+
removed,
|
|
269
|
+
message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed. Invalidated cache for ${affectedUsers} users.`
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async getRoleActions(roleId) {
|
|
273
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
274
|
+
const actionRepo = await this.getActionRepository();
|
|
275
|
+
const permissions = await permissionRepo.find({
|
|
276
|
+
where: {
|
|
277
|
+
permissionType: _useriampermissionentity.IamPermissionType.ROLE_ACTION,
|
|
278
|
+
sourceType: _useriampermissionentity.IamEntityType.ROLE,
|
|
279
|
+
sourceId: roleId
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
if (permissions.length === 0) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
const actionIds = permissions.map((p)=>p.targetId);
|
|
286
|
+
const actionWhere = {
|
|
287
|
+
id: (0, _typeorm.In)(actionIds)
|
|
288
|
+
};
|
|
289
|
+
const actions = await actionRepo.find({
|
|
290
|
+
where: actionWhere
|
|
291
|
+
});
|
|
292
|
+
const actionMap = new Map(actions.map((a)=>[
|
|
293
|
+
a.id,
|
|
294
|
+
a
|
|
295
|
+
]));
|
|
296
|
+
return permissions.filter((p)=>actionMap.has(p.targetId)).map((p)=>{
|
|
297
|
+
const action = actionMap.get(p.targetId);
|
|
298
|
+
return {
|
|
299
|
+
id: p.id,
|
|
300
|
+
roleId: p.sourceId,
|
|
301
|
+
actionId: action.id,
|
|
302
|
+
actionCode: action.code ?? '',
|
|
303
|
+
actionName: action.name,
|
|
304
|
+
createdAt: p.createdAt
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
// ==================== Company-Action Permissions (Whitelist) ====================
|
|
309
|
+
/**
|
|
310
|
+
* Assign or remove actions to/from a company (whitelist)
|
|
311
|
+
* This controls which actions are available to a company
|
|
312
|
+
*/ async assignCompanyActions(dto) {
|
|
313
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
314
|
+
const dataSource = permissionRepo.manager.connection;
|
|
315
|
+
const itemsToAdd = dto.items.filter((item)=>item.action === _permissiondto.PermissionAction.ADD);
|
|
316
|
+
const itemsToRemove = dto.items.filter((item)=>item.action === _permissiondto.PermissionAction.REMOVE);
|
|
317
|
+
let added = 0;
|
|
318
|
+
let removed = 0;
|
|
319
|
+
let removedRoleActions = 0;
|
|
320
|
+
let removedUserActions = 0;
|
|
321
|
+
await dataSource.transaction(async (manager)=>{
|
|
322
|
+
const transactionalPermissionRepo = manager.getRepository(permissionRepo.target);
|
|
323
|
+
if (itemsToAdd.length > 0) {
|
|
324
|
+
added = await this.addCompanyActions(transactionalPermissionRepo, dto.companyId, itemsToAdd.map((item)=>item.id));
|
|
325
|
+
}
|
|
326
|
+
if (itemsToRemove.length > 0) {
|
|
327
|
+
const actionIdsToRemove = itemsToRemove.map((item)=>item.id);
|
|
328
|
+
const cascadeResult = await this.removeCompanyActionsWithCascade(manager, dto.companyId, actionIdsToRemove);
|
|
329
|
+
removed = cascadeResult.removedCompanyActions;
|
|
330
|
+
removedRoleActions = cascadeResult.removedRoleActions;
|
|
331
|
+
removedUserActions = cascadeResult.removedUserActions;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
const affectedCacheEntries = await this.invalidateCompanyMembersCache(dto.companyId);
|
|
335
|
+
const cascadeInfo = removedRoleActions > 0 || removedUserActions > 0 ? ` Cascaded removal: ${removedRoleActions} role permissions, ${removedUserActions} user permissions.` : '';
|
|
336
|
+
return {
|
|
337
|
+
success: true,
|
|
338
|
+
added,
|
|
339
|
+
removed,
|
|
340
|
+
message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed.${cascadeInfo} Invalidated ${affectedCacheEntries} cache entries.`
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async addCompanyActions(permissionRepo, companyId, actionIds) {
|
|
344
|
+
const existingPermissions = await permissionRepo.find({
|
|
345
|
+
where: {
|
|
346
|
+
permissionType: _useriampermissionentity.IamPermissionType.COMPANY_ACTION,
|
|
347
|
+
sourceType: _useriampermissionentity.IamEntityType.COMPANY,
|
|
348
|
+
sourceId: companyId,
|
|
349
|
+
targetType: _useriampermissionentity.IamEntityType.ACTION,
|
|
350
|
+
targetId: (0, _typeorm.In)(actionIds)
|
|
351
|
+
},
|
|
352
|
+
select: [
|
|
353
|
+
'targetId'
|
|
354
|
+
]
|
|
355
|
+
});
|
|
356
|
+
const existingActionIds = new Set(existingPermissions.map((p)=>p.targetId));
|
|
357
|
+
const newActionIds = actionIds.filter((id)=>!existingActionIds.has(id));
|
|
358
|
+
if (newActionIds.length === 0) {
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
const newPermissions = newActionIds.map((actionId)=>permissionRepo.create({
|
|
362
|
+
permissionType: _useriampermissionentity.IamPermissionType.COMPANY_ACTION,
|
|
363
|
+
sourceType: _useriampermissionentity.IamEntityType.COMPANY,
|
|
364
|
+
sourceId: companyId,
|
|
365
|
+
targetType: _useriampermissionentity.IamEntityType.ACTION,
|
|
366
|
+
targetId: actionId,
|
|
367
|
+
userId: null
|
|
368
|
+
}));
|
|
369
|
+
await permissionRepo.save(newPermissions);
|
|
370
|
+
return newPermissions.length;
|
|
371
|
+
}
|
|
372
|
+
async removeCompanyActionsWithCascade(manager, companyId, actionIds) {
|
|
373
|
+
const permissionEntity = this.iamConfigService.isCompanyFeatureEnabled() ? _permissionwithcompanyentity.UserIamPermissionWithCompany : _useriampermissionentity.UserIamPermission;
|
|
374
|
+
const permissionRepo = manager.getRepository(permissionEntity);
|
|
375
|
+
const companyResult = await permissionRepo.delete({
|
|
376
|
+
permissionType: _useriampermissionentity.IamPermissionType.COMPANY_ACTION,
|
|
377
|
+
sourceType: _useriampermissionentity.IamEntityType.COMPANY,
|
|
378
|
+
sourceId: companyId,
|
|
379
|
+
targetType: _useriampermissionentity.IamEntityType.ACTION,
|
|
380
|
+
targetId: (0, _typeorm.In)(actionIds)
|
|
381
|
+
});
|
|
382
|
+
const roleEntity = this.iamConfigService.isCompanyFeatureEnabled() ? _rolewithcompanyentity.RoleWithCompany : _roleentity.Role;
|
|
383
|
+
const roleRepo = manager.getRepository(roleEntity);
|
|
384
|
+
const companyRoles = await roleRepo.find({
|
|
385
|
+
where: {
|
|
386
|
+
companyId,
|
|
387
|
+
deletedAt: (0, _typeorm.IsNull)()
|
|
388
|
+
},
|
|
389
|
+
select: [
|
|
390
|
+
'id'
|
|
391
|
+
]
|
|
392
|
+
});
|
|
393
|
+
let removedRoleActions = 0;
|
|
394
|
+
let removedUserActions = 0;
|
|
395
|
+
if (companyRoles.length > 0) {
|
|
396
|
+
const roleIds = companyRoles.map((role)=>role.id);
|
|
397
|
+
const roleResult = await permissionRepo.delete({
|
|
398
|
+
permissionType: _useriampermissionentity.IamPermissionType.ROLE_ACTION,
|
|
399
|
+
sourceType: _useriampermissionentity.IamEntityType.ROLE,
|
|
400
|
+
sourceId: (0, _typeorm.In)(roleIds),
|
|
401
|
+
targetType: _useriampermissionentity.IamEntityType.ACTION,
|
|
402
|
+
targetId: (0, _typeorm.In)(actionIds)
|
|
403
|
+
});
|
|
404
|
+
removedRoleActions = roleResult.affected || 0;
|
|
405
|
+
}
|
|
406
|
+
if (this.iamConfigService.isCompanyFeatureEnabled()) {
|
|
407
|
+
const userResult = await permissionRepo.delete({
|
|
408
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ACTION,
|
|
409
|
+
companyId,
|
|
410
|
+
targetType: _useriampermissionentity.IamEntityType.ACTION,
|
|
411
|
+
targetId: (0, _typeorm.In)(actionIds)
|
|
412
|
+
});
|
|
413
|
+
removedUserActions = userResult.affected || 0;
|
|
414
|
+
}
|
|
415
|
+
if (removedRoleActions > 0 || removedUserActions > 0) {
|
|
416
|
+
this.logger.log(`Cascade deleted for company ${companyId}: ${removedRoleActions} role actions, ${removedUserActions} user actions`);
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
removedCompanyActions: companyResult.affected || 0,
|
|
420
|
+
removedRoleActions,
|
|
421
|
+
removedUserActions
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Get all actions assigned to a company (whitelist)
|
|
426
|
+
*/ async getCompanyActions(companyId) {
|
|
427
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
428
|
+
const actionRepo = await this.getActionRepository();
|
|
429
|
+
const permissions = await permissionRepo.find({
|
|
430
|
+
where: {
|
|
431
|
+
permissionType: _useriampermissionentity.IamPermissionType.COMPANY_ACTION,
|
|
432
|
+
sourceType: _useriampermissionentity.IamEntityType.COMPANY,
|
|
433
|
+
sourceId: companyId
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
if (permissions.length === 0) {
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
const actionIds = permissions.map((p)=>p.targetId);
|
|
440
|
+
const actions = await actionRepo.find({
|
|
441
|
+
where: {
|
|
442
|
+
id: (0, _typeorm.In)(actionIds)
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
const actionMap = new Map(actions.map((a)=>[
|
|
446
|
+
a.id,
|
|
447
|
+
a
|
|
448
|
+
]));
|
|
449
|
+
return permissions.filter((p)=>actionMap.has(p.targetId)).map((p)=>{
|
|
450
|
+
const action = actionMap.get(p.targetId);
|
|
451
|
+
return {
|
|
452
|
+
id: p.id,
|
|
453
|
+
companyId: companyId,
|
|
454
|
+
actionId: action.id,
|
|
455
|
+
actionCode: action.code ?? '',
|
|
456
|
+
actionName: action.name,
|
|
457
|
+
createdAt: p.createdAt
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Get action IDs whitelisted for a company
|
|
463
|
+
* Used internally to filter actions
|
|
464
|
+
*/ async getCompanyActionIds(companyId) {
|
|
465
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
466
|
+
const permissions = await permissionRepo.find({
|
|
467
|
+
where: {
|
|
468
|
+
permissionType: _useriampermissionentity.IamPermissionType.COMPANY_ACTION,
|
|
469
|
+
sourceType: _useriampermissionentity.IamEntityType.COMPANY,
|
|
470
|
+
sourceId: companyId
|
|
471
|
+
},
|
|
472
|
+
select: [
|
|
473
|
+
'targetId'
|
|
474
|
+
]
|
|
475
|
+
});
|
|
476
|
+
return permissions.map((p)=>p.targetId);
|
|
477
|
+
}
|
|
478
|
+
// ==================== User-Role Permissions ====================
|
|
479
|
+
/**
|
|
480
|
+
* Assign user to roles (branch-scoped)
|
|
481
|
+
* User roles are now branch-specific when company feature is enabled.
|
|
482
|
+
*/ async assignUserRoles(dto) {
|
|
483
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
484
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
485
|
+
const branchId = dto.branchId ?? null;
|
|
486
|
+
const companyId = dto.companyId ?? null;
|
|
487
|
+
// Separate items into add and remove batches
|
|
488
|
+
const itemsToAdd = dto.items.filter((item)=>item.action === _permissiondto.PermissionAction.ADD);
|
|
489
|
+
const itemsToRemove = dto.items.filter((item)=>item.action === _permissiondto.PermissionAction.REMOVE);
|
|
490
|
+
let added = 0;
|
|
491
|
+
let removed = 0;
|
|
492
|
+
// Batch add: Find existing permissions in one query
|
|
493
|
+
if (itemsToAdd.length > 0) {
|
|
494
|
+
const roleIdsToAdd = itemsToAdd.map((item)=>item.id);
|
|
495
|
+
const whereFind = {
|
|
496
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ROLE,
|
|
497
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
498
|
+
sourceId: dto.userId,
|
|
499
|
+
targetType: _useriampermissionentity.IamEntityType.ROLE,
|
|
500
|
+
targetId: (0, _typeorm.In)(roleIdsToAdd)
|
|
501
|
+
};
|
|
502
|
+
if (enableCompanyFeature) {
|
|
503
|
+
if (companyId) whereFind.companyId = companyId;
|
|
504
|
+
if (branchId) whereFind.branchId = branchId;
|
|
505
|
+
}
|
|
506
|
+
const existingPermissions = await permissionRepo.find({
|
|
507
|
+
where: whereFind,
|
|
508
|
+
select: [
|
|
509
|
+
'targetId'
|
|
510
|
+
]
|
|
511
|
+
});
|
|
512
|
+
const existingRoleIds = new Set(existingPermissions.map((p)=>p.targetId));
|
|
513
|
+
const newPermissions = itemsToAdd.filter((item)=>!existingRoleIds.has(item.id)).map((item)=>({
|
|
514
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ROLE,
|
|
515
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
516
|
+
sourceId: dto.userId,
|
|
517
|
+
targetType: _useriampermissionentity.IamEntityType.ROLE,
|
|
518
|
+
targetId: item.id,
|
|
519
|
+
userId: dto.userId,
|
|
520
|
+
companyId: enableCompanyFeature ? companyId : null,
|
|
521
|
+
branchId: enableCompanyFeature ? branchId : null
|
|
522
|
+
}));
|
|
523
|
+
if (newPermissions.length > 0) {
|
|
524
|
+
await permissionRepo.save(newPermissions);
|
|
525
|
+
added = newPermissions.length;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Batch remove: Delete all at once using IN clause
|
|
529
|
+
if (itemsToRemove.length > 0) {
|
|
530
|
+
const roleIdsToRemove = itemsToRemove.map((item)=>item.id);
|
|
531
|
+
const whereDelete = {
|
|
532
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ROLE,
|
|
533
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
534
|
+
sourceId: dto.userId,
|
|
535
|
+
targetType: _useriampermissionentity.IamEntityType.ROLE,
|
|
536
|
+
targetId: (0, _typeorm.In)(roleIdsToRemove)
|
|
537
|
+
};
|
|
538
|
+
if (enableCompanyFeature) {
|
|
539
|
+
if (companyId) whereDelete.companyId = companyId;
|
|
540
|
+
if (branchId) whereDelete.branchId = branchId;
|
|
541
|
+
}
|
|
542
|
+
const result = await permissionRepo.delete(whereDelete);
|
|
543
|
+
removed = result.affected || 0;
|
|
544
|
+
}
|
|
545
|
+
// Invalidate permission cache for this user
|
|
546
|
+
await this.invalidateUserPermissionCache(dto.userId, branchId, companyId);
|
|
547
|
+
return {
|
|
548
|
+
success: true,
|
|
549
|
+
added,
|
|
550
|
+
removed,
|
|
551
|
+
message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed`
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Get user's roles (branch-scoped)
|
|
556
|
+
* Returns roles assigned to user, filtered by companyId and branchId if provided.
|
|
557
|
+
*/ async getUserRoles(userId, branchId, companyId) {
|
|
558
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
559
|
+
const roleRepo = await this.getRoleRepository();
|
|
560
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
561
|
+
// Build where clause with optional companyId and branchId filter
|
|
562
|
+
// When company feature enabled:
|
|
563
|
+
// - companyId provided → Filter by that company (optional additional filter)
|
|
564
|
+
// - branchId provided → Filter by that branch (branch-specific roles)
|
|
565
|
+
// - branchId not provided → Filter by IS NULL (company-wide roles)
|
|
566
|
+
const where = {
|
|
567
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ROLE,
|
|
568
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
569
|
+
sourceId: userId
|
|
570
|
+
};
|
|
571
|
+
if (enableCompanyFeature) {
|
|
572
|
+
if (companyId) {
|
|
573
|
+
// Filter by companyId if provided
|
|
574
|
+
where.companyId = companyId;
|
|
575
|
+
}
|
|
576
|
+
if (branchId) {
|
|
577
|
+
// Branch-specific roles only
|
|
578
|
+
where.branchId = branchId;
|
|
579
|
+
} else {
|
|
580
|
+
// Company-wide roles only (branchId IS NULL)
|
|
581
|
+
where.branchId = null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const permissions = await permissionRepo.find({
|
|
585
|
+
where
|
|
586
|
+
});
|
|
587
|
+
if (permissions.length === 0) {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
const roleIds = permissions.map((p)=>p.targetId);
|
|
591
|
+
const roleWhere = {
|
|
592
|
+
id: (0, _typeorm.In)(roleIds)
|
|
593
|
+
};
|
|
594
|
+
const roles = await roleRepo.find({
|
|
595
|
+
where: roleWhere
|
|
596
|
+
});
|
|
597
|
+
const roleMap = new Map(roles.map((r)=>[
|
|
598
|
+
r.id,
|
|
599
|
+
r
|
|
600
|
+
]));
|
|
601
|
+
return permissions.filter((p)=>roleMap.has(p.targetId)).map((p)=>{
|
|
602
|
+
const role = roleMap.get(p.targetId);
|
|
603
|
+
const permissionEntity = p;
|
|
604
|
+
return {
|
|
605
|
+
id: p.id,
|
|
606
|
+
userId: p.userId,
|
|
607
|
+
roleId: role.id,
|
|
608
|
+
roleName: role.name,
|
|
609
|
+
branchId: enableCompanyFeature ? permissionEntity.branchId ?? null : null,
|
|
610
|
+
createdAt: p.createdAt
|
|
611
|
+
};
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
// ==================== My Permissions ====================
|
|
615
|
+
/**
|
|
616
|
+
* Get user's effective permissions (cache-first approach)
|
|
617
|
+
* 1. Check cache for existing permissions
|
|
618
|
+
* 2. If cached, apply parentCodes filter and return
|
|
619
|
+
* 3. If not cached, fetch from DB, cache, and return
|
|
620
|
+
*
|
|
621
|
+
* Cache is invalidated when:
|
|
622
|
+
* - User's direct actions change (assignUserActions)
|
|
623
|
+
* - User's roles change (assignUserRoles)
|
|
624
|
+
* - Role's actions change (assignRoleActions) - affects all users with that role
|
|
625
|
+
* - Company's actions change (assignCompanyActions) - affects all users in that company
|
|
626
|
+
*
|
|
627
|
+
* @param userId - User ID
|
|
628
|
+
* @param branchId - Branch ID (null = only company-wide, value = company-wide + branch-specific)
|
|
629
|
+
* @param companyId - Company ID (for role filtering and company action whitelist)
|
|
630
|
+
* @param parentCodes - Optional parent action codes to filter by (only affects frontend actions)
|
|
631
|
+
*/ async getMyPermissions(userId, branchId, companyId, parentCodes) {
|
|
632
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
633
|
+
const cacheOptions = {
|
|
634
|
+
userId,
|
|
635
|
+
companyId,
|
|
636
|
+
branchId,
|
|
637
|
+
enableCompanyFeature
|
|
638
|
+
};
|
|
639
|
+
// Step 1: Check cache first
|
|
640
|
+
const cachedData = await this.permissionCacheService.getMyPermissions(cacheOptions);
|
|
641
|
+
if (cachedData) {
|
|
642
|
+
// Cache hit - use cached data (even if empty)
|
|
643
|
+
return this.buildResponseFromCache(cachedData, parentCodes);
|
|
644
|
+
}
|
|
645
|
+
// Step 2: Cache miss - fetch from DB and cache (even if empty)
|
|
646
|
+
const freshData = await this.fetchAndCachePermissions(userId, branchId, companyId);
|
|
647
|
+
// Step 3: Apply parentCodes filter and return
|
|
648
|
+
return this.buildResponseFromCache(freshData, parentCodes);
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Build response from cached data, applying optional parentCodes filter
|
|
652
|
+
* Uses cached action code → ID mapping to avoid DB query
|
|
653
|
+
*/ async buildResponseFromCache(cachedData, parentCodes) {
|
|
654
|
+
let frontendActions = cachedData.frontendActions;
|
|
655
|
+
// Apply parentCodes filter if provided
|
|
656
|
+
if (parentCodes?.length) {
|
|
657
|
+
const parentIds = await this.getParentIdsByCodesWithCache(parentCodes);
|
|
658
|
+
if (parentIds.size > 0) {
|
|
659
|
+
frontendActions = frontendActions.filter((a)=>a.parentId && parentIds.has(a.parentId));
|
|
660
|
+
} else {
|
|
661
|
+
frontendActions = [];
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
frontendActions: frontendActions.map((a)=>({
|
|
666
|
+
id: a.id,
|
|
667
|
+
code: a.code,
|
|
668
|
+
name: a.name,
|
|
669
|
+
description: a.description
|
|
670
|
+
})),
|
|
671
|
+
cachedEndpoints: cachedData.backendCodes.length
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Get parent IDs by codes, using cache first
|
|
676
|
+
*/ async getParentIdsByCodesWithCache(codes) {
|
|
677
|
+
// Try cache first
|
|
678
|
+
const cachedMap = await this.permissionCacheService.getActionIdsByCodes(codes);
|
|
679
|
+
if (cachedMap) {
|
|
680
|
+
return new Set(Object.values(cachedMap));
|
|
681
|
+
}
|
|
682
|
+
// Cache miss - fetch from DB and cache
|
|
683
|
+
const actionRepo = await this.getActionRepository();
|
|
684
|
+
const allActions = await actionRepo.find({
|
|
685
|
+
select: [
|
|
686
|
+
'id',
|
|
687
|
+
'code'
|
|
688
|
+
]
|
|
689
|
+
});
|
|
690
|
+
// Build full code → ID map and cache it
|
|
691
|
+
const fullMap = {};
|
|
692
|
+
for (const action of allActions){
|
|
693
|
+
if (action.code) {
|
|
694
|
+
fullMap[action.code] = action.id;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
await this.permissionCacheService.setActionCodeMap(fullMap);
|
|
698
|
+
// Return only requested codes
|
|
699
|
+
return new Set(codes.map((code)=>fullMap[code]).filter(Boolean));
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Fetch permissions from DB and cache them
|
|
703
|
+
* Always returns a valid cache data structure (may be empty)
|
|
704
|
+
* Empty permissions are also cached to avoid repeated DB hits
|
|
705
|
+
*/ async fetchAndCachePermissions(userId, branchId, companyId) {
|
|
706
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
707
|
+
const permissionMode = this.iamConfigService.getPermissionMode();
|
|
708
|
+
const cacheOptions = {
|
|
709
|
+
userId,
|
|
710
|
+
companyId,
|
|
711
|
+
branchId,
|
|
712
|
+
enableCompanyFeature
|
|
713
|
+
};
|
|
714
|
+
// Empty cache data for users with no permissions
|
|
715
|
+
const emptyData = {
|
|
716
|
+
frontendActions: [],
|
|
717
|
+
backendCodes: []
|
|
718
|
+
};
|
|
719
|
+
const allActionIds = new Set();
|
|
720
|
+
// Collect action IDs based on permission mode
|
|
721
|
+
if (permissionMode === _permissiontypeenum.IAMPermissionMode.RBAC || permissionMode === _permissiontypeenum.IAMPermissionMode.FULL) {
|
|
722
|
+
const userRoleIds = await this.getUserRoleIds(userId, branchId, companyId);
|
|
723
|
+
if (userRoleIds.length > 0) {
|
|
724
|
+
const roleActionIds = await this.getRoleActionIds(userRoleIds);
|
|
725
|
+
roleActionIds.forEach((id)=>allActionIds.add(id));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (permissionMode === _permissiontypeenum.IAMPermissionMode.DIRECT || permissionMode === _permissiontypeenum.IAMPermissionMode.FULL) {
|
|
729
|
+
const userActionIds = await this.getUserActionIds(userId, branchId, companyId);
|
|
730
|
+
userActionIds.forEach((id)=>allActionIds.add(id));
|
|
731
|
+
}
|
|
732
|
+
if (allActionIds.size === 0) {
|
|
733
|
+
// Cache empty result to avoid repeated DB hits
|
|
734
|
+
await this.permissionCacheService.setMyPermissions(cacheOptions, emptyData);
|
|
735
|
+
return emptyData;
|
|
736
|
+
}
|
|
737
|
+
// Filter by company whitelist
|
|
738
|
+
if (enableCompanyFeature && companyId) {
|
|
739
|
+
const companyActionIds = await this.getCompanyActionIds(companyId);
|
|
740
|
+
if (companyActionIds.length > 0) {
|
|
741
|
+
const allowedActionIds = new Set(companyActionIds);
|
|
742
|
+
for (const actionId of allActionIds){
|
|
743
|
+
if (!allowedActionIds.has(actionId)) {
|
|
744
|
+
allActionIds.delete(actionId);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (allActionIds.size === 0) {
|
|
750
|
+
// Cache empty result to avoid repeated DB hits
|
|
751
|
+
await this.permissionCacheService.setMyPermissions(cacheOptions, emptyData);
|
|
752
|
+
return emptyData;
|
|
753
|
+
}
|
|
754
|
+
// Fetch actions from DB
|
|
755
|
+
const actionRepo = await this.getActionRepository();
|
|
756
|
+
const actions = await actionRepo.find({
|
|
757
|
+
where: {
|
|
758
|
+
id: (0, _typeorm.In)(Array.from(allActionIds))
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
// Separate by type
|
|
762
|
+
const backendActions = actions.filter((a)=>a.actionType === _actiontypeenum.ActionType.BACKEND || a.actionType === _actiontypeenum.ActionType.BOTH);
|
|
763
|
+
const frontendActions = actions.filter((a)=>a.actionType === _actiontypeenum.ActionType.FRONTEND || a.actionType === _actiontypeenum.ActionType.BOTH);
|
|
764
|
+
const backendCodes = backendActions.map((a)=>a.code).filter((c)=>!!c);
|
|
765
|
+
// Build cache data (includes parentId for filtering)
|
|
766
|
+
const cacheData = {
|
|
767
|
+
frontendActions: frontendActions.map((a)=>({
|
|
768
|
+
id: a.id,
|
|
769
|
+
code: a.code ?? '',
|
|
770
|
+
name: a.name,
|
|
771
|
+
description: a.description,
|
|
772
|
+
parentId: a.parentId
|
|
773
|
+
})),
|
|
774
|
+
backendCodes
|
|
775
|
+
};
|
|
776
|
+
// Cache both: full permissions and backend codes for PermissionGuard
|
|
777
|
+
await Promise.all([
|
|
778
|
+
this.permissionCacheService.setMyPermissions(cacheOptions, cacheData),
|
|
779
|
+
this.permissionCacheService.setPermissions(cacheOptions, backendCodes)
|
|
780
|
+
]);
|
|
781
|
+
return cacheData;
|
|
782
|
+
}
|
|
783
|
+
// ==================== Helper Methods ====================
|
|
784
|
+
/**
|
|
785
|
+
* Get role IDs assigned to a user
|
|
786
|
+
* Merges company-wide roles (branchId=null) with branch-specific roles (branchId=value)
|
|
787
|
+
*
|
|
788
|
+
* @param userId - User ID
|
|
789
|
+
* @param branchId - Branch ID (if null or not provided, only company-wide roles returned)
|
|
790
|
+
* @param companyId - Company ID for filtering roles (security)
|
|
791
|
+
* @returns Array of unique role IDs
|
|
792
|
+
*/ async getUserRoleIds(userId, branchId, companyId) {
|
|
793
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
794
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
795
|
+
if (!enableCompanyFeature) {
|
|
796
|
+
// Simple case: no company feature, get all user roles
|
|
797
|
+
const permissions = await permissionRepo.find({
|
|
798
|
+
where: {
|
|
799
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ROLE,
|
|
800
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
801
|
+
sourceId: userId
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
return permissions.map((p)=>p.targetId);
|
|
805
|
+
}
|
|
806
|
+
// Company feature enabled: merge company-wide + branch-specific roles
|
|
807
|
+
const roleIds = new Set();
|
|
808
|
+
// Step 1: Get company-wide roles (branchId = null)
|
|
809
|
+
const companyWidePermissions = await permissionRepo.find({
|
|
810
|
+
where: {
|
|
811
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ROLE,
|
|
812
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
813
|
+
sourceId: userId,
|
|
814
|
+
branchId: (0, _typeorm.IsNull)(),
|
|
815
|
+
companyId: companyId
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
companyWidePermissions.forEach((p)=>roleIds.add(p.targetId));
|
|
819
|
+
// Step 2: Get branch-specific roles (branchId = value) if branchId provided
|
|
820
|
+
if (branchId) {
|
|
821
|
+
const branchPermissions = await permissionRepo.find({
|
|
822
|
+
where: {
|
|
823
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ROLE,
|
|
824
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
825
|
+
sourceId: userId,
|
|
826
|
+
branchId,
|
|
827
|
+
companyId: companyId
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
branchPermissions.forEach((p)=>roleIds.add(p.targetId));
|
|
831
|
+
}
|
|
832
|
+
return Array.from(roleIds);
|
|
833
|
+
}
|
|
834
|
+
async getRoleActionIds(roleIds) {
|
|
835
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
836
|
+
const permissions = await permissionRepo.find({
|
|
837
|
+
where: {
|
|
838
|
+
permissionType: _useriampermissionentity.IamPermissionType.ROLE_ACTION,
|
|
839
|
+
sourceType: _useriampermissionentity.IamEntityType.ROLE,
|
|
840
|
+
sourceId: (0, _typeorm.In)(roleIds)
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
return permissions.map((p)=>p.targetId);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Get action IDs directly assigned to a user
|
|
847
|
+
* Merges company-wide actions (branchId=null) with branch-specific actions (branchId=value)
|
|
848
|
+
*
|
|
849
|
+
* @param userId - User ID
|
|
850
|
+
* @param branchId - Branch ID (if null or not provided, only company-wide actions returned)
|
|
851
|
+
* @param companyId - Company ID for filtering actions (security)
|
|
852
|
+
* @returns Array of unique action IDs
|
|
853
|
+
*/ async getUserActionIds(userId, branchId, companyId) {
|
|
854
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
855
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
856
|
+
if (!enableCompanyFeature) {
|
|
857
|
+
// Simple case: no company feature, get all user actions
|
|
858
|
+
const permissions = await permissionRepo.find({
|
|
859
|
+
where: {
|
|
860
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ACTION,
|
|
861
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
862
|
+
sourceId: userId
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
return permissions.map((p)=>p.targetId);
|
|
866
|
+
}
|
|
867
|
+
// Company feature enabled: merge company-wide + branch-specific actions
|
|
868
|
+
const actionIds = new Set();
|
|
869
|
+
// Step 1: Get company-wide actions (branchId = null, companyId = current)
|
|
870
|
+
const companyWideWhere = {
|
|
871
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ACTION,
|
|
872
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
873
|
+
sourceId: userId,
|
|
874
|
+
branchId: (0, _typeorm.IsNull)()
|
|
875
|
+
};
|
|
876
|
+
if (companyId) {
|
|
877
|
+
companyWideWhere.companyId = companyId;
|
|
878
|
+
}
|
|
879
|
+
const companyWidePermissions = await permissionRepo.find({
|
|
880
|
+
where: companyWideWhere
|
|
881
|
+
});
|
|
882
|
+
companyWidePermissions.forEach((p)=>actionIds.add(p.targetId));
|
|
883
|
+
// Step 2: Get branch-specific actions (branchId = value, companyId = current) if branchId provided
|
|
884
|
+
if (branchId) {
|
|
885
|
+
const branchWhere = {
|
|
886
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ACTION,
|
|
887
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
888
|
+
sourceId: userId,
|
|
889
|
+
branchId
|
|
890
|
+
};
|
|
891
|
+
if (companyId) {
|
|
892
|
+
branchWhere.companyId = companyId;
|
|
893
|
+
}
|
|
894
|
+
const branchPermissions = await permissionRepo.find({
|
|
895
|
+
where: branchWhere
|
|
896
|
+
});
|
|
897
|
+
branchPermissions.forEach((p)=>actionIds.add(p.targetId));
|
|
898
|
+
}
|
|
899
|
+
return Array.from(actionIds);
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Invalidate permission cache for a user
|
|
903
|
+
* Delegates to PermissionCacheService
|
|
904
|
+
*/ async invalidateUserPermissionCache(userId, branchId, companyId) {
|
|
905
|
+
// Wrap branchId in array if provided, otherwise invalidate null branch only
|
|
906
|
+
const branchIds = branchId !== undefined ? [
|
|
907
|
+
branchId
|
|
908
|
+
] : [
|
|
909
|
+
null
|
|
910
|
+
];
|
|
911
|
+
await this.permissionCacheService.invalidateUser(userId, companyId, branchIds);
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Invalidate permission cache for all users with a specific role
|
|
915
|
+
* Called when role-action permissions are modified
|
|
916
|
+
* Invalidates ALL branches for affected users
|
|
917
|
+
*
|
|
918
|
+
* @param roleId - Role ID
|
|
919
|
+
* @returns Number of users whose cache was invalidated
|
|
920
|
+
*/ async invalidateRoleMembersCache(roleId) {
|
|
921
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
922
|
+
const roleRepo = await this.getRoleRepository();
|
|
923
|
+
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
924
|
+
// Find all users assigned to this role with their branch IDs
|
|
925
|
+
const userRoles = await permissionRepo.find({
|
|
926
|
+
where: {
|
|
927
|
+
permissionType: _useriampermissionentity.IamPermissionType.USER_ROLE,
|
|
928
|
+
sourceType: _useriampermissionentity.IamEntityType.USER,
|
|
929
|
+
targetType: _useriampermissionentity.IamEntityType.ROLE,
|
|
930
|
+
targetId: roleId
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
// Get unique user IDs
|
|
934
|
+
const userIds = [
|
|
935
|
+
...new Set(userRoles.map((ur)=>ur.sourceId))
|
|
936
|
+
];
|
|
937
|
+
if (userIds.length === 0) {
|
|
938
|
+
return 0;
|
|
939
|
+
}
|
|
940
|
+
// Get company ID from role (if company feature enabled)
|
|
941
|
+
const role = await roleRepo.findOne({
|
|
942
|
+
where: {
|
|
943
|
+
id: roleId
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
const companyId = role?.companyId || null;
|
|
947
|
+
// Get all branch IDs for these users (to invalidate all their cached branches)
|
|
948
|
+
let branchIds = [
|
|
949
|
+
null
|
|
950
|
+
];
|
|
951
|
+
if (enableCompanyFeature && companyId) {
|
|
952
|
+
const userBranches = await permissionRepo.createQueryBuilder('p').select('DISTINCT p.branch_id', 'branchId').where('p.user_id IN (:...userIds)', {
|
|
953
|
+
userIds
|
|
954
|
+
}).andWhere('p.company_id = :companyId', {
|
|
955
|
+
companyId
|
|
956
|
+
}).getRawMany();
|
|
957
|
+
branchIds = [
|
|
958
|
+
...new Set(userBranches.map((p)=>p.branchId))
|
|
959
|
+
];
|
|
960
|
+
}
|
|
961
|
+
// Invalidate cache for all users with all their branches
|
|
962
|
+
return await this.permissionCacheService.invalidateRole(roleId, userIds, companyId, branchIds);
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Invalidate permission cache for all users in a company
|
|
966
|
+
* Called when company-action permissions are modified
|
|
967
|
+
* Finds all users with permissions in this company and invalidates their caches for ALL branches
|
|
968
|
+
*
|
|
969
|
+
* @param companyId - Company ID
|
|
970
|
+
* @returns Number of cache entries invalidated
|
|
971
|
+
*/ async invalidateCompanyMembersCache(companyId) {
|
|
972
|
+
if (!this.iamConfigService.isCompanyFeatureEnabled()) {
|
|
973
|
+
return 0;
|
|
974
|
+
}
|
|
975
|
+
const permissionRepo = await this.getPermissionRepository();
|
|
976
|
+
// Find all unique user IDs and branch IDs that have permissions in this company
|
|
977
|
+
const userPermissions = await permissionRepo.createQueryBuilder('p').select('DISTINCT p.user_id', 'userId').addSelect('p.branch_id', 'branchId').where('p.company_id = :companyId', {
|
|
978
|
+
companyId
|
|
979
|
+
}).andWhere('p.user_id IS NOT NULL').getRawMany();
|
|
980
|
+
const userIds = [
|
|
981
|
+
...new Set(userPermissions.map((p)=>p.userId).filter(Boolean))
|
|
982
|
+
];
|
|
983
|
+
const branchIds = [
|
|
984
|
+
...new Set(userPermissions.map((p)=>p.branchId))
|
|
985
|
+
]; // includes null
|
|
986
|
+
if (userIds.length === 0) {
|
|
987
|
+
return 0;
|
|
988
|
+
}
|
|
989
|
+
return await this.permissionCacheService.invalidateUsers(userIds, companyId, branchIds);
|
|
990
|
+
}
|
|
991
|
+
// NOTE: @Inject() required for bundled code - type metadata may be lost during esbuild
|
|
992
|
+
constructor(permissionEvaluator, permissionCacheService, iamConfigService, dataSourceProvider){
|
|
993
|
+
_define_property(this, "permissionEvaluator", void 0);
|
|
994
|
+
_define_property(this, "permissionCacheService", void 0);
|
|
995
|
+
_define_property(this, "iamConfigService", void 0);
|
|
996
|
+
_define_property(this, "dataSourceProvider", void 0);
|
|
997
|
+
_define_property(this, "logger", void 0);
|
|
998
|
+
this.permissionEvaluator = permissionEvaluator;
|
|
999
|
+
this.permissionCacheService = permissionCacheService;
|
|
1000
|
+
this.iamConfigService = iamConfigService;
|
|
1001
|
+
this.dataSourceProvider = dataSourceProvider;
|
|
1002
|
+
this.logger = new _common.Logger(PermissionService.name);
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
PermissionService = _ts_decorate([
|
|
1006
|
+
(0, _common.Injectable)({
|
|
1007
|
+
scope: _common.Scope.REQUEST
|
|
1008
|
+
}),
|
|
1009
|
+
_ts_param(0, (0, _common.Inject)(_permissionevaluatorhelper.PermissionEvaluatorHelper)),
|
|
1010
|
+
_ts_param(1, (0, _common.Inject)(_permissioncacheservice.PermissionCacheService)),
|
|
1011
|
+
_ts_param(2, (0, _common.Inject)(_iamconfigservice.IAMConfigService)),
|
|
1012
|
+
_ts_param(3, (0, _common.Inject)(_iamdatasourceprovider.IAMDataSourceProvider)),
|
|
1013
|
+
_ts_metadata("design:type", Function),
|
|
1014
|
+
_ts_metadata("design:paramtypes", [
|
|
1015
|
+
typeof _permissionevaluatorhelper.PermissionEvaluatorHelper === "undefined" ? Object : _permissionevaluatorhelper.PermissionEvaluatorHelper,
|
|
1016
|
+
typeof _permissioncacheservice.PermissionCacheService === "undefined" ? Object : _permissioncacheservice.PermissionCacheService,
|
|
1017
|
+
typeof _iamconfigservice.IAMConfigService === "undefined" ? Object : _iamconfigservice.IAMConfigService,
|
|
1018
|
+
typeof _iamdatasourceprovider.IAMDataSourceProvider === "undefined" ? Object : _iamdatasourceprovider.IAMDataSourceProvider
|
|
1019
|
+
])
|
|
1020
|
+
], PermissionService);
|