secure-role-guard 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 +21 -0
- package/README.md +813 -0
- package/dist/adapters/express.d.mts +109 -0
- package/dist/adapters/express.d.ts +109 -0
- package/dist/adapters/express.js +122 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/express.mjs +118 -0
- package/dist/adapters/express.mjs.map +1 -0
- package/dist/adapters/index.d.mts +3 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +181 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/index.mjs +171 -0
- package/dist/adapters/index.mjs.map +1 -0
- package/dist/adapters/nextjs.d.mts +140 -0
- package/dist/adapters/nextjs.d.ts +140 -0
- package/dist/adapters/nextjs.js +138 -0
- package/dist/adapters/nextjs.js.map +1 -0
- package/dist/adapters/nextjs.mjs +131 -0
- package/dist/adapters/nextjs.mjs.map +1 -0
- package/dist/core/index.d.mts +100 -0
- package/dist/core/index.d.ts +100 -0
- package/dist/core/index.js +132 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/index.mjs +125 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +238 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +222 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react/index.d.mts +237 -0
- package/dist/react/index.d.ts +237 -0
- package/dist/react/index.js +177 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +167 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/types-CSUpaGsY.d.mts +76 -0
- package/dist/types-CSUpaGsY.d.ts +76 -0
- package/package.json +99 -0
package/README.md
ADDED
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
# secure-role-guard
|
|
2
|
+
|
|
3
|
+
> Zero-vulnerability, framework-agnostic RBAC authorization library for React and Node.js applications.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/secure-role-guard)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://www.npmjs.com/package/secure-role-guard)
|
|
9
|
+
|
|
10
|
+
**Author:** Sohel Rahaman
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [What This Package Does](#what-this-package-does)
|
|
17
|
+
- [What This Package Does NOT Do](#what-this-package-does-not-do)
|
|
18
|
+
- [Security Guarantees](#security-guarantees)
|
|
19
|
+
- [Installation](#installation)
|
|
20
|
+
- [Quick Start](#quick-start)
|
|
21
|
+
- [API Reference](#api-reference)
|
|
22
|
+
- [Real-World Examples](#real-world-examples)
|
|
23
|
+
- [Framework Compatibility](#framework-compatibility)
|
|
24
|
+
- [Common Mistakes to Avoid](#common-mistakes-to-avoid)
|
|
25
|
+
- [License](#license)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## What This Package Does ✅
|
|
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
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install secure-role-guard
|
|
84
|
+
# or
|
|
85
|
+
pnpm add secure-role-guard
|
|
86
|
+
# or
|
|
87
|
+
yarn add secure-role-guard
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Peer Dependencies:**
|
|
91
|
+
|
|
92
|
+
- React ≥16.8.0 (optional, only needed for React features)
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Quick Start
|
|
97
|
+
|
|
98
|
+
### 1. Define Your Roles
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// roles.ts
|
|
102
|
+
import { defineRoles } from "secure-role-guard";
|
|
103
|
+
|
|
104
|
+
export const roleRegistry = defineRoles({
|
|
105
|
+
superadmin: ["*"], // Full access
|
|
106
|
+
admin: ["user.read", "user.update", "user.delete", "report.view"],
|
|
107
|
+
manager: ["user.read", "report.*"], // Namespace wildcard
|
|
108
|
+
support: ["ticket.read", "ticket.reply"],
|
|
109
|
+
viewer: ["user.read"],
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 2. Check Permissions (Core - No React)
|
|
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
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { PermissionProvider, Can, useCan } from "secure-role-guard";
|
|
136
|
+
import { roleRegistry } from "./roles";
|
|
137
|
+
|
|
138
|
+
// Wrap your app with the provider
|
|
139
|
+
function App() {
|
|
140
|
+
const user = useAuth(); // YOUR auth hook (not from this package)
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<PermissionProvider user={user} registry={roleRegistry}>
|
|
144
|
+
<Dashboard />
|
|
145
|
+
</PermissionProvider>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Use the Can component for declarative rendering
|
|
150
|
+
function Dashboard() {
|
|
151
|
+
return (
|
|
152
|
+
<div>
|
|
153
|
+
<Can permission="user.update">
|
|
154
|
+
<EditUserButton />
|
|
155
|
+
</Can>
|
|
156
|
+
|
|
157
|
+
<Can permission="admin.access" fallback={<UpgradePrompt />}>
|
|
158
|
+
<AdminPanel />
|
|
159
|
+
</Can>
|
|
160
|
+
|
|
161
|
+
<Can permissions={["report.view", "report.export"]} anyOf>
|
|
162
|
+
<ReportSection />
|
|
163
|
+
</Can>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
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
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## API Reference
|
|
185
|
+
|
|
186
|
+
### Core Functions
|
|
187
|
+
|
|
188
|
+
#### `defineRoles(definitions)`
|
|
189
|
+
|
|
190
|
+
Creates an immutable role registry.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
const registry = defineRoles({
|
|
194
|
+
admin: ["user.read", "user.update"],
|
|
195
|
+
viewer: ["user.read"],
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### `canUser(user, permission, registry)`
|
|
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):**
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// server.ts
|
|
345
|
+
import express from "express";
|
|
346
|
+
import { defineRoles } from "secure-role-guard/core";
|
|
347
|
+
import { requirePermission } from "secure-role-guard/adapters/express";
|
|
348
|
+
|
|
349
|
+
const app = express();
|
|
350
|
+
|
|
351
|
+
// Define roles (same as frontend)
|
|
352
|
+
const roleRegistry = defineRoles({
|
|
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
|
|
359
|
+
|
|
360
|
+
// Protected routes
|
|
361
|
+
app.get(
|
|
362
|
+
"/api/users",
|
|
363
|
+
requirePermission("user.read", roleRegistry),
|
|
364
|
+
(req, res) => {
|
|
365
|
+
res.json({ users: [] });
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
app.put(
|
|
370
|
+
"/api/users/:id",
|
|
371
|
+
requirePermission("user.update", roleRegistry),
|
|
372
|
+
(req, res) => {
|
|
373
|
+
res.json({ success: true });
|
|
374
|
+
}
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
app.delete(
|
|
378
|
+
"/api/users/:id",
|
|
379
|
+
requirePermission("user.delete", roleRegistry),
|
|
380
|
+
(req, res) => {
|
|
381
|
+
res.json({ deleted: true });
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
app.listen(3000);
|
|
386
|
+
```
|
|
387
|
+
|
|
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
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
// src/lib/roles.ts
|
|
467
|
+
import { defineRoles } from 'secure-role-guard';
|
|
468
|
+
|
|
469
|
+
export const roleRegistry = defineRoles({
|
|
470
|
+
admin: ['page.edit', 'page.publish', 'settings.manage'],
|
|
471
|
+
editor: ['page.edit'],
|
|
472
|
+
viewer: [],
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// src/components/AdminPanel.tsx (React component)
|
|
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';
|
|
507
|
+
|
|
508
|
+
const user = await getUser(Astro.request);
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
<AdminPanel client:load user={user} />
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
### Example 4: Next.js API Routes (App Router)
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
// app/api/users/route.ts
|
|
520
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
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
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
import { defineRoles, canUser } from "secure-role-guard";
|
|
561
|
+
|
|
562
|
+
// Define roles for your multi-tenant application
|
|
563
|
+
const roleRegistry = defineRoles({
|
|
564
|
+
org_owner: ["*"],
|
|
565
|
+
org_admin: ["user.*", "billing.view", "settings.update"],
|
|
566
|
+
org_member: ["user.read", "project.*"],
|
|
567
|
+
org_viewer: ["user.read", "project.read"],
|
|
568
|
+
});
|
|
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
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## Framework Compatibility
|
|
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` |
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## Common Mistakes to Avoid
|
|
614
|
+
|
|
615
|
+
### ❌ DON'T: Parse JWT in this package
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
// WRONG - This package doesn't handle authentication
|
|
619
|
+
import { canUser } from "secure-role-guard";
|
|
620
|
+
|
|
621
|
+
const token = req.headers.authorization;
|
|
622
|
+
const decoded = jwt.verify(token, secret); // NOT our job
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### ✅ DO: Pass already-authenticated user context
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
// CORRECT - You handle auth, we handle authorization
|
|
629
|
+
import { canUser } from "secure-role-guard";
|
|
630
|
+
|
|
631
|
+
// Your auth middleware already verified and decoded the token
|
|
632
|
+
const user = req.user; // Set by YOUR auth middleware
|
|
633
|
+
const allowed = canUser(user, "admin.access", roleRegistry);
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
### ❌ DON'T: Store user in global state
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
// WRONG - Global state is a security smell
|
|
642
|
+
let currentUser = null; // Anti-pattern
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### ✅ DO: Pass user context explicitly
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
// CORRECT - Explicit is better than implicit
|
|
649
|
+
<PermissionProvider user={user} registry={roleRegistry}>
|
|
650
|
+
{children}
|
|
651
|
+
</PermissionProvider>
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
### ❌ DON'T: Use for authentication checks
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
// WRONG - This is authentication, not authorization
|
|
660
|
+
if (canUser(user, "logged-in", registry)) {
|
|
661
|
+
// ...
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### ✅ DO: Check actual permissions
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
// CORRECT - This is authorization
|
|
669
|
+
if (canUser(user, "user.update", registry)) {
|
|
670
|
+
// ...
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
### ❌ DON'T: Assume permissions exist
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
// WRONG - May throw or behave unexpectedly
|
|
680
|
+
if (user.permissions.includes("admin")) {
|
|
681
|
+
// ...
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### ✅ DO: Use the provided functions
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
// CORRECT - Handles null/undefined safely (deny by default)
|
|
689
|
+
if (canUser(user, "admin.access", registry)) {
|
|
690
|
+
// ...
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
## Backend Adapters
|
|
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
|
+
```
|
|
735
|
+
|
|
736
|
+
### Next.js Route Handlers
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
import {
|
|
740
|
+
withPermission,
|
|
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
|
+
);
|
|
753
|
+
|
|
754
|
+
// Manual check
|
|
755
|
+
export async function GET(request: NextRequest) {
|
|
756
|
+
const user = await getUser(request);
|
|
757
|
+
const result = checkNextPermission(user, "data.read", registry);
|
|
758
|
+
|
|
759
|
+
if (!result.allowed) {
|
|
760
|
+
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return Response.json({ data: [] });
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
769
|
+
## TypeScript Support
|
|
770
|
+
|
|
771
|
+
This package is written in TypeScript with strict mode enabled:
|
|
772
|
+
|
|
773
|
+
```typescript
|
|
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
|
+
```
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## License
|
|
799
|
+
|
|
800
|
+
MIT © [Sohel Rahaman](https://github.com/sohelrahaman)
|
|
801
|
+
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
## Security Note
|
|
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
|
|
812
|
+
|
|
813
|
+
If you find a security issue, please report it via [GitHub Issues](https://github.com/sohelrahaman/secure-role-guard/issues).
|