@plyaz/auth 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/pull_request_template.md +71 -0
- package/.github/workflows/deploy.yml +9 -0
- package/.github/workflows/publish.yml +14 -0
- package/.github/workflows/security.yml +20 -0
- package/README.md +89 -0
- package/commits.txt +5 -0
- package/dist/common/index.cjs +48 -0
- package/dist/common/index.cjs.map +1 -0
- package/dist/common/index.mjs +43 -0
- package/dist/common/index.mjs.map +1 -0
- package/dist/index.cjs +20411 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +5139 -0
- package/dist/index.mjs.map +1 -0
- package/eslint.config.mjs +13 -0
- package/index.html +13 -0
- package/package.json +141 -0
- package/src/adapters/auth-adapter-factory.ts +26 -0
- package/src/adapters/auth-adapter.mapper.ts +53 -0
- package/src/adapters/base-auth.adapter.ts +119 -0
- package/src/adapters/clerk/clerk.adapter.ts +204 -0
- package/src/adapters/custom/custom.adapter.ts +119 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/next-auth/authOptions.ts +81 -0
- package/src/adapters/next-auth/next-auth.adapter.ts +211 -0
- package/src/api/client.ts +37 -0
- package/src/audit/audit.logger.ts +52 -0
- package/src/client/components/ProtectedRoute.tsx +37 -0
- package/src/client/hooks/useAuth.ts +128 -0
- package/src/client/hooks/useConnectedAccounts.ts +108 -0
- package/src/client/hooks/usePermissions.ts +36 -0
- package/src/client/hooks/useRBAC.ts +36 -0
- package/src/client/hooks/useSession.ts +18 -0
- package/src/client/providers/AuthProvider.tsx +104 -0
- package/src/client/store/auth.store.ts +306 -0
- package/src/client/utils/storage.ts +70 -0
- package/src/common/constants/oauth-providers.ts +49 -0
- package/src/common/errors/auth.errors.ts +64 -0
- package/src/common/errors/specific-auth-errors.ts +201 -0
- package/src/common/index.ts +19 -0
- package/src/common/regex/index.ts +27 -0
- package/src/common/types/auth.types.ts +641 -0
- package/src/common/types/index.ts +297 -0
- package/src/common/utils/index.ts +84 -0
- package/src/core/blacklist/token.blacklist.ts +60 -0
- package/src/core/index.ts +2 -0
- package/src/core/jwt/jwt.manager.ts +131 -0
- package/src/core/session/session.manager.ts +56 -0
- package/src/db/repositories/connected-account.repository.ts +415 -0
- package/src/db/repositories/role.repository.ts +519 -0
- package/src/db/repositories/session.repository.ts +308 -0
- package/src/db/repositories/user.repository.ts +320 -0
- package/src/flows/index.ts +2 -0
- package/src/flows/sign-in.flow.ts +106 -0
- package/src/flows/sign-up.flow.ts +121 -0
- package/src/index.ts +54 -0
- package/src/libs/clerk.helper.ts +36 -0
- package/src/libs/supabase.helper.ts +255 -0
- package/src/libs/supabaseClient.ts +6 -0
- package/src/providers/base/auth-provider.interface.ts +42 -0
- package/src/providers/base/index.ts +1 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/oauth/facebook.provider.ts +97 -0
- package/src/providers/oauth/github.provider.ts +148 -0
- package/src/providers/oauth/google.provider.ts +126 -0
- package/src/providers/oauth/index.ts +3 -0
- package/src/rbac/dynamic-roles.ts +552 -0
- package/src/rbac/index.ts +4 -0
- package/src/rbac/permission-checker.ts +464 -0
- package/src/rbac/role-hierarchy.ts +545 -0
- package/src/rbac/role.manager.ts +75 -0
- package/src/security/csrf/csrf.protection.ts +37 -0
- package/src/security/index.ts +3 -0
- package/src/security/rate-limiting/auth/auth.controller.ts +12 -0
- package/src/security/rate-limiting/auth/rate-limiting.interface.ts +67 -0
- package/src/security/rate-limiting/auth.module.ts +32 -0
- package/src/server/auth.module.ts +158 -0
- package/src/server/decorators/auth.decorator.ts +43 -0
- package/src/server/decorators/auth.decorators.ts +31 -0
- package/src/server/decorators/current-user.decorator.ts +49 -0
- package/src/server/decorators/permission.decorator.ts +49 -0
- package/src/server/guards/auth.guard.ts +56 -0
- package/src/server/guards/custom-throttler.guard.ts +46 -0
- package/src/server/guards/permissions.guard.ts +115 -0
- package/src/server/guards/roles.guard.ts +31 -0
- package/src/server/middleware/auth.middleware.ts +46 -0
- package/src/server/middleware/index.ts +2 -0
- package/src/server/middleware/middleware.ts +11 -0
- package/src/server/middleware/session.middleware.ts +255 -0
- package/src/server/services/account.service.ts +269 -0
- package/src/server/services/auth.service.ts +79 -0
- package/src/server/services/brute-force.service.ts +98 -0
- package/src/server/services/index.ts +15 -0
- package/src/server/services/rate-limiter.service.ts +60 -0
- package/src/server/services/session.service.ts +287 -0
- package/src/server/services/token.service.ts +262 -0
- package/src/session/cookie-store.ts +255 -0
- package/src/session/enhanced-session-manager.ts +406 -0
- package/src/session/index.ts +14 -0
- package/src/session/memory-store.ts +320 -0
- package/src/session/redis-store.ts +443 -0
- package/src/strategies/oauth.strategy.ts +128 -0
- package/src/strategies/traditional-auth.strategy.ts +116 -0
- package/src/tokens/index.ts +4 -0
- package/src/tokens/refresh-token-manager.ts +448 -0
- package/src/tokens/token-validator.ts +311 -0
- package/tsconfig.build.json +28 -0
- package/tsconfig.json +38 -0
- package/tsup.config.mjs +28 -0
- package/vitest.config.mjs +16 -0
- package/vitest.setup.d.ts +2 -0
- package/vitest.setup.d.ts.map +1 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Role hierarchy manager for @plyaz/auth
|
|
3
|
+
* @module @plyaz/auth/rbac/role-hierarchy
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Manages role hierarchy and inheritance for role-based access control.
|
|
7
|
+
* Handles role precedence, permission inheritance, and hierarchical
|
|
8
|
+
* role validation. Enables complex organizational structures with
|
|
9
|
+
* inherited permissions and role-based delegation.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { RoleHierarchy } from '@plyaz/auth';
|
|
14
|
+
*
|
|
15
|
+
* const hierarchy = new RoleHierarchy();
|
|
16
|
+
* hierarchy.addRole({ id: 'admin', name: 'Admin', permissions: ['*'] });
|
|
17
|
+
* hierarchy.addRole({ id: 'moderator', name: 'Moderator', permissions: ['read', 'write'] });
|
|
18
|
+
* hierarchy.addRoleRelationship('moderator', 'admin');
|
|
19
|
+
*
|
|
20
|
+
* const hasAdminAccess = hierarchy.hasPermission('moderator', 'delete');
|
|
21
|
+
* console.log(hierarchy.getHierarchyTree());
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { Role, RoleHierarchyConfig, RoleNode } from "@plyaz/types";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Role hierarchy manager implementation
|
|
29
|
+
* Manages role relationships and permission inheritance
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
interface AddRole extends Role {
|
|
33
|
+
permissions?:[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class RoleHierarchy {
|
|
37
|
+
private readonly config: RoleHierarchyConfig;
|
|
38
|
+
private readonly roles = new Map<string, RoleNode>();
|
|
39
|
+
private readonly hierarchyCache = new Map<string, Set<string>>();
|
|
40
|
+
|
|
41
|
+
constructor(config: Partial<RoleHierarchyConfig> = {}) {
|
|
42
|
+
this.config = {
|
|
43
|
+
enableInheritance: true,
|
|
44
|
+
maxDepth: 10,
|
|
45
|
+
detectCircular: true,
|
|
46
|
+
cacheInheritance: true,
|
|
47
|
+
...config
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Add role to hierarchy
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
addRole(role: AddRole): void {
|
|
56
|
+
const roleNode: RoleNode = {
|
|
57
|
+
role,
|
|
58
|
+
parents: new Set(),
|
|
59
|
+
children: new Set(),
|
|
60
|
+
permissions: new Set(role.permissions ?? [])
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
this.roles.set(role.id, roleNode);
|
|
64
|
+
|
|
65
|
+
// Clear cache when hierarchy changes
|
|
66
|
+
if (this.config.cacheInheritance) {
|
|
67
|
+
this.hierarchyCache.clear();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Remove role from hierarchy
|
|
73
|
+
*/
|
|
74
|
+
removeRole(roleId: string): void {
|
|
75
|
+
const roleNode = this.roles.get(roleId);
|
|
76
|
+
|
|
77
|
+
if (!roleNode) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Remove from parent-child relationships
|
|
82
|
+
for (const parentId of roleNode.parents) {
|
|
83
|
+
const parent = this.roles.get(parentId);
|
|
84
|
+
if (parent) {
|
|
85
|
+
parent.children.delete(roleId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const childId of roleNode.children) {
|
|
90
|
+
const child = this.roles.get(childId);
|
|
91
|
+
if (child) {
|
|
92
|
+
child.parents.delete(roleId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.roles.delete(roleId);
|
|
97
|
+
|
|
98
|
+
// Clear cache when hierarchy changes
|
|
99
|
+
if (this.config.cacheInheritance) {
|
|
100
|
+
this.hierarchyCache.clear();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Add parent-child relationship between roles
|
|
106
|
+
*/
|
|
107
|
+
addRoleRelationship(childRoleId: string, parentRoleId: string): void {
|
|
108
|
+
const childRole = this.roles.get(childRoleId);
|
|
109
|
+
const parentRole = this.roles.get(parentRoleId);
|
|
110
|
+
|
|
111
|
+
if (!childRole || !parentRole) {
|
|
112
|
+
throw new Error('Role not found');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check for circular dependencies
|
|
116
|
+
if (this.config.detectCircular && this.inheritsFrom(parentRoleId, childRoleId)) {
|
|
117
|
+
throw new Error('Adding relationship would create circular dependency');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check hierarchy depth
|
|
121
|
+
const depth = this.calculateDepth(parentRoleId);
|
|
122
|
+
if (depth >= this.config.maxDepth) {
|
|
123
|
+
throw new Error(`Maximum hierarchy depth exceeded: ${depth}/${this.config.maxDepth}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add relationship
|
|
127
|
+
childRole.parents.add(parentRoleId);
|
|
128
|
+
parentRole.children.add(childRoleId);
|
|
129
|
+
|
|
130
|
+
// Clear cache when hierarchy changes
|
|
131
|
+
if (this.config.cacheInheritance) {
|
|
132
|
+
this.hierarchyCache.clear();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Remove parent-child relationship between roles
|
|
138
|
+
*/
|
|
139
|
+
removeRoleRelationship(childRoleId: string, parentRoleId: string): void {
|
|
140
|
+
const childRole = this.roles.get(childRoleId);
|
|
141
|
+
const parentRole = this.roles.get(parentRoleId);
|
|
142
|
+
|
|
143
|
+
if (childRole) {
|
|
144
|
+
childRole.parents.delete(parentRoleId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (parentRole) {
|
|
148
|
+
parentRole.children.delete(childRoleId);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Clear cache when hierarchy changes
|
|
152
|
+
if (this.config.cacheInheritance) {
|
|
153
|
+
this.hierarchyCache.clear();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Add permission to role
|
|
159
|
+
*/
|
|
160
|
+
addPermission(roleId: string, permission: string): void {
|
|
161
|
+
const roleNode = this.roles.get(roleId);
|
|
162
|
+
if (roleNode) {
|
|
163
|
+
roleNode.permissions.add(permission);
|
|
164
|
+
if (this.config.cacheInheritance) {
|
|
165
|
+
this.hierarchyCache.clear();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Remove permission from role
|
|
172
|
+
*/
|
|
173
|
+
removePermission(roleId: string, permission: string): void {
|
|
174
|
+
const roleNode = this.roles.get(roleId);
|
|
175
|
+
if (roleNode) {
|
|
176
|
+
roleNode.permissions.delete(permission);
|
|
177
|
+
if (this.config.cacheInheritance) {
|
|
178
|
+
this.hierarchyCache.clear();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if role has specific permission (including inherited)
|
|
185
|
+
*/
|
|
186
|
+
hasPermission(roleId: string, permission: string): boolean {
|
|
187
|
+
if (!this.config.enableInheritance) {
|
|
188
|
+
const roleNode = this.roles.get(roleId);
|
|
189
|
+
return roleNode ? roleNode.permissions.has(permission) : false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return this.getEffectivePermissions(roleId).has(permission);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check if role inherits from another role
|
|
197
|
+
*/
|
|
198
|
+
inheritsFrom(roleId: string, ancestorRoleId: string): boolean {
|
|
199
|
+
if (roleId === ancestorRoleId) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const ancestors = this.getAncestors(roleId);
|
|
204
|
+
return ancestors.has(ancestorRoleId);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get all ancestor roles (roles this role inherits from)
|
|
209
|
+
*/
|
|
210
|
+
getAncestors(roleId: string): Set<string> {
|
|
211
|
+
if (this.config.cacheInheritance) {
|
|
212
|
+
const cached = this.hierarchyCache.get(`ancestors:${roleId}`);
|
|
213
|
+
if (cached) {
|
|
214
|
+
return cached;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const ancestors = this.collectAncestorsSync(roleId);
|
|
219
|
+
|
|
220
|
+
if (this.config.cacheInheritance) {
|
|
221
|
+
this.hierarchyCache.set(`ancestors:${roleId}`, ancestors);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return ancestors;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get all descendant roles (roles that inherit from this role)
|
|
229
|
+
*/
|
|
230
|
+
getDescendants(roleId: string): Set<string> {
|
|
231
|
+
if (this.config.cacheInheritance) {
|
|
232
|
+
const cached = this.hierarchyCache.get(`descendants:${roleId}`);
|
|
233
|
+
if (cached) {
|
|
234
|
+
return cached;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const descendants = this.collectDescendantsSync(roleId);
|
|
239
|
+
|
|
240
|
+
if (this.config.cacheInheritance) {
|
|
241
|
+
this.hierarchyCache.set(`descendants:${roleId}`, descendants);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return descendants;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get effective permissions for role (including inherited)
|
|
249
|
+
*/
|
|
250
|
+
getEffectivePermissions(roleId: string): Set<string> {
|
|
251
|
+
if (!this.config.enableInheritance) {
|
|
252
|
+
const roleNode = this.roles.get(roleId);
|
|
253
|
+
return roleNode ? roleNode.permissions : new Set();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (this.config.cacheInheritance) {
|
|
257
|
+
const cached = this.hierarchyCache.get(`permissions:${roleId}`);
|
|
258
|
+
if (cached) {
|
|
259
|
+
return cached;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const effectivePermissions = new Set<string>();
|
|
264
|
+
const roleNode = this.roles.get(roleId);
|
|
265
|
+
|
|
266
|
+
if (!roleNode) {
|
|
267
|
+
return effectivePermissions;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Add direct permissions
|
|
271
|
+
for (const permission of roleNode.permissions) {
|
|
272
|
+
effectivePermissions.add(permission);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Add inherited permissions
|
|
276
|
+
const ancestors = this.getAncestors(roleId);
|
|
277
|
+
for (const ancestorId of ancestors) {
|
|
278
|
+
const ancestorNode = this.roles.get(ancestorId);
|
|
279
|
+
if (ancestorNode) {
|
|
280
|
+
for (const permission of ancestorNode.permissions) {
|
|
281
|
+
effectivePermissions.add(permission);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (this.config.cacheInheritance) {
|
|
287
|
+
this.hierarchyCache.set(`permissions:${roleId}`, effectivePermissions);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return effectivePermissions;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Validate hierarchy integrity
|
|
296
|
+
*/
|
|
297
|
+
validateHierarchy(): {
|
|
298
|
+
valid: boolean;
|
|
299
|
+
issues: string[];
|
|
300
|
+
} {
|
|
301
|
+
const issues: string[] = [];
|
|
302
|
+
|
|
303
|
+
// Check for circular dependencies
|
|
304
|
+
if (this.config.detectCircular) {
|
|
305
|
+
for (const roleId of this.roles.keys()) {
|
|
306
|
+
if (this.hasCircularDependency(roleId)) {
|
|
307
|
+
issues.push(`Circular dependency detected involving role: ${roleId}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check hierarchy depth
|
|
313
|
+
for (const roleId of this.roles.keys()) {
|
|
314
|
+
const depth = this.calculateDepth(roleId);
|
|
315
|
+
if (depth > this.config.maxDepth) {
|
|
316
|
+
issues.push(`Role ${roleId} exceeds maximum hierarchy depth: ${depth}/${this.config.maxDepth}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check for orphaned relationships
|
|
321
|
+
for (const [roleId, roleNode] of this.roles.entries()) {
|
|
322
|
+
for (const parentId of roleNode.parents) {
|
|
323
|
+
if (!this.roles.has(parentId)) {
|
|
324
|
+
issues.push(`Role ${roleId} references non-existent parent: ${parentId}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
for (const childId of roleNode.children) {
|
|
329
|
+
if (!this.roles.has(childId)) {
|
|
330
|
+
issues.push(`Role ${roleId} references non-existent child: ${childId}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
valid: issues.length === 0,
|
|
337
|
+
issues
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Clear hierarchy cache
|
|
343
|
+
*/
|
|
344
|
+
clearCache(): void {
|
|
345
|
+
this.hierarchyCache.clear();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Get hierarchy statistics
|
|
350
|
+
*/
|
|
351
|
+
getStats(): {
|
|
352
|
+
totalRoles: number;
|
|
353
|
+
maxDepth: number;
|
|
354
|
+
rootRoles: number;
|
|
355
|
+
leafRoles: number;
|
|
356
|
+
avgPermissions: number;
|
|
357
|
+
} {
|
|
358
|
+
let maxDepth = 0;
|
|
359
|
+
let rootRoles = 0;
|
|
360
|
+
let leafRoles = 0;
|
|
361
|
+
let totalPermissions = 0;
|
|
362
|
+
|
|
363
|
+
for (const [roleId, roleNode] of this.roles.entries()) {
|
|
364
|
+
if (roleNode.parents.size === 0) rootRoles++;
|
|
365
|
+
if (roleNode.children.size === 0) leafRoles++;
|
|
366
|
+
|
|
367
|
+
const depth = this.calculateDepth(roleId);
|
|
368
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
369
|
+
totalPermissions += roleNode.permissions.size;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
totalRoles: this.roles.size,
|
|
374
|
+
maxDepth,
|
|
375
|
+
rootRoles,
|
|
376
|
+
leafRoles,
|
|
377
|
+
avgPermissions: this.roles.size > 0 ? Math.round(totalPermissions / this.roles.size) : 0
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ==================== PRIVATE METHODS ====================
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Collect ancestors iteratively (synchronous)
|
|
385
|
+
*/
|
|
386
|
+
private collectAncestorsSync(roleId: string): Set<string> {
|
|
387
|
+
const ancestors = new Set<string>();
|
|
388
|
+
const stack: string[] = [];
|
|
389
|
+
const visited = new Set<string>();
|
|
390
|
+
|
|
391
|
+
const roleNode = this.roles.get(roleId);
|
|
392
|
+
if (!roleNode) return ancestors;
|
|
393
|
+
|
|
394
|
+
for (const parentId of roleNode.parents) {
|
|
395
|
+
stack.push(parentId);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
while (stack.length > 0) {
|
|
399
|
+
const parentId = stack.pop()!;
|
|
400
|
+
if (visited.has(parentId)) continue;
|
|
401
|
+
|
|
402
|
+
visited.add(parentId);
|
|
403
|
+
ancestors.add(parentId);
|
|
404
|
+
|
|
405
|
+
const parentNode = this.roles.get(parentId);
|
|
406
|
+
if (parentNode) {
|
|
407
|
+
for (const grandparentId of parentNode.parents) {
|
|
408
|
+
stack.push(grandparentId);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return ancestors;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Collect descendants iteratively (synchronous)
|
|
418
|
+
*/
|
|
419
|
+
private collectDescendantsSync(roleId: string): Set<string> {
|
|
420
|
+
const descendants = new Set<string>();
|
|
421
|
+
const stack: string[] = [];
|
|
422
|
+
const visited = new Set<string>();
|
|
423
|
+
|
|
424
|
+
const roleNode = this.roles.get(roleId);
|
|
425
|
+
if (!roleNode) return descendants;
|
|
426
|
+
|
|
427
|
+
for (const childId of roleNode.children) {
|
|
428
|
+
stack.push(childId);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
while (stack.length > 0) {
|
|
432
|
+
const childId = stack.pop()!;
|
|
433
|
+
if (visited.has(childId)) continue;
|
|
434
|
+
|
|
435
|
+
visited.add(childId);
|
|
436
|
+
descendants.add(childId);
|
|
437
|
+
|
|
438
|
+
const childNode = this.roles.get(childId);
|
|
439
|
+
if (childNode) {
|
|
440
|
+
for (const grandchildId of childNode.children) {
|
|
441
|
+
stack.push(grandchildId);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return descendants;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Check for circular dependency starting from role
|
|
451
|
+
*/
|
|
452
|
+
private hasCircularDependency(roleId: string): boolean {
|
|
453
|
+
const visited = new Set<string>();
|
|
454
|
+
const recursionStack = new Set<string>();
|
|
455
|
+
return this.detectCircularRecursive(roleId, visited, recursionStack);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Detect circular dependency recursively
|
|
460
|
+
*/
|
|
461
|
+
private detectCircularRecursive(
|
|
462
|
+
roleId: string,
|
|
463
|
+
visited: Set<string>,
|
|
464
|
+
recursionStack: Set<string>
|
|
465
|
+
): boolean {
|
|
466
|
+
visited.add(roleId);
|
|
467
|
+
recursionStack.add(roleId);
|
|
468
|
+
|
|
469
|
+
const roleNode = this.roles.get(roleId);
|
|
470
|
+
if (!roleNode) {
|
|
471
|
+
recursionStack.delete(roleId);
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
for (const parentId of roleNode.parents) {
|
|
476
|
+
if (!visited.has(parentId)) {
|
|
477
|
+
if (this.detectCircularRecursive(parentId, visited, recursionStack)) {
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
} else if (recursionStack.has(parentId)) {
|
|
481
|
+
return true; // Circular dependency found
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
recursionStack.delete(roleId);
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Calculate hierarchy depth for role (synchronous)
|
|
491
|
+
*/
|
|
492
|
+
private calculateDepth(roleId: string): number {
|
|
493
|
+
return this.collectAncestorsSync(roleId).size;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Build role tree recursively
|
|
498
|
+
*/
|
|
499
|
+
private buildRoleTree(roleId: string): { role: Role; children: Record<string, unknown> } | null {
|
|
500
|
+
const roleNode = this.roles.get(roleId);
|
|
501
|
+
if (!roleNode) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const tree: { role: Role; children: Record<string, unknown> } = {
|
|
506
|
+
role: roleNode.role,
|
|
507
|
+
children: {}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
for (const childId of roleNode.children) {
|
|
511
|
+
const childTree = this.buildRoleTree(childId);
|
|
512
|
+
if (childTree) {
|
|
513
|
+
tree.children[childId] = childTree;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return tree;
|
|
518
|
+
}
|
|
519
|
+
}
|