role-permission-engine 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/README.md ADDED
@@ -0,0 +1,682 @@
1
+ # role-permission-engine
2
+
3
+ > A flexible **Role-Based Access Control (RBAC)** and **Permission-Based Access Control (PBAC)**
4
+ > engine for React applications with route protection, component guards, hooks,
5
+ > utilities, and TypeScript support.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/role-permission-engine.svg)](https://www.npmjs.com/package/role-permission-engine)
8
+ [![license](https://img.shields.io/npm/l/role-permission-engine.svg)](./LICENSE)
9
+ [![tests](https://img.shields.io/badge/tests-jest%20%2B%20RTL-green)](./src/__tests__)
10
+
11
+ 🌐 **[Interactive Documentation & Live Demo](https://role-permission-engine.pages.dev/)**
12
+
13
+ A lightweight, flexible **role and permission-based route guard and UI gate** for React applications.
14
+
15
+ - ✅ **React Router v5 & v6** dual support
16
+ - ✅ **Role-based** and **permission-based** access control
17
+ - ✅ **AND / OR logic** for multi-value checks
18
+ - ✅ **Wildcard permissions** (`"*"`, `"read:*"`)
19
+ - ✅ **TypeScript declarations** included
20
+ - ✅ **Zero runtime dependencies** (only peer deps: React, React Router)
21
+ - ✅ **Full JSDoc** on every exported API
22
+
23
+ ---
24
+
25
+ ## Table of Contents
26
+
27
+ - [Installation](#installation)
28
+ - [Quick Start](#quick-start)
29
+ - [API Reference](#api-reference)
30
+ - [PermissionProvider](#permissionprovider)
31
+ - [BlockRoute](#blockroute)
32
+ - [PermissionGate](#permissiongate)
33
+ - [withPermission (HOC)](#withpermission)
34
+ - [usePermission](#usepermission)
35
+ - [usePermissionContext](#usepermissioncontext)
36
+ - [hasRole](#hasrole)
37
+ - [hasPermission](#haspermission)
38
+ - [checkAccess](#checkaccess)
39
+ - [Logic Operators](#logic-operators)
40
+ - [Wildcard Permissions](#wildcard-permissions)
41
+ - [React Router v5 Usage](#react-router-v5-usage)
42
+ - [Backend Subpath Usage](#backend-subpath-usage)
43
+ - [TypeScript](#typescript)
44
+ - [Examples](#examples)
45
+ - [Contributing](#contributing)
46
+ - [License](#license)
47
+
48
+ ---
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ npm install role-permission-engine
54
+ # or
55
+ yarn add role-permission-engine
56
+ ```
57
+
58
+ **Peer dependencies** (install these separately if not already present):
59
+
60
+ ```bash
61
+ npm install react react-dom react-router-dom
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Quick Start
67
+
68
+ ### 1. Wrap your app with `PermissionProvider`
69
+
70
+ ```jsx
71
+ // src/main.jsx (or App.jsx)
72
+ import { PermissionProvider } from 'role-permission-engine';
73
+ import { BrowserRouter } from 'react-router-dom';
74
+
75
+ function App() {
76
+ // Fetch your user from your auth system
77
+ const user = { roles: ['editor'], permissions: ['write:posts', 'read:users'] };
78
+
79
+ return (
80
+ <PermissionProvider
81
+ roles={user.roles}
82
+ permissions={user.permissions}
83
+ user={user}
84
+ isAuthenticated={true}
85
+ >
86
+ <BrowserRouter>
87
+ <AppRoutes />
88
+ </BrowserRouter>
89
+ </PermissionProvider>
90
+ );
91
+ }
92
+ ```
93
+
94
+ ### 2. Protect routes with `BlockRoute`
95
+
96
+ ```jsx
97
+ // src/AppRoutes.jsx (React Router v6)
98
+ import { Routes, Route } from 'react-router-dom';
99
+ import { BlockRoute } from 'role-permission-engine';
100
+
101
+ function AppRoutes() {
102
+ return (
103
+ <Routes>
104
+ <Route path="/" element={<HomePage />} />
105
+
106
+ {/* Only 'admin' role can access /admin */}
107
+ <Route
108
+ path="/admin"
109
+ element={
110
+ <BlockRoute roles={['admin']} redirectTo="/unauthorized">
111
+ <AdminPage />
112
+ </BlockRoute>
113
+ }
114
+ />
115
+
116
+ {/* Any 'editor' or 'admin' with write:posts permission */}
117
+ <Route
118
+ path="/posts/edit/:id"
119
+ element={
120
+ <BlockRoute
121
+ roles={['editor', 'admin']}
122
+ permissions={['write:posts']}
123
+ redirectTo="/unauthorized"
124
+ >
125
+ <EditPostPage />
126
+ </BlockRoute>
127
+ }
128
+ />
129
+
130
+ <Route path="/unauthorized" element={<UnauthorizedPage />} />
131
+ </Routes>
132
+ );
133
+ }
134
+ ```
135
+
136
+ ### 3. Gate UI elements with `PermissionGate`
137
+
138
+ ```jsx
139
+ import { PermissionGate } from 'role-permission-engine';
140
+
141
+ function UserActions({ userId }) {
142
+ return (
143
+ <div>
144
+ {/* Always visible */}
145
+ <ViewProfileButton userId={userId} />
146
+
147
+ {/* Only visible to admins */}
148
+ <PermissionGate roles={['admin']}>
149
+ <DeleteUserButton userId={userId} />
150
+ </PermissionGate>
151
+
152
+ {/* Only visible to users with write:users permission */}
153
+ <PermissionGate
154
+ permissions={['write:users']}
155
+ fallback={<span className="badge">Read-only</span>}
156
+ >
157
+ <EditUserButton userId={userId} />
158
+ </PermissionGate>
159
+ </div>
160
+ );
161
+ }
162
+ ```
163
+
164
+ ---
165
+
166
+ ## API Reference
167
+
168
+ ---
169
+
170
+ ### `PermissionProvider`
171
+
172
+ Provides the current user's roles, permissions, and auth state to all child components.
173
+
174
+ **Must wrap any component that uses `BlockRoute`, `PermissionGate`, or `usePermission`.**
175
+
176
+ #### Props
177
+
178
+ | Prop | Type | Default | Description |
179
+ |-------------------|-------------------|---------|---------------------------------------------------------|
180
+ | `roles` | `string[]` | `[]` | The current user's roles. |
181
+ | `permissions` | `string[]` | `[]` | The current user's permissions. |
182
+ | `user` | `object \| null` | `null` | The raw user object (any shape, for your own use). |
183
+ | `isAuthenticated` | `boolean` | `false` | Whether the user is authenticated. |
184
+ | `isLoading` | `boolean` | `false` | Set to `true` while auth state is being resolved. |
185
+ | `children` | `ReactNode` | — | **Required.** The component tree to provide context to. |
186
+
187
+ #### Example
188
+
189
+ ```jsx
190
+ <PermissionProvider
191
+ roles={['admin', 'editor']}
192
+ permissions={['read:users', 'write:posts']}
193
+ user={{ id: 'u_123', name: 'Alice' }}
194
+ isAuthenticated={true}
195
+ isLoading={false}
196
+ >
197
+ {/* your app */}
198
+ </PermissionProvider>
199
+ ```
200
+
201
+ ---
202
+
203
+ ### `BlockRoute`
204
+
205
+ A route guard that **redirects** unauthorized users. Compatible with **React Router v5 and v6**.
206
+
207
+ #### Props
208
+
209
+ | Prop | Type | Default | Description |
210
+ |---------------------|---------------|--------------------|-----------------------------------------------------------------------------|
211
+ | `children` | `ReactNode` | — | **Required.** Content to render when access is granted. |
212
+ | `roles` | `string[]` | `[]` | Required roles. Evaluated with `roleLogic`. |
213
+ | `permissions` | `string[]` | `[]` | Required permissions. Evaluated with `permissionLogic`. |
214
+ | `roleLogic` | `"any"\|"all"`| `"any"` | `"any"` = OR, `"all"` = AND logic for roles. |
215
+ | `permissionLogic` | `"any"\|"all"`| `"any"` | `"any"` = OR, `"all"` = AND logic for permissions. |
216
+ | `redirectTo` | `string` | `"/unauthorized"` | Path to redirect to when access is denied. |
217
+ | `loadingComponent` | `ReactNode` | `null` | Renders while `isLoading` is `true`. Defaults to `null` (nothing). |
218
+ | `replace` | `boolean` | `true` | Whether to replace the history entry (prevents Back button to blocked page).|
219
+ | `state` | `any` | `undefined` | Location state passed to the redirect (e.g. `{ from: location }`). |
220
+
221
+ #### Example — React Router v6
222
+
223
+ ```jsx
224
+ <Route
225
+ path="/settings"
226
+ element={
227
+ <BlockRoute
228
+ roles={['admin']}
229
+ permissions={['manage:settings']}
230
+ permissionLogic="any"
231
+ redirectTo="/login"
232
+ loadingComponent={<Spinner />}
233
+ >
234
+ <SettingsPage />
235
+ </BlockRoute>
236
+ }
237
+ />
238
+ ```
239
+
240
+ #### Example — Preserve redirect "from" location
241
+
242
+ ```jsx
243
+ import { useLocation } from 'react-router-dom';
244
+
245
+ function AuthRequired({ children }) {
246
+ const location = useLocation();
247
+ return (
248
+ <BlockRoute
249
+ roles={['user']}
250
+ redirectTo="/login"
251
+ state={{ from: location }} // login page can read this to redirect back
252
+ >
253
+ {children}
254
+ </BlockRoute>
255
+ );
256
+ }
257
+ ```
258
+
259
+ ---
260
+
261
+ ### `PermissionGate`
262
+
263
+ Conditionally **renders** or **hides** content based on roles/permissions.
264
+ Unlike `BlockRoute`, this never redirects — it simply shows or hides its children.
265
+
266
+ #### Props
267
+
268
+ | Prop | Type | Default | Description |
269
+ |--------------------|---------------|---------|----------------------------------------------------------------------|
270
+ | `children` | `ReactNode` | — | **Required.** Content shown when access is granted. |
271
+ | `roles` | `string[]` | `[]` | Required roles. |
272
+ | `permissions` | `string[]` | `[]` | Required permissions. |
273
+ | `roleLogic` | `"any"\|"all"`| `"any"` | Logic for role evaluation. |
274
+ | `permissionLogic` | `"any"\|"all"`| `"any"` | Logic for permission evaluation. |
275
+ | `fallback` | `ReactNode` | `null` | Content shown when access is denied. Defaults to `null`. |
276
+ | `loadingComponent` | `ReactNode` | `null` | Content shown while `isLoading` is `true`. |
277
+ | `negate` | `boolean` | `false` | When `true`, inverts the logic (shows children when access is denied)|
278
+
279
+ #### Example — With fallback
280
+
281
+ ```jsx
282
+ <PermissionGate
283
+ roles={['admin']}
284
+ fallback={<p>You need admin access to see this.</p>}
285
+ >
286
+ <AdminControls />
287
+ </PermissionGate>
288
+ ```
289
+
290
+ #### Example — Negate (show only to guests)
291
+
292
+ ```jsx
293
+ // Show login button ONLY when user does NOT have 'user' role
294
+ <PermissionGate roles={['user']} negate>
295
+ <LoginButton />
296
+ </PermissionGate>
297
+ ```
298
+
299
+ ---
300
+
301
+ ### `withPermission`
302
+
303
+ A Higher Order Component (HOC) that wraps and protects a component.
304
+ Ideal for wrapping class-based components, or when you prefer HOC composition over JSX-based nesting.
305
+
306
+ #### Options
307
+
308
+ | Option | Type | Default | Description |
309
+ |--------------------|------------------|---------|-------------------------------------------------------------------------|
310
+ | `roles` | `string[]` | `[]` | Required roles. |
311
+ | `permissions` | `string[]` | `[]` | Required permissions. |
312
+ | `roleLogic` | `"any"\|"all"` | `"any"` | Logic for role evaluation. |
313
+ | `permissionLogic` | `"any"\|"all"` | `"any"` | Logic for permission evaluation. |
314
+ | `fallback` | `ReactNode` | `null` | Content shown when access is denied. Defaults to `null`. |
315
+ | `loadingComponent` | `ReactNode` | `null` | Content shown while `isLoading` is `true`. |
316
+ | `negate` | `boolean` | `false` | When `true`, inverts the logic (shows component when access is denied). |
317
+
318
+ #### Example
319
+
320
+ ```jsx
321
+ import { withPermission } from 'role-permission-engine';
322
+
323
+ function Dashboard(props) {
324
+ return <div>Welcome, {props.username}!</div>;
325
+ }
326
+
327
+ // Wrap and protect Dashboard
328
+ const ProtectedDashboard = withPermission(Dashboard, {
329
+ roles: ['admin', 'manager'],
330
+ roleLogic: 'any',
331
+ fallback: <p>Access Denied</p>,
332
+ loadingComponent: <p>Loading...</p>
333
+ });
334
+
335
+ // Render the wrapped component and pass props normally
336
+ <ProtectedDashboard username="Alice" />
337
+ ```
338
+
339
+ ---
340
+
341
+ ### `usePermission`
342
+
343
+ A React hook for **programmatic** permission checking.
344
+
345
+ #### Signature
346
+
347
+ ```ts
348
+ function usePermission(options?: {
349
+ roles?: string[];
350
+ permissions?: string[];
351
+ roleLogic?: 'any' | 'all';
352
+ permissionLogic?: 'any' | 'all';
353
+ }): {
354
+ allowed: boolean;
355
+ denied: boolean;
356
+ isLoading: boolean;
357
+ isAuthenticated: boolean;
358
+ reason: string;
359
+ }
360
+ ```
361
+
362
+ #### Example
363
+
364
+ ```jsx
365
+ import { usePermission } from 'role-permission-engine';
366
+
367
+ function PublishButton() {
368
+ const { allowed, isLoading } = usePermission({
369
+ roles: ['editor', 'admin'],
370
+ permissions: ['publish:posts'],
371
+ roleLogic: 'any',
372
+ permissionLogic: 'any',
373
+ });
374
+
375
+ if (isLoading) return null;
376
+ if (!allowed) return <span>Cannot publish</span>;
377
+ return <button>Publish Post</button>;
378
+ }
379
+ ```
380
+
381
+ ---
382
+
383
+ ### `usePermissionContext`
384
+
385
+ Returns the raw permission context value. Useful for accessing the user object or auth state directly.
386
+
387
+ ```jsx
388
+ import { usePermissionContext } from 'role-permission-engine';
389
+
390
+ function UserGreeting() {
391
+ const { user, roles, isAuthenticated } = usePermissionContext();
392
+
393
+ if (!isAuthenticated) return <p>Welcome, guest!</p>;
394
+ return <p>Hello, {user?.name}! Roles: {roles.join(', ')}</p>;
395
+ }
396
+ ```
397
+
398
+ ---
399
+
400
+ ### `hasRole`
401
+
402
+ Pure utility function (no React required). Checks if a set of user roles satisfies the required roles.
403
+
404
+ ```ts
405
+ function hasRole(
406
+ userRoles: string[],
407
+ requiredRoles: string[],
408
+ logic?: 'any' | 'all'
409
+ ): { allowed: boolean; reason: string }
410
+ ```
411
+
412
+ ```js
413
+ import { hasRole } from 'role-permission-engine';
414
+
415
+ hasRole(['editor'], ['admin', 'editor'], 'any');
416
+ // → { allowed: true, reason: 'User has at least one required role.' }
417
+
418
+ hasRole(['editor'], ['admin', 'editor'], 'all');
419
+ // → { allowed: false, reason: 'User is missing required roles: admin' }
420
+ ```
421
+
422
+ ---
423
+
424
+ ### `hasPermission`
425
+
426
+ Pure utility. Checks permissions with wildcard support.
427
+
428
+ ```ts
429
+ function hasPermission(
430
+ userPermissions: string[],
431
+ requiredPermissions: string[],
432
+ logic?: 'any' | 'all'
433
+ ): { allowed: boolean; reason: string }
434
+ ```
435
+
436
+ ```js
437
+ import { hasPermission } from 'role-permission-engine';
438
+
439
+ // Wildcard
440
+ hasPermission(['*'], ['read:users', 'delete:posts'], 'all');
441
+ // → { allowed: true, reason: 'User has wildcard permission (*) — full access granted.' }
442
+
443
+ // Namespace wildcard
444
+ hasPermission(['read:*'], ['read:users', 'read:posts'], 'all');
445
+ // → { allowed: true, ... }
446
+ ```
447
+
448
+ ---
449
+
450
+ ### `checkAccess`
451
+
452
+ Combined role + permission check in one call.
453
+
454
+ ```ts
455
+ function checkAccess(options: {
456
+ userRoles?: string[];
457
+ userPermissions?: string[];
458
+ requiredRoles?: string[];
459
+ requiredPermissions?: string[];
460
+ roleLogic?: 'any' | 'all';
461
+ permissionLogic?: 'any' | 'all';
462
+ }): { allowed: boolean; reason: string }
463
+ ```
464
+
465
+ ```js
466
+ import { checkAccess } from 'role-permission-engine';
467
+
468
+ checkAccess({
469
+ userRoles: ['editor'],
470
+ userPermissions: ['write:posts'],
471
+ requiredRoles: ['editor'],
472
+ requiredPermissions: ['write:posts'],
473
+ });
474
+ // → { allowed: true, reason: 'Access granted.' }
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Logic Operators
480
+
481
+ All multi-value checks support two operators:
482
+
483
+ | Operator | Meaning | Example |
484
+ |----------|-----------------------------------------|-------------------------------------------------------------------------|
485
+ | `"any"` | User needs **at least one** match (OR) | `roles={['admin', 'editor']}` + `roleLogic="any"` — admin **or** editor |
486
+ | `"all"` | User needs **every** match (AND) | `roles={['admin', 'editor']}` + `roleLogic="all"` — admin **and** editor|
487
+
488
+ ---
489
+
490
+ ## Wildcard Permissions
491
+
492
+ | Pattern | Matches |
493
+ |-----------------|-------------------------------------------------------------|
494
+ | `"*"` | Everything — full superuser access |
495
+ | `"read:*"` | All `read:` permissions (`read:users`, `read:posts`, etc.) |
496
+ | `"write:posts"` | Only `write:posts` exactly |
497
+
498
+ ```jsx
499
+ // Superadmin with wildcard
500
+ <PermissionProvider permissions={['*']}>
501
+ {/* User can access all routes / gates */}
502
+ </PermissionProvider>
503
+ ```
504
+
505
+ ---
506
+
507
+ ## React Router v5 Usage
508
+
509
+ The `BlockRoute` component auto-detects whether React Router v5 or v6 is installed.
510
+
511
+ **React Router v5:**
512
+
513
+ ```jsx
514
+ import { Switch, Route } from 'react-router-dom'; // v5
515
+
516
+ <Switch>
517
+ <Route
518
+ path="/admin"
519
+ render={() => (
520
+ <BlockRoute roles={['admin']} redirectTo="/unauthorized">
521
+ <AdminPage />
522
+ </BlockRoute>
523
+ )}
524
+ />
525
+ </Switch>
526
+ ```
527
+
528
+ ---
529
+
530
+ ## Backend Subpath Usage
531
+
532
+ If you are building a backend project (like a Node.js/Express service) and want to use the pure permission utilities, you can import them from `role-permission-engine/utils`.
533
+
534
+ This entrypoint **has zero dependency on React or React Router**, meaning you can import and run it in a pure Node.js environment without any runtime errors or peer-dependency warning prompts.
535
+
536
+ ### Example
537
+
538
+ ```javascript
539
+ import { checkAccess } from 'role-permission-engine/utils';
540
+
541
+ // Express middleware
542
+ export function requirePermissions(required = {}) {
543
+ return (req, res, next) => {
544
+ const userRoles = req.user?.roles || [];
545
+ const userPermissions = req.user?.permissions || [];
546
+
547
+ const { allowed, reason } = checkAccess({
548
+ userRoles,
549
+ userPermissions,
550
+ requiredRoles: required.roles,
551
+ requiredPermissions: required.permissions
552
+ });
553
+
554
+ if (!allowed) {
555
+ return res.status(403).json({ error: 'Forbidden', reason });
556
+ }
557
+
558
+ next();
559
+ };
560
+ }
561
+ ```
562
+
563
+ ---
564
+
565
+ ## TypeScript
566
+
567
+ Full TypeScript declarations are included. All types are automatically available when you import from `role-permission-engine`:
568
+
569
+ ```ts
570
+ import {
571
+ BlockRoute,
572
+ BlockRouteProps,
573
+ PermissionGate,
574
+ PermissionProvider,
575
+ usePermission,
576
+ UsePermissionOptions,
577
+ UsePermissionResult,
578
+ hasRole,
579
+ hasPermission,
580
+ checkAccess,
581
+ Role,
582
+ Permission,
583
+ LogicOperator,
584
+ PermissionResult,
585
+ } from 'role-permission-engine';
586
+ ```
587
+
588
+ ---
589
+
590
+ ## Examples
591
+
592
+ ### Full app setup with async auth
593
+
594
+ ```jsx
595
+ import { useState, useEffect } from 'react';
596
+ import { PermissionProvider, BlockRoute } from 'role-permission-engine';
597
+
598
+ function App() {
599
+ const [auth, setAuth] = useState({ roles: [], permissions: [], user: null });
600
+ const [isLoading, setIsLoading] = useState(true);
601
+
602
+ useEffect(() => {
603
+ fetchCurrentUser()
604
+ .then((user) => setAuth({ roles: user.roles, permissions: user.permissions, user }))
605
+ .finally(() => setIsLoading(false));
606
+ }, []);
607
+
608
+ return (
609
+ <PermissionProvider
610
+ roles={auth.roles}
611
+ permissions={auth.permissions}
612
+ user={auth.user}
613
+ isAuthenticated={!!auth.user}
614
+ isLoading={isLoading}
615
+ >
616
+ <BrowserRouter>
617
+ <Routes>
618
+ <Route path="/" element={<Home />} />
619
+ <Route
620
+ path="/dashboard"
621
+ element={
622
+ <BlockRoute
623
+ roles={['user', 'admin']}
624
+ redirectTo="/login"
625
+ loadingComponent={<FullPageSpinner />}
626
+ >
627
+ <Dashboard />
628
+ </BlockRoute>
629
+ }
630
+ />
631
+ </Routes>
632
+ </BrowserRouter>
633
+ </PermissionProvider>
634
+ );
635
+ }
636
+ ```
637
+
638
+ ### Granular UI control
639
+
640
+ ```jsx
641
+ import { PermissionGate, usePermission } from 'role-permission-engine';
642
+
643
+ function DocumentPage({ doc }) {
644
+ const { allowed: canExport } = usePermission({ permissions: ['export:documents'] });
645
+
646
+ return (
647
+ <article>
648
+ <h1>{doc.title}</h1>
649
+ <p>{doc.body}</p>
650
+
651
+ {/* Show edit toolbar only to editors and admins */}
652
+ <PermissionGate roles={['editor', 'admin']}>
653
+ <EditToolbar docId={doc.id} />
654
+ </PermissionGate>
655
+
656
+ {/* Programmatic check */}
657
+ {canExport && <ExportButton docId={doc.id} />}
658
+
659
+ {/* Show "Request Access" to users without write permission */}
660
+ <PermissionGate permissions={['write:documents']} negate>
661
+ <RequestAccessButton />
662
+ </PermissionGate>
663
+ </article>
664
+ );
665
+ }
666
+ ```
667
+
668
+ ---
669
+
670
+ ## Contributing
671
+
672
+ 1. Fork the repository
673
+ 2. Create your feature branch: `git checkout -b feat/your-feature`
674
+ 3. Run tests: `npm test`
675
+ 4. Submit a pull request
676
+
677
+ ---
678
+
679
+ ## License
680
+
681
+ This project is licensed under the MIT License.
682
+ See the [LICENSE](LICENSE) file for details.