@faryzal2020/v-perms 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.
@@ -0,0 +1,476 @@
1
+ import BaseAdapter from './BaseAdapter.js';
2
+ import {
3
+ RoleNotFoundError,
4
+ PermissionNotFoundError,
5
+ RoleAlreadyAssignedError,
6
+ PermissionAlreadyExistsError,
7
+ RoleAlreadyExistsError,
8
+ CircularInheritanceError,
9
+ } from '../core/errors.js';
10
+
11
+ /**
12
+ * Prisma database adapter implementation
13
+ */
14
+ class PrismaAdapter extends BaseAdapter {
15
+ constructor(prismaClient, logger) {
16
+ super();
17
+ this.prisma = prismaClient;
18
+ this.logger = logger;
19
+ }
20
+
21
+ // ==================== User Operations ====================
22
+
23
+ async getUserRoles(userId) {
24
+ this.logger.debug('getUserRoles:', userId);
25
+
26
+ const userRoles = await this.prisma.userRole.findMany({
27
+ where: { userId },
28
+ include: { role: true },
29
+ });
30
+
31
+ return userRoles.map(ur => ur.role);
32
+ }
33
+
34
+ async assignRoleToUser(userId, roleId) {
35
+ this.logger.debug('assignRoleToUser:', userId, roleId);
36
+
37
+ // Check if role exists
38
+ const role = await this.getRole(roleId);
39
+ if (!role) {
40
+ throw new RoleNotFoundError(roleId);
41
+ }
42
+
43
+ // Check if already assigned
44
+ const existing = await this.prisma.userRole.findUnique({
45
+ where: {
46
+ userId_roleId: { userId, roleId },
47
+ },
48
+ });
49
+
50
+ if (existing) {
51
+ throw new RoleAlreadyAssignedError(userId, roleId);
52
+ }
53
+
54
+ return await this.prisma.userRole.create({
55
+ data: { userId, roleId },
56
+ include: { role: true },
57
+ });
58
+ }
59
+
60
+ async removeRoleFromUser(userId, roleId) {
61
+ this.logger.debug('removeRoleFromUser:', userId, roleId);
62
+
63
+ try {
64
+ await this.prisma.userRole.delete({
65
+ where: {
66
+ userId_roleId: { userId, roleId },
67
+ },
68
+ });
69
+ return true;
70
+ } catch (error) {
71
+ if (error.code === 'P2025') {
72
+ // Record not found
73
+ return false;
74
+ }
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ async userHasRole(userId, roleId) {
80
+ this.logger.debug('userHasRole:', userId, roleId);
81
+
82
+ const userRole = await this.prisma.userRole.findUnique({
83
+ where: {
84
+ userId_roleId: { userId, roleId },
85
+ },
86
+ });
87
+
88
+ return !!userRole;
89
+ }
90
+
91
+ // ==================== Role Operations ====================
92
+
93
+ async createRole(data) {
94
+ this.logger.debug('createRole:', data);
95
+
96
+ // Check if role already exists
97
+ const existing = await this.getRoleByName(data.name);
98
+ if (existing) {
99
+ throw new RoleAlreadyExistsError(data.name);
100
+ }
101
+
102
+ return await this.prisma.role.create({
103
+ data: {
104
+ name: data.name,
105
+ description: data.description || null,
106
+ priority: data.priority || 0,
107
+ isDefault: data.isDefault || false,
108
+ },
109
+ });
110
+ }
111
+
112
+ async getRole(roleId) {
113
+ this.logger.debug('getRole:', roleId);
114
+
115
+ return await this.prisma.role.findUnique({
116
+ where: { id: roleId },
117
+ });
118
+ }
119
+
120
+ async getRoleByName(name) {
121
+ this.logger.debug('getRoleByName:', name);
122
+
123
+ return await this.prisma.role.findUnique({
124
+ where: { name },
125
+ });
126
+ }
127
+
128
+ async updateRole(roleId, data) {
129
+ this.logger.debug('updateRole:', roleId, data);
130
+
131
+ return await this.prisma.role.update({
132
+ where: { id: roleId },
133
+ data,
134
+ });
135
+ }
136
+
137
+ async deleteRole(roleId) {
138
+ this.logger.debug('deleteRole:', roleId);
139
+
140
+ try {
141
+ await this.prisma.role.delete({
142
+ where: { id: roleId },
143
+ });
144
+ return true;
145
+ } catch (error) {
146
+ if (error.code === 'P2025') {
147
+ return false;
148
+ }
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ async getRolePermissions(roleId) {
154
+ this.logger.debug('getRolePermissions:', roleId);
155
+
156
+ const rolePermissions = await this.prisma.rolePermission.findMany({
157
+ where: { roleId },
158
+ include: { permission: true },
159
+ });
160
+
161
+ return rolePermissions.map(rp => ({
162
+ ...rp.permission,
163
+ granted: rp.granted,
164
+ }));
165
+ }
166
+
167
+ async getRoleInheritance(roleId) {
168
+ this.logger.debug('getRoleInheritance:', roleId);
169
+
170
+ return await this.prisma.roleInheritance.findMany({
171
+ where: { roleId },
172
+ include: { inheritsFrom: true },
173
+ orderBy: { priority: 'desc' },
174
+ });
175
+ }
176
+
177
+ async setRoleInheritance(roleId, inheritsFromId, priority = 0) {
178
+ this.logger.debug('setRoleInheritance:', roleId, inheritsFromId, priority);
179
+
180
+ // Prevent self-inheritance
181
+ if (roleId === inheritsFromId) {
182
+ throw new CircularInheritanceError(roleId, inheritsFromId);
183
+ }
184
+
185
+ // Check for circular inheritance
186
+ const hasCircular = await this._checkCircularInheritance(roleId, inheritsFromId);
187
+ if (hasCircular) {
188
+ throw new CircularInheritanceError(roleId, inheritsFromId);
189
+ }
190
+
191
+ // Upsert the inheritance
192
+ return await this.prisma.roleInheritance.upsert({
193
+ where: {
194
+ roleId_inheritsFromId: { roleId, inheritsFromId },
195
+ },
196
+ create: { roleId, inheritsFromId, priority },
197
+ update: { priority },
198
+ });
199
+ }
200
+
201
+ async removeRoleInheritance(roleId, inheritsFromId) {
202
+ this.logger.debug('removeRoleInheritance:', roleId, inheritsFromId);
203
+
204
+ try {
205
+ await this.prisma.roleInheritance.delete({
206
+ where: {
207
+ roleId_inheritsFromId: { roleId, inheritsFromId },
208
+ },
209
+ });
210
+ return true;
211
+ } catch (error) {
212
+ if (error.code === 'P2025') {
213
+ return false;
214
+ }
215
+ throw error;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Check for circular inheritance
221
+ * @private
222
+ */
223
+ async _checkCircularInheritance(roleId, inheritsFromId, visited = new Set()) {
224
+ if (visited.has(inheritsFromId)) {
225
+ return false; // Already checked this path
226
+ }
227
+
228
+ visited.add(inheritsFromId);
229
+
230
+ // Get what the inheritsFromId role inherits
231
+ const inheritances = await this.prisma.roleInheritance.findMany({
232
+ where: { roleId: inheritsFromId },
233
+ });
234
+
235
+ for (const inheritance of inheritances) {
236
+ // If the role we're trying to add inherits back to the original role, circular!
237
+ if (inheritance.inheritsFromId === roleId) {
238
+ return true;
239
+ }
240
+
241
+ // Recursively check
242
+ const hasCircular = await this._checkCircularInheritance(roleId, inheritance.inheritsFromId, visited);
243
+ if (hasCircular) {
244
+ return true;
245
+ }
246
+ }
247
+
248
+ return false;
249
+ }
250
+
251
+ // ==================== Permission Operations ====================
252
+
253
+ async createPermission(data) {
254
+ this.logger.debug('createPermission:', data);
255
+
256
+ // Check if permission already exists
257
+ const existing = await this.getPermission(data.key);
258
+ if (existing) {
259
+ throw new PermissionAlreadyExistsError(data.key);
260
+ }
261
+
262
+ return await this.prisma.permission.create({
263
+ data: {
264
+ key: data.key,
265
+ description: data.description || null,
266
+ category: data.category || null,
267
+ },
268
+ });
269
+ }
270
+
271
+ async getPermission(permissionKey) {
272
+ this.logger.debug('getPermission:', permissionKey);
273
+
274
+ return await this.prisma.permission.findUnique({
275
+ where: { key: permissionKey },
276
+ });
277
+ }
278
+
279
+ async getPermissionById(permissionId) {
280
+ this.logger.debug('getPermissionById:', permissionId);
281
+
282
+ return await this.prisma.permission.findUnique({
283
+ where: { id: permissionId },
284
+ });
285
+ }
286
+
287
+ async deletePermission(permissionKey) {
288
+ this.logger.debug('deletePermission:', permissionKey);
289
+
290
+ const permission = await this.getPermission(permissionKey);
291
+ if (!permission) {
292
+ return false;
293
+ }
294
+
295
+ try {
296
+ await this.prisma.permission.delete({
297
+ where: { key: permissionKey },
298
+ });
299
+ return true;
300
+ } catch (error) {
301
+ if (error.code === 'P2025') {
302
+ return false;
303
+ }
304
+ throw error;
305
+ }
306
+ }
307
+
308
+ async assignPermissionToRole(permissionKey, roleId, granted = true) {
309
+ this.logger.debug('assignPermissionToRole:', permissionKey, roleId, granted);
310
+
311
+ // Get or create permission
312
+ let permission = await this.getPermission(permissionKey);
313
+ if (!permission) {
314
+ // Create wildcard or regular permission
315
+ permission = await this.createPermission({ key: permissionKey });
316
+ }
317
+
318
+ // Check if role exists
319
+ const role = await this.getRole(roleId);
320
+ if (!role) {
321
+ throw new RoleNotFoundError(roleId);
322
+ }
323
+
324
+ // Upsert the role permission
325
+ return await this.prisma.rolePermission.upsert({
326
+ where: {
327
+ roleId_permissionId: { roleId, permissionId: permission.id },
328
+ },
329
+ create: { roleId, permissionId: permission.id, granted },
330
+ update: { granted },
331
+ include: { permission: true },
332
+ });
333
+ }
334
+
335
+ async removePermissionFromRole(permissionKey, roleId) {
336
+ this.logger.debug('removePermissionFromRole:', permissionKey, roleId);
337
+
338
+ const permission = await this.getPermission(permissionKey);
339
+ if (!permission) {
340
+ return false;
341
+ }
342
+
343
+ try {
344
+ await this.prisma.rolePermission.delete({
345
+ where: {
346
+ roleId_permissionId: { roleId, permissionId: permission.id },
347
+ },
348
+ });
349
+ return true;
350
+ } catch (error) {
351
+ if (error.code === 'P2025') {
352
+ return false;
353
+ }
354
+ throw error;
355
+ }
356
+ }
357
+
358
+ async assignPermissionToUser(permissionKey, userId, granted = true) {
359
+ this.logger.debug('assignPermissionToUser:', permissionKey, userId, granted);
360
+
361
+ // Get or create permission
362
+ let permission = await this.getPermission(permissionKey);
363
+ if (!permission) {
364
+ permission = await this.createPermission({ key: permissionKey });
365
+ }
366
+
367
+ // Upsert the user permission
368
+ return await this.prisma.userPermission.upsert({
369
+ where: {
370
+ userId_permissionId: { userId, permissionId: permission.id },
371
+ },
372
+ create: { userId, permissionId: permission.id, granted },
373
+ update: { granted },
374
+ include: { permission: true },
375
+ });
376
+ }
377
+
378
+ async removePermissionFromUser(permissionKey, userId) {
379
+ this.logger.debug('removePermissionFromUser:', permissionKey, userId);
380
+
381
+ const permission = await this.getPermission(permissionKey);
382
+ if (!permission) {
383
+ return false;
384
+ }
385
+
386
+ try {
387
+ await this.prisma.userPermission.delete({
388
+ where: {
389
+ userId_permissionId: { userId, permissionId: permission.id },
390
+ },
391
+ });
392
+ return true;
393
+ } catch (error) {
394
+ if (error.code === 'P2025') {
395
+ return false;
396
+ }
397
+ throw error;
398
+ }
399
+ }
400
+
401
+ async getUserDirectPermissions(userId) {
402
+ this.logger.debug('getUserDirectPermissions:', userId);
403
+
404
+ const userPermissions = await this.prisma.userPermission.findMany({
405
+ where: { userId },
406
+ include: { permission: true },
407
+ });
408
+
409
+ return userPermissions.map(up => ({
410
+ ...up.permission,
411
+ granted: up.granted,
412
+ }));
413
+ }
414
+
415
+ async getRolePermission(roleId, permissionKey) {
416
+ this.logger.debug('getRolePermission:', roleId, permissionKey);
417
+
418
+ const permission = await this.getPermission(permissionKey);
419
+ if (!permission) {
420
+ return null;
421
+ }
422
+
423
+ const rolePermission = await this.prisma.rolePermission.findUnique({
424
+ where: {
425
+ roleId_permissionId: { roleId, permissionId: permission.id },
426
+ },
427
+ });
428
+
429
+ if (!rolePermission) {
430
+ return null;
431
+ }
432
+
433
+ return { granted: rolePermission.granted };
434
+ }
435
+
436
+ async getUserPermission(userId, permissionKey) {
437
+ this.logger.debug('getUserPermission:', userId, permissionKey);
438
+
439
+ const permission = await this.getPermission(permissionKey);
440
+ if (!permission) {
441
+ return null;
442
+ }
443
+
444
+ const userPermission = await this.prisma.userPermission.findUnique({
445
+ where: {
446
+ userId_permissionId: { userId, permissionId: permission.id },
447
+ },
448
+ });
449
+
450
+ if (!userPermission) {
451
+ return null;
452
+ }
453
+
454
+ return { granted: userPermission.granted };
455
+ }
456
+
457
+ // ==================== Listing Operations ====================
458
+
459
+ async listAllPermissions() {
460
+ this.logger.debug('listAllPermissions');
461
+
462
+ return await this.prisma.permission.findMany({
463
+ orderBy: { key: 'asc' },
464
+ });
465
+ }
466
+
467
+ async listAllRoles() {
468
+ this.logger.debug('listAllRoles');
469
+
470
+ return await this.prisma.role.findMany({
471
+ orderBy: { priority: 'desc' },
472
+ });
473
+ }
474
+ }
475
+
476
+ export default PrismaAdapter;
@@ -0,0 +1,5 @@
1
+ import BaseAdapter from './BaseAdapter.js';
2
+ import PrismaAdapter from './PrismaAdapter.js';
3
+
4
+ export { BaseAdapter, PrismaAdapter };
5
+ export default PrismaAdapter;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Cache manager for Redis-based caching of permission checks
3
+ */
4
+ class CacheManager {
5
+ constructor(redisClient, options = {}) {
6
+ this.redis = redisClient;
7
+ this.options = {
8
+ enabled: true,
9
+ ttl: 300, // 5 minutes in seconds
10
+ prefix: 'v-perms:',
11
+ ...options,
12
+ };
13
+ }
14
+
15
+ /**
16
+ * Build a cache key from type and parts
17
+ * @private
18
+ */
19
+ _buildKey(type, ...parts) {
20
+ return `${this.options.prefix}${type}:${parts.join(':')}`;
21
+ }
22
+
23
+ /**
24
+ * Get value from cache
25
+ * @param {string} type - Cache type (e.g., 'user', 'role')
26
+ * @param {...string} parts - Key parts
27
+ * @returns {Promise<any|null>}
28
+ */
29
+ async get(type, ...parts) {
30
+ if (!this.options.enabled || !this.redis) return null;
31
+
32
+ try {
33
+ const key = this._buildKey(type, ...parts);
34
+ const value = await this.redis.get(key);
35
+ return value ? JSON.parse(value) : null;
36
+ } catch (error) {
37
+ // Fail silently on cache errors
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Set value in cache
44
+ * @param {string} type - Cache type
45
+ * @param {any} value - Value to cache
46
+ * @param {...string} parts - Key parts
47
+ */
48
+ async set(type, value, ...parts) {
49
+ if (!this.options.enabled || !this.redis) return;
50
+
51
+ try {
52
+ const key = this._buildKey(type, ...parts);
53
+ await this.redis.setEx(key, this.options.ttl, JSON.stringify(value));
54
+ } catch (error) {
55
+ // Fail silently on cache errors
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Delete specific cache entry
61
+ * @param {string} type - Cache type
62
+ * @param {...string} parts - Key parts
63
+ */
64
+ async delete(type, ...parts) {
65
+ if (!this.redis) return;
66
+
67
+ try {
68
+ const key = this._buildKey(type, ...parts);
69
+ await this.redis.del(key);
70
+ } catch (error) {
71
+ // Fail silently
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Invalidate all cache entries for a user
77
+ * @param {string} userId
78
+ */
79
+ async invalidateUser(userId) {
80
+ if (!this.redis) return;
81
+
82
+ try {
83
+ const pattern = this._buildKey('user', userId, '*');
84
+ const keys = await this.redis.keys(pattern);
85
+ if (keys.length > 0) {
86
+ await this.redis.del(...keys);
87
+ }
88
+ } catch (error) {
89
+ // Fail silently
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Invalidate all cache entries for a role
95
+ * @param {string} roleId
96
+ */
97
+ async invalidateRole(roleId) {
98
+ if (!this.redis) return;
99
+
100
+ try {
101
+ const pattern = this._buildKey('role', roleId, '*');
102
+ const keys = await this.redis.keys(pattern);
103
+ if (keys.length > 0) {
104
+ await this.redis.del(...keys);
105
+ }
106
+ } catch (error) {
107
+ // Fail silently
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Clear entire permission cache
113
+ */
114
+ async clear() {
115
+ if (!this.redis) return;
116
+
117
+ try {
118
+ const pattern = `${this.options.prefix}*`;
119
+ const keys = await this.redis.keys(pattern);
120
+ if (keys.length > 0) {
121
+ await this.redis.del(...keys);
122
+ }
123
+ } catch (error) {
124
+ // Fail silently
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Check if caching is enabled and available
130
+ * @returns {boolean}
131
+ */
132
+ isEnabled() {
133
+ return this.options.enabled && this.redis !== null;
134
+ }
135
+
136
+ /**
137
+ * Enable caching
138
+ */
139
+ enable() {
140
+ this.options.enabled = true;
141
+ }
142
+
143
+ /**
144
+ * Disable caching
145
+ */
146
+ disable() {
147
+ this.options.enabled = false;
148
+ }
149
+ }
150
+
151
+ export default CacheManager;