react-ability-kit 0.1.4 → 0.1.6
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 +192 -125
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
# React Ability
|
|
2
2
|
|
|
3
|
-
A small, typed permission layer for React
|
|
3
|
+
**A small, strongly-typed permission layer for React**
|
|
4
|
+
Keep authorization logic **out of your components** and **in one place**.
|
|
4
5
|
|
|
5
6
|
---
|
|
6
7
|
|
|
7
|
-
##
|
|
8
|
+
## Why this exists
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Most React apps don’t plan to become permission nightmares — they just grow into one.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
Permissions slowly spread across components as ad-hoc checks:
|
|
13
|
+
|
|
14
|
+
- `if (user.role === "admin")`
|
|
15
|
+
- `if (invoice.ownerId === user.id)`
|
|
16
|
+
- `if (permissions.includes("invoice:update"))`
|
|
17
|
+
|
|
18
|
+
This package introduces a **policy-first** approach to permissions, so your UI stays clean and your rules stay auditable.
|
|
12
19
|
|
|
13
20
|
---
|
|
14
21
|
|
|
15
|
-
##
|
|
22
|
+
## Core idea (one sentence)
|
|
23
|
+
|
|
24
|
+
> Define permission rules once, then query them everywhere — instead of scattering fragile `if` checks across your UI.
|
|
25
|
+
|
|
26
|
+
Everything else is just implementation details.
|
|
27
|
+
|
|
28
|
+
---
|
|
16
29
|
|
|
17
|
-
|
|
30
|
+
## The real problem (what goes wrong in real apps)
|
|
18
31
|
|
|
19
|
-
### ❌ Without a
|
|
32
|
+
### ❌ Without a permission layer
|
|
20
33
|
|
|
21
34
|
```tsx
|
|
22
35
|
// Button.tsx
|
|
@@ -41,69 +54,144 @@ if (user && user.role !== "guest") {
|
|
|
41
54
|
}
|
|
42
55
|
```
|
|
43
56
|
|
|
57
|
+
### Problems this creates
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
❌
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
- ❌ **Logic duplication** – same rules rewritten in different places
|
|
60
|
+
- ❌ **Rules drift** – one condition changes, others don’t
|
|
61
|
+
- ❌ **Impossible to audit** – “Who can edit invoices?” → grep the whole repo
|
|
62
|
+
- ❌ **UI inconsistencies**
|
|
63
|
+
- Button visible but API rejects
|
|
64
|
+
- Button hidden but API allows
|
|
65
|
+
- ❌ **No type safety**
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
```ts
|
|
68
|
+
"inovice:update" // typo = silent bug
|
|
69
|
+
```
|
|
54
70
|
|
|
55
|
-
❌
|
|
56
|
-
Button visible but API rejects
|
|
71
|
+
- ❌ **Hard to evolve roles** – adding a new role breaks logic everywhere
|
|
57
72
|
|
|
58
|
-
|
|
73
|
+
---
|
|
59
74
|
|
|
60
|
-
|
|
61
|
-
ts
|
|
62
|
-
Copier le code
|
|
63
|
-
"inovice:update" // typo = silent bug
|
|
64
|
-
❌ Hard to change roles
|
|
65
|
-
Adding a new role breaks logic everywhere.
|
|
75
|
+
## The missing abstraction: policy-first permissions
|
|
66
76
|
|
|
67
|
-
What this package introduces (the missing abstraction)
|
|
68
|
-
Key idea: policy-first permissions
|
|
69
77
|
Instead of asking:
|
|
70
78
|
|
|
71
|
-
“Can the user do this?”
|
|
72
|
-
|
|
73
|
-
everywhere in the UI…
|
|
79
|
+
> “Can the user do this?”
|
|
80
|
+
> everywhere in the UI…
|
|
74
81
|
|
|
75
82
|
You define rules once, then query them everywhere.
|
|
76
83
|
|
|
77
|
-
Mental model
|
|
78
|
-
Think of your app like this:
|
|
84
|
+
### Mental model
|
|
79
85
|
|
|
80
|
-
```
|
|
86
|
+
```text
|
|
81
87
|
User + Context → Ability → UI decisions
|
|
82
88
|
```
|
|
83
89
|
|
|
84
|
-
|
|
90
|
+
```text
|
|
91
|
+
User ──► Policy ──► Ability ──► UI / Components
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Installation
|
|
85
97
|
|
|
86
98
|
```bash
|
|
87
|
-
|
|
99
|
+
npm install react-ability
|
|
88
100
|
```
|
|
89
101
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
or
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pnpm add react-ability
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
or
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
yarn add react-ability
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Quick start (5 minutes)
|
|
117
|
+
|
|
118
|
+
### 1️⃣ Define your abilities (policy-first)
|
|
119
|
+
|
|
120
|
+
Create a single policy file.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// ability.ts
|
|
124
|
+
import { defineAbility } from "react-ability";
|
|
125
|
+
|
|
126
|
+
export const ability = defineAbility((allow, deny, user) => {
|
|
127
|
+
allow("read", "Invoice");
|
|
128
|
+
|
|
129
|
+
allow(
|
|
130
|
+
"update",
|
|
131
|
+
"Invoice",
|
|
132
|
+
invoice => invoice.ownerId === user.id && invoice.status === "draft"
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
deny("delete", "Invoice");
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
This file is your **single source of truth**.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### 2️⃣ Provide the ability to your app
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import { AbilityProvider } from "react-ability";
|
|
147
|
+
import { ability } from "./ability";
|
|
148
|
+
|
|
149
|
+
export function App() {
|
|
150
|
+
return (
|
|
151
|
+
<AbilityProvider ability={ability}>
|
|
152
|
+
<YourApp />
|
|
153
|
+
</AbilityProvider>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### 3️⃣ Use permissions anywhere
|
|
161
|
+
|
|
162
|
+
#### Using the `<Can />` component
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
<Can I="update" a="Invoice" this={invoice}>
|
|
166
|
+
<EditButton />
|
|
167
|
+
</Can>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### Using the `can()` function
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const canEdit = can("update", "Invoice", invoice);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## What this package actually solves
|
|
179
|
+
|
|
180
|
+
### 1️⃣ Single source of truth for permissions
|
|
93
181
|
|
|
94
182
|
```ts
|
|
95
|
-
// policy.ts
|
|
96
183
|
allow("update", "Invoice", invoice => invoice.ownerId === user.id);
|
|
97
184
|
deny("delete", "Invoice");
|
|
98
185
|
```
|
|
99
186
|
|
|
100
|
-
|
|
187
|
+
- All rules live in one place
|
|
188
|
+
- Easy to review, change, and reason about
|
|
189
|
+
- No scattered conditionals
|
|
101
190
|
|
|
102
|
-
|
|
191
|
+
---
|
|
103
192
|
|
|
104
|
-
|
|
193
|
+
### 2️⃣ Business rules become readable policies
|
|
105
194
|
|
|
106
|
-
2️⃣ Turns business rules into readable policies
|
|
107
195
|
❌ Before
|
|
108
196
|
|
|
109
197
|
```ts
|
|
@@ -115,7 +203,7 @@ if (
|
|
|
115
203
|
)
|
|
116
204
|
```
|
|
117
205
|
|
|
118
|
-
✅
|
|
206
|
+
✅ After
|
|
119
207
|
|
|
120
208
|
```ts
|
|
121
209
|
allow(
|
|
@@ -125,12 +213,15 @@ allow(
|
|
|
125
213
|
);
|
|
126
214
|
```
|
|
127
215
|
|
|
128
|
-
This is domain language
|
|
216
|
+
This is **domain language**, not UI logic.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
### 3️⃣ Removes permission logic from components
|
|
129
221
|
|
|
130
|
-
3️⃣ Removes permission logic from components
|
|
131
222
|
❌ Before
|
|
132
223
|
|
|
133
|
-
```
|
|
224
|
+
```tsx
|
|
134
225
|
{user?.role === "admin" && <DeleteButton />}
|
|
135
226
|
```
|
|
136
227
|
|
|
@@ -142,10 +233,11 @@ This is domain language, not UI logic.
|
|
|
142
233
|
</Can>
|
|
143
234
|
```
|
|
144
235
|
|
|
145
|
-
|
|
236
|
+
Your components focus on **rendering**, not authorization.
|
|
237
|
+
|
|
238
|
+
---
|
|
146
239
|
|
|
147
|
-
4️⃣
|
|
148
|
-
This is huge.
|
|
240
|
+
### 4️⃣ Type-safe permissions (TypeScript win)
|
|
149
241
|
|
|
150
242
|
❌ Without typing
|
|
151
243
|
|
|
@@ -160,56 +252,39 @@ can("updtae", "Invioce");
|
|
|
160
252
|
// ❌ TypeScript error immediately
|
|
161
253
|
```
|
|
162
254
|
|
|
163
|
-
This
|
|
164
|
-
|
|
165
|
-
5️⃣ Makes ownership rules first-class (not hacks)
|
|
166
|
-
Ownership checks are usually scattered:
|
|
255
|
+
This removes an entire class of bugs.
|
|
167
256
|
|
|
168
|
-
|
|
169
|
-
if (invoice.ownerId === user.id)
|
|
170
|
-
```
|
|
257
|
+
---
|
|
171
258
|
|
|
172
|
-
|
|
259
|
+
### 5️⃣ Ownership rules become first-class
|
|
173
260
|
|
|
174
261
|
```ts
|
|
175
262
|
allow("update", "Invoice", invoice => invoice.ownerId === user.id);
|
|
176
263
|
```
|
|
177
264
|
|
|
178
|
-
Ownership logic
|
|
179
|
-
|
|
180
|
-
consistent
|
|
265
|
+
Ownership logic is now:
|
|
181
266
|
|
|
182
|
-
|
|
267
|
+
- consistent
|
|
268
|
+
- reusable
|
|
269
|
+
- testable
|
|
183
270
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
6️⃣ Makes SSR and hydration predictable
|
|
187
|
-
Without a system:
|
|
188
|
-
|
|
189
|
-
UI flickers
|
|
190
|
-
|
|
191
|
-
Buttons appear/disappear after hydration
|
|
271
|
+
---
|
|
192
272
|
|
|
193
|
-
|
|
273
|
+
### 6️⃣ Predictable SSR & hydration
|
|
194
274
|
|
|
195
|
-
|
|
275
|
+
- No permission flicker
|
|
276
|
+
- No server/client mismatch
|
|
277
|
+
- Same rules, same result everywhere
|
|
196
278
|
|
|
197
|
-
|
|
279
|
+
---
|
|
198
280
|
|
|
199
|
-
|
|
281
|
+
## What `<Can />` actually does
|
|
200
282
|
|
|
201
|
-
What the <Can /> component really is
|
|
202
283
|
It’s not magic.
|
|
203
284
|
|
|
204
285
|
It simply means:
|
|
205
286
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
Instead of:
|
|
209
|
-
```tsx
|
|
210
|
-
if (!canEdit) return null;
|
|
211
|
-
```
|
|
212
|
-
You write:
|
|
287
|
+
> Render children **only if the permission passes**
|
|
213
288
|
|
|
214
289
|
```tsx
|
|
215
290
|
<Can I="update" a="Invoice" this={invoice}>
|
|
@@ -219,68 +294,60 @@ You write:
|
|
|
219
294
|
|
|
220
295
|
That’s it.
|
|
221
296
|
|
|
222
|
-
|
|
223
|
-
This is important.
|
|
224
|
-
|
|
225
|
-
❌ Not an auth system
|
|
226
|
-
❌ Not a backend security layer
|
|
227
|
-
❌ Not a role manager UI
|
|
228
|
-
❌ Not a permission database
|
|
229
|
-
|
|
230
|
-
This package:
|
|
231
|
-
|
|
232
|
-
does not replace backend checks
|
|
233
|
-
|
|
234
|
-
does not handle authentication
|
|
297
|
+
---
|
|
235
298
|
|
|
236
|
-
|
|
299
|
+
## What this package is NOT
|
|
237
300
|
|
|
238
|
-
|
|
301
|
+
❌ Not an authentication system
|
|
302
|
+
❌ Not a backend security layer
|
|
303
|
+
❌ Not a role management UI
|
|
304
|
+
❌ Not a permission database
|
|
239
305
|
|
|
240
|
-
|
|
306
|
+
This package **does not**:
|
|
241
307
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
✅ B2B products
|
|
246
|
-
✅ Apps with ownership rules
|
|
247
|
-
✅ Teams larger than 1 developer
|
|
308
|
+
- replace backend checks
|
|
309
|
+
- handle authentication
|
|
310
|
+
- store users or roles
|
|
248
311
|
|
|
249
|
-
|
|
250
|
-
❌ Landing pages
|
|
251
|
-
❌ Simple blogs
|
|
252
|
-
❌ Apps with only admin / non-admin logic
|
|
312
|
+
It answers one question only:
|
|
253
313
|
|
|
254
|
-
|
|
255
|
-
Most developers:
|
|
314
|
+
> **“Given a user and a resource, is this action allowed?”**
|
|
256
315
|
|
|
257
|
-
|
|
316
|
+
---
|
|
258
317
|
|
|
259
|
-
|
|
318
|
+
## When this package makes sense
|
|
260
319
|
|
|
261
|
-
|
|
320
|
+
- ✅ SaaS dashboards
|
|
321
|
+
- ✅ Multi-role applications
|
|
322
|
+
- ✅ B2B products
|
|
323
|
+
- ✅ Ownership-based rules
|
|
324
|
+
- ✅ Teams larger than one developer
|
|
262
325
|
|
|
263
|
-
|
|
326
|
+
---
|
|
264
327
|
|
|
265
|
-
|
|
328
|
+
## When it’s overkill
|
|
266
329
|
|
|
267
|
-
|
|
330
|
+
- ❌ Landing pages
|
|
331
|
+
- ❌ Simple blogs
|
|
332
|
+
- ❌ Admin / non-admin only apps
|
|
268
333
|
|
|
269
|
-
|
|
334
|
+
---
|
|
270
335
|
|
|
271
|
-
|
|
336
|
+
## Final summary
|
|
272
337
|
|
|
273
|
-
|
|
274
|
-
This package solves one problem:
|
|
338
|
+
**React Ability solves one problem:**
|
|
275
339
|
|
|
276
|
-
|
|
340
|
+
> How do I express and use permissions in React without scattering fragile conditional logic everywhere?
|
|
277
341
|
|
|
278
|
-
It
|
|
342
|
+
It does this by:
|
|
279
343
|
|
|
280
|
-
centralizing permission rules
|
|
344
|
+
- centralizing permission rules
|
|
345
|
+
- typing actions and resources
|
|
346
|
+
- exposing a clean `can()` API
|
|
347
|
+
- providing `<Can />` for UI rendering
|
|
281
348
|
|
|
282
|
-
|
|
349
|
+
---
|
|
283
350
|
|
|
284
|
-
|
|
351
|
+
## Credits
|
|
285
352
|
|
|
286
|
-
|
|
353
|
+
Created by **Mohamed Ali Sraieb**
|