@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,545 @@
|
|
|
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
|
+
* await hierarchy.addRole('admin', 100);
|
|
17
|
+
* await hierarchy.addRole('moderator', 50);
|
|
18
|
+
*
|
|
19
|
+
* const hasPermission = await hierarchy.checkInheritedPermission(
|
|
20
|
+
* 'admin', 'moderator'
|
|
21
|
+
* );
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { Role, RoleHierarchyConfig, RoleNode } from "@plyaz/types";
|
|
26
|
+
|
|
27
|
+
// /**
|
|
28
|
+
// * Role hierarchy node
|
|
29
|
+
// */
|
|
30
|
+
// export interface RoleNode {
|
|
31
|
+
// /** Role information */
|
|
32
|
+
// role: Role;
|
|
33
|
+
// /** Parent roles (higher hierarchy) */
|
|
34
|
+
// parents: Set<string>;
|
|
35
|
+
// /** Child roles (lower hierarchy) */
|
|
36
|
+
// children: Set<string>;
|
|
37
|
+
// /** Direct permissions */
|
|
38
|
+
// permissions: Set<string>;
|
|
39
|
+
// /** Inherited permissions (computed) */
|
|
40
|
+
// inheritedPermissions?: Set<string>;
|
|
41
|
+
// }
|
|
42
|
+
|
|
43
|
+
// /**
|
|
44
|
+
// * Role hierarchy configuration
|
|
45
|
+
// */
|
|
46
|
+
// export interface RoleHierarchyConfig {
|
|
47
|
+
// /** Enable permission inheritance */
|
|
48
|
+
// enableInheritance: boolean;
|
|
49
|
+
// /** Maximum hierarchy depth */
|
|
50
|
+
// maxDepth: number;
|
|
51
|
+
// /** Enable circular dependency detection */
|
|
52
|
+
// detectCircular: boolean;
|
|
53
|
+
// /** Cache inherited permissions */
|
|
54
|
+
// cacheInheritance: boolean;
|
|
55
|
+
// }
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Role hierarchy manager implementation
|
|
59
|
+
* Manages role relationships and permission inheritance
|
|
60
|
+
*/
|
|
61
|
+
export class RoleHierarchy {
|
|
62
|
+
private readonly config: RoleHierarchyConfig;
|
|
63
|
+
private readonly roles = new Map<string, RoleNode>();
|
|
64
|
+
private readonly hierarchyCache = new Map<string, Set<string>>();
|
|
65
|
+
|
|
66
|
+
constructor(config: Partial<RoleHierarchyConfig> = {}) {
|
|
67
|
+
this.config = {
|
|
68
|
+
enableInheritance: true,
|
|
69
|
+
maxDepth: 10,
|
|
70
|
+
detectCircular: true,
|
|
71
|
+
cacheInheritance: true,
|
|
72
|
+
...config,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Add role to hierarchy
|
|
78
|
+
* @param role - Role to add
|
|
79
|
+
* @returns Promise that resolves when role is added
|
|
80
|
+
*/
|
|
81
|
+
async addRole(role: Role): Promise<void> {
|
|
82
|
+
const roleNode: RoleNode = {
|
|
83
|
+
role,
|
|
84
|
+
parents: new Set(),
|
|
85
|
+
children: new Set(),
|
|
86
|
+
permissions: new Set(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
this.roles.set(role.id, roleNode);
|
|
90
|
+
|
|
91
|
+
// Clear cache when hierarchy changes
|
|
92
|
+
if (this.config.cacheInheritance) {
|
|
93
|
+
this.hierarchyCache.clear();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Remove role from hierarchy
|
|
99
|
+
* @param roleId - Role identifier
|
|
100
|
+
* @returns Promise that resolves when role is removed
|
|
101
|
+
*/
|
|
102
|
+
async removeRole(roleId: string): Promise<void> {
|
|
103
|
+
const roleNode = this.roles.get(roleId);
|
|
104
|
+
|
|
105
|
+
if (!roleNode) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Remove from parent-child relationships
|
|
110
|
+
for (const parentId of roleNode.parents) {
|
|
111
|
+
const parent = this.roles.get(parentId);
|
|
112
|
+
if (parent) {
|
|
113
|
+
parent.children.delete(roleId);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const childId of roleNode.children) {
|
|
118
|
+
const child = this.roles.get(childId);
|
|
119
|
+
if (child) {
|
|
120
|
+
child.parents.delete(roleId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.roles.delete(roleId);
|
|
125
|
+
|
|
126
|
+
// Clear cache when hierarchy changes
|
|
127
|
+
if (this.config.cacheInheritance) {
|
|
128
|
+
this.hierarchyCache.clear();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Add parent-child relationship between roles
|
|
134
|
+
* @param childRoleId - Child role identifier
|
|
135
|
+
* @param parentRoleId - Parent role identifier
|
|
136
|
+
* @returns Promise that resolves when relationship is added
|
|
137
|
+
*/
|
|
138
|
+
async addRoleRelationship(
|
|
139
|
+
childRoleId: string,
|
|
140
|
+
parentRoleId: string
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
const childRole = this.roles.get(childRoleId);
|
|
143
|
+
const parentRole = this.roles.get(parentRoleId);
|
|
144
|
+
|
|
145
|
+
if (!childRole || !parentRole) {
|
|
146
|
+
throw new Error("Role not found");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check for circular dependencies
|
|
150
|
+
if (
|
|
151
|
+
this.config.detectCircular &&
|
|
152
|
+
(await this.wouldCreateCircular(childRoleId, parentRoleId))
|
|
153
|
+
) {
|
|
154
|
+
throw new Error("Adding relationship would create circular dependency");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check hierarchy depth
|
|
158
|
+
const depth = await this.calculateDepth(parentRoleId);
|
|
159
|
+
if (depth >= this.config.maxDepth) {
|
|
160
|
+
throw new Error("Maximum hierarchy depth exceeded");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Add relationship
|
|
164
|
+
childRole.parents.add(parentRoleId);
|
|
165
|
+
parentRole.children.add(childRoleId);
|
|
166
|
+
|
|
167
|
+
// Clear cache when hierarchy changes
|
|
168
|
+
if (this.config.cacheInheritance) {
|
|
169
|
+
this.hierarchyCache.clear();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Remove parent-child relationship between roles
|
|
175
|
+
* @param childRoleId - Child role identifier
|
|
176
|
+
* @param parentRoleId - Parent role identifier
|
|
177
|
+
* @returns Promise that resolves when relationship is removed
|
|
178
|
+
*/
|
|
179
|
+
async removeRoleRelationship(
|
|
180
|
+
childRoleId: string,
|
|
181
|
+
parentRoleId: string
|
|
182
|
+
): Promise<void> {
|
|
183
|
+
const childRole = this.roles.get(childRoleId);
|
|
184
|
+
const parentRole = this.roles.get(parentRoleId);
|
|
185
|
+
|
|
186
|
+
if (childRole) {
|
|
187
|
+
childRole.parents.delete(parentRoleId);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (parentRole) {
|
|
191
|
+
parentRole.children.delete(childRoleId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Clear cache when hierarchy changes
|
|
195
|
+
if (this.config.cacheInheritance) {
|
|
196
|
+
this.hierarchyCache.clear();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if role inherits from another role
|
|
202
|
+
* @param roleId - Role identifier
|
|
203
|
+
* @param ancestorRoleId - Ancestor role identifier
|
|
204
|
+
* @returns True if role inherits from ancestor
|
|
205
|
+
*/
|
|
206
|
+
async inheritsFrom(roleId: string, ancestorRoleId: string): Promise<boolean> {
|
|
207
|
+
if (roleId === ancestorRoleId) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const ancestors = await this.getAncestors(roleId);
|
|
212
|
+
return ancestors.has(ancestorRoleId);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get all ancestor roles (roles this role inherits from)
|
|
217
|
+
* @param roleId - Role identifier
|
|
218
|
+
* @returns Set of ancestor role IDs
|
|
219
|
+
*/
|
|
220
|
+
async getAncestors(roleId: string): Promise<Set<string>> {
|
|
221
|
+
if (this.config.cacheInheritance) {
|
|
222
|
+
const cached = this.hierarchyCache.get(`ancestors:${roleId}`);
|
|
223
|
+
if (cached) {
|
|
224
|
+
return cached;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const ancestors = new Set<string>();
|
|
229
|
+
const visited = new Set<string>();
|
|
230
|
+
|
|
231
|
+
await this.collectAncestors(roleId, ancestors, visited);
|
|
232
|
+
|
|
233
|
+
if (this.config.cacheInheritance) {
|
|
234
|
+
this.hierarchyCache.set(`ancestors:${roleId}`, ancestors);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return ancestors;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get all descendant roles (roles that inherit from this role)
|
|
242
|
+
* @param roleId - Role identifier
|
|
243
|
+
* @returns Set of descendant role IDs
|
|
244
|
+
*/
|
|
245
|
+
async getDescendants(roleId: string): Promise<Set<string>> {
|
|
246
|
+
if (this.config.cacheInheritance) {
|
|
247
|
+
const cached = this.hierarchyCache.get(`descendants:${roleId}`);
|
|
248
|
+
if (cached) {
|
|
249
|
+
return cached;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const descendants = new Set<string>();
|
|
254
|
+
const visited = new Set<string>();
|
|
255
|
+
|
|
256
|
+
await this.collectDescendants(roleId, descendants, visited);
|
|
257
|
+
|
|
258
|
+
if (this.config.cacheInheritance) {
|
|
259
|
+
this.hierarchyCache.set(`descendants:${roleId}`, descendants);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return descendants;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get effective permissions for role (including inherited)
|
|
267
|
+
* @param roleId - Role identifier
|
|
268
|
+
* @returns Set of permission IDs
|
|
269
|
+
*/
|
|
270
|
+
async getEffectivePermissions(roleId: string): Promise<Set<string>> {
|
|
271
|
+
if (!this.config.enableInheritance) {
|
|
272
|
+
const roleNode = this.roles.get(roleId);
|
|
273
|
+
return roleNode ? roleNode.permissions : new Set();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (this.config.cacheInheritance) {
|
|
277
|
+
const cached = this.hierarchyCache.get(`permissions:${roleId}`);
|
|
278
|
+
if (cached) {
|
|
279
|
+
return cached;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const effectivePermissions = new Set<string>();
|
|
284
|
+
const roleNode = this.roles.get(roleId);
|
|
285
|
+
|
|
286
|
+
if (!roleNode) {
|
|
287
|
+
return effectivePermissions;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Add direct permissions
|
|
291
|
+
for (const permission of roleNode.permissions) {
|
|
292
|
+
effectivePermissions.add(permission);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Add inherited permissions
|
|
296
|
+
const ancestors = await this.getAncestors(roleId);
|
|
297
|
+
for (const ancestorId of ancestors) {
|
|
298
|
+
const ancestorNode = this.roles.get(ancestorId);
|
|
299
|
+
if (ancestorNode) {
|
|
300
|
+
for (const permission of ancestorNode.permissions) {
|
|
301
|
+
effectivePermissions.add(permission);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (this.config.cacheInheritance) {
|
|
307
|
+
this.hierarchyCache.set(`permissions:${roleId}`, effectivePermissions);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return effectivePermissions;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Validate hierarchy integrity
|
|
315
|
+
* @returns Validation result with any issues found
|
|
316
|
+
*/
|
|
317
|
+
async validateHierarchy(): Promise<{
|
|
318
|
+
valid: boolean;
|
|
319
|
+
issues: string[];
|
|
320
|
+
}> {
|
|
321
|
+
const issues: string[] = [];
|
|
322
|
+
|
|
323
|
+
// Check for circular dependencies
|
|
324
|
+
if (this.config.detectCircular) {
|
|
325
|
+
for (const roleId of this.roles.keys()) {
|
|
326
|
+
if (await this.hasCircularDependency(roleId)) {
|
|
327
|
+
issues.push(`Circular dependency detected involving role: ${roleId}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check hierarchy depth
|
|
333
|
+
for (const roleId of this.roles.keys()) {
|
|
334
|
+
const depth = await this.calculateDepth(roleId);
|
|
335
|
+
if (depth > this.config.maxDepth) {
|
|
336
|
+
issues.push(`Role ${roleId} exceeds maximum hierarchy depth: ${depth}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check for orphaned relationships
|
|
341
|
+
for (const [roleId, roleNode] of this.roles.entries()) {
|
|
342
|
+
for (const parentId of roleNode.parents) {
|
|
343
|
+
if (!this.roles.has(parentId)) {
|
|
344
|
+
issues.push(
|
|
345
|
+
`Role ${roleId} references non-existent parent: ${parentId}`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const childId of roleNode.children) {
|
|
351
|
+
if (!this.roles.has(childId)) {
|
|
352
|
+
issues.push(
|
|
353
|
+
`Role ${roleId} references non-existent child: ${childId}`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
valid: issues.length === 0,
|
|
361
|
+
issues,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Clear hierarchy cache
|
|
367
|
+
*/
|
|
368
|
+
clearCache(): void {
|
|
369
|
+
this.hierarchyCache.clear();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get hierarchy statistics
|
|
374
|
+
* @returns Hierarchy statistics
|
|
375
|
+
*/
|
|
376
|
+
getStats(): {
|
|
377
|
+
totalRoles: number;
|
|
378
|
+
maxDepth: number;
|
|
379
|
+
rootRoles: number;
|
|
380
|
+
leafRoles: number;
|
|
381
|
+
} {
|
|
382
|
+
let maxDepth = 0;
|
|
383
|
+
let rootRoles = 0;
|
|
384
|
+
let leafRoles = 0;
|
|
385
|
+
|
|
386
|
+
for (const roleNode of this.roles.values()) {
|
|
387
|
+
if (roleNode.parents.size === 0) {
|
|
388
|
+
rootRoles++;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (roleNode.children.size === 0) {
|
|
392
|
+
leafRoles++;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
totalRoles: this.roles.size,
|
|
398
|
+
maxDepth,
|
|
399
|
+
rootRoles,
|
|
400
|
+
leafRoles,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Collect ancestors recursively
|
|
406
|
+
* @param roleId - Role identifier
|
|
407
|
+
* @param ancestors - Set to collect ancestors
|
|
408
|
+
* @param visited - Set to track visited roles
|
|
409
|
+
* @private
|
|
410
|
+
*/
|
|
411
|
+
private async collectAncestors(
|
|
412
|
+
roleId: string,
|
|
413
|
+
ancestors: Set<string>,
|
|
414
|
+
visited: Set<string>
|
|
415
|
+
): Promise<void> {
|
|
416
|
+
if (visited.has(roleId)) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
visited.add(roleId);
|
|
421
|
+
const roleNode = this.roles.get(roleId);
|
|
422
|
+
|
|
423
|
+
if (!roleNode) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (const parentId of roleNode.parents) {
|
|
428
|
+
ancestors.add(parentId);
|
|
429
|
+
await this.collectAncestors(parentId, ancestors, visited);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Collect descendants recursively
|
|
435
|
+
* @param roleId - Role identifier
|
|
436
|
+
* @param descendants - Set to collect descendants
|
|
437
|
+
* @param visited - Set to track visited roles
|
|
438
|
+
* @private
|
|
439
|
+
*/
|
|
440
|
+
private async collectDescendants(
|
|
441
|
+
roleId: string,
|
|
442
|
+
descendants: Set<string>,
|
|
443
|
+
visited: Set<string>
|
|
444
|
+
): Promise<void> {
|
|
445
|
+
if (visited.has(roleId)) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
visited.add(roleId);
|
|
450
|
+
const roleNode = this.roles.get(roleId);
|
|
451
|
+
|
|
452
|
+
if (!roleNode) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
for (const childId of roleNode.children) {
|
|
457
|
+
descendants.add(childId);
|
|
458
|
+
await this.collectDescendants(childId, descendants, visited);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Check if adding relationship would create circular dependency
|
|
464
|
+
* @param childRoleId - Child role identifier
|
|
465
|
+
* @param parentRoleId - Parent role identifier
|
|
466
|
+
* @returns True if would create circular dependency
|
|
467
|
+
* @private
|
|
468
|
+
*/
|
|
469
|
+
private async wouldCreateCircular(
|
|
470
|
+
childRoleId: string,
|
|
471
|
+
parentRoleId: string
|
|
472
|
+
): Promise<boolean> {
|
|
473
|
+
// If parent inherits from child, adding this relationship would create a circle
|
|
474
|
+
return await this.inheritsFrom(parentRoleId, childRoleId);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Check for circular dependency starting from role
|
|
479
|
+
* @param roleId - Role identifier
|
|
480
|
+
* @returns True if circular dependency exists
|
|
481
|
+
* @private
|
|
482
|
+
*/
|
|
483
|
+
private async hasCircularDependency(roleId: string): Promise<boolean> {
|
|
484
|
+
const visited = new Set<string>();
|
|
485
|
+
const recursionStack = new Set<string>();
|
|
486
|
+
|
|
487
|
+
return await this.detectCircularRecursive(roleId, visited, recursionStack);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Detect circular dependency recursively
|
|
492
|
+
* @param roleId - Current role identifier
|
|
493
|
+
* @param visited - Set of visited roles
|
|
494
|
+
* @param recursionStack - Current recursion stack
|
|
495
|
+
* @returns True if circular dependency found
|
|
496
|
+
* @private
|
|
497
|
+
*/
|
|
498
|
+
private async detectCircularRecursive(
|
|
499
|
+
roleId: string,
|
|
500
|
+
visited: Set<string>,
|
|
501
|
+
recursionStack: Set<string>
|
|
502
|
+
): Promise<boolean> {
|
|
503
|
+
visited.add(roleId);
|
|
504
|
+
recursionStack.add(roleId);
|
|
505
|
+
|
|
506
|
+
const roleNode = this.roles.get(roleId);
|
|
507
|
+
if (!roleNode) {
|
|
508
|
+
recursionStack.delete(roleId);
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (const parentId of roleNode.parents) {
|
|
513
|
+
if (!visited.has(parentId)) {
|
|
514
|
+
if (
|
|
515
|
+
await this.detectCircularRecursive(parentId, visited, recursionStack)
|
|
516
|
+
) {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
} else if (recursionStack.has(parentId)) {
|
|
520
|
+
return true; // Circular dependency found
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
recursionStack.delete(roleId);
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Calculate hierarchy depth for role
|
|
530
|
+
* @param roleId - Role identifier
|
|
531
|
+
* @returns Hierarchy depth
|
|
532
|
+
* @private
|
|
533
|
+
*/
|
|
534
|
+
private async calculateDepth(roleId: string): Promise<number> {
|
|
535
|
+
const ancestors = await this.getAncestors(roleId);
|
|
536
|
+
return ancestors.size;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Build role tree recursively
|
|
541
|
+
* @param roleId - Role identifier
|
|
542
|
+
* @returns Role tree node
|
|
543
|
+
* @private
|
|
544
|
+
*/
|
|
545
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { RoleConfig, UserRepository } from '@plyaz/types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// export interface RoleConfig {
|
|
5
|
+
// hierarchyEnabled: boolean;
|
|
6
|
+
// cacheEnabled: boolean;
|
|
7
|
+
// cacheTTL: number;
|
|
8
|
+
// }
|
|
9
|
+
|
|
10
|
+
export class RoleManager {
|
|
11
|
+
private roleCache = new Map<string, { roles: string[]; expires: number }>();
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
private userRepo: UserRepository,
|
|
15
|
+
private config: RoleConfig
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
async assignRole(userId: string, role: string, assignedBy?: string): Promise<void> {
|
|
19
|
+
await this.userRepo.assignRole(userId, role, assignedBy);
|
|
20
|
+
this.invalidateCache(userId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async removeRole(userId: string, role: string): Promise<void> {
|
|
24
|
+
await this.userRepo.removeRole(userId, role);
|
|
25
|
+
this.invalidateCache(userId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async getUserRoles(userId: string): Promise<string[]> {
|
|
29
|
+
if (this.config.cacheEnabled) {
|
|
30
|
+
const cached = this.roleCache.get(userId);
|
|
31
|
+
if (cached && cached.expires > Date.now()) {
|
|
32
|
+
return cached.roles;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const roles = await this.userRepo.getUserRoles(userId);
|
|
37
|
+
|
|
38
|
+
if (this.config.cacheEnabled) {
|
|
39
|
+
this.roleCache.set(userId, {
|
|
40
|
+
roles,
|
|
41
|
+
expires: Date.now() + this.config.cacheTTL
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return roles;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async hasRole(userId: string, role: string): Promise<boolean> {
|
|
49
|
+
const roles = await this.getUserRoles(userId);
|
|
50
|
+
return roles.includes(role);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async hasAnyRole(userId: string, roles: string[]): Promise<boolean> {
|
|
54
|
+
const userRoles = await this.getUserRoles(userId);
|
|
55
|
+
return roles.some(role => userRoles.includes(role));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async hasPermission(userId: string, permission: string): Promise<boolean> {
|
|
59
|
+
const roles = await this.getUserRoles(userId);
|
|
60
|
+
return this.userRepo.checkPermission(roles, permission);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private invalidateCache(userId: string): void {
|
|
64
|
+
this.roleCache.delete(userId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
cleanup(): void {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
for (const [userId, data] of this.roleCache.entries()) {
|
|
70
|
+
if (data.expires < now) {
|
|
71
|
+
this.roleCache.delete(userId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NUMERIX } from '@plyaz/config';
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
3
|
+
|
|
4
|
+
export class CSRFProtection {
|
|
5
|
+
private tokens = new Map<string, { token: string; expires: number }>();
|
|
6
|
+
|
|
7
|
+
generateToken(sessionId: string): string {
|
|
8
|
+
const thirtyTwo = 32;
|
|
9
|
+
const token = randomBytes(thirtyTwo).toString('hex');
|
|
10
|
+
const expires = Date.now() + (NUMERIX.SIXTY * NUMERIX.SIXTY * NUMERIX.THOUSAND); // 1 hour
|
|
11
|
+
|
|
12
|
+
this.tokens.set(sessionId, { token, expires });
|
|
13
|
+
return token;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
validateToken(sessionId: string, token: string): boolean {
|
|
17
|
+
const stored = this.tokens.get(sessionId);
|
|
18
|
+
if (!stored || stored.expires < Date.now()) {
|
|
19
|
+
this.tokens.delete(sessionId);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return stored.token === token;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
removeToken(sessionId: string): void {
|
|
27
|
+
this.tokens.delete(sessionId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
cleanup(): void {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
for (const [sessionId, data] of this.tokens.entries()) {
|
|
33
|
+
if (data.expires < now) {
|
|
34
|
+
this.tokens.delete(sessionId);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
import { Controller, UseGuards } from "@nestjs/common";
|
|
3
|
+
import { RateLimiterGuard } from "../../../server/guards/custom-throttler.guard";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@Controller("auth")
|
|
7
|
+
export class AuthController {
|
|
8
|
+
@UseGuards(RateLimiterGuard)
|
|
9
|
+
testAuthEndPoint() : {message:string} {
|
|
10
|
+
return { message: 'Test endpoint works' };
|
|
11
|
+
}
|
|
12
|
+
}
|