secure-role-guard 1.0.1 → 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 +157 -1258
- 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,74 +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
|
-
- [Fixed Roles vs Dynamic Roles](#fixed-roles-vs-dynamic-roles)
|
|
25
|
-
- [Common Mistakes to Avoid](#common-mistakes-to-avoid)
|
|
26
|
-
- [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 |
|
|
27
21
|
|
|
28
22
|
---
|
|
29
23
|
|
|
30
|
-
##
|
|
31
|
-
|
|
32
|
-
| Feature | Description |
|
|
33
|
-
| ----------------------------- | ------------------------------------------------------- |
|
|
34
|
-
| **Role-Based Access Control** | Define roles with granular permissions |
|
|
35
|
-
| **Pure Permission Checking** | Deterministic, side-effect-free authorization |
|
|
36
|
-
| **React Integration** | Provider, hooks, and components for any React framework |
|
|
37
|
-
| **Backend Adapters** | Optional Express and Next.js middleware |
|
|
38
|
-
| **Wildcard Support** | Grant access with `*` (all) or `namespace.*` patterns |
|
|
39
|
-
| **TypeScript First** | Full type safety with strict mode |
|
|
40
|
-
| **Zero Dependencies** | Core has zero runtime dependencies |
|
|
41
|
-
| **Framework Agnostic** | Works with Next.js, Remix, Gatsby, Astro, Vite, CRA |
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## What This Package Does NOT Do ❌
|
|
46
|
-
|
|
47
|
-
> **CRITICAL:** This package handles **AUTHORIZATION** only, **NOT AUTHENTICATION**.
|
|
48
|
-
|
|
49
|
-
| This Package Does NOT | You Must Handle This |
|
|
50
|
-
| --------------------- | -------------------------------------- |
|
|
51
|
-
| Parse JWT tokens | Use a JWT library (jsonwebtoken, jose) |
|
|
52
|
-
| Verify authentication | Use Auth.js, Clerk, NextAuth, Passport |
|
|
53
|
-
| Read cookies | Use your framework's cookie API |
|
|
54
|
-
| Manage sessions | Use express-session, iron-session |
|
|
55
|
-
| Make network requests | Fetch user data yourself |
|
|
56
|
-
| Access databases | Query your DB to get user roles |
|
|
57
|
-
| Store global state | Pass user context explicitly |
|
|
58
|
-
|
|
59
|
-
### Why?
|
|
60
|
-
|
|
61
|
-
Authorization and authentication are **separate concerns**. Mixing them creates security vulnerabilities. This package focuses on **one job** and does it correctly.
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## Security Guarantees
|
|
66
|
-
|
|
67
|
-
| Guarantee | Implementation |
|
|
68
|
-
| ------------------------ | -------------------------------------------------- |
|
|
69
|
-
| ✅ **Deny by default** | Undefined permissions return `false` |
|
|
70
|
-
| ✅ **Immutable configs** | Role definitions are frozen with `Object.freeze()` |
|
|
71
|
-
| ✅ **Pure functions** | No side effects, no state mutations |
|
|
72
|
-
| ✅ **No eval/regex** | Only strict string matching |
|
|
73
|
-
| ✅ **Zero dependencies** | Core has zero runtime dependencies |
|
|
74
|
-
| ✅ **TypeScript strict** | Full strict mode compilation |
|
|
75
|
-
| ✅ **No global state** | All state is passed explicitly |
|
|
76
|
-
| ✅ **No network calls** | Never makes HTTP requests |
|
|
77
|
-
| ✅ **No file system** | Never reads or writes files |
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## Installation
|
|
24
|
+
## 📦 Installation
|
|
82
25
|
|
|
83
26
|
```bash
|
|
84
27
|
npm install secure-role-guard
|
|
@@ -88,275 +31,122 @@ pnpm add secure-role-guard
|
|
|
88
31
|
yarn add secure-role-guard
|
|
89
32
|
```
|
|
90
33
|
|
|
91
|
-
|
|
34
|
+
---
|
|
92
35
|
|
|
93
|
-
|
|
36
|
+
## 📖 Usage Guide
|
|
37
|
+
|
|
38
|
+
Choose your setup:
|
|
39
|
+
|
|
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) |
|
|
94
45
|
|
|
95
46
|
---
|
|
96
47
|
|
|
97
|
-
##
|
|
48
|
+
## Frontend Only
|
|
98
49
|
|
|
99
|
-
### 1
|
|
50
|
+
### Step 1: Define Roles
|
|
100
51
|
|
|
101
52
|
```typescript
|
|
102
|
-
// roles.ts
|
|
53
|
+
// lib/roles.ts
|
|
103
54
|
import { defineRoles } from "secure-role-guard";
|
|
104
55
|
|
|
105
56
|
export const roleRegistry = defineRoles({
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
manager: ["user.read", "report.*"], // Namespace wildcard
|
|
109
|
-
support: ["ticket.read", "ticket.reply"],
|
|
57
|
+
admin: ["user.create", "user.read", "user.update", "user.delete"],
|
|
58
|
+
manager: ["user.read", "user.update"],
|
|
110
59
|
viewer: ["user.read"],
|
|
111
60
|
});
|
|
112
61
|
```
|
|
113
62
|
|
|
114
|
-
### 2
|
|
115
|
-
|
|
116
|
-
```typescript
|
|
117
|
-
import { canUser } from "secure-role-guard";
|
|
118
|
-
import { roleRegistry } from "./roles";
|
|
119
|
-
|
|
120
|
-
// Your user context (from your auth system)
|
|
121
|
-
const user = {
|
|
122
|
-
userId: "user-123",
|
|
123
|
-
roles: ["admin"],
|
|
124
|
-
permissions: ["custom.feature"], // Direct permissions
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// Simple checks
|
|
128
|
-
canUser(user, "user.update", roleRegistry); // true
|
|
129
|
-
canUser(user, "user.delete", roleRegistry); // true
|
|
130
|
-
canUser(user, "billing.access", roleRegistry); // false (deny by default)
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### 3. React Integration
|
|
63
|
+
### Step 2: Setup Provider
|
|
134
64
|
|
|
135
65
|
```tsx
|
|
136
|
-
|
|
137
|
-
|
|
66
|
+
// app/providers.tsx (Next.js App Router)
|
|
67
|
+
// or src/App.tsx (Vite/CRA)
|
|
68
|
+
"use client";
|
|
138
69
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const user = useAuth(); // YOUR auth hook (not from this package)
|
|
70
|
+
import { PermissionProvider } from "secure-role-guard/react";
|
|
71
|
+
import { roleRegistry } from "@/lib/roles";
|
|
142
72
|
|
|
73
|
+
export function Providers({ children, user }) {
|
|
74
|
+
// user = { roles: ['admin'], permissions: [] } from your auth
|
|
143
75
|
return (
|
|
144
76
|
<PermissionProvider user={user} registry={roleRegistry}>
|
|
145
|
-
|
|
77
|
+
{children}
|
|
146
78
|
</PermissionProvider>
|
|
147
79
|
);
|
|
148
80
|
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Step 3: Use in Components
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { Can, useCan } from "secure-role-guard/react";
|
|
149
87
|
|
|
150
|
-
// Use the Can component for declarative rendering
|
|
151
88
|
function Dashboard() {
|
|
89
|
+
const canDelete = useCan("user.delete");
|
|
90
|
+
|
|
152
91
|
return (
|
|
153
92
|
<div>
|
|
93
|
+
{/* Method 1: Component */}
|
|
94
|
+
<Can permission="user.create">
|
|
95
|
+
<button>Add User</button>
|
|
96
|
+
</Can>
|
|
97
|
+
|
|
154
98
|
<Can permission="user.update">
|
|
155
|
-
<
|
|
99
|
+
<button>Edit User</button>
|
|
156
100
|
</Can>
|
|
157
101
|
|
|
158
|
-
|
|
102
|
+
{/* Method 2: Hook */}
|
|
103
|
+
{canDelete && <button>Delete User</button>}
|
|
104
|
+
|
|
105
|
+
{/* With fallback */}
|
|
106
|
+
<Can permission="admin.access" fallback={<p>Access Denied</p>}>
|
|
159
107
|
<AdminPanel />
|
|
160
108
|
</Can>
|
|
161
109
|
|
|
162
|
-
|
|
163
|
-
|
|
110
|
+
{/* Multiple permissions (ANY) */}
|
|
111
|
+
<Can permissions={["user.update", "user.delete"]} anyOf>
|
|
112
|
+
<UserActions />
|
|
164
113
|
</Can>
|
|
165
114
|
</div>
|
|
166
115
|
);
|
|
167
116
|
}
|
|
168
|
-
|
|
169
|
-
// Or use hooks for programmatic checks
|
|
170
|
-
function UserActions() {
|
|
171
|
-
const canEdit = useCan("user.update");
|
|
172
|
-
const canDelete = useCan("user.delete");
|
|
173
|
-
|
|
174
|
-
return (
|
|
175
|
-
<div>
|
|
176
|
-
{canEdit && <button>Edit</button>}
|
|
177
|
-
{canDelete && <button>Delete</button>}
|
|
178
|
-
</div>
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
117
|
```
|
|
182
118
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
## API Reference
|
|
119
|
+
**That's it for frontend!** ✅
|
|
186
120
|
|
|
187
|
-
|
|
121
|
+
---
|
|
188
122
|
|
|
189
|
-
|
|
123
|
+
## Backend Only
|
|
190
124
|
|
|
191
|
-
|
|
125
|
+
### Step 1: Define Roles
|
|
192
126
|
|
|
193
127
|
```typescript
|
|
194
|
-
|
|
195
|
-
|
|
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"],
|
|
196
134
|
viewer: ["user.read"],
|
|
197
135
|
});
|
|
198
136
|
```
|
|
199
137
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
Checks if a user has a specific permission. Returns `boolean`.
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
const allowed = canUser(user, "user.update", registry);
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
#### `canUserAll(user, permissions, registry)`
|
|
209
|
-
|
|
210
|
-
Checks if a user has ALL specified permissions.
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
const allowed = canUserAll(user, ["user.read", "user.update"], registry);
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
#### `canUserAny(user, permissions, registry)`
|
|
217
|
-
|
|
218
|
-
Checks if a user has ANY of the specified permissions.
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
const allowed = canUserAny(
|
|
222
|
-
user,
|
|
223
|
-
["admin.access", "moderator.access"],
|
|
224
|
-
registry
|
|
225
|
-
);
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### React Components
|
|
229
|
-
|
|
230
|
-
#### `<PermissionProvider>`
|
|
231
|
-
|
|
232
|
-
Provides permission context to child components.
|
|
233
|
-
|
|
234
|
-
```tsx
|
|
235
|
-
<PermissionProvider user={user} registry={registry}>
|
|
236
|
-
{children}
|
|
237
|
-
</PermissionProvider>
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
#### `<Can>`
|
|
241
|
-
|
|
242
|
-
Conditionally renders children based on permissions.
|
|
243
|
-
|
|
244
|
-
| Prop | Type | Description |
|
|
245
|
-
| ------------- | ----------- | ------------------------------------- |
|
|
246
|
-
| `permission` | `string` | Single permission to check |
|
|
247
|
-
| `permissions` | `string[]` | Multiple permissions to check |
|
|
248
|
-
| `anyOf` | `boolean` | If true, ANY permission grants access |
|
|
249
|
-
| `fallback` | `ReactNode` | Content to show if denied |
|
|
250
|
-
| `children` | `ReactNode` | Content to show if allowed |
|
|
251
|
-
|
|
252
|
-
#### `<Cannot>`
|
|
253
|
-
|
|
254
|
-
Inverse of `<Can>` - renders when permission is NOT granted.
|
|
255
|
-
|
|
256
|
-
### React Hooks
|
|
257
|
-
|
|
258
|
-
| Hook | Returns | Description |
|
|
259
|
-
| ------------------------ | ------------------------ | ----------------------- |
|
|
260
|
-
| `useCan(permission)` | `boolean` | Check single permission |
|
|
261
|
-
| `useCanAll(permissions)` | `boolean` | Check ALL permissions |
|
|
262
|
-
| `useCanAny(permissions)` | `boolean` | Check ANY permission |
|
|
263
|
-
| `usePermissions()` | `PermissionContextValue` | Full context access |
|
|
264
|
-
| `useUser()` | `UserContext \| null` | Current user |
|
|
265
|
-
|
|
266
|
-
### User Context Shape
|
|
267
|
-
|
|
268
|
-
```typescript
|
|
269
|
-
type UserContext = {
|
|
270
|
-
userId?: string; // Optional user identifier
|
|
271
|
-
roles?: string[]; // Array of role names
|
|
272
|
-
permissions?: string[]; // Direct permissions (bypass roles)
|
|
273
|
-
meta?: Record<string, unknown>; // Custom metadata (tenant, org, etc.)
|
|
274
|
-
};
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
### Wildcard Permissions
|
|
278
|
-
|
|
279
|
-
| Pattern | Grants Access To |
|
|
280
|
-
| ---------------- | ------------------------------------------------ |
|
|
281
|
-
| `*` | Everything |
|
|
282
|
-
| `user.*` | `user.read`, `user.update`, `user.delete`, etc. |
|
|
283
|
-
| `report.admin.*` | `report.admin.view`, `report.admin.export`, etc. |
|
|
284
|
-
|
|
285
|
-
---
|
|
286
|
-
|
|
287
|
-
## Real-World Examples
|
|
288
|
-
|
|
289
|
-
### Example 1: Next.js App with Express Backend
|
|
290
|
-
|
|
291
|
-
**Frontend (Next.js App Router):**
|
|
292
|
-
|
|
293
|
-
```tsx
|
|
294
|
-
// app/providers.tsx
|
|
295
|
-
"use client";
|
|
296
|
-
|
|
297
|
-
import { PermissionProvider } from "secure-role-guard/react";
|
|
298
|
-
import { roleRegistry } from "@/lib/roles";
|
|
299
|
-
|
|
300
|
-
export function Providers({
|
|
301
|
-
children,
|
|
302
|
-
user,
|
|
303
|
-
}: {
|
|
304
|
-
children: React.ReactNode;
|
|
305
|
-
user: UserContext;
|
|
306
|
-
}) {
|
|
307
|
-
return (
|
|
308
|
-
<PermissionProvider user={user} registry={roleRegistry}>
|
|
309
|
-
{children}
|
|
310
|
-
</PermissionProvider>
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// app/layout.tsx
|
|
315
|
-
import { Providers } from "./providers";
|
|
316
|
-
import { getUser } from "@/lib/auth"; // YOUR auth function
|
|
317
|
-
|
|
318
|
-
export default async function RootLayout({ children }) {
|
|
319
|
-
const user = await getUser(); // Fetch from session/JWT
|
|
320
|
-
|
|
321
|
-
return (
|
|
322
|
-
<html>
|
|
323
|
-
<body>
|
|
324
|
-
<Providers user={user}>{children}</Providers>
|
|
325
|
-
</body>
|
|
326
|
-
</html>
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// app/admin/page.tsx
|
|
331
|
-
import { Can } from "secure-role-guard/react";
|
|
332
|
-
|
|
333
|
-
export default function AdminPage() {
|
|
334
|
-
return (
|
|
335
|
-
<Can permission="admin.access" fallback={<p>Access Denied</p>}>
|
|
336
|
-
<h1>Admin Dashboard</h1>
|
|
337
|
-
</Can>
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
**Backend (Express.js):**
|
|
138
|
+
### Step 2: Use in Express
|
|
343
139
|
|
|
344
140
|
```typescript
|
|
345
141
|
// server.ts
|
|
346
142
|
import express from "express";
|
|
347
|
-
import { defineRoles } from "secure-role-guard/core";
|
|
348
143
|
import { requirePermission } from "secure-role-guard/adapters/express";
|
|
144
|
+
import { roleRegistry } from "./lib/roles";
|
|
349
145
|
|
|
350
146
|
const app = express();
|
|
351
147
|
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
admin: ["user.read", "user.update", "user.delete"],
|
|
355
|
-
viewer: ["user.read"],
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
// YOUR auth middleware (not from this package)
|
|
359
|
-
app.use(authMiddleware); // Sets req.user
|
|
148
|
+
// Your auth middleware (sets req.user)
|
|
149
|
+
app.use(yourAuthMiddleware);
|
|
360
150
|
|
|
361
151
|
// Protected routes
|
|
362
152
|
app.get(
|
|
@@ -367,11 +157,11 @@ app.get(
|
|
|
367
157
|
}
|
|
368
158
|
);
|
|
369
159
|
|
|
370
|
-
app.
|
|
371
|
-
"/api/users
|
|
372
|
-
requirePermission("user.
|
|
160
|
+
app.post(
|
|
161
|
+
"/api/users",
|
|
162
|
+
requirePermission("user.create", roleRegistry),
|
|
373
163
|
(req, res) => {
|
|
374
|
-
res.json({
|
|
164
|
+
res.json({ created: true });
|
|
375
165
|
}
|
|
376
166
|
);
|
|
377
167
|
|
|
@@ -382,1048 +172,157 @@ app.delete(
|
|
|
382
172
|
res.json({ deleted: true });
|
|
383
173
|
}
|
|
384
174
|
);
|
|
385
|
-
|
|
386
|
-
app.listen(3000);
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
---
|
|
390
|
-
|
|
391
|
-
### Example 2: React-Only App (Vite/CRA)
|
|
392
|
-
|
|
393
|
-
```tsx
|
|
394
|
-
// src/roles.ts
|
|
395
|
-
import { defineRoles } from "secure-role-guard";
|
|
396
|
-
|
|
397
|
-
export const roleRegistry = defineRoles({
|
|
398
|
-
admin: ["*"],
|
|
399
|
-
editor: ["post.read", "post.create", "post.update"],
|
|
400
|
-
viewer: ["post.read"],
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
// src/App.tsx
|
|
404
|
-
import { PermissionProvider } from "secure-role-guard";
|
|
405
|
-
import { roleRegistry } from "./roles";
|
|
406
|
-
import { useAuth } from "./auth"; // YOUR auth hook
|
|
407
|
-
|
|
408
|
-
function App() {
|
|
409
|
-
const { user, isLoading } = useAuth();
|
|
410
|
-
|
|
411
|
-
if (isLoading) return <div>Loading...</div>;
|
|
412
|
-
|
|
413
|
-
return (
|
|
414
|
-
<PermissionProvider user={user} registry={roleRegistry}>
|
|
415
|
-
<Router>
|
|
416
|
-
<Routes>
|
|
417
|
-
<Route path="/" element={<Home />} />
|
|
418
|
-
<Route path="/posts" element={<PostList />} />
|
|
419
|
-
<Route path="/admin" element={<AdminRoute />} />
|
|
420
|
-
</Routes>
|
|
421
|
-
</Router>
|
|
422
|
-
</PermissionProvider>
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// src/components/AdminRoute.tsx
|
|
427
|
-
import { useCan } from "secure-role-guard";
|
|
428
|
-
import { Navigate } from "react-router-dom";
|
|
429
|
-
|
|
430
|
-
function AdminRoute() {
|
|
431
|
-
const canAccess = useCan("admin.access");
|
|
432
|
-
|
|
433
|
-
if (!canAccess) {
|
|
434
|
-
return <Navigate to="/" replace />;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
return <AdminPanel />;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// src/components/PostActions.tsx
|
|
441
|
-
import { Can, Cannot } from "secure-role-guard";
|
|
442
|
-
|
|
443
|
-
function PostActions({ postId }: { postId: string }) {
|
|
444
|
-
return (
|
|
445
|
-
<div>
|
|
446
|
-
<Can permission="post.update">
|
|
447
|
-
<button onClick={() => editPost(postId)}>Edit</button>
|
|
448
|
-
</Can>
|
|
449
|
-
|
|
450
|
-
<Can permission="post.delete">
|
|
451
|
-
<button onClick={() => deletePost(postId)}>Delete</button>
|
|
452
|
-
</Can>
|
|
453
|
-
|
|
454
|
-
<Cannot permission="post.update">
|
|
455
|
-
<span>View Only</span>
|
|
456
|
-
</Cannot>
|
|
457
|
-
</div>
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
175
|
```
|
|
461
176
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
### Example 3: Astro with React
|
|
177
|
+
### Manual Check (Without Middleware)
|
|
465
178
|
|
|
466
179
|
```typescript
|
|
467
|
-
|
|
468
|
-
import { defineRoles } from 'secure-role-guard';
|
|
469
|
-
|
|
470
|
-
export const roleRegistry = defineRoles({
|
|
471
|
-
admin: ['page.edit', 'page.publish', 'settings.manage'],
|
|
472
|
-
editor: ['page.edit'],
|
|
473
|
-
viewer: [],
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
// src/components/AdminPanel.tsx (React component)
|
|
477
|
-
import { PermissionProvider, Can, useCan } from 'secure-role-guard';
|
|
478
|
-
import { roleRegistry } from '../lib/roles';
|
|
479
|
-
|
|
480
|
-
interface Props {
|
|
481
|
-
user: { roles: string[] } | null;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
export default function AdminPanel({ user }: Props) {
|
|
485
|
-
return (
|
|
486
|
-
<PermissionProvider user={user} registry={roleRegistry}>
|
|
487
|
-
<div className="admin-panel">
|
|
488
|
-
<Can permission="page.edit">
|
|
489
|
-
<PageEditor />
|
|
490
|
-
</Can>
|
|
491
|
-
|
|
492
|
-
<Can permission="settings.manage">
|
|
493
|
-
<SettingsPanel />
|
|
494
|
-
</Can>
|
|
495
|
-
|
|
496
|
-
<Can permission="page.publish" fallback={<p>Publishing not available</p>}>
|
|
497
|
-
<PublishButton />
|
|
498
|
-
</Can>
|
|
499
|
-
</div>
|
|
500
|
-
</PermissionProvider>
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// src/pages/admin.astro
|
|
505
|
-
---
|
|
506
|
-
import AdminPanel from '../components/AdminPanel';
|
|
507
|
-
import { getUser } from '../lib/auth';
|
|
508
|
-
|
|
509
|
-
const user = await getUser(Astro.request);
|
|
510
|
-
---
|
|
511
|
-
|
|
512
|
-
<AdminPanel client:load user={user} />
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
---
|
|
516
|
-
|
|
517
|
-
### Example 4: Next.js API Routes (App Router)
|
|
518
|
-
|
|
519
|
-
```typescript
|
|
520
|
-
// app/api/users/route.ts
|
|
521
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
522
|
-
import { defineRoles, canUser } from "secure-role-guard/core";
|
|
523
|
-
import { withPermission } from "secure-role-guard/adapters/nextjs";
|
|
524
|
-
import { getUser } from "@/lib/auth";
|
|
525
|
-
|
|
526
|
-
const roleRegistry = defineRoles({
|
|
527
|
-
admin: ["user.read", "user.update", "user.delete"],
|
|
528
|
-
viewer: ["user.read"],
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
// Option 1: Manual check
|
|
532
|
-
export async function GET(request: NextRequest) {
|
|
533
|
-
const user = await getUser(request);
|
|
534
|
-
|
|
535
|
-
if (!canUser(user, "user.read", roleRegistry)) {
|
|
536
|
-
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const users = await fetchUsers();
|
|
540
|
-
return NextResponse.json(users);
|
|
541
|
-
}
|
|
180
|
+
import { canUser } from "secure-role-guard/core";
|
|
542
181
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
roleRegistry,
|
|
547
|
-
{ getUser: async (req) => getUser(req) },
|
|
548
|
-
async (request, user) => {
|
|
549
|
-
const body = await request.json();
|
|
550
|
-
const updated = await updateUser(body);
|
|
551
|
-
return NextResponse.json(updated);
|
|
182
|
+
app.put("/api/users/:id", (req, res) => {
|
|
183
|
+
if (!canUser(req.user, "user.update", roleRegistry)) {
|
|
184
|
+
return res.status(403).json({ error: "Forbidden" });
|
|
552
185
|
}
|
|
553
|
-
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
---
|
|
557
|
-
|
|
558
|
-
### Example 5: Multi-Tenant SaaS
|
|
559
|
-
|
|
560
|
-
```typescript
|
|
561
|
-
import { defineRoles, canUser } from "secure-role-guard";
|
|
562
|
-
|
|
563
|
-
// Define roles for your multi-tenant application
|
|
564
|
-
const roleRegistry = defineRoles({
|
|
565
|
-
org_owner: ["*"],
|
|
566
|
-
org_admin: ["user.*", "billing.view", "settings.update"],
|
|
567
|
-
org_member: ["user.read", "project.*"],
|
|
568
|
-
org_viewer: ["user.read", "project.read"],
|
|
186
|
+
// ... your logic
|
|
569
187
|
});
|
|
570
|
-
|
|
571
|
-
// User context with tenant metadata
|
|
572
|
-
const currentUser = {
|
|
573
|
-
userId: "usr_abc123",
|
|
574
|
-
roles: ["org_admin"],
|
|
575
|
-
permissions: ["beta.feature"], // Direct permission for beta access
|
|
576
|
-
meta: {
|
|
577
|
-
tenantId: "tenant_xyz",
|
|
578
|
-
orgId: "org_456",
|
|
579
|
-
plan: "enterprise",
|
|
580
|
-
},
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
// Authorization check
|
|
584
|
-
if (canUser(currentUser, "billing.view", roleRegistry)) {
|
|
585
|
-
// Show billing dashboard
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Access tenant metadata for additional business logic
|
|
589
|
-
const tenantId = currentUser.meta?.tenantId;
|
|
590
|
-
if (tenantId) {
|
|
591
|
-
// Filter data by tenant
|
|
592
|
-
}
|
|
593
188
|
```
|
|
594
189
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
## Framework Compatibility
|
|
598
|
-
|
|
599
|
-
| Framework | Status | Import |
|
|
600
|
-
| ---------------------- | ---------------- | ------------------------------------ |
|
|
601
|
-
| Next.js (App Router) | ✅ Full support | `secure-role-guard` |
|
|
602
|
-
| Next.js (Pages Router) | ✅ Full support | `secure-role-guard` |
|
|
603
|
-
| Remix | ✅ Full support | `secure-role-guard` |
|
|
604
|
-
| Gatsby | ✅ Full support | `secure-role-guard` |
|
|
605
|
-
| Astro (React) | ✅ Full support | `secure-role-guard` |
|
|
606
|
-
| Vite + React | ✅ Full support | `secure-role-guard` |
|
|
607
|
-
| Create React App | ✅ Full support | `secure-role-guard` |
|
|
608
|
-
| Express.js | ✅ Full support | `secure-role-guard/adapters/express` |
|
|
609
|
-
| Fastify | 🔧 Adapter-ready | Use core directly |
|
|
610
|
-
| Node HTTP | ✅ Full support | `secure-role-guard/core` |
|
|
611
|
-
|
|
612
|
-
---
|
|
613
|
-
|
|
614
|
-
## Fixed Roles vs Dynamic Roles
|
|
615
|
-
|
|
616
|
-
This package supports both **Fixed (Hardcoded) Roles** and **Dynamic (Database-driven) Roles**. Choose the approach that fits your application.
|
|
617
|
-
|
|
618
|
-
### Architecture Overview
|
|
619
|
-
|
|
620
|
-
```
|
|
621
|
-
┌─────────────────────────────────────────────────────────────────────────┐
|
|
622
|
-
│ YOUR APPLICATION │
|
|
623
|
-
├─────────────────────────────────────────────────────────────────────────┤
|
|
624
|
-
│ │
|
|
625
|
-
│ OPTION A: Fixed Roles OPTION B: Dynamic Roles │
|
|
626
|
-
│ ┌───────────────────┐ ┌───────────────────┐ │
|
|
627
|
-
│ │ roles.ts file │ │ Database │ │
|
|
628
|
-
│ │ (hardcoded) │ │ (MongoDB/PG/SQL) │ │
|
|
629
|
-
│ └─────────┬─────────┘ └─────────┬─────────┘ │
|
|
630
|
-
│ │ │ │
|
|
631
|
-
│ ▼ ▼ │
|
|
632
|
-
│ ┌───────────────────┐ ┌───────────────────┐ │
|
|
633
|
-
│ │ defineRoles() │ │ loadRolesFromDB()│ │
|
|
634
|
-
│ │ (at build time) │ │ (at runtime) │ │
|
|
635
|
-
│ └─────────┬─────────┘ └─────────┬─────────┘ │
|
|
636
|
-
│ │ │ │
|
|
637
|
-
│ └──────────────┬───────────────────┘ │
|
|
638
|
-
│ ▼ │
|
|
639
|
-
│ ┌───────────────────────────────────┐ │
|
|
640
|
-
│ │ secure-role-guard │ │
|
|
641
|
-
│ │ (same API for both approaches) │ │
|
|
642
|
-
│ └───────────────────────────────────┘ │
|
|
643
|
-
│ │
|
|
644
|
-
└─────────────────────────────────────────────────────────────────────────┘
|
|
645
|
-
```
|
|
190
|
+
**That's it for backend!** ✅
|
|
646
191
|
|
|
647
192
|
---
|
|
648
193
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
Best for applications with **predefined, unchanging roles**.
|
|
194
|
+
## Full Stack
|
|
652
195
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
- ✅ Small to medium applications
|
|
656
|
-
- ✅ Roles rarely change
|
|
657
|
-
- ✅ Simple admin/user/viewer hierarchy
|
|
658
|
-
- ✅ You want faster startup (no DB query needed)
|
|
659
|
-
|
|
660
|
-
#### Frontend Example (React/Next.js)
|
|
196
|
+
Use **same role definitions** for both:
|
|
661
197
|
|
|
662
198
|
```typescript
|
|
663
|
-
//
|
|
199
|
+
// shared/roles.ts (shared between frontend & backend)
|
|
664
200
|
import { defineRoles } from "secure-role-guard";
|
|
665
201
|
|
|
666
202
|
export const roleRegistry = defineRoles({
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
support: ["ticket.read", "ticket.reply", "user.read"],
|
|
671
|
-
viewer: ["user.read", "report.view"],
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
// -------------------------------------------------------
|
|
675
|
-
// app/providers.tsx - Setup Provider
|
|
676
|
-
("use client");
|
|
677
|
-
|
|
678
|
-
import { PermissionProvider } from "secure-role-guard/react";
|
|
679
|
-
import { roleRegistry } from "@/lib/roles";
|
|
680
|
-
|
|
681
|
-
interface User {
|
|
682
|
-
id: string;
|
|
683
|
-
roles: string[];
|
|
684
|
-
permissions?: string[];
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
export function AuthProvider({
|
|
688
|
-
children,
|
|
689
|
-
user,
|
|
690
|
-
}: {
|
|
691
|
-
children: React.ReactNode;
|
|
692
|
-
user: User | null;
|
|
693
|
-
}) {
|
|
694
|
-
return (
|
|
695
|
-
<PermissionProvider user={user} registry={roleRegistry}>
|
|
696
|
-
{children}
|
|
697
|
-
</PermissionProvider>
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// -------------------------------------------------------
|
|
702
|
-
// components/Dashboard.tsx - Use Permissions
|
|
703
|
-
import { Can, useCan } from "secure-role-guard/react";
|
|
704
|
-
|
|
705
|
-
export function Dashboard() {
|
|
706
|
-
const canManageUsers = useCan("user.update");
|
|
707
|
-
|
|
708
|
-
return (
|
|
709
|
-
<div>
|
|
710
|
-
<h1>Dashboard</h1>
|
|
711
|
-
|
|
712
|
-
{/* Declarative approach */}
|
|
713
|
-
<Can permission="user.create">
|
|
714
|
-
<button>Add New User</button>
|
|
715
|
-
</Can>
|
|
716
|
-
|
|
717
|
-
<Can permission="report.view">
|
|
718
|
-
<ReportsSection />
|
|
719
|
-
</Can>
|
|
720
|
-
|
|
721
|
-
<Can permissions={["user.delete", "user.update"]} anyOf>
|
|
722
|
-
<UserManagement />
|
|
723
|
-
</Can>
|
|
724
|
-
|
|
725
|
-
{/* Programmatic approach */}
|
|
726
|
-
{canManageUsers && <EditUserButton />}
|
|
727
|
-
</div>
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
#### Backend Example (Express/Fastify)
|
|
733
|
-
|
|
734
|
-
```typescript
|
|
735
|
-
// server.ts - Express with Fixed Roles
|
|
736
|
-
import express from "express";
|
|
737
|
-
import { defineRoles, canUser } from "secure-role-guard/core";
|
|
738
|
-
import { requirePermission } from "secure-role-guard/adapters/express";
|
|
739
|
-
|
|
740
|
-
const app = express();
|
|
741
|
-
|
|
742
|
-
// Same role definitions as frontend
|
|
743
|
-
const roleRegistry = defineRoles({
|
|
744
|
-
superadmin: ["*"],
|
|
745
|
-
admin: ["user.read", "user.create", "user.update", "user.delete"],
|
|
746
|
-
manager: ["user.read", "user.update"],
|
|
203
|
+
admin: ["*"], // Full access
|
|
204
|
+
manager: ["user.read", "user.update", "report.*"],
|
|
205
|
+
support: ["ticket.read", "ticket.reply"],
|
|
747
206
|
viewer: ["user.read"],
|
|
748
207
|
});
|
|
749
|
-
|
|
750
|
-
// YOUR auth middleware (this package doesn't do auth)
|
|
751
|
-
app.use(yourAuthMiddleware); // Sets req.user
|
|
752
|
-
|
|
753
|
-
// Protected routes with middleware
|
|
754
|
-
app.get(
|
|
755
|
-
"/api/users",
|
|
756
|
-
requirePermission("user.read", roleRegistry),
|
|
757
|
-
async (req, res) => {
|
|
758
|
-
const users = await db.users.findAll();
|
|
759
|
-
res.json(users);
|
|
760
|
-
}
|
|
761
|
-
);
|
|
762
|
-
|
|
763
|
-
app.post(
|
|
764
|
-
"/api/users",
|
|
765
|
-
requirePermission("user.create", roleRegistry),
|
|
766
|
-
async (req, res) => {
|
|
767
|
-
const user = await db.users.create(req.body);
|
|
768
|
-
res.json(user);
|
|
769
|
-
}
|
|
770
|
-
);
|
|
771
|
-
|
|
772
|
-
// Manual permission check (for complex logic)
|
|
773
|
-
app.put("/api/users/:id", async (req, res) => {
|
|
774
|
-
const user = req.user;
|
|
775
|
-
|
|
776
|
-
if (!canUser(user, "user.update", roleRegistry)) {
|
|
777
|
-
return res.status(403).json({ error: "Forbidden" });
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Additional business logic
|
|
781
|
-
const targetUser = await db.users.findById(req.params.id);
|
|
782
|
-
|
|
783
|
-
// Example: Managers can only edit non-admin users
|
|
784
|
-
if (
|
|
785
|
-
targetUser.roles.includes("admin") &&
|
|
786
|
-
!canUser(user, "admin.manage", roleRegistry)
|
|
787
|
-
) {
|
|
788
|
-
return res.status(403).json({ error: "Cannot edit admin users" });
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
const updated = await db.users.update(req.params.id, req.body);
|
|
792
|
-
res.json(updated);
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
app.listen(3000);
|
|
796
208
|
```
|
|
797
209
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
### Approach 2: Dynamic Roles (Database-Driven)
|
|
801
|
-
|
|
802
|
-
Best for applications where **admin can create/modify roles at runtime**.
|
|
803
|
-
|
|
804
|
-
#### When to Use Dynamic Roles
|
|
805
|
-
|
|
806
|
-
- ✅ Enterprise SaaS applications
|
|
807
|
-
- ✅ Admin should create custom roles (e.g., "HR Manager", "Finance Lead")
|
|
808
|
-
- ✅ Roles change frequently
|
|
809
|
-
- ✅ Multi-tenant with different roles per tenant
|
|
810
|
-
|
|
811
|
-
#### Database Schema Examples
|
|
210
|
+
**Frontend:** Follow [Frontend Only](#frontend-only) steps
|
|
211
|
+
**Backend:** Follow [Backend Only](#backend-only) steps
|
|
812
212
|
|
|
813
|
-
**
|
|
814
|
-
|
|
815
|
-
```javascript
|
|
816
|
-
// roles collection
|
|
817
|
-
{
|
|
818
|
-
_id: ObjectId("..."),
|
|
819
|
-
name: "hr_manager",
|
|
820
|
-
display_name: "HR Manager",
|
|
821
|
-
permissions: ["employee.read", "employee.create", "employee.update", "leave.approve"],
|
|
822
|
-
is_active: true,
|
|
823
|
-
tenant_id: ObjectId("..."), // For multi-tenant
|
|
824
|
-
created_at: ISODate("...")
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// users collection
|
|
828
|
-
{
|
|
829
|
-
_id: ObjectId("..."),
|
|
830
|
-
email: "john@example.com",
|
|
831
|
-
roles: [ObjectId("role1"), ObjectId("role2")],
|
|
832
|
-
direct_permissions: ["special.feature"], // User-specific permissions
|
|
833
|
-
tenant_id: ObjectId("...")
|
|
834
|
-
}
|
|
835
|
-
```
|
|
836
|
-
|
|
837
|
-
**PostgreSQL:**
|
|
838
|
-
|
|
839
|
-
```sql
|
|
840
|
-
-- roles table
|
|
841
|
-
CREATE TABLE roles (
|
|
842
|
-
id SERIAL PRIMARY KEY,
|
|
843
|
-
name VARCHAR(50) UNIQUE NOT NULL,
|
|
844
|
-
display_name VARCHAR(100),
|
|
845
|
-
is_active BOOLEAN DEFAULT true,
|
|
846
|
-
tenant_id INTEGER REFERENCES tenants(id),
|
|
847
|
-
created_at TIMESTAMP DEFAULT NOW()
|
|
848
|
-
);
|
|
849
|
-
|
|
850
|
-
-- permissions table
|
|
851
|
-
CREATE TABLE permissions (
|
|
852
|
-
id SERIAL PRIMARY KEY,
|
|
853
|
-
code VARCHAR(100) UNIQUE NOT NULL, -- e.g., 'user.read'
|
|
854
|
-
description TEXT
|
|
855
|
-
);
|
|
856
|
-
|
|
857
|
-
-- role_permissions (many-to-many)
|
|
858
|
-
CREATE TABLE role_permissions (
|
|
859
|
-
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
|
|
860
|
-
permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
|
|
861
|
-
PRIMARY KEY (role_id, permission_id)
|
|
862
|
-
);
|
|
863
|
-
|
|
864
|
-
-- user_roles (many-to-many)
|
|
865
|
-
CREATE TABLE user_roles (
|
|
866
|
-
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
867
|
-
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
|
|
868
|
-
PRIMARY KEY (user_id, role_id)
|
|
869
|
-
);
|
|
870
|
-
|
|
871
|
-
-- user_permissions (direct permissions, bypass roles)
|
|
872
|
-
CREATE TABLE user_permissions (
|
|
873
|
-
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
|
874
|
-
permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
|
|
875
|
-
PRIMARY KEY (user_id, permission_id)
|
|
876
|
-
);
|
|
877
|
-
```
|
|
878
|
-
|
|
879
|
-
**MySQL:**
|
|
880
|
-
|
|
881
|
-
```sql
|
|
882
|
-
-- Similar to PostgreSQL, with MySQL syntax
|
|
883
|
-
CREATE TABLE roles (
|
|
884
|
-
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
885
|
-
name VARCHAR(50) UNIQUE NOT NULL,
|
|
886
|
-
display_name VARCHAR(100),
|
|
887
|
-
is_active TINYINT(1) DEFAULT 1,
|
|
888
|
-
tenant_id INT,
|
|
889
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
890
|
-
);
|
|
891
|
-
|
|
892
|
-
CREATE TABLE permissions (
|
|
893
|
-
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
894
|
-
code VARCHAR(100) UNIQUE NOT NULL,
|
|
895
|
-
description TEXT
|
|
896
|
-
);
|
|
897
|
-
|
|
898
|
-
CREATE TABLE role_permissions (
|
|
899
|
-
role_id INT,
|
|
900
|
-
permission_id INT,
|
|
901
|
-
PRIMARY KEY (role_id, permission_id),
|
|
902
|
-
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
|
903
|
-
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
|
|
904
|
-
);
|
|
905
|
-
```
|
|
906
|
-
|
|
907
|
-
#### Backend: Loading Dynamic Roles
|
|
908
|
-
|
|
909
|
-
```typescript
|
|
910
|
-
// lib/dynamic-roles.ts
|
|
911
|
-
import { defineRoles, RoleRegistry } from "secure-role-guard/core";
|
|
912
|
-
|
|
913
|
-
// Interface for database abstraction
|
|
914
|
-
interface RoleFromDB {
|
|
915
|
-
name: string;
|
|
916
|
-
permissions: string[];
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
interface IRoleRepository {
|
|
920
|
-
getAllActiveRoles(): Promise<RoleFromDB[]>;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// ============================================================
|
|
924
|
-
// MongoDB Implementation
|
|
925
|
-
// ============================================================
|
|
926
|
-
class MongoRoleRepository implements IRoleRepository {
|
|
927
|
-
async getAllActiveRoles(): Promise<RoleFromDB[]> {
|
|
928
|
-
const roles = await RoleModel.find({ is_active: true })
|
|
929
|
-
.populate("permissions")
|
|
930
|
-
.lean();
|
|
931
|
-
|
|
932
|
-
return roles.map((role) => ({
|
|
933
|
-
name: role.name,
|
|
934
|
-
permissions: role.permissions.map((p: any) => p.code),
|
|
935
|
-
}));
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// ============================================================
|
|
940
|
-
// PostgreSQL Implementation (using Prisma)
|
|
941
|
-
// ============================================================
|
|
942
|
-
class PostgresRoleRepository implements IRoleRepository {
|
|
943
|
-
async getAllActiveRoles(): Promise<RoleFromDB[]> {
|
|
944
|
-
const roles = await prisma.role.findMany({
|
|
945
|
-
where: { is_active: true },
|
|
946
|
-
include: {
|
|
947
|
-
role_permissions: {
|
|
948
|
-
include: { permission: true },
|
|
949
|
-
},
|
|
950
|
-
},
|
|
951
|
-
});
|
|
952
|
-
|
|
953
|
-
return roles.map((role) => ({
|
|
954
|
-
name: role.name,
|
|
955
|
-
permissions: role.role_permissions.map((rp) => rp.permission.code),
|
|
956
|
-
}));
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// ============================================================
|
|
961
|
-
// MySQL Implementation (using mysql2)
|
|
962
|
-
// ============================================================
|
|
963
|
-
class MySQLRoleRepository implements IRoleRepository {
|
|
964
|
-
async getAllActiveRoles(): Promise<RoleFromDB[]> {
|
|
965
|
-
const [rows] = await pool.query(`
|
|
966
|
-
SELECT r.name, GROUP_CONCAT(p.code) as permissions
|
|
967
|
-
FROM roles r
|
|
968
|
-
LEFT JOIN role_permissions rp ON r.id = rp.role_id
|
|
969
|
-
LEFT JOIN permissions p ON rp.permission_id = p.id
|
|
970
|
-
WHERE r.is_active = 1
|
|
971
|
-
GROUP BY r.id, r.name
|
|
972
|
-
`);
|
|
973
|
-
|
|
974
|
-
return (rows as any[]).map((row) => ({
|
|
975
|
-
name: row.name,
|
|
976
|
-
permissions: row.permissions ? row.permissions.split(",") : [],
|
|
977
|
-
}));
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// ============================================================
|
|
982
|
-
// Dynamic Role Registry Factory
|
|
983
|
-
// ============================================================
|
|
984
|
-
let cachedRegistry: RoleRegistry | null = null;
|
|
985
|
-
let cacheExpiry = 0;
|
|
986
|
-
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
987
|
-
|
|
988
|
-
export async function getDynamicRoleRegistry(
|
|
989
|
-
repository: IRoleRepository
|
|
990
|
-
): Promise<RoleRegistry> {
|
|
991
|
-
const now = Date.now();
|
|
992
|
-
|
|
993
|
-
// Return cached if valid
|
|
994
|
-
if (cachedRegistry && now < cacheExpiry) {
|
|
995
|
-
return cachedRegistry;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// Fetch from database
|
|
999
|
-
const rolesFromDB = await repository.getAllActiveRoles();
|
|
1000
|
-
|
|
1001
|
-
// Transform to RoleDefinition format
|
|
1002
|
-
const roleDefinition: Record<string, readonly string[]> = {};
|
|
1003
|
-
for (const role of rolesFromDB) {
|
|
1004
|
-
roleDefinition[role.name] = role.permissions;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
// Create registry
|
|
1008
|
-
cachedRegistry = defineRoles(roleDefinition);
|
|
1009
|
-
cacheExpiry = now + CACHE_TTL;
|
|
1010
|
-
|
|
1011
|
-
return cachedRegistry;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Force refresh (call when admin updates roles)
|
|
1015
|
-
export function invalidateRoleCache(): void {
|
|
1016
|
-
cachedRegistry = null;
|
|
1017
|
-
cacheExpiry = 0;
|
|
1018
|
-
}
|
|
1019
|
-
```
|
|
1020
|
-
|
|
1021
|
-
#### Backend: Using Dynamic Roles in Express
|
|
1022
|
-
|
|
1023
|
-
```typescript
|
|
1024
|
-
// server.ts
|
|
1025
|
-
import express from "express";
|
|
1026
|
-
import { canUser } from "secure-role-guard/core";
|
|
1027
|
-
import {
|
|
1028
|
-
getDynamicRoleRegistry,
|
|
1029
|
-
invalidateRoleCache,
|
|
1030
|
-
} from "./lib/dynamic-roles";
|
|
1031
|
-
|
|
1032
|
-
const app = express();
|
|
1033
|
-
const roleRepository = new MongoRoleRepository(); // or PostgresRoleRepository
|
|
1034
|
-
|
|
1035
|
-
// Middleware to attach registry to request
|
|
1036
|
-
app.use(async (req, res, next) => {
|
|
1037
|
-
try {
|
|
1038
|
-
req.roleRegistry = await getDynamicRoleRegistry(roleRepository);
|
|
1039
|
-
next();
|
|
1040
|
-
} catch (error) {
|
|
1041
|
-
console.error("Failed to load roles:", error);
|
|
1042
|
-
res.status(500).json({ error: "Internal server error" });
|
|
1043
|
-
}
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
// Protected routes using dynamic roles
|
|
1047
|
-
app.get("/api/employees", async (req, res) => {
|
|
1048
|
-
if (!canUser(req.user, "employee.read", req.roleRegistry)) {
|
|
1049
|
-
return res.status(403).json({ error: "Forbidden" });
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
const employees = await db.employees.findAll();
|
|
1053
|
-
res.json(employees);
|
|
1054
|
-
});
|
|
1055
|
-
|
|
1056
|
-
// Admin creates a new role
|
|
1057
|
-
app.post("/api/admin/roles", async (req, res) => {
|
|
1058
|
-
if (!canUser(req.user, "role.create", req.roleRegistry)) {
|
|
1059
|
-
return res.status(403).json({ error: "Forbidden" });
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
const { name, permissions } = req.body;
|
|
1063
|
-
|
|
1064
|
-
// Save to database
|
|
1065
|
-
await RoleModel.create({ name, permissions, is_active: true });
|
|
1066
|
-
|
|
1067
|
-
// Invalidate cache so new role is available
|
|
1068
|
-
invalidateRoleCache();
|
|
1069
|
-
|
|
1070
|
-
res.json({ success: true });
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
app.listen(3000);
|
|
1074
|
-
```
|
|
1075
|
-
|
|
1076
|
-
#### Frontend: Using Dynamic Roles
|
|
1077
|
-
|
|
1078
|
-
```typescript
|
|
1079
|
-
// lib/auth-context.tsx
|
|
1080
|
-
"use client";
|
|
1081
|
-
|
|
1082
|
-
import { createContext, useContext, useEffect, useState } from "react";
|
|
1083
|
-
import {
|
|
1084
|
-
PermissionProvider,
|
|
1085
|
-
RoleRegistry,
|
|
1086
|
-
defineRoles,
|
|
1087
|
-
} from "secure-role-guard/react";
|
|
1088
|
-
|
|
1089
|
-
interface User {
|
|
1090
|
-
id: string;
|
|
1091
|
-
email: string;
|
|
1092
|
-
roles: string[];
|
|
1093
|
-
permissions: string[];
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
interface AuthContextValue {
|
|
1097
|
-
user: User | null;
|
|
1098
|
-
isLoading: boolean;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
const AuthContext = createContext<AuthContextValue>({
|
|
1102
|
-
user: null,
|
|
1103
|
-
isLoading: true,
|
|
1104
|
-
});
|
|
1105
|
-
|
|
1106
|
-
export function DynamicAuthProvider({
|
|
1107
|
-
children,
|
|
1108
|
-
}: {
|
|
1109
|
-
children: React.ReactNode;
|
|
1110
|
-
}) {
|
|
1111
|
-
const [user, setUser] = useState<User | null>(null);
|
|
1112
|
-
const [roleRegistry, setRoleRegistry] = useState<RoleRegistry | null>(null);
|
|
1113
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
1114
|
-
|
|
1115
|
-
useEffect(() => {
|
|
1116
|
-
async function loadUserAndRoles() {
|
|
1117
|
-
try {
|
|
1118
|
-
// Fetch current user
|
|
1119
|
-
const userRes = await fetch("/api/auth/me");
|
|
1120
|
-
const userData = await userRes.json();
|
|
1121
|
-
|
|
1122
|
-
if (!userData.user) {
|
|
1123
|
-
setIsLoading(false);
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// Fetch dynamic roles from backend
|
|
1128
|
-
const rolesRes = await fetch("/api/auth/roles");
|
|
1129
|
-
const rolesData = await rolesRes.json();
|
|
1130
|
-
|
|
1131
|
-
// Create registry from dynamic roles
|
|
1132
|
-
// rolesData format: { admin: ['user.read', ...], manager: [...] }
|
|
1133
|
-
const registry = defineRoles(rolesData.roles);
|
|
1134
|
-
|
|
1135
|
-
setUser(userData.user);
|
|
1136
|
-
setRoleRegistry(registry);
|
|
1137
|
-
} catch (error) {
|
|
1138
|
-
console.error("Failed to load auth:", error);
|
|
1139
|
-
} finally {
|
|
1140
|
-
setIsLoading(false);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
loadUserAndRoles();
|
|
1145
|
-
}, []);
|
|
1146
|
-
|
|
1147
|
-
if (isLoading) {
|
|
1148
|
-
return <div>Loading...</div>;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
if (!roleRegistry) {
|
|
1152
|
-
return <div>Failed to load permissions</div>;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
return (
|
|
1156
|
-
<AuthContext.Provider value={{ user, isLoading }}>
|
|
1157
|
-
<PermissionProvider user={user} registry={roleRegistry}>
|
|
1158
|
-
{children}
|
|
1159
|
-
</PermissionProvider>
|
|
1160
|
-
</AuthContext.Provider>
|
|
1161
|
-
);
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// -------------------------------------------------------
|
|
1165
|
-
// API Route: Return roles for frontend
|
|
1166
|
-
// app/api/auth/roles/route.ts
|
|
1167
|
-
|
|
1168
|
-
import { NextResponse } from "next/server";
|
|
1169
|
-
|
|
1170
|
-
export async function GET() {
|
|
1171
|
-
// Fetch roles from database
|
|
1172
|
-
const roles = await RoleModel.find({ is_active: true }).lean();
|
|
1173
|
-
|
|
1174
|
-
// Transform to { roleName: permissions[] } format
|
|
1175
|
-
const roleMap: Record<string, string[]> = {};
|
|
1176
|
-
for (const role of roles) {
|
|
1177
|
-
roleMap[role.name] = role.permissions;
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
return NextResponse.json({ roles: roleMap });
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// -------------------------------------------------------
|
|
1184
|
-
// Usage in Components (same as fixed roles!)
|
|
1185
|
-
|
|
1186
|
-
import { Can, useCan } from "secure-role-guard/react";
|
|
1187
|
-
|
|
1188
|
-
function EmployeeDashboard() {
|
|
1189
|
-
const canApproveLeave = useCan("leave.approve");
|
|
1190
|
-
|
|
1191
|
-
return (
|
|
1192
|
-
<div>
|
|
1193
|
-
<Can permission="employee.read">
|
|
1194
|
-
<EmployeeList />
|
|
1195
|
-
</Can>
|
|
1196
|
-
|
|
1197
|
-
<Can permission="employee.create">
|
|
1198
|
-
<AddEmployeeButton />
|
|
1199
|
-
</Can>
|
|
1200
|
-
|
|
1201
|
-
{canApproveLeave && <LeaveApprovalQueue />}
|
|
1202
|
-
</div>
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
1205
|
-
```
|
|
213
|
+
> 💡 **Pro tip:** Keep roles in a shared package or copy to both projects.
|
|
1206
214
|
|
|
1207
215
|
---
|
|
1208
216
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
| Feature | Fixed Roles | Dynamic Roles |
|
|
1212
|
-
| ----------------------- | --------------------- | ---------------------------- |
|
|
1213
|
-
| **Setup Complexity** | Simple | More complex |
|
|
1214
|
-
| **Runtime Performance** | Faster (no DB query) | Slight overhead (cached) |
|
|
1215
|
-
| **Flexibility** | Limited | Full flexibility |
|
|
1216
|
-
| **Admin Control** | Code changes required | UI-based role management |
|
|
1217
|
-
| **Use Case** | Simple apps, MVPs | Enterprise, SaaS |
|
|
1218
|
-
| **Role Changes** | Deploy required | Instant (cache invalidation) |
|
|
1219
|
-
|
|
1220
|
-
### Key Points
|
|
1221
|
-
|
|
1222
|
-
1. **Package is database-agnostic** - You fetch data, we check permissions
|
|
1223
|
-
2. **Same API for both approaches** - `canUser()`, `<Can>`, `useCan()` work identically
|
|
1224
|
-
3. **Frontend mirrors backend** - Keep role definitions in sync
|
|
1225
|
-
4. **Always validate on backend** - Frontend is for UX, backend is for security
|
|
1226
|
-
|
|
1227
|
-
---
|
|
1228
|
-
|
|
1229
|
-
## Common Mistakes to Avoid
|
|
1230
|
-
|
|
1231
|
-
### ❌ DON'T: Parse JWT in this package
|
|
217
|
+
## 📚 API Reference
|
|
1232
218
|
|
|
1233
|
-
|
|
1234
|
-
// WRONG - This package doesn't handle authentication
|
|
1235
|
-
import { canUser } from "secure-role-guard";
|
|
219
|
+
### Core Functions
|
|
1236
220
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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 |
|
|
1240
227
|
|
|
1241
|
-
###
|
|
228
|
+
### React Components
|
|
1242
229
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
230
|
+
| Component | Description |
|
|
231
|
+
| --------------------------- | ------------------- |
|
|
232
|
+
| `<PermissionProvider>` | Wrap your app |
|
|
233
|
+
| `<Can permission="...">` | Show if allowed |
|
|
234
|
+
| `<Cannot permission="...">` | Show if NOT allowed |
|
|
1246
235
|
|
|
1247
|
-
|
|
1248
|
-
const user = req.user; // Set by YOUR auth middleware
|
|
1249
|
-
const allowed = canUser(user, "admin.access", roleRegistry);
|
|
1250
|
-
```
|
|
236
|
+
### React Hooks
|
|
1251
237
|
|
|
1252
|
-
|
|
238
|
+
| Hook | Returns |
|
|
239
|
+
| ------------------------ | --------- |
|
|
240
|
+
| `useCan(permission)` | `boolean` |
|
|
241
|
+
| `useCanAll(permissions)` | `boolean` |
|
|
242
|
+
| `useCanAny(permissions)` | `boolean` |
|
|
1253
243
|
|
|
1254
|
-
###
|
|
244
|
+
### User Context Shape
|
|
1255
245
|
|
|
1256
246
|
```typescript
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
+
};
|
|
1259
253
|
```
|
|
1260
254
|
|
|
1261
|
-
###
|
|
255
|
+
### Wildcard Permissions
|
|
1262
256
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
```
|
|
257
|
+
| Pattern | Grants |
|
|
258
|
+
| ---------------- | ------------------------------------------------ |
|
|
259
|
+
| `*` | Everything |
|
|
260
|
+
| `user.*` | `user.read`, `user.update`, etc. |
|
|
261
|
+
| `report.admin.*` | `report.admin.view`, `report.admin.export`, etc. |
|
|
1269
262
|
|
|
1270
263
|
---
|
|
1271
264
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
```typescript
|
|
1275
|
-
// WRONG - This is authentication, not authorization
|
|
1276
|
-
if (canUser(user, "logged-in", registry)) {
|
|
1277
|
-
// ...
|
|
1278
|
-
}
|
|
1279
|
-
```
|
|
265
|
+
## 🔄 Dynamic Roles (From Database)
|
|
1280
266
|
|
|
1281
|
-
|
|
267
|
+
If admin creates roles at runtime:
|
|
1282
268
|
|
|
1283
269
|
```typescript
|
|
1284
|
-
//
|
|
1285
|
-
|
|
1286
|
-
// ...
|
|
1287
|
-
}
|
|
1288
|
-
```
|
|
270
|
+
// Fetch roles from your database
|
|
271
|
+
const rolesFromDB = await fetchRolesFromDB();
|
|
1289
272
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
273
|
+
// Transform to: { roleName: ['permission1', 'permission2'] }
|
|
274
|
+
const roleDefinition = {};
|
|
275
|
+
rolesFromDB.forEach((role) => {
|
|
276
|
+
roleDefinition[role.name] = role.permissions;
|
|
277
|
+
});
|
|
1293
278
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
if (user.permissions.includes("admin")) {
|
|
1297
|
-
// ...
|
|
1298
|
-
}
|
|
279
|
+
// Create registry
|
|
280
|
+
const registry = defineRoles(roleDefinition);
|
|
1299
281
|
```
|
|
1300
282
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
```typescript
|
|
1304
|
-
// CORRECT - Handles null/undefined safely (deny by default)
|
|
1305
|
-
if (canUser(user, "admin.access", registry)) {
|
|
1306
|
-
// ...
|
|
1307
|
-
}
|
|
1308
|
-
```
|
|
283
|
+
Works with **any database**: MongoDB, PostgreSQL, MySQL, SQLite, etc.
|
|
1309
284
|
|
|
1310
285
|
---
|
|
1311
286
|
|
|
1312
|
-
##
|
|
1313
|
-
|
|
1314
|
-
### Express Middleware
|
|
1315
|
-
|
|
1316
|
-
```typescript
|
|
1317
|
-
import {
|
|
1318
|
-
requirePermission,
|
|
1319
|
-
requireAllPermissions,
|
|
1320
|
-
requireAnyPermission,
|
|
1321
|
-
} from "secure-role-guard/adapters/express";
|
|
1322
|
-
|
|
1323
|
-
// Single permission
|
|
1324
|
-
app.get("/api/users", requirePermission("user.read", registry), handler);
|
|
287
|
+
## ⚠️ Important Notes
|
|
1325
288
|
|
|
1326
|
-
|
|
1327
|
-
app.delete(
|
|
1328
|
-
"/api/admin",
|
|
1329
|
-
requireAllPermissions(["admin.access", "data.delete"], registry),
|
|
1330
|
-
handler
|
|
1331
|
-
);
|
|
289
|
+
### This Package Does NOT:
|
|
1332
290
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
requireAnyPermission(["report.view", "report.admin"], registry),
|
|
1337
|
-
handler
|
|
1338
|
-
);
|
|
1339
|
-
|
|
1340
|
-
// Custom options
|
|
1341
|
-
app.put(
|
|
1342
|
-
"/api/settings",
|
|
1343
|
-
requirePermission("settings.update", registry, {
|
|
1344
|
-
statusCode: 401,
|
|
1345
|
-
message: "Unauthorized",
|
|
1346
|
-
getUser: (req) => req.session?.user,
|
|
1347
|
-
}),
|
|
1348
|
-
handler
|
|
1349
|
-
);
|
|
1350
|
-
```
|
|
1351
|
-
|
|
1352
|
-
### Next.js Route Handlers
|
|
1353
|
-
|
|
1354
|
-
```typescript
|
|
1355
|
-
import {
|
|
1356
|
-
withPermission,
|
|
1357
|
-
checkNextPermission,
|
|
1358
|
-
} from "secure-role-guard/adapters/nextjs";
|
|
1359
|
-
|
|
1360
|
-
// Using wrapper
|
|
1361
|
-
export const POST = withPermission(
|
|
1362
|
-
"post.create",
|
|
1363
|
-
registry,
|
|
1364
|
-
{ getUser: async (req) => getUserFromSession(req) },
|
|
1365
|
-
async (request, user) => {
|
|
1366
|
-
return Response.json({ created: true });
|
|
1367
|
-
}
|
|
1368
|
-
);
|
|
291
|
+
- ❌ Handle authentication (JWT, sessions, cookies)
|
|
292
|
+
- ❌ Make API/database calls
|
|
293
|
+
- ❌ Store global state
|
|
1369
294
|
|
|
1370
|
-
|
|
1371
|
-
export async function GET(request: NextRequest) {
|
|
1372
|
-
const user = await getUser(request);
|
|
1373
|
-
const result = checkNextPermission(user, "data.read", registry);
|
|
295
|
+
**You provide:** User with roles → **We check:** Permissions
|
|
1374
296
|
|
|
1375
|
-
|
|
1376
|
-
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
1377
|
-
}
|
|
297
|
+
### Security
|
|
1378
298
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
299
|
+
- ✅ Deny by default (undefined = false)
|
|
300
|
+
- ✅ Zero dependencies in core
|
|
301
|
+
- ✅ Immutable configurations
|
|
302
|
+
- ✅ Pure functions (no side effects)
|
|
1382
303
|
|
|
1383
304
|
---
|
|
1384
305
|
|
|
1385
|
-
##
|
|
306
|
+
## 🔒 Backward Compatibility
|
|
1386
307
|
|
|
1387
|
-
|
|
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 |
|
|
1388
313
|
|
|
1389
|
-
|
|
1390
|
-
// tsconfig.json (package configuration)
|
|
1391
|
-
{
|
|
1392
|
-
"compilerOptions": {
|
|
1393
|
-
"strict": true,
|
|
1394
|
-
"noImplicitAny": true,
|
|
1395
|
-
"strictNullChecks": true,
|
|
1396
|
-
"exactOptionalPropertyTypes": true
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
```
|
|
1400
|
-
|
|
1401
|
-
All types are exported:
|
|
1402
|
-
|
|
1403
|
-
```typescript
|
|
1404
|
-
import type {
|
|
1405
|
-
UserContext,
|
|
1406
|
-
RoleDefinition,
|
|
1407
|
-
RoleRegistry,
|
|
1408
|
-
PermissionCheckResult,
|
|
1409
|
-
} from "secure-role-guard";
|
|
1410
|
-
```
|
|
314
|
+
**Promise:** v1.x APIs will never break. Update with confidence.
|
|
1411
315
|
|
|
1412
316
|
---
|
|
1413
317
|
|
|
1414
|
-
## License
|
|
318
|
+
## 📄 License
|
|
1415
319
|
|
|
1416
320
|
MIT © [Sohel Rahaman](https://github.com/sohelrahaman)
|
|
1417
321
|
|
|
1418
322
|
---
|
|
1419
323
|
|
|
1420
|
-
##
|
|
1421
|
-
|
|
1422
|
-
This package is designed to be **boring, predictable, and auditable**. It intentionally avoids:
|
|
1423
|
-
|
|
1424
|
-
- Magic behavior
|
|
1425
|
-
- Clever hacks
|
|
1426
|
-
- Hidden side effects
|
|
1427
|
-
- Runtime code generation
|
|
324
|
+
## 🔗 Links
|
|
1428
325
|
|
|
1429
|
-
|
|
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)
|