@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 faryzal2020
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,527 @@
1
+ # @faryzal2020/v-perms
2
+
3
+ Minimal, flexible role-based permission system for JavaScript/Bun.js applications with Prisma.
4
+
5
+ ## Features
6
+
7
+ - **Role-based access control (RBAC)** - Assign roles to users, permissions to roles
8
+ - **Role inheritance** - Roles can inherit permissions from other roles
9
+ - **User-specific overrides** - Direct user permissions take priority over role permissions
10
+ - **Wildcard permissions** - Grant access to multiple permissions at once (`endpoint.*`, `*`)
11
+ - **Ban/deny permissions** - Explicitly deny access even when wildcards would allow
12
+ - **Redis caching** - Optional distributed caching for high performance
13
+ - **Database agnostic** - Compatible with PostgreSQL, MySQL, SQLite via Prisma
14
+ - **Runtime flexible** - Works with both Bun.js and Node.js
15
+ - **Zero dependencies** - Only requires Prisma (peer dependency)
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @faryzal2020/v-perms
21
+ # or
22
+ bun add @faryzal2020/v-perms
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ### 1. Add Schema to Your Prisma Schema
28
+
29
+ Copy the models from `src/prisma/schema.prisma` into your existing Prisma schema file. See [examples/schema-usage.md](examples/schema-usage.md) for detailed instructions.
30
+
31
+ ### 2. Run Migrations
32
+
33
+ ```bash
34
+ npx prisma migrate dev --name add-permissions
35
+ ```
36
+
37
+ ### 3. Initialize in Your Code
38
+
39
+ ```javascript
40
+ import { PrismaClient } from '@prisma/client';
41
+ import { createPermissionSystem } from '@faryzal2020/v-perms';
42
+
43
+ const prisma = new PrismaClient();
44
+ const perms = createPermissionSystem(prisma, {
45
+ debug: true, // Enable debug logging
46
+ enableCache: true,
47
+ cacheTTL: 300, // 5 minutes
48
+ redis: redisClient, // optional
49
+ });
50
+ ```
51
+
52
+ ### 4. Create Permissions and Roles
53
+
54
+ ```javascript
55
+ // Create permissions
56
+ await perms.createPermission('page.admin', 'Access admin pages', 'page');
57
+ await perms.createPermission('endpoint.users.list', 'List users', 'endpoint');
58
+ await perms.createPermission('endpoint.users.delete', 'Delete users', 'endpoint');
59
+
60
+ // Create roles
61
+ await perms.createRole('member', 'Basic user role', 1, true); // isDefault=true
62
+ await perms.createRole('admin', 'Administrator', 10);
63
+
64
+ // Assign permissions to roles
65
+ await perms.assignPermission('page.admin', 'admin', 'role');
66
+ await perms.assignPermission('endpoint.*', 'admin', 'role'); // Wildcard
67
+
68
+ // Set role inheritance (admin inherits member permissions)
69
+ const memberRole = await perms.manager.getRole('member');
70
+ const adminRole = await perms.manager.getRole('admin');
71
+ await perms.manager.setRoleInheritance(adminRole.id, memberRole.id);
72
+ ```
73
+
74
+ ### 5. Assign Roles to Users
75
+
76
+ ```javascript
77
+ const adminRole = await perms.manager.getRole('admin');
78
+ await perms.assignRole(adminRole.id, userId);
79
+ ```
80
+
81
+ ### 6. Check Permissions
82
+
83
+ ```javascript
84
+ // Check user permission
85
+ const canAccess = await perms.can(userId, 'page.admin');
86
+
87
+ // Check role permission
88
+ const roleCanAccess = await perms.canRole(roleId, 'endpoint.users.delete');
89
+ ```
90
+
91
+ ## API Reference
92
+
93
+ ### Factory Function
94
+
95
+ #### `createPermissionSystem(prismaClient, options)`
96
+
97
+ Creates a permission system instance.
98
+
99
+ **Parameters:**
100
+ - `prismaClient` (PrismaClient): Prisma client instance
101
+ - `options` (object):
102
+ - `redis` (RedisClient): Redis client for caching (optional)
103
+ - `enableCache` (boolean): Enable caching (default: `true`)
104
+ - `cacheTTL` (number): Cache TTL in seconds (default: `300`)
105
+ - `debug` (boolean): Enable debug logging (default: `false`)
106
+
107
+ **Returns:** Permission system instance with methods
108
+
109
+ ### Permission Operations
110
+
111
+ #### `createPermission(key, description?, category?)`
112
+
113
+ Create a new permission.
114
+
115
+ ```javascript
116
+ await perms.createPermission('endpoint.posts.create', 'Create posts', 'endpoint');
117
+ ```
118
+
119
+ #### `deletePermission(permissionKey)`
120
+
121
+ Delete a permission.
122
+
123
+ ```javascript
124
+ await perms.deletePermission('endpoint.posts.create');
125
+ ```
126
+
127
+ #### `manager.listPermissions()`
128
+
129
+ List all permissions.
130
+
131
+ ```javascript
132
+ const permissions = await perms.manager.listPermissions();
133
+ ```
134
+
135
+ ### Role Operations
136
+
137
+ #### `createRole(name, description?, priority?, isDefault?)`
138
+
139
+ Create a new role.
140
+
141
+ ```javascript
142
+ await perms.createRole('moderator', 'Moderator role', 5, false);
143
+ ```
144
+
145
+ **Parameters:**
146
+ - `name` (string): Unique role name
147
+ - `description` (string): Role description
148
+ - `priority` (number): Role priority (higher = more important, default: `0`)
149
+ - `isDefault` (boolean): Auto-assign to new users (default: `false`)
150
+
151
+ #### `deleteRole(roleIdOrName)`
152
+
153
+ Delete a role.
154
+
155
+ ```javascript
156
+ await perms.manager.deleteRole('moderator');
157
+ ```
158
+
159
+ #### `manager.getRole(roleIdOrName)`
160
+
161
+ Get role by ID or name.
162
+
163
+ ```javascript
164
+ const role = await perms.manager.getRole('admin');
165
+ ```
166
+
167
+ #### `manager.listRoles()`
168
+
169
+ List all roles.
170
+
171
+ ```javascript
172
+ const roles = await perms.manager.listRoles();
173
+ ```
174
+
175
+ ### Assignment Operations
176
+
177
+ #### `assignPermission(permissionKey, targetId, targetType)`
178
+
179
+ Assign permission to role or user.
180
+
181
+ ```javascript
182
+ // Assign to role
183
+ await perms.assignPermission('page.admin', roleId, 'role');
184
+
185
+ // Assign to user (user-specific override)
186
+ await perms.assignPermission('feature.export', userId, 'user');
187
+
188
+ // Wildcard assignment
189
+ await perms.assignPermission('endpoint.*', roleId, 'role');
190
+ ```
191
+
192
+ #### `banPermission(permissionKey, targetId, targetType)`
193
+
194
+ Explicitly deny a permission (sets `granted=false`).
195
+
196
+ ```javascript
197
+ // Ban admin page access for specific user
198
+ await perms.banPermission('page.admin', userId, 'user');
199
+
200
+ // Admin has endpoint.* but ban delete specifically
201
+ await perms.assignPermission('endpoint.*', adminRoleId, 'role');
202
+ await perms.banPermission('endpoint.users.delete', adminRoleId, 'role');
203
+ ```
204
+
205
+ #### `assignRole(roleIdOrName, userId)`
206
+
207
+ Assign role to user.
208
+
209
+ ```javascript
210
+ await perms.assignRole('admin', userId);
211
+ ```
212
+
213
+ #### `manager.removeRole(roleIdOrName, userId)`
214
+
215
+ Remove role from user.
216
+
217
+ ```javascript
218
+ await perms.manager.removeRole('admin', userId);
219
+ ```
220
+
221
+ #### `manager.setRoleInheritance(roleId, inheritFromRoleId, priority?)`
222
+
223
+ Set role inheritance.
224
+
225
+ ```javascript
226
+ // Admin inherits all member permissions
227
+ await perms.manager.setRoleInheritance(adminRole.id, memberRole.id, 1);
228
+ ```
229
+
230
+ ### Check Operations
231
+
232
+ #### `can(userId, permissionKey)`
233
+
234
+ Check if user has permission. This is the main method you'll use.
235
+
236
+ ```javascript
237
+ const hasAccess = await perms.can(userId, 'endpoint.users.delete');
238
+
239
+ if (hasAccess) {
240
+ // Allow action
241
+ } else {
242
+ // Deny action
243
+ }
244
+ ```
245
+
246
+ **Resolution Order:**
247
+ 1. User-specific permissions (highest priority)
248
+ 2. User-specific wildcard permissions
249
+ 3. Direct role permissions (by role priority)
250
+ 4. Role wildcard permissions
251
+ 5. Inherited role permissions
252
+ 6. Inherited role wildcard permissions
253
+ 7. Default deny
254
+
255
+ #### `canRole(roleId, permissionKey)`
256
+
257
+ Check if role has permission.
258
+
259
+ ```javascript
260
+ const roleHasAccess = await perms.canRole(roleId, 'page.admin');
261
+ ```
262
+
263
+ ### Cache Operations
264
+
265
+ #### `invalidateUserCache(userId)`
266
+
267
+ Clear cache for specific user.
268
+
269
+ ```javascript
270
+ await perms.invalidateUserCache(userId);
271
+ ```
272
+
273
+ #### `invalidateRoleCache(roleId)`
274
+
275
+ Clear cache for specific role.
276
+
277
+ ```javascript
278
+ await perms.invalidateRoleCache(roleId);
279
+ ```
280
+
281
+ #### `clearCache()`
282
+
283
+ Clear entire permission cache.
284
+
285
+ ```javascript
286
+ await perms.clearCache();
287
+ ```
288
+
289
+ ## Wildcard Permissions
290
+
291
+ Wildcard permissions allow you to grant access to multiple permissions at once.
292
+
293
+ ```javascript
294
+ // Grant access to all endpoint permissions
295
+ await perms.assignPermission('endpoint.*', roleId, 'role');
296
+
297
+ // Now user has access to:
298
+ // - endpoint.users.list
299
+ // - endpoint.users.create
300
+ // - endpoint.posts.delete
301
+ // etc.
302
+
303
+ // Grant all permissions (god mode)
304
+ await perms.assignPermission('*', superadminRoleId, 'role');
305
+ ```
306
+
307
+ ### Wildcard Matching Rules
308
+
309
+ - `*` matches everything
310
+ - `endpoint.*` matches `endpoint.users`, `endpoint.posts.create`, etc.
311
+ - `endpoint.users.*` matches `endpoint.users.list`, `endpoint.users.delete`, etc.
312
+ - Wildcards don't match partial segments: `endpoint.*` does NOT match `endpoint` itself
313
+
314
+ ## Ban/Deny Permissions
315
+
316
+ You can explicitly deny permissions, which is useful for exceptions.
317
+
318
+ ```javascript
319
+ // Admin has all endpoint access
320
+ await perms.assignPermission('endpoint.*', adminRoleId, 'role');
321
+
322
+ // But ban delete operations
323
+ await perms.banPermission('endpoint.users.delete', adminRoleId, 'role');
324
+ await perms.banPermission('endpoint.posts.delete', adminRoleId, 'role');
325
+
326
+ // User-specific ban (override role permissions)
327
+ await perms.banPermission('endpoint.sensitive', userId, 'user');
328
+ ```
329
+
330
+ **Priority:** Deny (`granted=false`) takes precedence at the point it's checked (user level, then role level).
331
+
332
+ ## Role Inheritance
333
+
334
+ Roles can inherit permissions from other roles.
335
+
336
+ ```javascript
337
+ // Create role hierarchy
338
+ await perms.createRole('member', 'Basic user', 1);
339
+ await perms.createRole('moderator', 'Moderator', 5);
340
+ await perms.createRole('admin', 'Administrator', 10);
341
+
342
+ // Assign permissions
343
+ await perms.assignPermission('page.home', 'member', 'role');
344
+ await perms.assignPermission('page.profile', 'member', 'role');
345
+
346
+ // Moderator inherits member permissions + has more
347
+ await perms.manager.setRoleInheritance('moderator', 'member');
348
+ await perms.assignPermission('page.moderation', 'moderator', 'role');
349
+
350
+ // Admin inherits moderator (which inherits member)
351
+ await perms.manager.setRoleInheritance('admin', 'moderator');
352
+ await perms.assignPermission('page.admin', 'admin', 'role');
353
+ ```
354
+
355
+ **Result:** Admin has permissions from member + moderator + admin
356
+
357
+ **Circular Inheritance:** The system prevents circular inheritance and will throw `CircularInheritanceError`.
358
+
359
+ ## Redis Caching
360
+
361
+ For production environments with multiple instances, use Redis for distributed caching.
362
+
363
+ ```javascript
364
+ import { createClient } from 'redis';
365
+
366
+ const redisClient = createClient({
367
+ url: 'redis://localhost:6379'
368
+ });
369
+ await redisClient.connect();
370
+
371
+ const perms = createPermissionSystem(prisma, {
372
+ redis: redisClient,
373
+ enableCache: true,
374
+ cacheTTL: 300, // 5 minutes
375
+ });
376
+ ```
377
+
378
+ **Cache Keys Format:**
379
+ - User permissions: `v-perms:user:{userId}:{permissionKey}`
380
+ - Role permissions: `v-perms:role:{roleId}:{permissionKey}`
381
+
382
+ **When to Invalidate Cache:**
383
+ - After assigning/removing permissions: `invalidateUserCache(userId)` or `invalidateRoleCache(roleId)`
384
+ - After role changes: `invalidateRoleCache(roleId)`
385
+ - After role inheritance changes: invalidate both roles
386
+
387
+ ## Error Handling
388
+
389
+ The package throws descriptive errors instead of returning `false`.
390
+
391
+ ```javascript
392
+ import { errors } from '@faryzal2020/v-perms';
393
+
394
+ try {
395
+ await perms.assignRole('nonexistent-role', userId);
396
+ } catch (error) {
397
+ if (error instanceof errors.RoleNotFoundError) {
398
+ console.error('Role not found:', error.details.roleId);
399
+ }
400
+ }
401
+ ```
402
+
403
+ **Error Types:**
404
+ - `UserNotFoundError`
405
+ - `RoleNotFoundError`
406
+ - `PermissionNotFoundError`
407
+ - `RoleAlreadyAssignedError`
408
+ - `PermissionAlreadyExistsError`
409
+ - `RoleAlreadyExistsError`
410
+ - `CircularInheritanceError`
411
+
412
+ ## Debug Logging
413
+
414
+ Enable debug logging to see what's happening under the hood.
415
+
416
+ ```javascript
417
+ const perms = createPermissionSystem(prisma, {
418
+ debug: true,
419
+ });
420
+
421
+ // Or enable later
422
+ perms.logger.enable();
423
+
424
+ // Disable
425
+ perms.logger.disable();
426
+ ```
427
+
428
+ **Log Output:**
429
+ ```
430
+ [v-perms:debug] checkPermission: user-123 endpoint.users.list
431
+ [v-perms:debug] Cache miss
432
+ [v-perms:debug] User roles (with inheritance): ['admin', 'member']
433
+ [v-perms:debug] Role wildcard match: admin endpoint.* true
434
+ ```
435
+
436
+ ## Common Patterns
437
+
438
+ ### Middleware (Express Example)
439
+
440
+ ```javascript
441
+ function requirePermission(permission) {
442
+ return async (req, res, next) => {
443
+ const userId = req.user?.id;
444
+
445
+ if (!userId) {
446
+ return res.status(401).json({ error: 'Unauthorized' });
447
+ }
448
+
449
+ try {
450
+ const hasPermission = await perms.can(userId, permission);
451
+
452
+ if (!hasPermission) {
453
+ return res.status(403).json({ error: 'Forbidden' });
454
+ }
455
+
456
+ next();
457
+ } catch (error) {
458
+ console.error('Permission check error:', error);
459
+ res.status(500).json({ error: 'Internal server error' });
460
+ }
461
+ };
462
+ }
463
+
464
+ // Usage
465
+ app.get('/api/users',
466
+ authenticate,
467
+ requirePermission('endpoint.users.list'),
468
+ async (req, res) => {
469
+ // Handler
470
+ }
471
+ );
472
+ ```
473
+
474
+ ### Assign Default Role on User Registration
475
+
476
+ ```javascript
477
+ async function registerUser(userData) {
478
+ const user = await prisma.user.create({
479
+ data: userData,
480
+ });
481
+
482
+ // Get default role
483
+ const defaultRole = await prisma.role.findFirst({
484
+ where: { isDefault: true },
485
+ });
486
+
487
+ if (defaultRole) {
488
+ await perms.assignRole(defaultRole.id, user.id);
489
+ }
490
+
491
+ return user;
492
+ }
493
+ ```
494
+
495
+ ### Seed Initial Permissions
496
+
497
+ ```javascript
498
+ async function seedPermissions() {
499
+ const permissions = [
500
+ { key: 'page.home', desc: 'Home page', cat: 'page' },
501
+ { key: 'page.admin', desc: 'Admin dashboard', cat: 'page' },
502
+ { key: 'endpoint.users.list', desc: 'List users', cat: 'endpoint' },
503
+ { key: 'endpoint.users.create', desc: 'Create user', cat: 'endpoint' },
504
+ { key: 'endpoint.users.delete', desc: 'Delete user', cat: 'endpoint' },
505
+ ];
506
+
507
+ for (const perm of permissions) {
508
+ try {
509
+ await perms.createPermission(perm.key, perm.desc, perm.cat);
510
+ } catch (error) {
511
+ // Permission already exists, skip
512
+ }
513
+ }
514
+ }
515
+ ```
516
+
517
+ ## License
518
+
519
+ MIT
520
+
521
+ ## Contributing
522
+
523
+ Contributions welcome! Please open an issue or PR.
524
+
525
+ ## Support
526
+
527
+ For issues and questions, please use GitHub Issues.
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@faryzal2020/v-perms",
3
+ "version": "1.0.0",
4
+ "description": "Minimal, flexible role-based permission system for JavaScript/Bun.js applications with Prisma",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "files": [
8
+ "src",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "scripts": {
13
+ "prepublishOnly": "echo 'Ready to publish'"
14
+ },
15
+ "keywords": [
16
+ "permissions",
17
+ "authorization",
18
+ "rbac",
19
+ "roles",
20
+ "access-control",
21
+ "prisma",
22
+ "bun",
23
+ "nodejs"
24
+ ],
25
+ "author": "faryzal2020",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/faryzal2020/v-perms"
30
+ },
31
+ "peerDependencies": {
32
+ "@prisma/client": "^5.0.0"
33
+ },
34
+ "optionalDependencies": {
35
+ "redis": "^4.0.0"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0",
39
+ "bun": ">=1.0.0"
40
+ }
41
+ }