@massimo.mazzoleni/cognito-max 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +2410 -0
  2. package/dist/chunk-AD7T42HJ.js +3 -0
  3. package/dist/chunk-AD7T42HJ.js.map +1 -0
  4. package/dist/chunk-DKPFVGTY.js +683 -0
  5. package/dist/chunk-DKPFVGTY.js.map +1 -0
  6. package/dist/chunk-N4OQLBV6.js +135 -0
  7. package/dist/chunk-N4OQLBV6.js.map +1 -0
  8. package/dist/client-63FraVdm.d.ts +69 -0
  9. package/dist/client-BAoL8h4E.d.cts +69 -0
  10. package/dist/core/index.cjs +696 -0
  11. package/dist/core/index.cjs.map +1 -0
  12. package/dist/core/index.d.cts +3 -0
  13. package/dist/core/index.d.ts +3 -0
  14. package/dist/core/index.js +4 -0
  15. package/dist/core/index.js.map +1 -0
  16. package/dist/errors-BkUDHleb.d.cts +22 -0
  17. package/dist/errors-BkUDHleb.d.ts +22 -0
  18. package/dist/index.cjs +696 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.cts +3 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.js +4 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/react/index.cjs +844 -0
  25. package/dist/react/index.cjs.map +1 -0
  26. package/dist/react/index.d.cts +104 -0
  27. package/dist/react/index.d.ts +104 -0
  28. package/dist/react/index.js +64 -0
  29. package/dist/react/index.js.map +1 -0
  30. package/dist/types-bxA1vonL.d.cts +113 -0
  31. package/dist/types-bxA1vonL.d.ts +113 -0
  32. package/dist/ui/index.cjs +1183 -0
  33. package/dist/ui/index.cjs.map +1 -0
  34. package/dist/ui/index.d.cts +241 -0
  35. package/dist/ui/index.d.ts +241 -0
  36. package/dist/ui/index.js +1109 -0
  37. package/dist/ui/index.js.map +1 -0
  38. package/package.json +81 -0
  39. package/src/core/client.ts +604 -0
  40. package/src/core/errors.ts +91 -0
  41. package/src/core/event-bus.ts +41 -0
  42. package/src/core/index.ts +5 -0
  43. package/src/core/internal/converters.ts +32 -0
  44. package/src/core/storage.ts +79 -0
  45. package/src/core/types.ts +87 -0
  46. package/src/index.ts +1 -0
  47. package/src/react/components/ProtectedRoute.tsx +56 -0
  48. package/src/react/context.tsx +126 -0
  49. package/src/react/hooks/useAuth.ts +75 -0
  50. package/src/react/hooks/useMfa.ts +19 -0
  51. package/src/react/hooks/useSession.ts +16 -0
  52. package/src/react/hooks/useUser.ts +24 -0
  53. package/src/react/index.ts +10 -0
  54. package/src/ui/components/ChangePasswordForm.tsx +105 -0
  55. package/src/ui/components/ForgotPasswordForm.tsx +159 -0
  56. package/src/ui/components/MfaSetupWizard.tsx +136 -0
  57. package/src/ui/components/RegisterForm.tsx +159 -0
  58. package/src/ui/components/SignInForm.tsx +296 -0
  59. package/src/ui/hooks/useChangePasswordForm.ts +81 -0
  60. package/src/ui/hooks/useForgotPasswordForm.ts +109 -0
  61. package/src/ui/hooks/useMfaSetup.ts +93 -0
  62. package/src/ui/hooks/useRegisterForm.ts +120 -0
  63. package/src/ui/hooks/useSignInForm.ts +245 -0
  64. package/src/ui/index.ts +31 -0
package/README.md ADDED
@@ -0,0 +1,2410 @@
1
+ # cognito-max
2
+
3
+ AWS Cognito authentication library for React. Handles SRP login, MFA (TOTP + SMS), session auto-refresh, password flows, and user attribute management — all with full TypeScript types.
4
+
5
+ ```bash
6
+ npm install cognito-max
7
+ ```
8
+
9
+ **Peer dependencies**: `react >= 18` (optional — the core works without React)
10
+
11
+ ---
12
+
13
+ ## Table of contents
14
+
15
+ - [Prerequisites](#prerequisites)
16
+ - [Quick start](#quick-start)
17
+ - [Entry points](#entry-points)
18
+ - [Configuration](#configuration)
19
+ - [React integration](#react-integration)
20
+ - [AuthProvider](#authprovider)
21
+ - [useAuth](#useauth)
22
+ - [useSession](#usesession)
23
+ - [useUser](#useuser)
24
+ - [useMfa](#usemfa)
25
+ - [ProtectedRoute](#protectedroute)
26
+ - [useRequireAuth](#userequireauth)
27
+ - [Authentication flows](#authentication-flows)
28
+ - [Sign in](#sign-in)
29
+ - [MFA challenge (TOTP / SMS)](#mfa-challenge-totp--sms)
30
+ - [Forced password change](#forced-password-change)
31
+ - [TOTP setup during login](#totp-setup-during-login)
32
+ - [Forgot password](#forgot-password)
33
+ - [Sign up](#sign-up)
34
+ - [UI layer](#ui-layer)
35
+ - [Ready-made components](#ready-made-components)
36
+ - [Headless hooks](#headless-hooks)
37
+ - [Core API](#core-api)
38
+ - [Storage adapters](#storage-adapters)
39
+ - [Events](#events)
40
+ - [Error handling](#error-handling)
41
+ - [TypeScript reference](#typescript-reference)
42
+
43
+ ---
44
+
45
+ ## Prerequisites
46
+
47
+ Your Cognito User Pool App Client must have these **explicit auth flows** enabled:
48
+
49
+ | Flow | Why |
50
+ |------|-----|
51
+ | `ALLOW_USER_SRP_AUTH` | SRP login (the default flow used by this library) |
52
+ | `ALLOW_REFRESH_TOKEN_AUTH` | Automatic token refresh |
53
+ | `ALLOW_USER_PASSWORD_AUTH` | Optional — only if you use `USER_PASSWORD_AUTH` directly |
54
+
55
+ Enable them in the AWS Console → User Pools → App clients → Edit, or with the CLI:
56
+
57
+ ```bash
58
+ aws cognito-idp update-user-pool-client \
59
+ --user-pool-id YOUR_POOL_ID \
60
+ --client-id YOUR_CLIENT_ID \
61
+ --region YOUR_REGION \
62
+ --explicit-auth-flows \
63
+ ALLOW_USER_SRP_AUTH \
64
+ ALLOW_REFRESH_TOKEN_AUTH
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Quick start
70
+
71
+ ```tsx
72
+ import { AuthProvider, useAuth } from 'cognito-max/react'
73
+
74
+ const config = {
75
+ userPoolId: 'eu-west-1_xxxxxxxxx',
76
+ clientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
77
+ region: 'eu-west-1',
78
+ }
79
+
80
+ function App() {
81
+ return (
82
+ <AuthProvider config={config}>
83
+ <LoginPage />
84
+ </AuthProvider>
85
+ )
86
+ }
87
+
88
+ function LoginPage() {
89
+ const { signIn, isAuthenticated, user } = useAuth()
90
+
91
+ if (isAuthenticated) return <p>Welcome, {user?.email}</p>
92
+
93
+ return (
94
+ <button onClick={() => signIn('user@example.com', 'password')}>
95
+ Sign in
96
+ </button>
97
+ )
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Entry points
104
+
105
+ The package is split into four tree-shakeable entry points so you only bundle what you use:
106
+
107
+ | Import | Contents |
108
+ |--------|----------|
109
+ | `cognito-max` | Re-exports everything (convenience) |
110
+ | `cognito-max/core` | `CognitoAuthClient`, types, errors, storage adapters |
111
+ | `cognito-max/react` | `AuthProvider`, all hooks, `ProtectedRoute` |
112
+ | `cognito-max/ui` | Headless hooks + ready-made HTML components |
113
+
114
+ ```ts
115
+ // Only import the React layer
116
+ import { AuthProvider, useAuth } from 'cognito-max/react'
117
+
118
+ // Only use the vanilla client (no React)
119
+ import { CognitoAuthClient } from 'cognito-max/core'
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Configuration
125
+
126
+ ```ts
127
+ import type { AuthConfig } from 'cognito-max/core'
128
+
129
+ const config: AuthConfig = {
130
+ // Required
131
+ userPoolId: 'eu-west-1_xxxxxxxxx',
132
+ clientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
133
+ region: 'eu-west-1',
134
+
135
+ // Optional
136
+ autoRefresh: true, // Renew token before expiry. Default: true
137
+ refreshMarginSeconds: 300, // Refresh 5 min before expiry. Default: 300
138
+ totpIssuer: 'MyApp', // Label in authenticator apps. Default: clientId
139
+ storage: new LocalStorageAdapter(), // Default: AutoStorageAdapter
140
+ }
141
+ ```
142
+
143
+ ### `AuthConfig` fields
144
+
145
+ | Field | Type | Default | Description |
146
+ |-------|------|---------|-------------|
147
+ | `userPoolId` | `string` | required | Cognito User Pool ID |
148
+ | `clientId` | `string` | required | App Client ID |
149
+ | `region` | `string` | required | AWS region (e.g. `eu-west-1`) |
150
+ | `autoRefresh` | `boolean` | `true` | Auto-renew the session before expiry |
151
+ | `refreshMarginSeconds` | `number` | `300` | How many seconds before expiry to trigger refresh |
152
+ | `totpIssuer` | `string` | `clientId` | Label shown in Google Authenticator / Authy |
153
+ | `storage` | `StorageAdapter` | `AutoStorageAdapter` | Where to persist tokens |
154
+
155
+ ---
156
+
157
+ ## React integration
158
+
159
+ ### AuthProvider
160
+
161
+ Wrap your application (or the auth-protected section) with `AuthProvider`. It initializes the Cognito client, restores any existing session from storage, and provides the auth context to all child components.
162
+
163
+ ```tsx
164
+ import { AuthProvider } from 'cognito-max/react'
165
+
166
+ function App() {
167
+ return (
168
+ <AuthProvider config={config}>
169
+ <Router />
170
+ </AuthProvider>
171
+ )
172
+ }
173
+ ```
174
+
175
+ `AuthProvider` takes a single `config` prop (an `AuthConfig` object). The config is read once at mount; changes after mount are ignored.
176
+
177
+ ---
178
+
179
+ ### useAuth
180
+
181
+ The main hook. Returns auth state + all authentication actions.
182
+
183
+ ```ts
184
+ import { useAuth } from 'cognito-max/react'
185
+
186
+ const {
187
+ // State
188
+ user, // AuthUser | null
189
+ state, // AuthState
190
+ isLoading, // boolean — true during session initialization
191
+ isAuthenticated, // boolean
192
+
193
+ // Actions (all return Promises)
194
+ signIn,
195
+ signOut,
196
+ signUp,
197
+ confirmSignUp,
198
+ resendConfirmationCode,
199
+ forgotPassword,
200
+ confirmForgotPassword,
201
+ changePassword,
202
+ respondToMfaChallenge,
203
+ respondToNewPasswordChallenge,
204
+ setupTotpChallenge,
205
+ verifyTotpChallenge,
206
+
207
+ // Raw client for advanced use
208
+ client,
209
+ } = useAuth()
210
+ ```
211
+
212
+ #### `signIn(email, password)`
213
+
214
+ Returns a `SignInResult` discriminated union:
215
+
216
+ ```ts
217
+ const result = await signIn('user@example.com', 'password')
218
+
219
+ switch (result.status) {
220
+ case 'SUCCESS':
221
+ // result.user: AuthUser
222
+ // result.session: AuthSession
223
+ break
224
+
225
+ case 'MFA_REQUIRED':
226
+ // result.mfaType: 'TOTP' | 'SMS'
227
+ // result.challengeSession: string ← pass to respondToMfaChallenge
228
+ break
229
+
230
+ case 'NEW_PASSWORD_REQUIRED':
231
+ // result.requiredAttributes: string[]
232
+ // result.challengeSession: string ← pass to respondToNewPasswordChallenge
233
+ break
234
+
235
+ case 'MFA_SETUP_REQUIRED':
236
+ // result.challengeSession: string ← pass to setupTotpChallenge / verifyTotpChallenge
237
+ break
238
+
239
+ case 'CONFIRM_SIGNUP':
240
+ // Account not yet confirmed — prompt for email verification code
241
+ break
242
+ }
243
+ ```
244
+
245
+ #### `signOut(global?)`
246
+
247
+ ```ts
248
+ await signOut() // Local sign-out (clears tokens from storage)
249
+ await signOut(true) // Global sign-out (invalidates all sessions on Cognito)
250
+ ```
251
+
252
+ #### `changePassword(currentPassword, newPassword)`
253
+
254
+ ```ts
255
+ await changePassword('OldPass123!', 'NewPass456!')
256
+ ```
257
+
258
+ ---
259
+
260
+ ### useSession
261
+
262
+ Returns the current session tokens and expiry information.
263
+
264
+ ```ts
265
+ import { useSession } from 'cognito-max/react'
266
+
267
+ const {
268
+ accessToken, // string | null
269
+ idToken, // string | null
270
+ refreshToken, // string | null
271
+ expiresAt, // Date | null
272
+ isExpired, // boolean
273
+ isAuthenticated,// boolean
274
+ refresh, // () => Promise<AuthSession> — force token refresh
275
+ } = useSession()
276
+ ```
277
+
278
+ Use `accessToken` or `idToken` as the `Authorization: Bearer` header in API calls.
279
+
280
+ ```ts
281
+ // Example with fetch
282
+ const response = await fetch('/api/data', {
283
+ headers: { Authorization: `Bearer ${accessToken}` }
284
+ })
285
+ ```
286
+
287
+ ---
288
+
289
+ ### useUser
290
+
291
+ Read and update the current user's Cognito profile.
292
+
293
+ ```ts
294
+ import { useUser } from 'cognito-max/react'
295
+
296
+ const {
297
+ user, // AuthUser | null
298
+ attributes, // Record<string, string> — all Cognito attributes
299
+ groups, // string[] — Cognito groups the user belongs to
300
+
301
+ update, // (attrs: Record<string, string>) => Promise<void>
302
+ verifyAttribute, // (attr: 'email' | 'phone_number', code: string) => Promise<void>
303
+ sendVerificationCode,// (attr: 'email' | 'phone_number') => Promise<void>
304
+ refreshAttributes, // () => Promise<Record<string, string>>
305
+ deleteAccount, // () => Promise<void>
306
+ } = useUser()
307
+ ```
308
+
309
+ ```ts
310
+ // Update the user's display name
311
+ await update({ name: 'Mario Rossi' })
312
+
313
+ // Trigger email re-verification
314
+ await sendVerificationCode('email')
315
+ // User receives code by email, then:
316
+ await verifyAttribute('email', '123456')
317
+ ```
318
+
319
+ ---
320
+
321
+ ### useMfa
322
+
323
+ Manage TOTP and SMS second-factor settings for an already-authenticated user.
324
+
325
+ ```ts
326
+ import { useMfa } from 'cognito-max/react'
327
+
328
+ const {
329
+ setup, // () => Promise<MfaSetupResult> — associate TOTP device
330
+ verifySetup, // (code: string) => Promise<void> — confirm TOTP device
331
+ getPreference,// () => Promise<MfaPreference>
332
+ setPreference,// (type: 'TOTP' | 'SMS') => Promise<void>
333
+ disable, // () => Promise<void>
334
+ } = useMfa()
335
+ ```
336
+
337
+ ```ts
338
+ // Set up TOTP for the first time
339
+ const { secretCode, qrCodeUri } = await setup()
340
+ // Show the QR code (qrCodeUri) to the user — they scan it with their authenticator app
341
+ // Then ask for the 6-digit code to confirm:
342
+ await verifySetup('123456')
343
+
344
+ // Switch from TOTP to SMS
345
+ await setPreference('SMS')
346
+
347
+ // Read current MFA state
348
+ const pref = await getPreference()
349
+ // pref.totp → boolean, pref.sms → boolean, pref.preferred → 'TOTP' | 'SMS' | null
350
+ ```
351
+
352
+ ---
353
+
354
+ ### ProtectedRoute
355
+
356
+ A React component that renders children only when the user is authenticated. Optionally enforces Cognito group membership.
357
+
358
+ ```tsx
359
+ import { ProtectedRoute } from 'cognito-max/react'
360
+ import { Navigate } from 'react-router-dom'
361
+
362
+ // Basic protection
363
+ <ProtectedRoute fallback={<Navigate to="/login" replace />}>
364
+ <Dashboard />
365
+ </ProtectedRoute>
366
+
367
+ // Group-based access control
368
+ <ProtectedRoute
369
+ requiredGroups={['admin', 'superuser']}
370
+ fallback={<Navigate to="/unauthorized" replace />}
371
+ loading={<Spinner />}
372
+ >
373
+ <AdminPanel />
374
+ </ProtectedRoute>
375
+ ```
376
+
377
+ | Prop | Type | Default | Description |
378
+ |------|------|---------|-------------|
379
+ | `children` | `ReactNode` | required | Rendered when auth passes |
380
+ | `fallback` | `ReactNode` | `null` | Rendered when not authenticated / no group access |
381
+ | `loading` | `ReactNode` | `null` | Rendered while auth state is initializing |
382
+ | `requiredGroups` | `string[]` | — | User must belong to at least one of these Cognito groups |
383
+
384
+ ---
385
+
386
+ ### useRequireAuth
387
+
388
+ Imperative alternative to `ProtectedRoute` for redirect logic inside components.
389
+
390
+ ```ts
391
+ import { useRequireAuth } from 'cognito-max/react'
392
+ import { useNavigate } from 'react-router-dom'
393
+
394
+ function AdminPage() {
395
+ const navigate = useNavigate()
396
+ const { isAllowed, isLoading } = useRequireAuth({ requiredGroups: ['admin'] })
397
+
398
+ useEffect(() => {
399
+ if (!isLoading && !isAllowed) navigate('/login')
400
+ }, [isLoading, isAllowed])
401
+
402
+ if (isLoading) return <Spinner />
403
+ return <AdminContent />
404
+ }
405
+ ```
406
+
407
+ ---
408
+
409
+ ## Authentication flows
410
+
411
+ ### Sign in
412
+
413
+ ```tsx
414
+ function LoginForm() {
415
+ const { signIn } = useAuth()
416
+ const navigate = useNavigate()
417
+
418
+ async function handleSubmit(email: string, password: string) {
419
+ const result = await signIn(email, password)
420
+
421
+ if (result.status === 'SUCCESS') {
422
+ navigate('/dashboard')
423
+ } else if (result.status === 'MFA_REQUIRED') {
424
+ navigate('/mfa', { state: result })
425
+ } else if (result.status === 'NEW_PASSWORD_REQUIRED') {
426
+ navigate('/new-password', { state: result })
427
+ } else if (result.status === 'MFA_SETUP_REQUIRED') {
428
+ navigate('/mfa-setup', { state: result })
429
+ }
430
+ }
431
+ }
432
+ ```
433
+
434
+ ---
435
+
436
+ ### MFA challenge (TOTP / SMS)
437
+
438
+ After `signIn` returns `MFA_REQUIRED`, the user must provide their 6-digit code:
439
+
440
+ ```tsx
441
+ function MfaPage() {
442
+ const { respondToMfaChallenge } = useAuth()
443
+ const { challengeSession, mfaType } = useLocation().state
444
+
445
+ async function handleCode(code: string) {
446
+ const result = await respondToMfaChallenge(challengeSession, code, mfaType)
447
+ if (result.status === 'SUCCESS') navigate('/dashboard')
448
+ }
449
+ }
450
+ ```
451
+
452
+ ---
453
+
454
+ ### Forced password change
455
+
456
+ After `signIn` returns `NEW_PASSWORD_REQUIRED`:
457
+
458
+ ```tsx
459
+ function NewPasswordPage() {
460
+ const { respondToNewPasswordChallenge } = useAuth()
461
+ const { challengeSession, requiredAttributes } = useLocation().state
462
+
463
+ async function handleSubmit(newPassword: string) {
464
+ const result = await respondToNewPasswordChallenge(
465
+ challengeSession,
466
+ newPassword,
467
+ // Pass any required attributes Cognito asks for:
468
+ { name: 'Mario Rossi' }
469
+ )
470
+ if (result.status === 'SUCCESS') navigate('/dashboard')
471
+ }
472
+ }
473
+ ```
474
+
475
+ ---
476
+
477
+ ### TOTP setup during login
478
+
479
+ When Cognito is configured to require TOTP before the first login (`MFA_SETUP_REQUIRED`), the user must set up their authenticator app as part of the sign-in flow — before a session exists:
480
+
481
+ ```tsx
482
+ function MfaSetupPage() {
483
+ const { setupTotpChallenge, verifyTotpChallenge } = useAuth()
484
+ const { challengeSession } = useLocation().state
485
+ const [qrUri, setQrUri] = useState<string | null>(null)
486
+
487
+ useEffect(() => {
488
+ setupTotpChallenge(challengeSession).then(({ qrCodeUri }) => setQrUri(qrCodeUri))
489
+ }, [])
490
+
491
+ async function handleCode(code: string) {
492
+ const result = await verifyTotpChallenge(challengeSession, code)
493
+ if (result.status === 'SUCCESS') navigate('/dashboard')
494
+ }
495
+
496
+ return (
497
+ <>
498
+ {qrUri && <img src={`https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(qrUri)}`} alt="Scan with authenticator" />}
499
+ <input placeholder="6-digit code" onKeyDown={(e) => e.key === 'Enter' && handleCode(e.currentTarget.value)} />
500
+ </>
501
+ )
502
+ }
503
+ ```
504
+
505
+ ---
506
+
507
+ ### Forgot password
508
+
509
+ ```tsx
510
+ // Step 1 — request reset code
511
+ const { forgotPassword } = useAuth()
512
+ await forgotPassword('user@example.com')
513
+ // Cognito sends a code to the user's email
514
+
515
+ // Step 2 — set new password
516
+ const { confirmForgotPassword } = useAuth()
517
+ await confirmForgotPassword('user@example.com', '123456', 'NewPass789!')
518
+ ```
519
+
520
+ ---
521
+
522
+ ### Sign up
523
+
524
+ ```tsx
525
+ const { signUp, confirmSignUp, resendConfirmationCode } = useAuth()
526
+
527
+ // Register
528
+ await signUp('user@example.com', 'Password123!', {
529
+ name: 'Mario Rossi',
530
+ // Any additional Cognito attributes
531
+ })
532
+
533
+ // Confirm email (code sent by Cognito)
534
+ await confirmSignUp('user@example.com', '654321')
535
+
536
+ // Didn't receive the code?
537
+ await resendConfirmationCode('user@example.com')
538
+ ```
539
+
540
+ ---
541
+
542
+ ## UI layer
543
+
544
+ Import from `cognito-max/ui`.
545
+
546
+ ### Ready-made components
547
+
548
+ Unstyled HTML components that wire up all the form logic internally. Apply your own CSS classes via the `className` props.
549
+
550
+ #### SignInForm
551
+
552
+ Handles the full sign-in flow including MFA and forced password change.
553
+
554
+ ```tsx
555
+ import { SignInForm } from 'cognito-max/ui'
556
+
557
+ <SignInForm
558
+ onSuccess={() => navigate('/dashboard')}
559
+ onForgotPassword={() => navigate('/forgot-password')}
560
+ />
561
+ ```
562
+
563
+ #### RegisterForm
564
+
565
+ ```tsx
566
+ import { RegisterForm } from 'cognito-max/ui'
567
+
568
+ <RegisterForm onSuccess={() => navigate('/confirm-email')} />
569
+ ```
570
+
571
+ #### ForgotPasswordForm
572
+
573
+ Two-step form: email input → code + new password.
574
+
575
+ ```tsx
576
+ import { ForgotPasswordForm } from 'cognito-max/ui'
577
+
578
+ <ForgotPasswordForm onSuccess={() => navigate('/login')} />
579
+ ```
580
+
581
+ #### ChangePasswordForm
582
+
583
+ For authenticated users who want to change their password.
584
+
585
+ ```tsx
586
+ import { ChangePasswordForm } from 'cognito-max/ui'
587
+
588
+ <ChangePasswordForm onSuccess={() => alert('Password changed')} />
589
+ ```
590
+
591
+ #### MfaSetupWizard
592
+
593
+ Guides the user through TOTP device association: show QR → enter code → confirm.
594
+
595
+ ```tsx
596
+ import { MfaSetupWizard } from 'cognito-max/ui'
597
+
598
+ <MfaSetupWizard onSuccess={() => navigate('/dashboard')} />
599
+ ```
600
+
601
+ ---
602
+
603
+ ### Headless hooks
604
+
605
+ If the default HTML components don't match your design system, use the headless hooks instead. They handle all state transitions and expose only the data and callbacks your UI needs.
606
+
607
+ #### useSignInForm
608
+
609
+ ```ts
610
+ import { useSignInForm } from 'cognito-max/ui'
611
+
612
+ const {
613
+ step, // 'credentials' | 'mfa' | 'new_password' | 'mfa_setup'
614
+ email, setEmail,
615
+ password, setPassword,
616
+ mfaCode, setMfaCode,
617
+ newPassword, setNewPassword,
618
+ confirmPassword, setConfirmPassword,
619
+ isLoading,
620
+ error, // string | null
621
+ submitCredentials, // () => Promise<void>
622
+ submitMfaCode, // () => Promise<void>
623
+ submitNewPassword, // () => Promise<void>
624
+ } = useSignInForm({ onSuccess: () => navigate('/dashboard') })
625
+ ```
626
+
627
+ #### useRegisterForm
628
+
629
+ ```ts
630
+ import { useRegisterForm } from 'cognito-max/ui'
631
+
632
+ const {
633
+ step, // 'register' | 'confirm'
634
+ email, setEmail,
635
+ password, setPassword,
636
+ code, setCode,
637
+ isLoading,
638
+ error,
639
+ submitRegister,
640
+ submitConfirmation,
641
+ resendCode,
642
+ } = useRegisterForm({ onSuccess: () => navigate('/login') })
643
+ ```
644
+
645
+ #### useForgotPasswordForm
646
+
647
+ ```ts
648
+ import { useForgotPasswordForm } from 'cognito-max/ui'
649
+
650
+ const {
651
+ step, // 'request' | 'reset'
652
+ email, setEmail,
653
+ code, setCode,
654
+ newPassword, setNewPassword,
655
+ isLoading,
656
+ error,
657
+ submitEmail,
658
+ submitReset,
659
+ } = useForgotPasswordForm({ onSuccess: () => navigate('/login') })
660
+ ```
661
+
662
+ #### useChangePasswordForm
663
+
664
+ ```ts
665
+ import { useChangePasswordForm } from 'cognito-max/ui'
666
+
667
+ const {
668
+ currentPassword, setCurrentPassword,
669
+ newPassword, setNewPassword,
670
+ confirmPassword, setConfirmPassword,
671
+ isLoading,
672
+ error,
673
+ submit,
674
+ } = useChangePasswordForm({ onSuccess: () => alert('Done') })
675
+ ```
676
+
677
+ #### useMfaSetup
678
+
679
+ ```ts
680
+ import { useMfaSetup } from 'cognito-max/ui'
681
+
682
+ const {
683
+ step, // 'scan' | 'verify' | 'done'
684
+ secretCode, // string — manual entry fallback
685
+ qrCodeUri, // string — otpauth:// URI for QR code
686
+ code, setCode,
687
+ isLoading,
688
+ error,
689
+ startSetup, // () => Promise<void> — call on mount or on user action
690
+ submitCode, // () => Promise<void>
691
+ } = useMfaSetup({ onSuccess: () => navigate('/dashboard') })
692
+ ```
693
+
694
+ ---
695
+
696
+ ## Core API
697
+
698
+ For use without React, or to extend the client with custom behavior.
699
+
700
+ ```ts
701
+ import { CognitoAuthClient } from 'cognito-max/core'
702
+
703
+ const client = new CognitoAuthClient({
704
+ userPoolId: 'eu-west-1_xxxxxxxxx',
705
+ clientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
706
+ region: 'eu-west-1',
707
+ })
708
+ ```
709
+
710
+ `CognitoAuthClient` extends `TypedEventEmitter` and exposes all methods available through the React hooks.
711
+
712
+ ### Full method list
713
+
714
+ | Method | Description |
715
+ |--------|-------------|
716
+ | `signIn(email, password)` | Initiates SRP login |
717
+ | `respondToMfaChallenge(session, code, type)` | Completes SMS or TOTP challenge |
718
+ | `respondToNewPasswordChallenge(session, password, attrs?)` | Completes forced password change |
719
+ | `setupTotpChallenge(session)` | Associates TOTP device during login flow |
720
+ | `verifyTotpChallenge(session, code)` | Verifies TOTP and completes login |
721
+ | `signOut(global?)` | Signs out locally or globally |
722
+ | `signUp(email, password, attrs?)` | Registers a new user |
723
+ | `confirmSignUp(email, code)` | Confirms email after sign-up |
724
+ | `resendConfirmationCode(email)` | Re-sends the confirmation email |
725
+ | `forgotPassword(email)` | Triggers password reset email |
726
+ | `confirmForgotPassword(email, code, newPassword)` | Sets new password with reset code |
727
+ | `changePassword(current, next)` | Changes password for authenticated user |
728
+ | `getSession()` | Returns current `AuthSession` (refreshes if needed) |
729
+ | `getCurrentUser()` | Returns `AuthUser \| null` from stored session |
730
+ | `getUserAttributes()` | Fetches all Cognito attributes |
731
+ | `updateUserAttributes(attrs)` | Updates Cognito attributes |
732
+ | `verifyUserAttribute(attr, code)` | Verifies email or phone with code |
733
+ | `sendAttributeVerificationCode(attr)` | Sends verification code for email/phone |
734
+ | `deleteUser()` | Permanently deletes the account |
735
+ | `setupTotp()` | Associates TOTP for authenticated user |
736
+ | `verifyTotpSetup(code)` | Confirms TOTP device for authenticated user |
737
+ | `getMfaPreference()` | Returns current MFA settings |
738
+ | `setMfaPreference(type)` | Sets preferred MFA type |
739
+ | `disableMfa()` | Disables all MFA factors |
740
+
741
+ ---
742
+
743
+ ## Storage adapters
744
+
745
+ Controls where Cognito tokens are persisted. Import from `cognito-max/core`.
746
+
747
+ | Adapter | Persists in | Use case |
748
+ |---------|-------------|----------|
749
+ | `AutoStorageAdapter` | `localStorage` (or in-memory if unavailable) | Default — works in browser and SSR |
750
+ | `LocalStorageAdapter` | `localStorage` | Standard browser |
751
+ | `SessionStorageAdapter` | `sessionStorage` | Token cleared on tab close |
752
+ | `InMemoryStorageAdapter` | JavaScript `Map` | SSR, tests, or when persistence is unwanted |
753
+
754
+ ```ts
755
+ import { InMemoryStorageAdapter } from 'cognito-max/core'
756
+
757
+ const client = new CognitoAuthClient({
758
+ // ...
759
+ storage: new InMemoryStorageAdapter(),
760
+ })
761
+ ```
762
+
763
+ You can also provide a custom adapter by implementing the `StorageAdapter` interface:
764
+
765
+ ```ts
766
+ import type { StorageAdapter } from 'cognito-max/core'
767
+
768
+ const encryptedStorage: StorageAdapter = {
769
+ getItem: (key) => decrypt(localStorage.getItem(key)),
770
+ setItem: (key, value) => localStorage.setItem(key, encrypt(value)),
771
+ removeItem: (key) => localStorage.removeItem(key),
772
+ }
773
+ ```
774
+
775
+ ---
776
+
777
+ ## Events
778
+
779
+ `CognitoAuthClient` emits typed events you can subscribe to from outside React:
780
+
781
+ ```ts
782
+ import { CognitoAuthClient } from 'cognito-max/core'
783
+
784
+ const client = new CognitoAuthClient(config)
785
+
786
+ // Subscribe
787
+ const unsub = client.on('signedIn', (user) => {
788
+ console.log('User signed in:', user.email)
789
+ })
790
+
791
+ // One-time
792
+ client.once('sessionExpired', () => {
793
+ window.location.href = '/login'
794
+ })
795
+
796
+ // Unsubscribe
797
+ unsub()
798
+ ```
799
+
800
+ ### Available events
801
+
802
+ | Event | Payload | Fired when |
803
+ |-------|---------|------------|
804
+ | `signedIn` | `AuthUser` | Login completed successfully |
805
+ | `signedOut` | — | User signed out |
806
+ | `tokenRefreshed` | `AuthSession` | Access token was automatically renewed |
807
+ | `sessionExpired` | — | Token refresh failed — session is no longer valid |
808
+ | `mfaRequired` | `{ mfaType, challengeSession }` | Login requires MFA code |
809
+ | `newPasswordRequired` | `{ requiredAttributes, challengeSession }` | User must set a new password |
810
+ | `userUpdated` | `AuthUser` | User attributes were changed |
811
+ | `stateChanged` | `AuthState` | Auth state machine transitioned |
812
+
813
+ ---
814
+
815
+ ## Error handling
816
+
817
+ All methods throw `CognitoAuthError` on failure. Import specific error classes to handle them by type:
818
+
819
+ ```ts
820
+ import {
821
+ CognitoAuthError,
822
+ NotAuthorizedError,
823
+ InvalidCodeError,
824
+ SessionExpiredError,
825
+ } from 'cognito-max/core'
826
+
827
+ try {
828
+ await signIn(email, password)
829
+ } catch (error) {
830
+ if (error instanceof NotAuthorizedError) {
831
+ setError('Wrong email or password')
832
+ } else if (error instanceof CognitoAuthError) {
833
+ // error.code is one of AuthErrorCode
834
+ // error.message is a human-readable string
835
+ setError(error.message)
836
+ }
837
+ }
838
+ ```
839
+
840
+ ### Error codes
841
+
842
+ | Class | `code` | When thrown |
843
+ |-------|--------|-------------|
844
+ | `NotAuthorizedError` | `NOT_AUTHORIZED` | Wrong password or disabled account |
845
+ | `UserNotConfirmedError` | `USER_NOT_CONFIRMED` | Account not email-verified yet |
846
+ | `InvalidCodeError` | `CODE_MISMATCH` / `EXPIRED_CODE` | Wrong or expired verification code |
847
+ | `SessionExpiredError` | `SESSION_EXPIRED` | Session no longer valid |
848
+ | `CognitoAuthError` | various | All other Cognito errors |
849
+
850
+ All codes (`AuthErrorCode`):
851
+
852
+ ```
853
+ NOT_AUTHORIZED · USER_NOT_FOUND · USER_NOT_CONFIRMED · INVALID_PARAMETER
854
+ INVALID_PASSWORD · CODE_MISMATCH · EXPIRED_CODE · CODE_DELIVERY_FAILURE
855
+ LIMIT_EXCEEDED · TOO_MANY_REQUESTS · TOO_MANY_FAILED_ATTEMPTS
856
+ PASSWORD_RESET_REQUIRED · MFA_METHOD_NOT_FOUND · SOFTWARE_TOKEN_MFA_NOT_FOUND
857
+ SESSION_EXPIRED · NETWORK_ERROR · UNKNOWN
858
+ ```
859
+
860
+ ---
861
+
862
+ ## TypeScript reference
863
+
864
+ ### `AuthConfig`
865
+
866
+ ```ts
867
+ interface AuthConfig {
868
+ userPoolId: string
869
+ clientId: string
870
+ region: string
871
+ storage?: StorageAdapter
872
+ autoRefresh?: boolean
873
+ refreshMarginSeconds?: number
874
+ totpIssuer?: string
875
+ }
876
+ ```
877
+
878
+ ### `AuthState`
879
+
880
+ ```ts
881
+ type AuthState =
882
+ | 'idle' // Not yet initialized
883
+ | 'loading' // Operation in progress
884
+ | 'authenticated' // Valid session
885
+ | 'unauthenticated' // No session
886
+ | 'mfa_required' // Waiting for MFA code
887
+ | 'new_password_required' // Waiting for new password
888
+ | 'confirm_signup' // Waiting for email confirmation
889
+ ```
890
+
891
+ ### `SignInResult`
892
+
893
+ ```ts
894
+ type SignInResult =
895
+ | { status: 'SUCCESS'; user: AuthUser; session: AuthSession }
896
+ | { status: 'MFA_REQUIRED'; mfaType: MfaType; challengeSession: string }
897
+ | { status: 'NEW_PASSWORD_REQUIRED'; requiredAttributes: string[]; challengeSession: string }
898
+ | { status: 'MFA_SETUP_REQUIRED'; challengeSession: string }
899
+ | { status: 'CONFIRM_SIGNUP' }
900
+ ```
901
+
902
+ ### `AuthUser`
903
+
904
+ ```ts
905
+ interface AuthUser {
906
+ sub: string
907
+ username: string
908
+ email: string
909
+ emailVerified: boolean
910
+ groups: string[]
911
+ attributes: Record<string, string>
912
+ }
913
+ ```
914
+
915
+ ### `AuthSession`
916
+
917
+ ```ts
918
+ interface AuthSession {
919
+ accessToken: string
920
+ idToken: string
921
+ refreshToken: string
922
+ expiresAt: Date
923
+ isValid(): boolean
924
+ }
925
+ ```
926
+
927
+ ### `MfaSetupResult`
928
+
929
+ ```ts
930
+ interface MfaSetupResult {
931
+ secretCode: string // For manual entry in the authenticator app
932
+ qrCodeUri: string // otpauth://totp/<issuer>:<email>?secret=…
933
+ }
934
+ ```
935
+
936
+ ### `MfaPreference`
937
+
938
+ ```ts
939
+ interface MfaPreference {
940
+ enabled: boolean
941
+ preferred: 'TOTP' | 'SMS' | null
942
+ totp: boolean
943
+ sms: boolean
944
+ }
945
+ ```
946
+
947
+ ---
948
+
949
+ ## Integrazione con AWS Cloudscape
950
+
951
+ Questa sezione mostra pattern completi per integrare cognito-max in un'applicazione che usa [AWS Cloudscape Design System](https://cloudscape.design/). Gli esempi sono TypeScript/TSX pronti all'uso — non pseudocodice.
952
+
953
+ ### Setup
954
+
955
+ Installa le dipendenze:
956
+
957
+ ```bash
958
+ npm install cognito-max @cloudscape-design/components @cloudscape-design/global-styles react-router-dom
959
+ # Opzionale — per il QR code nella pagina di setup MFA
960
+ npm install react-qr-code
961
+ ```
962
+
963
+ Struttura minima di `App.tsx`:
964
+
965
+ ```tsx
966
+ // src/App.tsx
967
+ import '@cloudscape-design/global-styles/index.css'
968
+ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
969
+ import { AuthProvider } from 'cognito-max/react'
970
+ import type { AuthConfig } from 'cognito-max'
971
+ import LoginPage from './pages/login'
972
+ import MfaVerifyPage from './pages/mfa-verify'
973
+ import MfaSetupPage from './pages/mfa-setup'
974
+ import NewPasswordPage from './pages/new-password'
975
+ import ForgotPasswordPage from './pages/forgot-password'
976
+ import ResetPasswordPage from './pages/reset-password'
977
+ import RegisterPage from './pages/register'
978
+ import ConfirmSignupPage from './pages/confirm-signup'
979
+ import ChangePasswordPage from './pages/change-password'
980
+ import ChangeMfaPage from './pages/change-mfa'
981
+ import Dashboard from './pages/dashboard'
982
+ import ProtectedRoute from './components/ProtectedRoute'
983
+ import CognitoTokenSync from './components/CognitoTokenSync'
984
+
985
+ const cognitoConfig: AuthConfig = {
986
+ userPoolId: import.meta.env.VITE_USER_POOL_ID,
987
+ clientId: import.meta.env.VITE_CLIENT_ID,
988
+ region: import.meta.env.VITE_REGION,
989
+ totpIssuer: 'MyApp',
990
+ }
991
+
992
+ export default function App() {
993
+ return (
994
+ <AuthProvider config={cognitoConfig}>
995
+ <CognitoTokenSync>
996
+ <BrowserRouter>
997
+ <Routes>
998
+ <Route path="/login" element={<LoginPage />} />
999
+ <Route path="/mfa-verify" element={<MfaVerifyPage />} />
1000
+ <Route path="/mfa-setup" element={<MfaSetupPage />} />
1001
+ <Route path="/new-password" element={<NewPasswordPage />} />
1002
+ <Route path="/forgot-password" element={<ForgotPasswordPage />} />
1003
+ <Route path="/reset-password" element={<ResetPasswordPage />} />
1004
+ <Route path="/register" element={<RegisterPage />} />
1005
+ <Route path="/confirm-signup" element={<ConfirmSignupPage />} />
1006
+ <Route
1007
+ path="/change-password"
1008
+ element={<ProtectedRoute><ChangePasswordPage /></ProtectedRoute>}
1009
+ />
1010
+ <Route
1011
+ path="/change-mfa"
1012
+ element={<ProtectedRoute><ChangeMfaPage /></ProtectedRoute>}
1013
+ />
1014
+ <Route
1015
+ path="/"
1016
+ element={<ProtectedRoute><Dashboard /></ProtectedRoute>}
1017
+ />
1018
+ <Route path="*" element={<Navigate to="/" />} />
1019
+ </Routes>
1020
+ </BrowserRouter>
1021
+ </CognitoTokenSync>
1022
+ </AuthProvider>
1023
+ )
1024
+ }
1025
+ ```
1026
+
1027
+ ---
1028
+
1029
+ ### Login completo
1030
+
1031
+ Gestisce tutti i possibili esiti di `signIn`: accesso riuscito, MFA, nuova password obbligatoria, setup MFA, account non confermato.
1032
+
1033
+ ```tsx
1034
+ // src/pages/login.tsx
1035
+ import { useState } from 'react'
1036
+ import { useNavigate } from 'react-router-dom'
1037
+ import { useAuth } from 'cognito-max/react'
1038
+ import Container from '@cloudscape-design/components/container'
1039
+ import Header from '@cloudscape-design/components/header'
1040
+ import FormField from '@cloudscape-design/components/form-field'
1041
+ import Input from '@cloudscape-design/components/input'
1042
+ import Button from '@cloudscape-design/components/button'
1043
+ import Alert from '@cloudscape-design/components/alert'
1044
+ import SpaceBetween from '@cloudscape-design/components/space-between'
1045
+ import Checkbox from '@cloudscape-design/components/checkbox'
1046
+
1047
+ export default function LoginPage() {
1048
+ const navigate = useNavigate()
1049
+ const { signIn } = useAuth()
1050
+
1051
+ const [email, setEmail] = useState('')
1052
+ const [password, setPassword] = useState('')
1053
+ const [showPassword, setShowPassword] = useState(false)
1054
+ const [isLoading, setIsLoading] = useState(false)
1055
+ const [error, setError] = useState('')
1056
+
1057
+ const handleSubmit = async (e: React.FormEvent) => {
1058
+ e.preventDefault()
1059
+ setError('')
1060
+ setIsLoading(true)
1061
+ try {
1062
+ const result = await signIn(email, password)
1063
+
1064
+ switch (result.status) {
1065
+ case 'SUCCESS':
1066
+ navigate('/')
1067
+ break
1068
+ case 'MFA_REQUIRED':
1069
+ navigate('/mfa-verify', {
1070
+ state: {
1071
+ challengeSession: result.challengeSession,
1072
+ mfaType: result.mfaType,
1073
+ email,
1074
+ },
1075
+ })
1076
+ break
1077
+ case 'NEW_PASSWORD_REQUIRED':
1078
+ navigate('/new-password', {
1079
+ state: {
1080
+ challengeSession: result.challengeSession,
1081
+ requiredAttributes: result.requiredAttributes,
1082
+ email,
1083
+ },
1084
+ })
1085
+ break
1086
+ case 'MFA_SETUP_REQUIRED':
1087
+ navigate('/mfa-setup', {
1088
+ state: {
1089
+ challengeSession: result.challengeSession,
1090
+ email,
1091
+ },
1092
+ })
1093
+ break
1094
+ case 'CONFIRM_SIGNUP':
1095
+ setError('Account non confermato. Controlla la tua email.')
1096
+ break
1097
+ }
1098
+ } catch (err: unknown) {
1099
+ setError(err instanceof Error ? err.message : 'Errore durante il login')
1100
+ } finally {
1101
+ setIsLoading(false)
1102
+ }
1103
+ }
1104
+
1105
+ return (
1106
+ <Container header={<Header variant="h1">Accedi al tuo account</Header>}>
1107
+ <form onSubmit={handleSubmit}>
1108
+ <SpaceBetween size="m">
1109
+ {error && (
1110
+ <Alert type="error" dismissible onDismiss={() => setError('')}>
1111
+ {error}
1112
+ </Alert>
1113
+ )}
1114
+
1115
+ <FormField label="Email">
1116
+ <Input
1117
+ type="email"
1118
+ value={email}
1119
+ onChange={({ detail }) => setEmail(detail.value)}
1120
+ autoFocus
1121
+ disabled={isLoading}
1122
+ />
1123
+ </FormField>
1124
+
1125
+ <FormField label="Password">
1126
+ <Input
1127
+ type={showPassword ? 'text' : 'password'}
1128
+ value={password}
1129
+ onChange={({ detail }) => setPassword(detail.value)}
1130
+ disabled={isLoading}
1131
+ />
1132
+ </FormField>
1133
+
1134
+ <Checkbox
1135
+ checked={showPassword}
1136
+ onChange={({ detail }) => setShowPassword(detail.checked)}
1137
+ >
1138
+ Mostra password
1139
+ </Checkbox>
1140
+
1141
+ <Button variant="primary" formAction="submit" loading={isLoading}>
1142
+ Accedi
1143
+ </Button>
1144
+
1145
+ <Button variant="link" onClick={() => navigate('/forgot-password')} disabled={isLoading}>
1146
+ Password dimenticata?
1147
+ </Button>
1148
+
1149
+ <Button variant="link" onClick={() => navigate('/register')} disabled={isLoading}>
1150
+ Non hai un account? Registrati
1151
+ </Button>
1152
+ </SpaceBetween>
1153
+ </form>
1154
+ </Container>
1155
+ )
1156
+ }
1157
+ ```
1158
+
1159
+ **Pagina MFA verify** (`/mfa-verify`) — riceve `challengeSession` e `mfaType` dallo state di navigazione:
1160
+
1161
+ ```tsx
1162
+ // src/pages/mfa-verify.tsx
1163
+ import { useState } from 'react'
1164
+ import { useLocation, useNavigate } from 'react-router-dom'
1165
+ import { useAuth } from 'cognito-max/react'
1166
+ import type { MfaType } from 'cognito-max'
1167
+ import Container from '@cloudscape-design/components/container'
1168
+ import Header from '@cloudscape-design/components/header'
1169
+ import FormField from '@cloudscape-design/components/form-field'
1170
+ import Input from '@cloudscape-design/components/input'
1171
+ import Button from '@cloudscape-design/components/button'
1172
+ import Alert from '@cloudscape-design/components/alert'
1173
+ import SpaceBetween from '@cloudscape-design/components/space-between'
1174
+
1175
+ export default function MfaVerifyPage() {
1176
+ const navigate = useNavigate()
1177
+ const location = useLocation()
1178
+ const { respondToMfaChallenge } = useAuth()
1179
+
1180
+ const { challengeSession, mfaType } = (location.state ?? {}) as {
1181
+ challengeSession: string
1182
+ mfaType: MfaType
1183
+ }
1184
+
1185
+ const [code, setCode] = useState('')
1186
+ const [isLoading, setIsLoading] = useState(false)
1187
+ const [error, setError] = useState('')
1188
+
1189
+ const handleSubmit = async (e: React.FormEvent) => {
1190
+ e.preventDefault()
1191
+ if (!challengeSession) {
1192
+ setError('Sessione non valida. Rieffettua il login.')
1193
+ return
1194
+ }
1195
+ setError('')
1196
+ setIsLoading(true)
1197
+ try {
1198
+ const result = await respondToMfaChallenge(challengeSession, code, mfaType ?? 'TOTP')
1199
+ if (result.status === 'SUCCESS') {
1200
+ navigate('/')
1201
+ }
1202
+ } catch (err: unknown) {
1203
+ setError(err instanceof Error ? err.message : 'Codice MFA non valido')
1204
+ } finally {
1205
+ setIsLoading(false)
1206
+ }
1207
+ }
1208
+
1209
+ return (
1210
+ <Container header={<Header variant="h1">Verifica codice MFA</Header>}>
1211
+ <form onSubmit={handleSubmit}>
1212
+ <SpaceBetween size="m">
1213
+ <p>
1214
+ Inserisci il codice{' '}
1215
+ {mfaType === 'TOTP' ? "dall'app authenticator" : 'ricevuto via SMS'}.
1216
+ </p>
1217
+
1218
+ {error && (
1219
+ <Alert type="error" dismissible onDismiss={() => setError('')}>
1220
+ {error}
1221
+ </Alert>
1222
+ )}
1223
+
1224
+ <FormField label="Codice MFA" errorText={error || undefined}>
1225
+ <Input
1226
+ type="text"
1227
+ inputMode="numeric"
1228
+ value={code}
1229
+ onChange={({ detail }) => setCode(detail.value)}
1230
+ autoFocus
1231
+ disabled={isLoading}
1232
+ />
1233
+ </FormField>
1234
+
1235
+ <Button variant="primary" formAction="submit" loading={isLoading}>
1236
+ Verifica
1237
+ </Button>
1238
+
1239
+ <Button variant="link" onClick={() => navigate('/login')} disabled={isLoading}>
1240
+ Torna al login
1241
+ </Button>
1242
+ </SpaceBetween>
1243
+ </form>
1244
+ </Container>
1245
+ )
1246
+ }
1247
+ ```
1248
+
1249
+ **Pagina nuova password obbligatoria** (`/new-password`):
1250
+
1251
+ ```tsx
1252
+ // src/pages/new-password.tsx
1253
+ import { useState } from 'react'
1254
+ import { useLocation, useNavigate } from 'react-router-dom'
1255
+ import { useAuth } from 'cognito-max/react'
1256
+ import Container from '@cloudscape-design/components/container'
1257
+ import Header from '@cloudscape-design/components/header'
1258
+ import FormField from '@cloudscape-design/components/form-field'
1259
+ import Input from '@cloudscape-design/components/input'
1260
+ import Button from '@cloudscape-design/components/button'
1261
+ import Alert from '@cloudscape-design/components/alert'
1262
+ import SpaceBetween from '@cloudscape-design/components/space-between'
1263
+
1264
+ export default function NewPasswordPage() {
1265
+ const navigate = useNavigate()
1266
+ const location = useLocation()
1267
+ const { respondToNewPasswordChallenge } = useAuth()
1268
+
1269
+ const { challengeSession, email } = (location.state ?? {}) as {
1270
+ challengeSession: string
1271
+ email?: string
1272
+ }
1273
+
1274
+ const [newPassword, setNewPassword] = useState('')
1275
+ const [confirmPassword, setConfirmPassword] = useState('')
1276
+ const [isLoading, setIsLoading] = useState(false)
1277
+ const [error, setError] = useState('')
1278
+
1279
+ const handleSubmit = async (e: React.FormEvent) => {
1280
+ e.preventDefault()
1281
+ if (newPassword !== confirmPassword) {
1282
+ setError('Le password non corrispondono')
1283
+ return
1284
+ }
1285
+ setError('')
1286
+ setIsLoading(true)
1287
+ try {
1288
+ const result = await respondToNewPasswordChallenge(
1289
+ challengeSession,
1290
+ newPassword,
1291
+ email ? { email } : undefined,
1292
+ )
1293
+
1294
+ switch (result.status) {
1295
+ case 'SUCCESS':
1296
+ navigate('/')
1297
+ break
1298
+ case 'MFA_REQUIRED':
1299
+ navigate('/mfa-verify', {
1300
+ state: { challengeSession: result.challengeSession, mfaType: result.mfaType, email },
1301
+ })
1302
+ break
1303
+ case 'MFA_SETUP_REQUIRED':
1304
+ navigate('/mfa-setup', {
1305
+ state: { challengeSession: result.challengeSession, email },
1306
+ })
1307
+ break
1308
+ default:
1309
+ setError('Errore imprevisto durante il cambio password')
1310
+ }
1311
+ } catch (err: unknown) {
1312
+ setError(err instanceof Error ? err.message : 'Errore durante il cambio password')
1313
+ } finally {
1314
+ setIsLoading(false)
1315
+ }
1316
+ }
1317
+
1318
+ return (
1319
+ <Container header={<Header variant="h1">Imposta nuova password</Header>}>
1320
+ <form onSubmit={handleSubmit}>
1321
+ <SpaceBetween size="m">
1322
+ <p>È necessario impostare una nuova password per completare l&apos;accesso.</p>
1323
+
1324
+ {error && (
1325
+ <Alert type="error" dismissible onDismiss={() => setError('')}>
1326
+ {error}
1327
+ </Alert>
1328
+ )}
1329
+
1330
+ <FormField label="Nuova password">
1331
+ <Input
1332
+ type="password"
1333
+ value={newPassword}
1334
+ onChange={({ detail }) => setNewPassword(detail.value)}
1335
+ autoFocus
1336
+ disabled={isLoading}
1337
+ />
1338
+ </FormField>
1339
+
1340
+ <FormField label="Conferma nuova password">
1341
+ <Input
1342
+ type="password"
1343
+ value={confirmPassword}
1344
+ onChange={({ detail }) => setConfirmPassword(detail.value)}
1345
+ disabled={isLoading}
1346
+ />
1347
+ </FormField>
1348
+
1349
+ <Button variant="primary" formAction="submit" loading={isLoading}>
1350
+ Imposta password
1351
+ </Button>
1352
+ </SpaceBetween>
1353
+ </form>
1354
+ </Container>
1355
+ )
1356
+ }
1357
+ ```
1358
+
1359
+ ---
1360
+
1361
+ ### Registrazione
1362
+
1363
+ Due pagine separate: `register.tsx` per la registrazione e `confirm-signup.tsx` per la verifica email.
1364
+
1365
+ ```tsx
1366
+ // src/pages/register.tsx
1367
+ import { useState } from 'react'
1368
+ import { useNavigate } from 'react-router-dom'
1369
+ import { useAuth } from 'cognito-max/react'
1370
+ import Container from '@cloudscape-design/components/container'
1371
+ import Header from '@cloudscape-design/components/header'
1372
+ import FormField from '@cloudscape-design/components/form-field'
1373
+ import Input from '@cloudscape-design/components/input'
1374
+ import Button from '@cloudscape-design/components/button'
1375
+ import Alert from '@cloudscape-design/components/alert'
1376
+ import SpaceBetween from '@cloudscape-design/components/space-between'
1377
+
1378
+ export default function RegisterPage() {
1379
+ const navigate = useNavigate()
1380
+ const { signUp } = useAuth()
1381
+
1382
+ const [email, setEmail] = useState('')
1383
+ const [password, setPassword] = useState('')
1384
+ const [confirmPassword, setConfirmPassword] = useState('')
1385
+ const [isLoading, setIsLoading] = useState(false)
1386
+ const [error, setError] = useState('')
1387
+
1388
+ const handleSubmit = async (e: React.FormEvent) => {
1389
+ e.preventDefault()
1390
+ if (password !== confirmPassword) {
1391
+ setError('Le password non corrispondono')
1392
+ return
1393
+ }
1394
+ setError('')
1395
+ setIsLoading(true)
1396
+ try {
1397
+ await signUp(email, password)
1398
+ // Cognito invia un codice di verifica all'email
1399
+ navigate('/confirm-signup', { state: { email } })
1400
+ } catch (err: unknown) {
1401
+ setError(err instanceof Error ? err.message : 'Errore durante la registrazione')
1402
+ } finally {
1403
+ setIsLoading(false)
1404
+ }
1405
+ }
1406
+
1407
+ return (
1408
+ <Container header={<Header variant="h1">Crea account</Header>}>
1409
+ <form onSubmit={handleSubmit}>
1410
+ <SpaceBetween size="m">
1411
+ {error && (
1412
+ <Alert type="error" dismissible onDismiss={() => setError('')}>
1413
+ {error}
1414
+ </Alert>
1415
+ )}
1416
+
1417
+ <FormField label="Email">
1418
+ <Input
1419
+ type="email"
1420
+ value={email}
1421
+ onChange={({ detail }) => setEmail(detail.value)}
1422
+ autoFocus
1423
+ disabled={isLoading}
1424
+ />
1425
+ </FormField>
1426
+
1427
+ <FormField label="Password">
1428
+ <Input
1429
+ type="password"
1430
+ value={password}
1431
+ onChange={({ detail }) => setPassword(detail.value)}
1432
+ disabled={isLoading}
1433
+ />
1434
+ </FormField>
1435
+
1436
+ <FormField label="Conferma password">
1437
+ <Input
1438
+ type="password"
1439
+ value={confirmPassword}
1440
+ onChange={({ detail }) => setConfirmPassword(detail.value)}
1441
+ disabled={isLoading}
1442
+ />
1443
+ </FormField>
1444
+
1445
+ <Button variant="primary" formAction="submit" loading={isLoading}>
1446
+ Registrati
1447
+ </Button>
1448
+
1449
+ <Button variant="link" onClick={() => navigate('/login')} disabled={isLoading}>
1450
+ Hai già un account? Accedi
1451
+ </Button>
1452
+ </SpaceBetween>
1453
+ </form>
1454
+ </Container>
1455
+ )
1456
+ }
1457
+ ```
1458
+
1459
+ ```tsx
1460
+ // src/pages/confirm-signup.tsx
1461
+ import { useState } from 'react'
1462
+ import { useLocation, useNavigate } from 'react-router-dom'
1463
+ import { useAuth } from 'cognito-max/react'
1464
+ import Container from '@cloudscape-design/components/container'
1465
+ import Header from '@cloudscape-design/components/header'
1466
+ import FormField from '@cloudscape-design/components/form-field'
1467
+ import Input from '@cloudscape-design/components/input'
1468
+ import Button from '@cloudscape-design/components/button'
1469
+ import Alert from '@cloudscape-design/components/alert'
1470
+ import SpaceBetween from '@cloudscape-design/components/space-between'
1471
+
1472
+ export default function ConfirmSignupPage() {
1473
+ const navigate = useNavigate()
1474
+ const location = useLocation()
1475
+ const { confirmSignUp, resendConfirmationCode } = useAuth()
1476
+
1477
+ const { email } = (location.state ?? {}) as { email: string }
1478
+
1479
+ const [code, setCode] = useState('')
1480
+ const [isLoading, setIsLoading] = useState(false)
1481
+ const [isResending, setIsResending] = useState(false)
1482
+ const [error, setError] = useState('')
1483
+ const [success, setSuccess] = useState('')
1484
+
1485
+ const handleSubmit = async (e: React.FormEvent) => {
1486
+ e.preventDefault()
1487
+ setError('')
1488
+ setIsLoading(true)
1489
+ try {
1490
+ await confirmSignUp(email, code)
1491
+ navigate('/login')
1492
+ } catch (err: unknown) {
1493
+ setError(err instanceof Error ? err.message : 'Codice non valido')
1494
+ } finally {
1495
+ setIsLoading(false)
1496
+ }
1497
+ }
1498
+
1499
+ const handleResend = async () => {
1500
+ setError('')
1501
+ setSuccess('')
1502
+ setIsResending(true)
1503
+ try {
1504
+ await resendConfirmationCode(email)
1505
+ setSuccess('Nuovo codice inviato via email.')
1506
+ } catch (err: unknown) {
1507
+ setError(err instanceof Error ? err.message : "Errore nell'invio del codice")
1508
+ } finally {
1509
+ setIsResending(false)
1510
+ }
1511
+ }
1512
+
1513
+ return (
1514
+ <Container header={<Header variant="h1">Conferma registrazione</Header>}>
1515
+ <form onSubmit={handleSubmit}>
1516
+ <SpaceBetween size="m">
1517
+ <p>
1518
+ Controlla la tua email <strong>{email}</strong> e inserisci il codice di verifica.
1519
+ </p>
1520
+
1521
+ {error && (
1522
+ <Alert type="error" dismissible onDismiss={() => setError('')}>
1523
+ {error}
1524
+ </Alert>
1525
+ )}
1526
+ {success && <Alert type="success">{success}</Alert>}
1527
+
1528
+ <FormField label="Codice di verifica">
1529
+ <Input
1530
+ type="text"
1531
+ inputMode="numeric"
1532
+ value={code}
1533
+ onChange={({ detail }) => setCode(detail.value)}
1534
+ autoFocus
1535
+ disabled={isLoading}
1536
+ />
1537
+ </FormField>
1538
+
1539
+ <Button variant="primary" formAction="submit" loading={isLoading}>
1540
+ Conferma
1541
+ </Button>
1542
+
1543
+ <Button
1544
+ variant="link"
1545
+ onClick={handleResend}
1546
+ disabled={isLoading || isResending}
1547
+ loading={isResending}
1548
+ >
1549
+ Non hai ricevuto il codice? Invia di nuovo
1550
+ </Button>
1551
+ </SpaceBetween>
1552
+ </form>
1553
+ </Container>
1554
+ )
1555
+ }
1556
+ ```
1557
+
1558
+ ---
1559
+
1560
+ ### Password dimenticata
1561
+
1562
+ Due pagine: `forgot-password.tsx` per richiedere il codice, `reset-password.tsx` per inserire codice e nuova password.
1563
+
1564
+ ```tsx
1565
+ // src/pages/forgot-password.tsx
1566
+ import { useState } from 'react'
1567
+ import { useNavigate } from 'react-router-dom'
1568
+ import { useAuth } from 'cognito-max/react'
1569
+ import Container from '@cloudscape-design/components/container'
1570
+ import Header from '@cloudscape-design/components/header'
1571
+ import FormField from '@cloudscape-design/components/form-field'
1572
+ import Input from '@cloudscape-design/components/input'
1573
+ import Button from '@cloudscape-design/components/button'
1574
+ import Alert from '@cloudscape-design/components/alert'
1575
+ import SpaceBetween from '@cloudscape-design/components/space-between'
1576
+
1577
+ export default function ForgotPasswordPage() {
1578
+ const navigate = useNavigate()
1579
+ const { forgotPassword } = useAuth()
1580
+
1581
+ const [email, setEmail] = useState('')
1582
+ const [isLoading, setIsLoading] = useState(false)
1583
+ const [error, setError] = useState('')
1584
+
1585
+ const handleSubmit = async (e: React.FormEvent) => {
1586
+ e.preventDefault()
1587
+ setError('')
1588
+ setIsLoading(true)
1589
+ try {
1590
+ await forgotPassword(email)
1591
+ // Cognito invia un codice all'email
1592
+ navigate('/reset-password', { state: { email } })
1593
+ } catch (err: unknown) {
1594
+ setError(err instanceof Error ? err.message : "Errore nell'invio del codice")
1595
+ } finally {
1596
+ setIsLoading(false)
1597
+ }
1598
+ }
1599
+
1600
+ return (
1601
+ <Container header={<Header variant="h1">Password dimenticata</Header>}>
1602
+ <form onSubmit={handleSubmit}>
1603
+ <SpaceBetween size="m">
1604
+ <p>Inserisci la tua email per ricevere il codice di reimpostazione.</p>
1605
+
1606
+ {error && (
1607
+ <Alert type="error" dismissible onDismiss={() => setError('')}>
1608
+ {error}
1609
+ </Alert>
1610
+ )}
1611
+
1612
+ <FormField label="Email">
1613
+ <Input
1614
+ type="email"
1615
+ value={email}
1616
+ onChange={({ detail }) => setEmail(detail.value)}
1617
+ autoFocus
1618
+ disabled={isLoading}
1619
+ />
1620
+ </FormField>
1621
+
1622
+ <Button variant="primary" formAction="submit" loading={isLoading}>
1623
+ Invia codice
1624
+ </Button>
1625
+
1626
+ <Button variant="link" onClick={() => navigate('/login')} disabled={isLoading}>
1627
+ Torna al login
1628
+ </Button>
1629
+ </SpaceBetween>
1630
+ </form>
1631
+ </Container>
1632
+ )
1633
+ }
1634
+ ```
1635
+
1636
+ ```tsx
1637
+ // src/pages/reset-password.tsx
1638
+ import { useState } from 'react'
1639
+ import { useLocation, useNavigate } from 'react-router-dom'
1640
+ import { useAuth } from 'cognito-max/react'
1641
+ import Container from '@cloudscape-design/components/container'
1642
+ import Header from '@cloudscape-design/components/header'
1643
+ import FormField from '@cloudscape-design/components/form-field'
1644
+ import Input from '@cloudscape-design/components/input'
1645
+ import Button from '@cloudscape-design/components/button'
1646
+ import Alert from '@cloudscape-design/components/alert'
1647
+ import SpaceBetween from '@cloudscape-design/components/space-between'
1648
+ import Checkbox from '@cloudscape-design/components/checkbox'
1649
+
1650
+ export default function ResetPasswordPage() {
1651
+ const navigate = useNavigate()
1652
+ const location = useLocation()
1653
+ const { confirmForgotPassword, forgotPassword } = useAuth()
1654
+
1655
+ const { email } = (location.state ?? {}) as { email: string }
1656
+
1657
+ const [code, setCode] = useState('')
1658
+ const [newPassword, setNewPassword] = useState('')
1659
+ const [confirmPassword, setConfirmPassword] = useState('')
1660
+ const [showPasswords, setShowPasswords] = useState(false)
1661
+ const [isLoading, setIsLoading] = useState(false)
1662
+ const [isResending, setIsResending] = useState(false)
1663
+ const [error, setError] = useState('')
1664
+ const [success, setSuccess] = useState('')
1665
+
1666
+ const handleSubmit = async (e: React.FormEvent) => {
1667
+ e.preventDefault()
1668
+ if (newPassword !== confirmPassword) {
1669
+ setError('Le password non corrispondono')
1670
+ return
1671
+ }
1672
+ setError('')
1673
+ setIsLoading(true)
1674
+ try {
1675
+ await confirmForgotPassword(email, code, newPassword)
1676
+ setSuccess('Password reimpostata con successo!')
1677
+ setTimeout(() => navigate('/login'), 2000)
1678
+ } catch (err: unknown) {
1679
+ setError(err instanceof Error ? err.message : 'Errore durante il reset della password')
1680
+ } finally {
1681
+ setIsLoading(false)
1682
+ }
1683
+ }
1684
+
1685
+ const handleResend = async () => {
1686
+ setError('')
1687
+ setSuccess('')
1688
+ setIsResending(true)
1689
+ try {
1690
+ await forgotPassword(email)
1691
+ setSuccess('Nuovo codice inviato via email.')
1692
+ } catch (err: unknown) {
1693
+ setError(err instanceof Error ? err.message : "Errore nell'invio del codice")
1694
+ } finally {
1695
+ setIsResending(false)
1696
+ }
1697
+ }
1698
+
1699
+ return (
1700
+ <Container header={<Header variant="h1">Reimposta password</Header>}>
1701
+ <form onSubmit={handleSubmit}>
1702
+ <SpaceBetween size="m">
1703
+ <p>
1704
+ Abbiamo inviato un codice a <strong>{email}</strong>. Inseriscilo insieme alla nuova
1705
+ password.
1706
+ </p>
1707
+
1708
+ {error && (
1709
+ <Alert type="error" dismissible onDismiss={() => setError('')}>
1710
+ {error}
1711
+ </Alert>
1712
+ )}
1713
+ {success && <Alert type="success">{success}</Alert>}
1714
+
1715
+ <FormField label="Codice di verifica">
1716
+ <Input
1717
+ type="text"
1718
+ inputMode="numeric"
1719
+ value={code}
1720
+ onChange={({ detail }) => setCode(detail.value)}
1721
+ autoFocus
1722
+ disabled={isLoading}
1723
+ />
1724
+ </FormField>
1725
+
1726
+ <FormField label="Nuova password">
1727
+ <Input
1728
+ type={showPasswords ? 'text' : 'password'}
1729
+ value={newPassword}
1730
+ onChange={({ detail }) => setNewPassword(detail.value)}
1731
+ disabled={isLoading}
1732
+ />
1733
+ </FormField>
1734
+
1735
+ <FormField label="Conferma nuova password">
1736
+ <Input
1737
+ type={showPasswords ? 'text' : 'password'}
1738
+ value={confirmPassword}
1739
+ onChange={({ detail }) => setConfirmPassword(detail.value)}
1740
+ disabled={isLoading}
1741
+ />
1742
+ </FormField>
1743
+
1744
+ <Checkbox
1745
+ checked={showPasswords}
1746
+ onChange={({ detail }) => setShowPasswords(detail.checked)}
1747
+ >
1748
+ Mostra password
1749
+ </Checkbox>
1750
+
1751
+ <Button variant="primary" formAction="submit" loading={isLoading}>
1752
+ Reimposta password
1753
+ </Button>
1754
+
1755
+ <Button
1756
+ variant="link"
1757
+ onClick={handleResend}
1758
+ disabled={isLoading || isResending}
1759
+ loading={isResending}
1760
+ >
1761
+ Invia nuovo codice
1762
+ </Button>
1763
+
1764
+ <Button variant="link" onClick={() => navigate('/login')} disabled={isLoading}>
1765
+ Torna al login
1766
+ </Button>
1767
+ </SpaceBetween>
1768
+ </form>
1769
+ </Container>
1770
+ )
1771
+ }
1772
+ ```
1773
+
1774
+ ---
1775
+
1776
+ ### Cambio password (utente autenticato)
1777
+
1778
+ ```tsx
1779
+ // src/pages/change-password.tsx
1780
+ import { useState } from 'react'
1781
+ import { useNavigate } from 'react-router-dom'
1782
+ import { useAuth } from 'cognito-max/react'
1783
+ import Container from '@cloudscape-design/components/container'
1784
+ import Header from '@cloudscape-design/components/header'
1785
+ import FormField from '@cloudscape-design/components/form-field'
1786
+ import Input from '@cloudscape-design/components/input'
1787
+ import Button from '@cloudscape-design/components/button'
1788
+ import Alert from '@cloudscape-design/components/alert'
1789
+ import SpaceBetween from '@cloudscape-design/components/space-between'
1790
+ import Checkbox from '@cloudscape-design/components/checkbox'
1791
+
1792
+ export default function ChangePasswordPage() {
1793
+ const navigate = useNavigate()
1794
+ const { changePassword } = useAuth()
1795
+
1796
+ const [oldPassword, setOldPassword] = useState('')
1797
+ const [newPassword, setNewPassword] = useState('')
1798
+ const [confirmPassword, setConfirmPassword] = useState('')
1799
+ const [showPasswords, setShowPasswords] = useState(false)
1800
+ const [isLoading, setIsLoading] = useState(false)
1801
+ const [error, setError] = useState('')
1802
+ const [success, setSuccess] = useState('')
1803
+
1804
+ const handleSubmit = async (e: React.FormEvent) => {
1805
+ e.preventDefault()
1806
+ if (newPassword !== confirmPassword) {
1807
+ setError('Le nuove password non corrispondono')
1808
+ return
1809
+ }
1810
+ if (oldPassword === newPassword) {
1811
+ setError('La nuova password deve essere diversa dalla vecchia')
1812
+ return
1813
+ }
1814
+ setError('')
1815
+ setIsLoading(true)
1816
+ try {
1817
+ await changePassword(oldPassword, newPassword)
1818
+ setSuccess('Password cambiata con successo!')
1819
+ setTimeout(() => navigate('/'), 2000)
1820
+ } catch (err: unknown) {
1821
+ setError(err instanceof Error ? err.message : 'Errore durante il cambio password')
1822
+ } finally {
1823
+ setIsLoading(false)
1824
+ }
1825
+ }
1826
+
1827
+ return (
1828
+ <Container header={<Header variant="h1">Cambia password</Header>}>
1829
+ <form onSubmit={handleSubmit}>
1830
+ <SpaceBetween size="m">
1831
+ {error && (
1832
+ <Alert type="error" dismissible onDismiss={() => setError('')}>
1833
+ {error}
1834
+ </Alert>
1835
+ )}
1836
+ {success && <Alert type="success">{success}</Alert>}
1837
+
1838
+ <FormField label="Password attuale">
1839
+ <Input
1840
+ type={showPasswords ? 'text' : 'password'}
1841
+ value={oldPassword}
1842
+ onChange={({ detail }) => setOldPassword(detail.value)}
1843
+ autoFocus
1844
+ disabled={isLoading}
1845
+ />
1846
+ </FormField>
1847
+
1848
+ <FormField label="Nuova password">
1849
+ <Input
1850
+ type={showPasswords ? 'text' : 'password'}
1851
+ value={newPassword}
1852
+ onChange={({ detail }) => setNewPassword(detail.value)}
1853
+ disabled={isLoading}
1854
+ />
1855
+ </FormField>
1856
+
1857
+ <FormField label="Conferma nuova password">
1858
+ <Input
1859
+ type={showPasswords ? 'text' : 'password'}
1860
+ value={confirmPassword}
1861
+ onChange={({ detail }) => setConfirmPassword(detail.value)}
1862
+ disabled={isLoading}
1863
+ />
1864
+ </FormField>
1865
+
1866
+ <Checkbox
1867
+ checked={showPasswords}
1868
+ onChange={({ detail }) => setShowPasswords(detail.checked)}
1869
+ >
1870
+ Mostra password
1871
+ </Checkbox>
1872
+
1873
+ <Button variant="primary" formAction="submit" loading={isLoading}>
1874
+ Cambia password
1875
+ </Button>
1876
+
1877
+ <Button variant="link" onClick={() => navigate('/')} disabled={isLoading}>
1878
+ Annulla
1879
+ </Button>
1880
+ </SpaceBetween>
1881
+ </form>
1882
+ </Container>
1883
+ )
1884
+ }
1885
+ ```
1886
+
1887
+ ---
1888
+
1889
+ ### Setup MFA (utente autenticato)
1890
+
1891
+ Usa `useMfa()` da `cognito-max/react` e `react-qr-code` per il QR code.
1892
+
1893
+ ```tsx
1894
+ // src/pages/change-mfa.tsx
1895
+ import { useState, useEffect } from 'react'
1896
+ import { useNavigate } from 'react-router-dom'
1897
+ import { useMfa } from 'cognito-max/react'
1898
+ import QRCode from 'react-qr-code'
1899
+ import Container from '@cloudscape-design/components/container'
1900
+ import Header from '@cloudscape-design/components/header'
1901
+ import FormField from '@cloudscape-design/components/form-field'
1902
+ import Input from '@cloudscape-design/components/input'
1903
+ import Button from '@cloudscape-design/components/button'
1904
+ import Alert from '@cloudscape-design/components/alert'
1905
+ import SpaceBetween from '@cloudscape-design/components/space-between'
1906
+ import Spinner from '@cloudscape-design/components/spinner'
1907
+ import Box from '@cloudscape-design/components/box'
1908
+
1909
+ export default function ChangeMfaPage() {
1910
+ const navigate = useNavigate()
1911
+ const { setup, verifySetup } = useMfa()
1912
+
1913
+ const [isInitializing, setIsInitializing] = useState(true)
1914
+ const [isLoading, setIsLoading] = useState(false)
1915
+ const [secretCode, setSecretCode] = useState('')
1916
+ const [qrCodeUri, setQrCodeUri] = useState('')
1917
+ const [totpCode, setTotpCode] = useState('')
1918
+ const [error, setError] = useState('')
1919
+ const [success, setSuccess] = useState('')
1920
+
1921
+ // Avvia il setup TOTP al mount — ottiene secretCode e qrCodeUri
1922
+ useEffect(() => {
1923
+ const init = async () => {
1924
+ try {
1925
+ const result = await setup()
1926
+ setSecretCode(result.secretCode)
1927
+ setQrCodeUri(result.qrCodeUri)
1928
+ } catch (err: unknown) {
1929
+ setError(
1930
+ err instanceof Error ? err.message : "Errore nell'inizializzazione. Riprova.",
1931
+ )
1932
+ } finally {
1933
+ setIsInitializing(false)
1934
+ }
1935
+ }
1936
+ init()
1937
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
1938
+
1939
+ const handleSubmit = async (e: React.FormEvent) => {
1940
+ e.preventDefault()
1941
+ setError('')
1942
+ setIsLoading(true)
1943
+ try {
1944
+ await verifySetup(totpCode)
1945
+ setSuccess('App autenticatrice aggiornata con successo!')
1946
+ setTimeout(() => navigate('/'), 2500)
1947
+ } catch (err: unknown) {
1948
+ setError(err instanceof Error ? err.message : 'Codice non valido. Verifica e riprova.')
1949
+ } finally {
1950
+ setIsLoading(false)
1951
+ }
1952
+ }
1953
+
1954
+ return (
1955
+ <Container header={<Header variant="h1">Configura autenticazione a due fattori</Header>}>
1956
+ <form onSubmit={handleSubmit}>
1957
+ <SpaceBetween size="m">
1958
+ {error && (
1959
+ <Alert type="error" dismissible onDismiss={() => setError('')}>
1960
+ {error}
1961
+ </Alert>
1962
+ )}
1963
+ {success && <Alert type="success">{success}</Alert>}
1964
+
1965
+ {isInitializing ? (
1966
+ <Box textAlign="center">
1967
+ <Spinner size="large" />
1968
+ <Box margin={{ top: 's' }}>Generazione codice in corso...</Box>
1969
+ </Box>
1970
+ ) : (
1971
+ secretCode && (
1972
+ <SpaceBetween size="m">
1973
+ <Alert type="info">
1974
+ 1. Apri la tua app authenticator (Google Authenticator, Authy…)
1975
+ <br />
1976
+ 2. Aggiungi un nuovo account scansionando il QR code o inserendo il codice segreto
1977
+ <br />
1978
+ 3. Inserisci il codice a 6 cifre generato dall&apos;app
1979
+ </Alert>
1980
+
1981
+ <FormField label="QR Code — scansiona con la nuova app">
1982
+ <div
1983
+ style={{
1984
+ padding: '20px',
1985
+ backgroundColor: '#ffffff',
1986
+ display: 'flex',
1987
+ justifyContent: 'center',
1988
+ border: '1px solid #ddd',
1989
+ borderRadius: '4px',
1990
+ }}
1991
+ >
1992
+ <QRCode
1993
+ size={200}
1994
+ style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
1995
+ value={qrCodeUri}
1996
+ viewBox="0 0 200 200"
1997
+ />
1998
+ </div>
1999
+ </FormField>
2000
+
2001
+ <FormField label="Codice segreto (inserimento manuale nell'app)">
2002
+ <div
2003
+ style={{
2004
+ padding: '10px',
2005
+ backgroundColor: '#f0f0f0',
2006
+ fontFamily: 'monospace',
2007
+ wordBreak: 'break-all',
2008
+ fontSize: '14px',
2009
+ borderRadius: '4px',
2010
+ }}
2011
+ >
2012
+ {secretCode}
2013
+ </div>
2014
+ </FormField>
2015
+ </SpaceBetween>
2016
+ )
2017
+ )}
2018
+
2019
+ <FormField label="Codice MFA (6 cifre)">
2020
+ <Input
2021
+ type="text"
2022
+ inputMode="numeric"
2023
+ value={totpCode}
2024
+ onChange={({ detail }) => setTotpCode(detail.value)}
2025
+ autoFocus={!isInitializing}
2026
+ disabled={isInitializing || !!success}
2027
+ />
2028
+ </FormField>
2029
+
2030
+ <Button
2031
+ variant="primary"
2032
+ formAction="submit"
2033
+ loading={isLoading}
2034
+ disabled={isInitializing || !secretCode || totpCode.length !== 6 || !!success}
2035
+ >
2036
+ Conferma
2037
+ </Button>
2038
+
2039
+ <Button variant="link" onClick={() => navigate('/')} disabled={isLoading}>
2040
+ Annulla
2041
+ </Button>
2042
+ </SpaceBetween>
2043
+ </form>
2044
+ </Container>
2045
+ )
2046
+ }
2047
+ ```
2048
+
2049
+ **Setup MFA durante il login** (`/mfa-setup`) — riceve `challengeSession` e usa `setupTotpChallenge` / `verifyTotpChallenge`:
2050
+
2051
+ ```tsx
2052
+ // src/pages/mfa-setup.tsx
2053
+ import { useState, useEffect } from 'react'
2054
+ import { useLocation, useNavigate } from 'react-router-dom'
2055
+ import { useAuth } from 'cognito-max/react'
2056
+ import QRCode from 'react-qr-code'
2057
+ import Container from '@cloudscape-design/components/container'
2058
+ import Header from '@cloudscape-design/components/header'
2059
+ import FormField from '@cloudscape-design/components/form-field'
2060
+ import Input from '@cloudscape-design/components/input'
2061
+ import Button from '@cloudscape-design/components/button'
2062
+ import Alert from '@cloudscape-design/components/alert'
2063
+ import SpaceBetween from '@cloudscape-design/components/space-between'
2064
+ import Spinner from '@cloudscape-design/components/spinner'
2065
+ import Box from '@cloudscape-design/components/box'
2066
+
2067
+ export default function MfaSetupPage() {
2068
+ const navigate = useNavigate()
2069
+ const location = useLocation()
2070
+ const { setupTotpChallenge, verifyTotpChallenge } = useAuth()
2071
+
2072
+ const { challengeSession } = (location.state ?? {}) as { challengeSession: string }
2073
+
2074
+ const [isInitializing, setIsInitializing] = useState(true)
2075
+ const [isLoading, setIsLoading] = useState(false)
2076
+ const [secretCode, setSecretCode] = useState('')
2077
+ const [qrCodeUri, setQrCodeUri] = useState('')
2078
+ const [totpCode, setTotpCode] = useState('')
2079
+ const [error, setError] = useState('')
2080
+
2081
+ useEffect(() => {
2082
+ if (!challengeSession) {
2083
+ setError('Dati di sessione non validi. Rieffettua il login.')
2084
+ setTimeout(() => navigate('/login'), 3000)
2085
+ return
2086
+ }
2087
+ const init = async () => {
2088
+ try {
2089
+ const result = await setupTotpChallenge(challengeSession)
2090
+ setSecretCode(result.secretCode)
2091
+ setQrCodeUri(result.qrCodeUri)
2092
+ } catch (err: unknown) {
2093
+ const msg = err instanceof Error ? err.message : 'Errore durante inizializzazione MFA'
2094
+ if (msg.includes('session can only be used once')) {
2095
+ setError('La sessione è scaduta. Effettua nuovamente il login.')
2096
+ setTimeout(() => navigate('/login'), 5000)
2097
+ } else {
2098
+ setError(msg)
2099
+ }
2100
+ } finally {
2101
+ setIsInitializing(false)
2102
+ }
2103
+ }
2104
+ init()
2105
+ }, [challengeSession]) // eslint-disable-line react-hooks/exhaustive-deps
2106
+
2107
+ const handleSubmit = async (e: React.FormEvent) => {
2108
+ e.preventDefault()
2109
+ if (!secretCode) return
2110
+ setError('')
2111
+ setIsLoading(true)
2112
+ try {
2113
+ const result = await verifyTotpChallenge(challengeSession, totpCode)
2114
+ if (result.status === 'SUCCESS') {
2115
+ navigate('/')
2116
+ } else {
2117
+ setError('Errore imprevisto durante la verifica MFA')
2118
+ }
2119
+ } catch (err: unknown) {
2120
+ setError(err instanceof Error ? err.message : 'Errore durante la verifica del codice MFA')
2121
+ } finally {
2122
+ setIsLoading(false)
2123
+ }
2124
+ }
2125
+
2126
+ return (
2127
+ <Container header={<Header variant="h1">Configurazione MFA</Header>}>
2128
+ <form onSubmit={handleSubmit}>
2129
+ <SpaceBetween size="m">
2130
+ <p>Configura l&apos;autenticazione a due fattori per completare l&apos;accesso.</p>
2131
+
2132
+ {error && (
2133
+ <Alert type="error" dismissible onDismiss={() => setError('')}>
2134
+ {error}
2135
+ </Alert>
2136
+ )}
2137
+
2138
+ {isInitializing ? (
2139
+ <Box textAlign="center">
2140
+ <Spinner size="large" />
2141
+ </Box>
2142
+ ) : (
2143
+ secretCode && (
2144
+ <SpaceBetween size="m">
2145
+ <Alert type="info">
2146
+ 1. Installa un&apos;app TOTP (Google Authenticator, Authy…)
2147
+ <br />
2148
+ 2. Scansiona il QR code o inserisci manualmente il codice segreto
2149
+ <br />
2150
+ 3. Inserisci il codice a 6 cifre generato dall&apos;app
2151
+ </Alert>
2152
+
2153
+ <FormField label="QR Code — scansiona con la tua app TOTP">
2154
+ <div
2155
+ style={{
2156
+ padding: '20px',
2157
+ backgroundColor: '#ffffff',
2158
+ display: 'flex',
2159
+ justifyContent: 'center',
2160
+ border: '1px solid #ddd',
2161
+ borderRadius: '4px',
2162
+ }}
2163
+ >
2164
+ <QRCode
2165
+ size={200}
2166
+ style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
2167
+ value={qrCodeUri}
2168
+ viewBox="0 0 200 200"
2169
+ />
2170
+ </div>
2171
+ </FormField>
2172
+
2173
+ <FormField label="Codice segreto (inserimento manuale nell'app)">
2174
+ <div
2175
+ style={{
2176
+ padding: '10px',
2177
+ backgroundColor: '#f0f0f0',
2178
+ fontFamily: 'monospace',
2179
+ wordBreak: 'break-all',
2180
+ fontSize: '14px',
2181
+ }}
2182
+ >
2183
+ {secretCode}
2184
+ </div>
2185
+ </FormField>
2186
+ </SpaceBetween>
2187
+ )
2188
+ )}
2189
+
2190
+ <FormField label="Codice MFA (6 cifre)">
2191
+ <Input
2192
+ type="text"
2193
+ inputMode="numeric"
2194
+ value={totpCode}
2195
+ onChange={({ detail }) => setTotpCode(detail.value)}
2196
+ autoFocus={!isInitializing}
2197
+ disabled={isInitializing || !secretCode}
2198
+ />
2199
+ </FormField>
2200
+
2201
+ <Button
2202
+ variant="primary"
2203
+ formAction="submit"
2204
+ loading={isLoading}
2205
+ disabled={isInitializing || !secretCode || totpCode.length !== 6}
2206
+ >
2207
+ Verifica e completa setup
2208
+ </Button>
2209
+ </SpaceBetween>
2210
+ </form>
2211
+ </Container>
2212
+ )
2213
+ }
2214
+ ```
2215
+
2216
+ ---
2217
+
2218
+ ### Token sync per axios
2219
+
2220
+ Il provider Cognito salva i token nel context React, non in `localStorage`. Se l'applicazione usa un interceptor axios che legge il token da `localStorage`, aggiungere questo componente wrapper sotto `<AuthProvider>`:
2221
+
2222
+ ```tsx
2223
+ // src/components/CognitoTokenSync.tsx
2224
+ import { useEffect, type ReactNode } from 'react'
2225
+ import { useSession } from 'cognito-max/react'
2226
+
2227
+ interface Props {
2228
+ children: ReactNode
2229
+ }
2230
+
2231
+ /**
2232
+ * Sincronizza il token Cognito in localStorage.
2233
+ * Necessario se l'interceptor axios legge il token tramite
2234
+ * localStorage.getItem("idToken") invece che dal context React.
2235
+ */
2236
+ export default function CognitoTokenSync({ children }: Props) {
2237
+ const { idToken, isAuthenticated } = useSession()
2238
+
2239
+ useEffect(() => {
2240
+ if (isAuthenticated && idToken) {
2241
+ localStorage.setItem('idToken', idToken)
2242
+ localStorage.setItem('accessToken', idToken)
2243
+ } else if (!isAuthenticated) {
2244
+ localStorage.removeItem('idToken')
2245
+ localStorage.removeItem('accessToken')
2246
+ localStorage.removeItem('refreshToken')
2247
+ }
2248
+ }, [idToken, isAuthenticated])
2249
+
2250
+ return <>{children}</>
2251
+ }
2252
+ ```
2253
+
2254
+ Interceptor axios corrispondente:
2255
+
2256
+ ```ts
2257
+ // src/api/axios-instance.ts
2258
+ import axios from 'axios'
2259
+
2260
+ const api = axios.create({
2261
+ baseURL: import.meta.env.VITE_API_BASE_URL,
2262
+ })
2263
+
2264
+ api.interceptors.request.use((config) => {
2265
+ const token = localStorage.getItem('idToken')
2266
+ if (token) {
2267
+ config.headers.Authorization = `Bearer ${token}`
2268
+ }
2269
+ return config
2270
+ })
2271
+
2272
+ api.interceptors.response.use(
2273
+ (res) => res,
2274
+ (err) => {
2275
+ if (err.response?.status === 401) {
2276
+ // Token scaduto — reindirizza al login
2277
+ window.location.href = '/login'
2278
+ }
2279
+ return Promise.reject(err)
2280
+ },
2281
+ )
2282
+
2283
+ export default api
2284
+ ```
2285
+
2286
+ In alternativa, senza `localStorage`, usa `useSession()` direttamente nel componente o in un custom hook:
2287
+
2288
+ ```ts
2289
+ // src/hooks/useApiClient.ts
2290
+ import { useMemo } from 'react'
2291
+ import { useSession } from 'cognito-max/react'
2292
+ import axios from 'axios'
2293
+
2294
+ export function useApiClient() {
2295
+ const { idToken } = useSession()
2296
+
2297
+ return useMemo(
2298
+ () =>
2299
+ axios.create({
2300
+ baseURL: import.meta.env.VITE_API_BASE_URL,
2301
+ headers: idToken ? { Authorization: `Bearer ${idToken}` } : {},
2302
+ }),
2303
+ [idToken],
2304
+ )
2305
+ }
2306
+ ```
2307
+
2308
+ ---
2309
+
2310
+ ### Routing completo
2311
+
2312
+ Snippet completo di `App.tsx` con tutte le route auth e protezione per le route private:
2313
+
2314
+ ```tsx
2315
+ // src/App.tsx — snippet completo
2316
+ import '@cloudscape-design/global-styles/index.css'
2317
+ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
2318
+ import { AuthProvider, ProtectedRoute } from 'cognito-max/react'
2319
+ import type { AuthConfig } from 'cognito-max'
2320
+
2321
+ // Pagine auth
2322
+ import LoginPage from './pages/login'
2323
+ import MfaVerifyPage from './pages/mfa-verify'
2324
+ import MfaSetupPage from './pages/mfa-setup'
2325
+ import NewPasswordPage from './pages/new-password'
2326
+ import ForgotPasswordPage from './pages/forgot-password'
2327
+ import ResetPasswordPage from './pages/reset-password'
2328
+ import RegisterPage from './pages/register'
2329
+ import ConfirmSignupPage from './pages/confirm-signup'
2330
+
2331
+ // Pagine autenticate
2332
+ import ChangePasswordPage from './pages/change-password'
2333
+ import ChangeMfaPage from './pages/change-mfa'
2334
+ import Dashboard from './pages/dashboard'
2335
+
2336
+ // Componenti
2337
+ import CognitoTokenSync from './components/CognitoTokenSync'
2338
+ import Spinner from '@cloudscape-design/components/spinner'
2339
+
2340
+ const cognitoConfig: AuthConfig = {
2341
+ userPoolId: import.meta.env.VITE_USER_POOL_ID,
2342
+ clientId: import.meta.env.VITE_CLIENT_ID,
2343
+ region: import.meta.env.VITE_REGION,
2344
+ totpIssuer: 'MyApp',
2345
+ }
2346
+
2347
+ export default function App() {
2348
+ return (
2349
+ <AuthProvider config={cognitoConfig}>
2350
+ <CognitoTokenSync>
2351
+ <BrowserRouter>
2352
+ <Routes>
2353
+ {/* Route pubbliche */}
2354
+ <Route path="/login" element={<LoginPage />} />
2355
+ <Route path="/mfa-verify" element={<MfaVerifyPage />} />
2356
+ <Route path="/mfa-setup" element={<MfaSetupPage />} />
2357
+ <Route path="/new-password" element={<NewPasswordPage />} />
2358
+ <Route path="/forgot-password" element={<ForgotPasswordPage />} />
2359
+ <Route path="/reset-password" element={<ResetPasswordPage />} />
2360
+ <Route path="/register" element={<RegisterPage />} />
2361
+ <Route path="/confirm-signup" element={<ConfirmSignupPage />} />
2362
+
2363
+ {/* Route protette — richiedono autenticazione */}
2364
+ <Route
2365
+ path="/change-password"
2366
+ element={
2367
+ <ProtectedRoute
2368
+ fallback={<Navigate to="/login" replace />}
2369
+ loading={<Spinner />}
2370
+ >
2371
+ <ChangePasswordPage />
2372
+ </ProtectedRoute>
2373
+ }
2374
+ />
2375
+ <Route
2376
+ path="/change-mfa"
2377
+ element={
2378
+ <ProtectedRoute
2379
+ fallback={<Navigate to="/login" replace />}
2380
+ loading={<Spinner />}
2381
+ >
2382
+ <ChangeMfaPage />
2383
+ </ProtectedRoute>
2384
+ }
2385
+ />
2386
+ <Route
2387
+ path="/"
2388
+ element={
2389
+ <ProtectedRoute
2390
+ fallback={<Navigate to="/login" replace />}
2391
+ loading={<Spinner />}
2392
+ >
2393
+ <Dashboard />
2394
+ </ProtectedRoute>
2395
+ }
2396
+ />
2397
+ <Route path="*" element={<Navigate to="/" />} />
2398
+ </Routes>
2399
+ </BrowserRouter>
2400
+ </CognitoTokenSync>
2401
+ </AuthProvider>
2402
+ )
2403
+ }
2404
+ ```
2405
+
2406
+ ---
2407
+
2408
+ ## License
2409
+
2410
+ MIT