@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.
- package/README.md +2410 -0
- package/dist/chunk-AD7T42HJ.js +3 -0
- package/dist/chunk-AD7T42HJ.js.map +1 -0
- package/dist/chunk-DKPFVGTY.js +683 -0
- package/dist/chunk-DKPFVGTY.js.map +1 -0
- package/dist/chunk-N4OQLBV6.js +135 -0
- package/dist/chunk-N4OQLBV6.js.map +1 -0
- package/dist/client-63FraVdm.d.ts +69 -0
- package/dist/client-BAoL8h4E.d.cts +69 -0
- package/dist/core/index.cjs +696 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +3 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/errors-BkUDHleb.d.cts +22 -0
- package/dist/errors-BkUDHleb.d.ts +22 -0
- package/dist/index.cjs +696 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +844 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +104 -0
- package/dist/react/index.d.ts +104 -0
- package/dist/react/index.js +64 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types-bxA1vonL.d.cts +113 -0
- package/dist/types-bxA1vonL.d.ts +113 -0
- package/dist/ui/index.cjs +1183 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +241 -0
- package/dist/ui/index.d.ts +241 -0
- package/dist/ui/index.js +1109 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +81 -0
- package/src/core/client.ts +604 -0
- package/src/core/errors.ts +91 -0
- package/src/core/event-bus.ts +41 -0
- package/src/core/index.ts +5 -0
- package/src/core/internal/converters.ts +32 -0
- package/src/core/storage.ts +79 -0
- package/src/core/types.ts +87 -0
- package/src/index.ts +1 -0
- package/src/react/components/ProtectedRoute.tsx +56 -0
- package/src/react/context.tsx +126 -0
- package/src/react/hooks/useAuth.ts +75 -0
- package/src/react/hooks/useMfa.ts +19 -0
- package/src/react/hooks/useSession.ts +16 -0
- package/src/react/hooks/useUser.ts +24 -0
- package/src/react/index.ts +10 -0
- package/src/ui/components/ChangePasswordForm.tsx +105 -0
- package/src/ui/components/ForgotPasswordForm.tsx +159 -0
- package/src/ui/components/MfaSetupWizard.tsx +136 -0
- package/src/ui/components/RegisterForm.tsx +159 -0
- package/src/ui/components/SignInForm.tsx +296 -0
- package/src/ui/hooks/useChangePasswordForm.ts +81 -0
- package/src/ui/hooks/useForgotPasswordForm.ts +109 -0
- package/src/ui/hooks/useMfaSetup.ts +93 -0
- package/src/ui/hooks/useRegisterForm.ts +120 -0
- package/src/ui/hooks/useSignInForm.ts +245 -0
- 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'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'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'autenticazione a due fattori per completare l'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'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'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
|