@rqdhw3n/react-auth-flow 1.0.4 → 1.0.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 +459 -631
- package/dist/index.cjs.js +1178 -537
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +132 -71
- package/dist/index.es.js +1144 -503
- package/dist/index.es.js.map +1 -1
- package/dist/style.css +415 -0
- package/package.json +26 -21
package/README.md
CHANGED
|
@@ -1,631 +1,459 @@
|
|
|
1
|
-
# @rqdhw3n/react-auth-flow
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
function App() {
|
|
37
|
-
return (
|
|
38
|
-
<
|
|
39
|
-
baseURL="
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return (
|
|
268
|
-
<
|
|
269
|
-
<
|
|
270
|
-
<
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (!
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
<
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
{
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
{
|
|
431
|
-
"
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
'X-API-Key': 'your-api-key',
|
|
461
|
-
'X-Client-Version': '1.0.0',
|
|
462
|
-
},
|
|
463
|
-
credentials: 'include', // for HttpOnly cookies
|
|
464
|
-
adapter: async (url, options) => {
|
|
465
|
-
// Custom request logic
|
|
466
|
-
console.log('Making request to:', url);
|
|
467
|
-
return fetch(url, options);
|
|
468
|
-
},
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
// Use in AuthProvider
|
|
472
|
-
<AuthProvider
|
|
473
|
-
baseURL="https://api.example.com"
|
|
474
|
-
// ... other config
|
|
475
|
-
>
|
|
476
|
-
{/* Your app */}
|
|
477
|
-
</AuthProvider>
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
## TypeScript Customization
|
|
481
|
-
|
|
482
|
-
Extend types for your application:
|
|
483
|
-
|
|
484
|
-
```tsx
|
|
485
|
-
import { AuthUser, AuthContextValue } from '@rqdhw3n/react-auth-flow';
|
|
486
|
-
|
|
487
|
-
// Extend the AuthUser type
|
|
488
|
-
interface AppUser extends AuthUser {
|
|
489
|
-
id: number;
|
|
490
|
-
name: string;
|
|
491
|
-
email: string;
|
|
492
|
-
roles: string[];
|
|
493
|
-
permissions: string[];
|
|
494
|
-
company?: string;
|
|
495
|
-
department?: string;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Use in your components
|
|
499
|
-
import { useAuth } from '@rqdhw3n/react-auth-flow';
|
|
500
|
-
|
|
501
|
-
function MyComponent() {
|
|
502
|
-
const { user } = useAuth();
|
|
503
|
-
const appUser = user as AppUser;
|
|
504
|
-
|
|
505
|
-
return <div>Working in {appUser.department}</div>;
|
|
506
|
-
}
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
## Styling Components
|
|
510
|
-
|
|
511
|
-
All form components use semantic class names for styling:
|
|
512
|
-
|
|
513
|
-
```css
|
|
514
|
-
.auth-form-group {
|
|
515
|
-
margin-bottom: 1rem;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
.auth-form-label {
|
|
519
|
-
display: block;
|
|
520
|
-
margin-bottom: 0.5rem;
|
|
521
|
-
font-weight: 500;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
.auth-form-input {
|
|
525
|
-
width: 100%;
|
|
526
|
-
padding: 0.5rem;
|
|
527
|
-
border: 1px solid #ccc;
|
|
528
|
-
border-radius: 4px;
|
|
529
|
-
font-size: 1rem;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
.auth-form-input:disabled {
|
|
533
|
-
background-color: #f5f5f5;
|
|
534
|
-
cursor: not-allowed;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
.auth-form-error {
|
|
538
|
-
color: #dc3545;
|
|
539
|
-
margin-top: 0.5rem;
|
|
540
|
-
font-size: 0.875rem;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
.auth-form-button {
|
|
544
|
-
padding: 0.5rem 1rem;
|
|
545
|
-
border: none;
|
|
546
|
-
border-radius: 4px;
|
|
547
|
-
font-size: 1rem;
|
|
548
|
-
cursor: pointer;
|
|
549
|
-
width: 100%;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
.auth-form-button-primary {
|
|
553
|
-
background-color: #007bff;
|
|
554
|
-
color: white;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
.auth-form-button-primary:hover {
|
|
558
|
-
background-color: #0056b3;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
.auth-form-button:disabled {
|
|
562
|
-
opacity: 0.5;
|
|
563
|
-
cursor: not-allowed;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
.auth-form-checkbox {
|
|
567
|
-
display: flex;
|
|
568
|
-
align-items: center;
|
|
569
|
-
gap: 0.5rem;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
.auth-form-checkbox .auth-form-label {
|
|
573
|
-
margin-bottom: 0;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
.auth-form-success {
|
|
577
|
-
padding: 1rem;
|
|
578
|
-
background-color: #d4edda;
|
|
579
|
-
border: 1px solid #c3e6cb;
|
|
580
|
-
border-radius: 4px;
|
|
581
|
-
color: #155724;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
.auth-form-success-message {
|
|
585
|
-
margin: 0;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
.auth-loading {
|
|
589
|
-
text-align: center;
|
|
590
|
-
padding: 2rem;
|
|
591
|
-
font-size: 1rem;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
.auth-forbidden {
|
|
595
|
-
padding: 2rem;
|
|
596
|
-
text-align: center;
|
|
597
|
-
color: #dc3545;
|
|
598
|
-
}
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
## Error Handling
|
|
602
|
-
|
|
603
|
-
The package normalizes all errors to a consistent format:
|
|
604
|
-
|
|
605
|
-
```tsx
|
|
606
|
-
import { useAuth, AuthError } from '@rqdhw3n/react-auth-flow';
|
|
607
|
-
|
|
608
|
-
function MyComponent() {
|
|
609
|
-
const { error } = useAuth();
|
|
610
|
-
|
|
611
|
-
if (error) {
|
|
612
|
-
const authError: AuthError = error;
|
|
613
|
-
console.log({
|
|
614
|
-
code: authError.code, // e.g., "INVALID_CREDENTIALS"
|
|
615
|
-
message: authError.message,
|
|
616
|
-
statusCode: authError.statusCode, // e.g., 401
|
|
617
|
-
details: authError.details, // additional error info
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
return <div>Status: {error?.message}</div>;
|
|
622
|
-
}
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
## License
|
|
626
|
-
|
|
627
|
-
MIT
|
|
628
|
-
|
|
629
|
-
## Support
|
|
630
|
-
|
|
631
|
-
For issues and feature requests, please visit the [GitHub repository](https://github.com/rqdhw3n/react-auth-flow).
|
|
1
|
+
# @rqdhw3n/react-auth-flow
|
|
2
|
+
|
|
3
|
+
Reusable auth flows for React apps with packaged UI, route guards, mock mode, theming, and cookie-friendly request handling.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @rqdhw3n/react-auth-flow react react-dom react-router-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package injects its own auth CSS automatically. Consumers do not need Tailwind or a manual CSS import.
|
|
12
|
+
|
|
13
|
+
## Basic setup
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import {
|
|
17
|
+
AuthLayout,
|
|
18
|
+
AuthProvider,
|
|
19
|
+
LoginForm,
|
|
20
|
+
ProtectedRoute,
|
|
21
|
+
} from "@rqdhw3n/react-auth-flow";
|
|
22
|
+
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
|
23
|
+
|
|
24
|
+
function LoginPage() {
|
|
25
|
+
return (
|
|
26
|
+
<AuthLayout
|
|
27
|
+
title="Welcome back"
|
|
28
|
+
subtitle="Sign in to continue"
|
|
29
|
+
brand={<strong>Acme</strong>}
|
|
30
|
+
>
|
|
31
|
+
<LoginForm />
|
|
32
|
+
</AuthLayout>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function App() {
|
|
37
|
+
return (
|
|
38
|
+
<BrowserRouter>
|
|
39
|
+
<AuthProvider baseURL="http://localhost:8000/api">
|
|
40
|
+
<Routes>
|
|
41
|
+
<Route path="/login" element={<LoginPage />} />
|
|
42
|
+
<Route
|
|
43
|
+
path="/dashboard"
|
|
44
|
+
element={
|
|
45
|
+
<ProtectedRoute redirectTo="/login">
|
|
46
|
+
<div>Dashboard</div>
|
|
47
|
+
</ProtectedRoute>
|
|
48
|
+
}
|
|
49
|
+
/>
|
|
50
|
+
</Routes>
|
|
51
|
+
</AuthProvider>
|
|
52
|
+
</BrowserRouter>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`baseURL` and `baseUrl` are both supported.
|
|
58
|
+
|
|
59
|
+
## Mock mode local test
|
|
60
|
+
|
|
61
|
+
Use mock mode when you want the package to work without a backend.
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { AuthProvider } from "@rqdhw3n/react-auth-flow";
|
|
65
|
+
|
|
66
|
+
export function Root() {
|
|
67
|
+
return (
|
|
68
|
+
<AuthProvider mock mockStorageKey="rq-auth-user">
|
|
69
|
+
<App />
|
|
70
|
+
</AuthProvider>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Mock behavior:
|
|
76
|
+
|
|
77
|
+
- `login()` creates a fake admin user and stores it in `localStorage`
|
|
78
|
+
- `register()` creates a fake normal user and stores it in `localStorage`
|
|
79
|
+
- `logout()` clears the fake session
|
|
80
|
+
- `restoreSession()` and `me()` read the fake session back
|
|
81
|
+
- `forgotPassword()`, `resetPassword()`, and `verifyEmail()` return success
|
|
82
|
+
- `verifyTwoFactor()` accepts any code with the configured length
|
|
83
|
+
|
|
84
|
+
Custom mock user:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<AuthProvider
|
|
88
|
+
mock
|
|
89
|
+
mockStorageKey="rq-auth-user"
|
|
90
|
+
mockUser={{
|
|
91
|
+
id: 99,
|
|
92
|
+
name: "Demo User",
|
|
93
|
+
email: "demo@example.com",
|
|
94
|
+
roles: ["admin"],
|
|
95
|
+
permissions: ["users.manage", "billing.edit"],
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<App />
|
|
99
|
+
</AuthProvider>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Custom requestAdapter
|
|
103
|
+
|
|
104
|
+
If `mock` is `true`, mock mode wins. Otherwise the provider uses:
|
|
105
|
+
|
|
106
|
+
1. `requestAdapter`
|
|
107
|
+
2. built-in `fetch`
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { AuthProvider, type AuthRequestAdapter } from "@rqdhw3n/react-auth-flow";
|
|
111
|
+
import axios from "axios";
|
|
112
|
+
|
|
113
|
+
const requestAdapter: AuthRequestAdapter = async ({
|
|
114
|
+
endpoint,
|
|
115
|
+
method,
|
|
116
|
+
data,
|
|
117
|
+
headers,
|
|
118
|
+
}) => {
|
|
119
|
+
const response = await axios({
|
|
120
|
+
url: `http://localhost:8000/api${endpoint}`,
|
|
121
|
+
method,
|
|
122
|
+
data,
|
|
123
|
+
withCredentials: true,
|
|
124
|
+
headers,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return response.data;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
<AuthProvider requestAdapter={requestAdapter}>
|
|
131
|
+
<App />
|
|
132
|
+
</AuthProvider>;
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Theme customization
|
|
136
|
+
|
|
137
|
+
Theme values are applied through CSS variables on the provider wrapper.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<AuthProvider
|
|
141
|
+
baseURL="http://localhost:8000/api"
|
|
142
|
+
theme={{
|
|
143
|
+
primaryColor: "#0f766e",
|
|
144
|
+
primaryHoverColor: "#115e59",
|
|
145
|
+
radius: "20px",
|
|
146
|
+
fontFamily: "'Manrope', sans-serif",
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
<App />
|
|
150
|
+
</AuthProvider>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Available theme keys:
|
|
154
|
+
|
|
155
|
+
- `primaryColor`
|
|
156
|
+
- `primaryHoverColor`
|
|
157
|
+
- `radius`
|
|
158
|
+
- `fontFamily`
|
|
159
|
+
|
|
160
|
+
## AuthLayout usage
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
import { AuthLayout, LoginForm } from "@rqdhw3n/react-auth-flow";
|
|
164
|
+
|
|
165
|
+
function LoginPage() {
|
|
166
|
+
return (
|
|
167
|
+
<AuthLayout
|
|
168
|
+
title="Welcome back"
|
|
169
|
+
subtitle="Login to continue"
|
|
170
|
+
brand={<img src="/logo.svg" alt="Brand" height={32} />}
|
|
171
|
+
footer={<a href="/register">Create account</a>}
|
|
172
|
+
>
|
|
173
|
+
<LoginForm />
|
|
174
|
+
</AuthLayout>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Login/Register/Forgot/Reset forms
|
|
180
|
+
|
|
181
|
+
Included components:
|
|
182
|
+
|
|
183
|
+
- `LoginForm`
|
|
184
|
+
- `RegisterForm`
|
|
185
|
+
- `ForgotPasswordForm`
|
|
186
|
+
- `ResetPasswordForm`
|
|
187
|
+
- `VerifyEmailForm`
|
|
188
|
+
|
|
189
|
+
Example:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import {
|
|
193
|
+
ForgotPasswordForm,
|
|
194
|
+
LoginForm,
|
|
195
|
+
RegisterForm,
|
|
196
|
+
ResetPasswordForm,
|
|
197
|
+
VerifyEmailForm,
|
|
198
|
+
} from "@rqdhw3n/react-auth-flow";
|
|
199
|
+
|
|
200
|
+
<LoginForm onSuccess={(user) => console.log("logged in", user)} />;
|
|
201
|
+
|
|
202
|
+
<RegisterForm onSuccess={(user) => console.log("registered", user)} />;
|
|
203
|
+
|
|
204
|
+
<ForgotPasswordForm onSuccess={() => console.log("email sent")} />;
|
|
205
|
+
|
|
206
|
+
<ResetPasswordForm token="reset-token-from-url" />;
|
|
207
|
+
|
|
208
|
+
<VerifyEmailForm token="email-token" email="hello@example.com" />;
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## ProtectedRoute
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
import { ProtectedRoute } from "@rqdhw3n/react-auth-flow";
|
|
215
|
+
|
|
216
|
+
<ProtectedRoute
|
|
217
|
+
redirectTo="/login"
|
|
218
|
+
unauthorizedTo="/forbidden"
|
|
219
|
+
roles={["admin"]}
|
|
220
|
+
permissions={["users.manage", "billing.edit"]}
|
|
221
|
+
requireAllPermissions={true}
|
|
222
|
+
fallback={<div>Checking session...</div>}
|
|
223
|
+
>
|
|
224
|
+
<AdminDashboard />
|
|
225
|
+
</ProtectedRoute>;
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Behavior:
|
|
229
|
+
|
|
230
|
+
- loading: renders `fallback`
|
|
231
|
+
- not authenticated: redirects to `redirectTo`
|
|
232
|
+
- authenticated but unauthorized: redirects to `unauthorizedTo` or `redirectTo`
|
|
233
|
+
- `requireAllPermissions={false}` switches permission checks to "any match"
|
|
234
|
+
|
|
235
|
+
## GuestRoute
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
import { GuestRoute } from "@rqdhw3n/react-auth-flow";
|
|
239
|
+
|
|
240
|
+
<GuestRoute redirectTo="/" fallback={<div>Loading...</div>}>
|
|
241
|
+
<LoginPage />
|
|
242
|
+
</GuestRoute>;
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Authenticated users are redirected away from guest-only pages like login and register.
|
|
246
|
+
|
|
247
|
+
## Roles and permissions helpers
|
|
248
|
+
|
|
249
|
+
`useAuth()` now exposes:
|
|
250
|
+
|
|
251
|
+
- `hasRole(role)`
|
|
252
|
+
- `hasAnyRole(roles)`
|
|
253
|
+
- `hasPermission(permission)`
|
|
254
|
+
- `hasAnyPermission(permissions)`
|
|
255
|
+
- `hasAllPermissions(permissions)`
|
|
256
|
+
- `refreshSession()`
|
|
257
|
+
- `restoreSession()`
|
|
258
|
+
- `clearError()`
|
|
259
|
+
- `setError()`
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
import { useAuth } from "@rqdhw3n/react-auth-flow";
|
|
263
|
+
|
|
264
|
+
function Toolbar() {
|
|
265
|
+
const { user, hasRole, hasAnyPermission, logout } = useAuth();
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div>
|
|
269
|
+
<span>{user?.name}</span>
|
|
270
|
+
{hasRole("admin") && <button>Admin</button>}
|
|
271
|
+
{hasAnyPermission(["billing.edit", "users.manage"]) && (
|
|
272
|
+
<button>Manage</button>
|
|
273
|
+
)}
|
|
274
|
+
<button onClick={logout}>Logout</button>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## OTP / 2FA
|
|
281
|
+
|
|
282
|
+
Included components:
|
|
283
|
+
|
|
284
|
+
- `OtpInput`
|
|
285
|
+
- `TwoFactorForm`
|
|
286
|
+
|
|
287
|
+
Provider support:
|
|
288
|
+
|
|
289
|
+
- endpoint: `twoFactorVerify`, default `/auth/2fa/verify`
|
|
290
|
+
- hook method: `verifyTwoFactor({ code })`
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
import { TwoFactorForm } from "@rqdhw3n/react-auth-flow";
|
|
294
|
+
|
|
295
|
+
<TwoFactorForm
|
|
296
|
+
length={6}
|
|
297
|
+
title="Enter your code"
|
|
298
|
+
subtitle="Check your authenticator app"
|
|
299
|
+
onSuccess={(response) => console.log(response)}
|
|
300
|
+
/>;
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Using the hook directly:
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
import { useAuth } from "@rqdhw3n/react-auth-flow";
|
|
307
|
+
|
|
308
|
+
function VerifyButton() {
|
|
309
|
+
const { verifyTwoFactor } = useAuth();
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<button onClick={() => verifyTwoFactor({ code: "123456" })}>
|
|
313
|
+
Verify
|
|
314
|
+
</button>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## SocialLoginButton
|
|
320
|
+
|
|
321
|
+
UI-only social button component. No OAuth flow is built in.
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
import { SocialLoginButton } from "@rqdhw3n/react-auth-flow";
|
|
325
|
+
|
|
326
|
+
<SocialLoginButton provider="google" onClick={() => startGoogleLogin()} />;
|
|
327
|
+
<SocialLoginButton provider="github" label="Continue with GitHub" />;
|
|
328
|
+
<SocialLoginButton provider="custom" icon={<span>SAML</span>} label="SSO" />;
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## HttpOnly cookie JWT backend example
|
|
332
|
+
|
|
333
|
+
This package works well with backends that issue cookies instead of exposing tokens to the browser.
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
// Example request adapter when the backend uses HttpOnly cookies
|
|
337
|
+
const requestAdapter = async ({ endpoint, method, data, headers }) => {
|
|
338
|
+
const response = await fetch(`https://api.example.com${endpoint}`, {
|
|
339
|
+
method,
|
|
340
|
+
credentials: "include",
|
|
341
|
+
headers: {
|
|
342
|
+
"Content-Type": "application/json",
|
|
343
|
+
...headers,
|
|
344
|
+
},
|
|
345
|
+
body: data ? JSON.stringify(data) : undefined,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
throw await response.json();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return response.status === 204 ? {} : await response.json();
|
|
353
|
+
};
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Expected backend flow:
|
|
357
|
+
|
|
358
|
+
- `POST /auth/login` sets the auth cookie and returns `{ user }`
|
|
359
|
+
- `GET /auth/me` returns `{ user }`
|
|
360
|
+
- `POST /auth/logout` clears the cookie
|
|
361
|
+
- `POST /auth/refresh` rotates the session and returns `{ user }`
|
|
362
|
+
|
|
363
|
+
## Laravel example
|
|
364
|
+
|
|
365
|
+
```php
|
|
366
|
+
// routes/api.php
|
|
367
|
+
Route::post('/auth/login', [AuthController::class, 'login']);
|
|
368
|
+
Route::post('/auth/register', [AuthController::class, 'register']);
|
|
369
|
+
Route::middleware('auth:sanctum')->group(function () {
|
|
370
|
+
Route::get('/auth/me', [AuthController::class, 'me']);
|
|
371
|
+
Route::post('/auth/logout', [AuthController::class, 'logout']);
|
|
372
|
+
Route::post('/auth/refresh', [AuthController::class, 'refresh']);
|
|
373
|
+
Route::post('/auth/2fa/verify', [AuthController::class, 'verifyTwoFactor']);
|
|
374
|
+
});
|
|
375
|
+
Route::post('/auth/forgot-password', [PasswordController::class, 'forgot']);
|
|
376
|
+
Route::post('/auth/reset-password', [PasswordController::class, 'reset']);
|
|
377
|
+
Route::post('/auth/verify-email', [VerificationController::class, 'verify']);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Typical provider setup with Sanctum:
|
|
381
|
+
|
|
382
|
+
```tsx
|
|
383
|
+
<AuthProvider baseURL="http://localhost:8000/api">
|
|
384
|
+
<App />
|
|
385
|
+
</AuthProvider>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Express fake backend example
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
import cookieParser from "cookie-parser";
|
|
392
|
+
import express from "express";
|
|
393
|
+
|
|
394
|
+
const app = express();
|
|
395
|
+
|
|
396
|
+
app.use(express.json());
|
|
397
|
+
app.use(cookieParser());
|
|
398
|
+
|
|
399
|
+
app.post("/api/auth/login", (_req, res) => {
|
|
400
|
+
res.cookie("session", "demo-session", { httpOnly: true, sameSite: "lax" });
|
|
401
|
+
res.json({
|
|
402
|
+
user: {
|
|
403
|
+
id: 1,
|
|
404
|
+
name: "Admin Test",
|
|
405
|
+
email: "admin@example.com",
|
|
406
|
+
roles: ["admin"],
|
|
407
|
+
permissions: ["users.manage", "billing.edit"],
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
app.get("/api/auth/me", (req, res) => {
|
|
413
|
+
if (!req.cookies.session) {
|
|
414
|
+
return res.status(401).json({
|
|
415
|
+
error: { code: "UNAUTHENTICATED", message: "Unauthenticated" },
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return res.json({
|
|
420
|
+
user: {
|
|
421
|
+
id: 1,
|
|
422
|
+
name: "Admin Test",
|
|
423
|
+
email: "admin@example.com",
|
|
424
|
+
roles: ["admin"],
|
|
425
|
+
permissions: ["users.manage", "billing.edit"],
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
app.post("/api/auth/logout", (_req, res) => {
|
|
431
|
+
res.clearCookie("session");
|
|
432
|
+
res.status(204).end();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
app.post("/api/auth/forgot-password", (_req, res) => {
|
|
436
|
+
res.json({ success: true, message: "Reset email queued" });
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
app.post("/api/auth/reset-password", (_req, res) => {
|
|
440
|
+
res.json({ success: true, message: "Password updated" });
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
app.post("/api/auth/verify-email", (_req, res) => {
|
|
444
|
+
res.json({ success: true, message: "Email verified" });
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
app.post("/api/auth/2fa/verify", (_req, res) => {
|
|
448
|
+
res.json({ success: true, message: "2FA verified" });
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
app.listen(8000);
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Notes
|
|
455
|
+
|
|
456
|
+
- `AuthProvider` restores the session on mount by default. Disable with `autoRestore={false}`.
|
|
457
|
+
- React 18 and React 19 are supported through peer dependencies.
|
|
458
|
+
- React, React DOM, React Router, and `react/jsx-runtime` are externalized from the build.
|
|
459
|
+
- The published package includes `dist/style.css` and also injects the auth CSS automatically from the package entry.
|