secure-role-guard 1.0.0 → 1.0.2
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 +162 -647
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# secure-role-guard
|
|
2
2
|
|
|
3
|
-
> Zero-
|
|
3
|
+
> Zero-dependency RBAC authorization for React & Node.js. Define roles once, use everywhere.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/secure-role-guard)
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
6
|
[](https://www.typescriptlang.org/)
|
|
8
7
|
[](https://www.npmjs.com/package/secure-role-guard)
|
|
9
8
|
|
|
@@ -11,73 +10,18 @@
|
|
|
11
10
|
|
|
12
11
|
---
|
|
13
12
|
|
|
14
|
-
##
|
|
13
|
+
## 🚀 Quick Overview
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
- [Real-World Examples](#real-world-examples)
|
|
23
|
-
- [Framework Compatibility](#framework-compatibility)
|
|
24
|
-
- [Common Mistakes to Avoid](#common-mistakes-to-avoid)
|
|
25
|
-
- [License](#license)
|
|
15
|
+
| Custom Code (Without Package) | With secure-role-guard |
|
|
16
|
+
| ----------------------------------- | --------------------------- |
|
|
17
|
+
| Write permission logic everywhere | Define once, use everywhere |
|
|
18
|
+
| Handle null/undefined edge cases | Built-in, tested |
|
|
19
|
+
| Different code for frontend/backend | Same API everywhere |
|
|
20
|
+
| 2-4 hours setup | 10 minutes setup |
|
|
26
21
|
|
|
27
22
|
---
|
|
28
23
|
|
|
29
|
-
##
|
|
30
|
-
|
|
31
|
-
| Feature | Description |
|
|
32
|
-
| ----------------------------- | ------------------------------------------------------- |
|
|
33
|
-
| **Role-Based Access Control** | Define roles with granular permissions |
|
|
34
|
-
| **Pure Permission Checking** | Deterministic, side-effect-free authorization |
|
|
35
|
-
| **React Integration** | Provider, hooks, and components for any React framework |
|
|
36
|
-
| **Backend Adapters** | Optional Express and Next.js middleware |
|
|
37
|
-
| **Wildcard Support** | Grant access with `*` (all) or `namespace.*` patterns |
|
|
38
|
-
| **TypeScript First** | Full type safety with strict mode |
|
|
39
|
-
| **Zero Dependencies** | Core has zero runtime dependencies |
|
|
40
|
-
| **Framework Agnostic** | Works with Next.js, Remix, Gatsby, Astro, Vite, CRA |
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## What This Package Does NOT Do ❌
|
|
45
|
-
|
|
46
|
-
> **CRITICAL:** This package handles **AUTHORIZATION** only, **NOT AUTHENTICATION**.
|
|
47
|
-
|
|
48
|
-
| This Package Does NOT | You Must Handle This |
|
|
49
|
-
| --------------------- | -------------------------------------- |
|
|
50
|
-
| Parse JWT tokens | Use a JWT library (jsonwebtoken, jose) |
|
|
51
|
-
| Verify authentication | Use Auth.js, Clerk, NextAuth, Passport |
|
|
52
|
-
| Read cookies | Use your framework's cookie API |
|
|
53
|
-
| Manage sessions | Use express-session, iron-session |
|
|
54
|
-
| Make network requests | Fetch user data yourself |
|
|
55
|
-
| Access databases | Query your DB to get user roles |
|
|
56
|
-
| Store global state | Pass user context explicitly |
|
|
57
|
-
|
|
58
|
-
### Why?
|
|
59
|
-
|
|
60
|
-
Authorization and authentication are **separate concerns**. Mixing them creates security vulnerabilities. This package focuses on **one job** and does it correctly.
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## Security Guarantees
|
|
65
|
-
|
|
66
|
-
| Guarantee | Implementation |
|
|
67
|
-
| ------------------------ | -------------------------------------------------- |
|
|
68
|
-
| ✅ **Deny by default** | Undefined permissions return `false` |
|
|
69
|
-
| ✅ **Immutable configs** | Role definitions are frozen with `Object.freeze()` |
|
|
70
|
-
| ✅ **Pure functions** | No side effects, no state mutations |
|
|
71
|
-
| ✅ **No eval/regex** | Only strict string matching |
|
|
72
|
-
| ✅ **Zero dependencies** | Core has zero runtime dependencies |
|
|
73
|
-
| ✅ **TypeScript strict** | Full strict mode compilation |
|
|
74
|
-
| ✅ **No global state** | All state is passed explicitly |
|
|
75
|
-
| ✅ **No network calls** | Never makes HTTP requests |
|
|
76
|
-
| ✅ **No file system** | Never reads or writes files |
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## Installation
|
|
24
|
+
## 📦 Installation
|
|
81
25
|
|
|
82
26
|
```bash
|
|
83
27
|
npm install secure-role-guard
|
|
@@ -87,275 +31,122 @@ pnpm add secure-role-guard
|
|
|
87
31
|
yarn add secure-role-guard
|
|
88
32
|
```
|
|
89
33
|
|
|
90
|
-
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 📖 Usage Guide
|
|
37
|
+
|
|
38
|
+
Choose your setup:
|
|
91
39
|
|
|
92
|
-
|
|
40
|
+
| I want to use in... | Jump to |
|
|
41
|
+
| --------------------------- | ------------------------------- |
|
|
42
|
+
| **React/Next.js only** | [Frontend Only](#frontend-only) |
|
|
43
|
+
| **Express/Node.js only** | [Backend Only](#backend-only) |
|
|
44
|
+
| **Both Frontend + Backend** | [Full Stack](#full-stack) |
|
|
93
45
|
|
|
94
46
|
---
|
|
95
47
|
|
|
96
|
-
##
|
|
48
|
+
## Frontend Only
|
|
97
49
|
|
|
98
|
-
### 1
|
|
50
|
+
### Step 1: Define Roles
|
|
99
51
|
|
|
100
52
|
```typescript
|
|
101
|
-
// roles.ts
|
|
53
|
+
// lib/roles.ts
|
|
102
54
|
import { defineRoles } from "secure-role-guard";
|
|
103
55
|
|
|
104
56
|
export const roleRegistry = defineRoles({
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
manager: ["user.read", "report.*"], // Namespace wildcard
|
|
108
|
-
support: ["ticket.read", "ticket.reply"],
|
|
57
|
+
admin: ["user.create", "user.read", "user.update", "user.delete"],
|
|
58
|
+
manager: ["user.read", "user.update"],
|
|
109
59
|
viewer: ["user.read"],
|
|
110
60
|
});
|
|
111
61
|
```
|
|
112
62
|
|
|
113
|
-
### 2
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
import { canUser } from "secure-role-guard";
|
|
117
|
-
import { roleRegistry } from "./roles";
|
|
118
|
-
|
|
119
|
-
// Your user context (from your auth system)
|
|
120
|
-
const user = {
|
|
121
|
-
userId: "user-123",
|
|
122
|
-
roles: ["admin"],
|
|
123
|
-
permissions: ["custom.feature"], // Direct permissions
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// Simple checks
|
|
127
|
-
canUser(user, "user.update", roleRegistry); // true
|
|
128
|
-
canUser(user, "user.delete", roleRegistry); // true
|
|
129
|
-
canUser(user, "billing.access", roleRegistry); // false (deny by default)
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### 3. React Integration
|
|
63
|
+
### Step 2: Setup Provider
|
|
133
64
|
|
|
134
65
|
```tsx
|
|
135
|
-
|
|
136
|
-
|
|
66
|
+
// app/providers.tsx (Next.js App Router)
|
|
67
|
+
// or src/App.tsx (Vite/CRA)
|
|
68
|
+
"use client";
|
|
137
69
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const user = useAuth(); // YOUR auth hook (not from this package)
|
|
70
|
+
import { PermissionProvider } from "secure-role-guard/react";
|
|
71
|
+
import { roleRegistry } from "@/lib/roles";
|
|
141
72
|
|
|
73
|
+
export function Providers({ children, user }) {
|
|
74
|
+
// user = { roles: ['admin'], permissions: [] } from your auth
|
|
142
75
|
return (
|
|
143
76
|
<PermissionProvider user={user} registry={roleRegistry}>
|
|
144
|
-
|
|
77
|
+
{children}
|
|
145
78
|
</PermissionProvider>
|
|
146
79
|
);
|
|
147
80
|
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Step 3: Use in Components
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { Can, useCan } from "secure-role-guard/react";
|
|
148
87
|
|
|
149
|
-
// Use the Can component for declarative rendering
|
|
150
88
|
function Dashboard() {
|
|
89
|
+
const canDelete = useCan("user.delete");
|
|
90
|
+
|
|
151
91
|
return (
|
|
152
92
|
<div>
|
|
93
|
+
{/* Method 1: Component */}
|
|
94
|
+
<Can permission="user.create">
|
|
95
|
+
<button>Add User</button>
|
|
96
|
+
</Can>
|
|
97
|
+
|
|
153
98
|
<Can permission="user.update">
|
|
154
|
-
<
|
|
99
|
+
<button>Edit User</button>
|
|
155
100
|
</Can>
|
|
156
101
|
|
|
157
|
-
|
|
102
|
+
{/* Method 2: Hook */}
|
|
103
|
+
{canDelete && <button>Delete User</button>}
|
|
104
|
+
|
|
105
|
+
{/* With fallback */}
|
|
106
|
+
<Can permission="admin.access" fallback={<p>Access Denied</p>}>
|
|
158
107
|
<AdminPanel />
|
|
159
108
|
</Can>
|
|
160
109
|
|
|
161
|
-
|
|
162
|
-
|
|
110
|
+
{/* Multiple permissions (ANY) */}
|
|
111
|
+
<Can permissions={["user.update", "user.delete"]} anyOf>
|
|
112
|
+
<UserActions />
|
|
163
113
|
</Can>
|
|
164
114
|
</div>
|
|
165
115
|
);
|
|
166
116
|
}
|
|
167
|
-
|
|
168
|
-
// Or use hooks for programmatic checks
|
|
169
|
-
function UserActions() {
|
|
170
|
-
const canEdit = useCan("user.update");
|
|
171
|
-
const canDelete = useCan("user.delete");
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<div>
|
|
175
|
-
{canEdit && <button>Edit</button>}
|
|
176
|
-
{canDelete && <button>Delete</button>}
|
|
177
|
-
</div>
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
117
|
```
|
|
181
118
|
|
|
182
|
-
|
|
119
|
+
**That's it for frontend!** ✅
|
|
183
120
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
### Core Functions
|
|
121
|
+
---
|
|
187
122
|
|
|
188
|
-
|
|
123
|
+
## Backend Only
|
|
189
124
|
|
|
190
|
-
|
|
125
|
+
### Step 1: Define Roles
|
|
191
126
|
|
|
192
127
|
```typescript
|
|
193
|
-
|
|
194
|
-
|
|
128
|
+
// lib/roles.ts
|
|
129
|
+
import { defineRoles } from "secure-role-guard/core";
|
|
130
|
+
|
|
131
|
+
export const roleRegistry = defineRoles({
|
|
132
|
+
admin: ["user.create", "user.read", "user.update", "user.delete"],
|
|
133
|
+
manager: ["user.read", "user.update"],
|
|
195
134
|
viewer: ["user.read"],
|
|
196
135
|
});
|
|
197
136
|
```
|
|
198
137
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
Checks if a user has a specific permission. Returns `boolean`.
|
|
202
|
-
|
|
203
|
-
```typescript
|
|
204
|
-
const allowed = canUser(user, "user.update", registry);
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
#### `canUserAll(user, permissions, registry)`
|
|
208
|
-
|
|
209
|
-
Checks if a user has ALL specified permissions.
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
const allowed = canUserAll(user, ["user.read", "user.update"], registry);
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
#### `canUserAny(user, permissions, registry)`
|
|
216
|
-
|
|
217
|
-
Checks if a user has ANY of the specified permissions.
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
const allowed = canUserAny(
|
|
221
|
-
user,
|
|
222
|
-
["admin.access", "moderator.access"],
|
|
223
|
-
registry
|
|
224
|
-
);
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
### React Components
|
|
228
|
-
|
|
229
|
-
#### `<PermissionProvider>`
|
|
230
|
-
|
|
231
|
-
Provides permission context to child components.
|
|
232
|
-
|
|
233
|
-
```tsx
|
|
234
|
-
<PermissionProvider user={user} registry={registry}>
|
|
235
|
-
{children}
|
|
236
|
-
</PermissionProvider>
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
#### `<Can>`
|
|
240
|
-
|
|
241
|
-
Conditionally renders children based on permissions.
|
|
242
|
-
|
|
243
|
-
| Prop | Type | Description |
|
|
244
|
-
| ------------- | ----------- | ------------------------------------- |
|
|
245
|
-
| `permission` | `string` | Single permission to check |
|
|
246
|
-
| `permissions` | `string[]` | Multiple permissions to check |
|
|
247
|
-
| `anyOf` | `boolean` | If true, ANY permission grants access |
|
|
248
|
-
| `fallback` | `ReactNode` | Content to show if denied |
|
|
249
|
-
| `children` | `ReactNode` | Content to show if allowed |
|
|
250
|
-
|
|
251
|
-
#### `<Cannot>`
|
|
252
|
-
|
|
253
|
-
Inverse of `<Can>` - renders when permission is NOT granted.
|
|
254
|
-
|
|
255
|
-
### React Hooks
|
|
256
|
-
|
|
257
|
-
| Hook | Returns | Description |
|
|
258
|
-
| ------------------------ | ------------------------ | ----------------------- |
|
|
259
|
-
| `useCan(permission)` | `boolean` | Check single permission |
|
|
260
|
-
| `useCanAll(permissions)` | `boolean` | Check ALL permissions |
|
|
261
|
-
| `useCanAny(permissions)` | `boolean` | Check ANY permission |
|
|
262
|
-
| `usePermissions()` | `PermissionContextValue` | Full context access |
|
|
263
|
-
| `useUser()` | `UserContext \| null` | Current user |
|
|
264
|
-
|
|
265
|
-
### User Context Shape
|
|
266
|
-
|
|
267
|
-
```typescript
|
|
268
|
-
type UserContext = {
|
|
269
|
-
userId?: string; // Optional user identifier
|
|
270
|
-
roles?: string[]; // Array of role names
|
|
271
|
-
permissions?: string[]; // Direct permissions (bypass roles)
|
|
272
|
-
meta?: Record<string, unknown>; // Custom metadata (tenant, org, etc.)
|
|
273
|
-
};
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### Wildcard Permissions
|
|
277
|
-
|
|
278
|
-
| Pattern | Grants Access To |
|
|
279
|
-
| ---------------- | ------------------------------------------------ |
|
|
280
|
-
| `*` | Everything |
|
|
281
|
-
| `user.*` | `user.read`, `user.update`, `user.delete`, etc. |
|
|
282
|
-
| `report.admin.*` | `report.admin.view`, `report.admin.export`, etc. |
|
|
283
|
-
|
|
284
|
-
---
|
|
285
|
-
|
|
286
|
-
## Real-World Examples
|
|
287
|
-
|
|
288
|
-
### Example 1: Next.js App with Express Backend
|
|
289
|
-
|
|
290
|
-
**Frontend (Next.js App Router):**
|
|
291
|
-
|
|
292
|
-
```tsx
|
|
293
|
-
// app/providers.tsx
|
|
294
|
-
"use client";
|
|
295
|
-
|
|
296
|
-
import { PermissionProvider } from "secure-role-guard/react";
|
|
297
|
-
import { roleRegistry } from "@/lib/roles";
|
|
298
|
-
|
|
299
|
-
export function Providers({
|
|
300
|
-
children,
|
|
301
|
-
user,
|
|
302
|
-
}: {
|
|
303
|
-
children: React.ReactNode;
|
|
304
|
-
user: UserContext;
|
|
305
|
-
}) {
|
|
306
|
-
return (
|
|
307
|
-
<PermissionProvider user={user} registry={roleRegistry}>
|
|
308
|
-
{children}
|
|
309
|
-
</PermissionProvider>
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// app/layout.tsx
|
|
314
|
-
import { Providers } from "./providers";
|
|
315
|
-
import { getUser } from "@/lib/auth"; // YOUR auth function
|
|
316
|
-
|
|
317
|
-
export default async function RootLayout({ children }) {
|
|
318
|
-
const user = await getUser(); // Fetch from session/JWT
|
|
319
|
-
|
|
320
|
-
return (
|
|
321
|
-
<html>
|
|
322
|
-
<body>
|
|
323
|
-
<Providers user={user}>{children}</Providers>
|
|
324
|
-
</body>
|
|
325
|
-
</html>
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// app/admin/page.tsx
|
|
330
|
-
import { Can } from "secure-role-guard/react";
|
|
331
|
-
|
|
332
|
-
export default function AdminPage() {
|
|
333
|
-
return (
|
|
334
|
-
<Can permission="admin.access" fallback={<p>Access Denied</p>}>
|
|
335
|
-
<h1>Admin Dashboard</h1>
|
|
336
|
-
</Can>
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
**Backend (Express.js):**
|
|
138
|
+
### Step 2: Use in Express
|
|
342
139
|
|
|
343
140
|
```typescript
|
|
344
141
|
// server.ts
|
|
345
142
|
import express from "express";
|
|
346
|
-
import { defineRoles } from "secure-role-guard/core";
|
|
347
143
|
import { requirePermission } from "secure-role-guard/adapters/express";
|
|
144
|
+
import { roleRegistry } from "./lib/roles";
|
|
348
145
|
|
|
349
146
|
const app = express();
|
|
350
147
|
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
admin: ["user.read", "user.update", "user.delete"],
|
|
354
|
-
viewer: ["user.read"],
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// YOUR auth middleware (not from this package)
|
|
358
|
-
app.use(authMiddleware); // Sets req.user
|
|
148
|
+
// Your auth middleware (sets req.user)
|
|
149
|
+
app.use(yourAuthMiddleware);
|
|
359
150
|
|
|
360
151
|
// Protected routes
|
|
361
152
|
app.get(
|
|
@@ -366,11 +157,11 @@ app.get(
|
|
|
366
157
|
}
|
|
367
158
|
);
|
|
368
159
|
|
|
369
|
-
app.
|
|
370
|
-
"/api/users
|
|
371
|
-
requirePermission("user.
|
|
160
|
+
app.post(
|
|
161
|
+
"/api/users",
|
|
162
|
+
requirePermission("user.create", roleRegistry),
|
|
372
163
|
(req, res) => {
|
|
373
|
-
res.json({
|
|
164
|
+
res.json({ created: true });
|
|
374
165
|
}
|
|
375
166
|
);
|
|
376
167
|
|
|
@@ -381,433 +172,157 @@ app.delete(
|
|
|
381
172
|
res.json({ deleted: true });
|
|
382
173
|
}
|
|
383
174
|
);
|
|
384
|
-
|
|
385
|
-
app.listen(3000);
|
|
386
175
|
```
|
|
387
176
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
### Example 2: React-Only App (Vite/CRA)
|
|
391
|
-
|
|
392
|
-
```tsx
|
|
393
|
-
// src/roles.ts
|
|
394
|
-
import { defineRoles } from "secure-role-guard";
|
|
395
|
-
|
|
396
|
-
export const roleRegistry = defineRoles({
|
|
397
|
-
admin: ["*"],
|
|
398
|
-
editor: ["post.read", "post.create", "post.update"],
|
|
399
|
-
viewer: ["post.read"],
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
// src/App.tsx
|
|
403
|
-
import { PermissionProvider } from "secure-role-guard";
|
|
404
|
-
import { roleRegistry } from "./roles";
|
|
405
|
-
import { useAuth } from "./auth"; // YOUR auth hook
|
|
406
|
-
|
|
407
|
-
function App() {
|
|
408
|
-
const { user, isLoading } = useAuth();
|
|
409
|
-
|
|
410
|
-
if (isLoading) return <div>Loading...</div>;
|
|
411
|
-
|
|
412
|
-
return (
|
|
413
|
-
<PermissionProvider user={user} registry={roleRegistry}>
|
|
414
|
-
<Router>
|
|
415
|
-
<Routes>
|
|
416
|
-
<Route path="/" element={<Home />} />
|
|
417
|
-
<Route path="/posts" element={<PostList />} />
|
|
418
|
-
<Route path="/admin" element={<AdminRoute />} />
|
|
419
|
-
</Routes>
|
|
420
|
-
</Router>
|
|
421
|
-
</PermissionProvider>
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// src/components/AdminRoute.tsx
|
|
426
|
-
import { useCan } from "secure-role-guard";
|
|
427
|
-
import { Navigate } from "react-router-dom";
|
|
428
|
-
|
|
429
|
-
function AdminRoute() {
|
|
430
|
-
const canAccess = useCan("admin.access");
|
|
431
|
-
|
|
432
|
-
if (!canAccess) {
|
|
433
|
-
return <Navigate to="/" replace />;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return <AdminPanel />;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// src/components/PostActions.tsx
|
|
440
|
-
import { Can, Cannot } from "secure-role-guard";
|
|
441
|
-
|
|
442
|
-
function PostActions({ postId }: { postId: string }) {
|
|
443
|
-
return (
|
|
444
|
-
<div>
|
|
445
|
-
<Can permission="post.update">
|
|
446
|
-
<button onClick={() => editPost(postId)}>Edit</button>
|
|
447
|
-
</Can>
|
|
448
|
-
|
|
449
|
-
<Can permission="post.delete">
|
|
450
|
-
<button onClick={() => deletePost(postId)}>Delete</button>
|
|
451
|
-
</Can>
|
|
452
|
-
|
|
453
|
-
<Cannot permission="post.update">
|
|
454
|
-
<span>View Only</span>
|
|
455
|
-
</Cannot>
|
|
456
|
-
</div>
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
---
|
|
462
|
-
|
|
463
|
-
### Example 3: Astro with React
|
|
177
|
+
### Manual Check (Without Middleware)
|
|
464
178
|
|
|
465
179
|
```typescript
|
|
466
|
-
|
|
467
|
-
import { defineRoles } from 'secure-role-guard';
|
|
180
|
+
import { canUser } from "secure-role-guard/core";
|
|
468
181
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
182
|
+
app.put("/api/users/:id", (req, res) => {
|
|
183
|
+
if (!canUser(req.user, "user.update", roleRegistry)) {
|
|
184
|
+
return res.status(403).json({ error: "Forbidden" });
|
|
185
|
+
}
|
|
186
|
+
// ... your logic
|
|
473
187
|
});
|
|
188
|
+
```
|
|
474
189
|
|
|
475
|
-
|
|
476
|
-
import { PermissionProvider, Can, useCan } from 'secure-role-guard';
|
|
477
|
-
import { roleRegistry } from '../lib/roles';
|
|
478
|
-
|
|
479
|
-
interface Props {
|
|
480
|
-
user: { roles: string[] } | null;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export default function AdminPanel({ user }: Props) {
|
|
484
|
-
return (
|
|
485
|
-
<PermissionProvider user={user} registry={roleRegistry}>
|
|
486
|
-
<div className="admin-panel">
|
|
487
|
-
<Can permission="page.edit">
|
|
488
|
-
<PageEditor />
|
|
489
|
-
</Can>
|
|
490
|
-
|
|
491
|
-
<Can permission="settings.manage">
|
|
492
|
-
<SettingsPanel />
|
|
493
|
-
</Can>
|
|
494
|
-
|
|
495
|
-
<Can permission="page.publish" fallback={<p>Publishing not available</p>}>
|
|
496
|
-
<PublishButton />
|
|
497
|
-
</Can>
|
|
498
|
-
</div>
|
|
499
|
-
</PermissionProvider>
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// src/pages/admin.astro
|
|
504
|
-
---
|
|
505
|
-
import AdminPanel from '../components/AdminPanel';
|
|
506
|
-
import { getUser } from '../lib/auth';
|
|
190
|
+
**That's it for backend!** ✅
|
|
507
191
|
|
|
508
|
-
const user = await getUser(Astro.request);
|
|
509
192
|
---
|
|
510
193
|
|
|
511
|
-
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
---
|
|
194
|
+
## Full Stack
|
|
515
195
|
|
|
516
|
-
|
|
196
|
+
Use **same role definitions** for both:
|
|
517
197
|
|
|
518
198
|
```typescript
|
|
519
|
-
//
|
|
520
|
-
import {
|
|
521
|
-
import { defineRoles, canUser } from "secure-role-guard/core";
|
|
522
|
-
import { withPermission } from "secure-role-guard/adapters/nextjs";
|
|
523
|
-
import { getUser } from "@/lib/auth";
|
|
524
|
-
|
|
525
|
-
const roleRegistry = defineRoles({
|
|
526
|
-
admin: ["user.read", "user.update", "user.delete"],
|
|
527
|
-
viewer: ["user.read"],
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
// Option 1: Manual check
|
|
531
|
-
export async function GET(request: NextRequest) {
|
|
532
|
-
const user = await getUser(request);
|
|
533
|
-
|
|
534
|
-
if (!canUser(user, "user.read", roleRegistry)) {
|
|
535
|
-
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const users = await fetchUsers();
|
|
539
|
-
return NextResponse.json(users);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Option 2: Using wrapper
|
|
543
|
-
export const PUT = withPermission(
|
|
544
|
-
"user.update",
|
|
545
|
-
roleRegistry,
|
|
546
|
-
{ getUser: async (req) => getUser(req) },
|
|
547
|
-
async (request, user) => {
|
|
548
|
-
const body = await request.json();
|
|
549
|
-
const updated = await updateUser(body);
|
|
550
|
-
return NextResponse.json(updated);
|
|
551
|
-
}
|
|
552
|
-
);
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
---
|
|
556
|
-
|
|
557
|
-
### Example 5: Multi-Tenant SaaS
|
|
199
|
+
// shared/roles.ts (shared between frontend & backend)
|
|
200
|
+
import { defineRoles } from "secure-role-guard";
|
|
558
201
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
org_owner: ["*"],
|
|
565
|
-
org_admin: ["user.*", "billing.view", "settings.update"],
|
|
566
|
-
org_member: ["user.read", "project.*"],
|
|
567
|
-
org_viewer: ["user.read", "project.read"],
|
|
202
|
+
export const roleRegistry = defineRoles({
|
|
203
|
+
admin: ["*"], // Full access
|
|
204
|
+
manager: ["user.read", "user.update", "report.*"],
|
|
205
|
+
support: ["ticket.read", "ticket.reply"],
|
|
206
|
+
viewer: ["user.read"],
|
|
568
207
|
});
|
|
569
|
-
|
|
570
|
-
// User context with tenant metadata
|
|
571
|
-
const currentUser = {
|
|
572
|
-
userId: "usr_abc123",
|
|
573
|
-
roles: ["org_admin"],
|
|
574
|
-
permissions: ["beta.feature"], // Direct permission for beta access
|
|
575
|
-
meta: {
|
|
576
|
-
tenantId: "tenant_xyz",
|
|
577
|
-
orgId: "org_456",
|
|
578
|
-
plan: "enterprise",
|
|
579
|
-
},
|
|
580
|
-
};
|
|
581
|
-
|
|
582
|
-
// Authorization check
|
|
583
|
-
if (canUser(currentUser, "billing.view", roleRegistry)) {
|
|
584
|
-
// Show billing dashboard
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Access tenant metadata for additional business logic
|
|
588
|
-
const tenantId = currentUser.meta?.tenantId;
|
|
589
|
-
if (tenantId) {
|
|
590
|
-
// Filter data by tenant
|
|
591
|
-
}
|
|
592
208
|
```
|
|
593
209
|
|
|
594
|
-
|
|
210
|
+
**Frontend:** Follow [Frontend Only](#frontend-only) steps
|
|
211
|
+
**Backend:** Follow [Backend Only](#backend-only) steps
|
|
595
212
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
| Framework | Status | Import |
|
|
599
|
-
| ---------------------- | ---------------- | ------------------------------------ |
|
|
600
|
-
| Next.js (App Router) | ✅ Full support | `secure-role-guard` |
|
|
601
|
-
| Next.js (Pages Router) | ✅ Full support | `secure-role-guard` |
|
|
602
|
-
| Remix | ✅ Full support | `secure-role-guard` |
|
|
603
|
-
| Gatsby | ✅ Full support | `secure-role-guard` |
|
|
604
|
-
| Astro (React) | ✅ Full support | `secure-role-guard` |
|
|
605
|
-
| Vite + React | ✅ Full support | `secure-role-guard` |
|
|
606
|
-
| Create React App | ✅ Full support | `secure-role-guard` |
|
|
607
|
-
| Express.js | ✅ Full support | `secure-role-guard/adapters/express` |
|
|
608
|
-
| Fastify | 🔧 Adapter-ready | Use core directly |
|
|
609
|
-
| Node HTTP | ✅ Full support | `secure-role-guard/core` |
|
|
213
|
+
> 💡 **Pro tip:** Keep roles in a shared package or copy to both projects.
|
|
610
214
|
|
|
611
215
|
---
|
|
612
216
|
|
|
613
|
-
##
|
|
217
|
+
## 📚 API Reference
|
|
614
218
|
|
|
615
|
-
###
|
|
616
|
-
|
|
617
|
-
```typescript
|
|
618
|
-
// WRONG - This package doesn't handle authentication
|
|
619
|
-
import { canUser } from "secure-role-guard";
|
|
219
|
+
### Core Functions
|
|
620
220
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
221
|
+
| Function | Description |
|
|
222
|
+
| ----------------------------------------- | ----------------------- |
|
|
223
|
+
| `defineRoles(roles)` | Create role registry |
|
|
224
|
+
| `canUser(user, permission, registry)` | Check single permission |
|
|
225
|
+
| `canUserAll(user, permissions, registry)` | Check ALL permissions |
|
|
226
|
+
| `canUserAny(user, permissions, registry)` | Check ANY permission |
|
|
624
227
|
|
|
625
|
-
###
|
|
228
|
+
### React Components
|
|
626
229
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
230
|
+
| Component | Description |
|
|
231
|
+
| --------------------------- | ------------------- |
|
|
232
|
+
| `<PermissionProvider>` | Wrap your app |
|
|
233
|
+
| `<Can permission="...">` | Show if allowed |
|
|
234
|
+
| `<Cannot permission="...">` | Show if NOT allowed |
|
|
630
235
|
|
|
631
|
-
|
|
632
|
-
const user = req.user; // Set by YOUR auth middleware
|
|
633
|
-
const allowed = canUser(user, "admin.access", roleRegistry);
|
|
634
|
-
```
|
|
236
|
+
### React Hooks
|
|
635
237
|
|
|
636
|
-
|
|
238
|
+
| Hook | Returns |
|
|
239
|
+
| ------------------------ | --------- |
|
|
240
|
+
| `useCan(permission)` | `boolean` |
|
|
241
|
+
| `useCanAll(permissions)` | `boolean` |
|
|
242
|
+
| `useCanAny(permissions)` | `boolean` |
|
|
637
243
|
|
|
638
|
-
###
|
|
244
|
+
### User Context Shape
|
|
639
245
|
|
|
640
246
|
```typescript
|
|
641
|
-
|
|
642
|
-
|
|
247
|
+
const user = {
|
|
248
|
+
userId: "user-123", // Optional
|
|
249
|
+
roles: ["admin", "manager"], // Role names
|
|
250
|
+
permissions: ["custom.perm"], // Direct permissions (bypass roles)
|
|
251
|
+
meta: { tenantId: "..." }, // Optional metadata
|
|
252
|
+
};
|
|
643
253
|
```
|
|
644
254
|
|
|
645
|
-
###
|
|
255
|
+
### Wildcard Permissions
|
|
646
256
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
```
|
|
257
|
+
| Pattern | Grants |
|
|
258
|
+
| ---------------- | ------------------------------------------------ |
|
|
259
|
+
| `*` | Everything |
|
|
260
|
+
| `user.*` | `user.read`, `user.update`, etc. |
|
|
261
|
+
| `report.admin.*` | `report.admin.view`, `report.admin.export`, etc. |
|
|
653
262
|
|
|
654
263
|
---
|
|
655
264
|
|
|
656
|
-
|
|
265
|
+
## 🔄 Dynamic Roles (From Database)
|
|
657
266
|
|
|
658
|
-
|
|
659
|
-
// WRONG - This is authentication, not authorization
|
|
660
|
-
if (canUser(user, "logged-in", registry)) {
|
|
661
|
-
// ...
|
|
662
|
-
}
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
### ✅ DO: Check actual permissions
|
|
267
|
+
If admin creates roles at runtime:
|
|
666
268
|
|
|
667
269
|
```typescript
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
// ...
|
|
671
|
-
}
|
|
672
|
-
```
|
|
270
|
+
// Fetch roles from your database
|
|
271
|
+
const rolesFromDB = await fetchRolesFromDB();
|
|
673
272
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
273
|
+
// Transform to: { roleName: ['permission1', 'permission2'] }
|
|
274
|
+
const roleDefinition = {};
|
|
275
|
+
rolesFromDB.forEach((role) => {
|
|
276
|
+
roleDefinition[role.name] = role.permissions;
|
|
277
|
+
});
|
|
677
278
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if (user.permissions.includes("admin")) {
|
|
681
|
-
// ...
|
|
682
|
-
}
|
|
279
|
+
// Create registry
|
|
280
|
+
const registry = defineRoles(roleDefinition);
|
|
683
281
|
```
|
|
684
282
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
```typescript
|
|
688
|
-
// CORRECT - Handles null/undefined safely (deny by default)
|
|
689
|
-
if (canUser(user, "admin.access", registry)) {
|
|
690
|
-
// ...
|
|
691
|
-
}
|
|
692
|
-
```
|
|
283
|
+
Works with **any database**: MongoDB, PostgreSQL, MySQL, SQLite, etc.
|
|
693
284
|
|
|
694
285
|
---
|
|
695
286
|
|
|
696
|
-
##
|
|
697
|
-
|
|
698
|
-
### Express Middleware
|
|
699
|
-
|
|
700
|
-
```typescript
|
|
701
|
-
import {
|
|
702
|
-
requirePermission,
|
|
703
|
-
requireAllPermissions,
|
|
704
|
-
requireAnyPermission,
|
|
705
|
-
} from "secure-role-guard/adapters/express";
|
|
706
|
-
|
|
707
|
-
// Single permission
|
|
708
|
-
app.get("/api/users", requirePermission("user.read", registry), handler);
|
|
709
|
-
|
|
710
|
-
// All permissions required
|
|
711
|
-
app.delete(
|
|
712
|
-
"/api/admin",
|
|
713
|
-
requireAllPermissions(["admin.access", "data.delete"], registry),
|
|
714
|
-
handler
|
|
715
|
-
);
|
|
716
|
-
|
|
717
|
-
// Any permission
|
|
718
|
-
app.get(
|
|
719
|
-
"/api/reports",
|
|
720
|
-
requireAnyPermission(["report.view", "report.admin"], registry),
|
|
721
|
-
handler
|
|
722
|
-
);
|
|
723
|
-
|
|
724
|
-
// Custom options
|
|
725
|
-
app.put(
|
|
726
|
-
"/api/settings",
|
|
727
|
-
requirePermission("settings.update", registry, {
|
|
728
|
-
statusCode: 401,
|
|
729
|
-
message: "Unauthorized",
|
|
730
|
-
getUser: (req) => req.session?.user,
|
|
731
|
-
}),
|
|
732
|
-
handler
|
|
733
|
-
);
|
|
734
|
-
```
|
|
287
|
+
## ⚠️ Important Notes
|
|
735
288
|
|
|
736
|
-
###
|
|
289
|
+
### This Package Does NOT:
|
|
737
290
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
checkNextPermission,
|
|
742
|
-
} from "secure-role-guard/adapters/nextjs";
|
|
743
|
-
|
|
744
|
-
// Using wrapper
|
|
745
|
-
export const POST = withPermission(
|
|
746
|
-
"post.create",
|
|
747
|
-
registry,
|
|
748
|
-
{ getUser: async (req) => getUserFromSession(req) },
|
|
749
|
-
async (request, user) => {
|
|
750
|
-
return Response.json({ created: true });
|
|
751
|
-
}
|
|
752
|
-
);
|
|
291
|
+
- ❌ Handle authentication (JWT, sessions, cookies)
|
|
292
|
+
- ❌ Make API/database calls
|
|
293
|
+
- ❌ Store global state
|
|
753
294
|
|
|
754
|
-
|
|
755
|
-
export async function GET(request: NextRequest) {
|
|
756
|
-
const user = await getUser(request);
|
|
757
|
-
const result = checkNextPermission(user, "data.read", registry);
|
|
295
|
+
**You provide:** User with roles → **We check:** Permissions
|
|
758
296
|
|
|
759
|
-
|
|
760
|
-
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
761
|
-
}
|
|
297
|
+
### Security
|
|
762
298
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
299
|
+
- ✅ Deny by default (undefined = false)
|
|
300
|
+
- ✅ Zero dependencies in core
|
|
301
|
+
- ✅ Immutable configurations
|
|
302
|
+
- ✅ Pure functions (no side effects)
|
|
766
303
|
|
|
767
304
|
---
|
|
768
305
|
|
|
769
|
-
##
|
|
306
|
+
## 🔒 Backward Compatibility
|
|
770
307
|
|
|
771
|
-
|
|
308
|
+
| Version | Meaning |
|
|
309
|
+
| ------------- | ------------------------------------------ |
|
|
310
|
+
| 1.0.x → 1.0.y | Bug fixes, safe to update |
|
|
311
|
+
| 1.x.0 → 1.y.0 | New features, no breaking changes |
|
|
312
|
+
| 1.x.x → 2.0.0 | Breaking changes, migration guide provided |
|
|
772
313
|
|
|
773
|
-
|
|
774
|
-
// tsconfig.json (package configuration)
|
|
775
|
-
{
|
|
776
|
-
"compilerOptions": {
|
|
777
|
-
"strict": true,
|
|
778
|
-
"noImplicitAny": true,
|
|
779
|
-
"strictNullChecks": true,
|
|
780
|
-
"exactOptionalPropertyTypes": true
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
All types are exported:
|
|
786
|
-
|
|
787
|
-
```typescript
|
|
788
|
-
import type {
|
|
789
|
-
UserContext,
|
|
790
|
-
RoleDefinition,
|
|
791
|
-
RoleRegistry,
|
|
792
|
-
PermissionCheckResult,
|
|
793
|
-
} from "secure-role-guard";
|
|
794
|
-
```
|
|
314
|
+
**Promise:** v1.x APIs will never break. Update with confidence.
|
|
795
315
|
|
|
796
316
|
---
|
|
797
317
|
|
|
798
|
-
## License
|
|
318
|
+
## 📄 License
|
|
799
319
|
|
|
800
320
|
MIT © [Sohel Rahaman](https://github.com/sohelrahaman)
|
|
801
321
|
|
|
802
322
|
---
|
|
803
323
|
|
|
804
|
-
##
|
|
805
|
-
|
|
806
|
-
This package is designed to be **boring, predictable, and auditable**. It intentionally avoids:
|
|
807
|
-
|
|
808
|
-
- Magic behavior
|
|
809
|
-
- Clever hacks
|
|
810
|
-
- Hidden side effects
|
|
811
|
-
- Runtime code generation
|
|
324
|
+
## 🔗 Links
|
|
812
325
|
|
|
813
|
-
|
|
326
|
+
- [GitHub Repository](https://github.com/Sohel-Rahaman-Developer/secure-role-guard)
|
|
327
|
+
- [NPM Package](https://www.npmjs.com/package/secure-role-guard)
|
|
328
|
+
- [Report Issues](https://github.com/Sohel-Rahaman-Developer/secure-role-guard/issues)
|