@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
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { useEffect, useId, useRef, type ReactNode } from 'react'
|
|
2
|
+
import { useSignInForm, type UseSignInFormOptions } from '../hooks/useSignInForm'
|
|
3
|
+
|
|
4
|
+
export interface SignInFormProps extends UseSignInFormOptions {
|
|
5
|
+
className?: string
|
|
6
|
+
labels?: {
|
|
7
|
+
email?: string
|
|
8
|
+
password?: string
|
|
9
|
+
submit?: string
|
|
10
|
+
mfaCode?: string
|
|
11
|
+
mfaSubmit?: string
|
|
12
|
+
newPassword?: string
|
|
13
|
+
confirmNewPassword?: string
|
|
14
|
+
newPasswordSubmit?: string
|
|
15
|
+
mfaSetupScanInstruction?: string
|
|
16
|
+
mfaSetupSecretLabel?: string
|
|
17
|
+
mfaSetupCode?: string
|
|
18
|
+
mfaSetupSubmit?: string
|
|
19
|
+
}
|
|
20
|
+
/** Slot per il link "Password dimenticata?" */
|
|
21
|
+
forgotPasswordLink?: ReactNode
|
|
22
|
+
/** Slot per il link "Crea account" */
|
|
23
|
+
registerLink?: ReactNode
|
|
24
|
+
/**
|
|
25
|
+
* Render del QR code per lo step mfa_setup.
|
|
26
|
+
* Riceve il URI otpauth:// — usa react-qr-code o simile.
|
|
27
|
+
* Default: mostra il URI come testo copiabile.
|
|
28
|
+
*/
|
|
29
|
+
renderQrCode?: (uri: string) => ReactNode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function SignInForm({
|
|
33
|
+
className,
|
|
34
|
+
labels = {},
|
|
35
|
+
forgotPasswordLink,
|
|
36
|
+
registerLink,
|
|
37
|
+
renderQrCode,
|
|
38
|
+
onSuccess,
|
|
39
|
+
onError,
|
|
40
|
+
}: SignInFormProps) {
|
|
41
|
+
const uid = useId()
|
|
42
|
+
const form = useSignInForm({ onSuccess, onError })
|
|
43
|
+
const firstInputRef = useRef<HTMLInputElement>(null)
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
firstInputRef.current?.focus()
|
|
47
|
+
// Auto-start TOTP challenge setup when step transitions to mfa_setup
|
|
48
|
+
if (form.step === 'mfa_setup') {
|
|
49
|
+
void form.mfaSetup.start()
|
|
50
|
+
}
|
|
51
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
|
+
}, [form.step])
|
|
53
|
+
|
|
54
|
+
const l = {
|
|
55
|
+
email: labels.email ?? 'Email',
|
|
56
|
+
password: labels.password ?? 'Password',
|
|
57
|
+
submit: labels.submit ?? 'Accedi',
|
|
58
|
+
mfaCode: labels.mfaCode ?? 'Codice',
|
|
59
|
+
mfaSubmit: labels.mfaSubmit ?? 'Verifica',
|
|
60
|
+
newPassword: labels.newPassword ?? 'Nuova password',
|
|
61
|
+
confirmNewPassword: labels.confirmNewPassword ?? 'Conferma password',
|
|
62
|
+
newPasswordSubmit: labels.newPasswordSubmit ?? 'Imposta password',
|
|
63
|
+
mfaSetupScanInstruction:
|
|
64
|
+
labels.mfaSetupScanInstruction ??
|
|
65
|
+
"Scansiona il QR code con la tua app authenticator (Google Authenticator, Authy, 1Password…)",
|
|
66
|
+
mfaSetupSecretLabel: labels.mfaSetupSecretLabel ?? 'Oppure inserisci il codice manualmente',
|
|
67
|
+
mfaSetupCode: labels.mfaSetupCode ?? 'Codice a 6 cifre',
|
|
68
|
+
mfaSetupSubmit: labels.mfaSetupSubmit ?? 'Verifica e attiva',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const errorId = `${uid}-error`
|
|
72
|
+
|
|
73
|
+
if (form.step === 'mfa') {
|
|
74
|
+
return (
|
|
75
|
+
<form
|
|
76
|
+
onSubmit={form.mfa.onSubmit}
|
|
77
|
+
className={className}
|
|
78
|
+
aria-label="Verifica codice MFA"
|
|
79
|
+
>
|
|
80
|
+
<p>
|
|
81
|
+
Inserisci il codice{' '}
|
|
82
|
+
{form.mfaType === 'TOTP' ? "dall'app authenticator" : 'ricevuto via SMS'}
|
|
83
|
+
</p>
|
|
84
|
+
<div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
|
|
85
|
+
{form.error?.message}
|
|
86
|
+
</div>
|
|
87
|
+
<div>
|
|
88
|
+
<label htmlFor={`${uid}-mfa-code`}>{l.mfaCode}</label>
|
|
89
|
+
<input
|
|
90
|
+
id={`${uid}-mfa-code`}
|
|
91
|
+
ref={firstInputRef}
|
|
92
|
+
type="text"
|
|
93
|
+
inputMode="numeric"
|
|
94
|
+
maxLength={6}
|
|
95
|
+
autoComplete="one-time-code"
|
|
96
|
+
autoFocus
|
|
97
|
+
required
|
|
98
|
+
aria-required="true"
|
|
99
|
+
aria-invalid={!!form.error}
|
|
100
|
+
aria-describedby={form.error ? errorId : undefined}
|
|
101
|
+
value={form.mfa.code}
|
|
102
|
+
onChange={(e) => form.mfa.setCode(e.target.value)}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
<button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
|
|
106
|
+
{form.isLoading ? (
|
|
107
|
+
<>
|
|
108
|
+
<span aria-hidden="true">...</span>
|
|
109
|
+
<span className="sr-only">Caricamento...</span>
|
|
110
|
+
</>
|
|
111
|
+
) : (
|
|
112
|
+
l.mfaSubmit
|
|
113
|
+
)}
|
|
114
|
+
</button>
|
|
115
|
+
</form>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (form.step === 'new_password') {
|
|
120
|
+
return (
|
|
121
|
+
<form
|
|
122
|
+
onSubmit={form.newPassword.onSubmit}
|
|
123
|
+
className={className}
|
|
124
|
+
aria-label="Imposta nuova password"
|
|
125
|
+
>
|
|
126
|
+
<p>Devi impostare una nuova password per continuare.</p>
|
|
127
|
+
<div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
|
|
128
|
+
{form.error?.message}
|
|
129
|
+
</div>
|
|
130
|
+
<div>
|
|
131
|
+
<label htmlFor={`${uid}-new-password`}>{l.newPassword}</label>
|
|
132
|
+
<input
|
|
133
|
+
id={`${uid}-new-password`}
|
|
134
|
+
ref={firstInputRef}
|
|
135
|
+
type="password"
|
|
136
|
+
autoComplete="new-password"
|
|
137
|
+
autoFocus
|
|
138
|
+
required
|
|
139
|
+
aria-required="true"
|
|
140
|
+
aria-invalid={!!form.error}
|
|
141
|
+
aria-describedby={form.error ? errorId : undefined}
|
|
142
|
+
value={form.newPassword.password}
|
|
143
|
+
onChange={(e) => form.newPassword.setPassword(e.target.value)}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
<div>
|
|
147
|
+
<label htmlFor={`${uid}-confirm-password`}>{l.confirmNewPassword}</label>
|
|
148
|
+
<input
|
|
149
|
+
id={`${uid}-confirm-password`}
|
|
150
|
+
type="password"
|
|
151
|
+
autoComplete="new-password"
|
|
152
|
+
required
|
|
153
|
+
aria-required="true"
|
|
154
|
+
aria-invalid={!!form.error}
|
|
155
|
+
value={form.newPassword.confirmPassword}
|
|
156
|
+
onChange={(e) => form.newPassword.setConfirmPassword(e.target.value)}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
<button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
|
|
160
|
+
{form.isLoading ? (
|
|
161
|
+
<>
|
|
162
|
+
<span aria-hidden="true">...</span>
|
|
163
|
+
<span className="sr-only">Caricamento...</span>
|
|
164
|
+
</>
|
|
165
|
+
) : (
|
|
166
|
+
l.newPasswordSubmit
|
|
167
|
+
)}
|
|
168
|
+
</button>
|
|
169
|
+
</form>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (form.step === 'mfa_setup') {
|
|
174
|
+
return (
|
|
175
|
+
<div className={className}>
|
|
176
|
+
<div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
|
|
177
|
+
{form.error?.message}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
{form.isLoading && !form.mfaSetup.qrCodeUri && (
|
|
181
|
+
<p aria-live="polite" aria-atomic="true">
|
|
182
|
+
Inizializzazione in corso...
|
|
183
|
+
</p>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{form.mfaSetup.qrCodeUri && (
|
|
187
|
+
<div>
|
|
188
|
+
<p>{l.mfaSetupScanInstruction}</p>
|
|
189
|
+
{renderQrCode ? (
|
|
190
|
+
renderQrCode(form.mfaSetup.qrCodeUri)
|
|
191
|
+
) : (
|
|
192
|
+
<code style={{ wordBreak: 'break-all', fontSize: '0.75em' }}>
|
|
193
|
+
{form.mfaSetup.qrCodeUri}
|
|
194
|
+
</code>
|
|
195
|
+
)}
|
|
196
|
+
{form.mfaSetup.secretCode && (
|
|
197
|
+
<p>
|
|
198
|
+
{l.mfaSetupSecretLabel}: <code>{form.mfaSetup.secretCode}</code>
|
|
199
|
+
</p>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
<form onSubmit={form.mfaSetup.onVerify} aria-label="Verifica configurazione MFA">
|
|
205
|
+
<div>
|
|
206
|
+
<label htmlFor={`${uid}-totp-code`}>{l.mfaSetupCode}</label>
|
|
207
|
+
<input
|
|
208
|
+
id={`${uid}-totp-code`}
|
|
209
|
+
ref={firstInputRef}
|
|
210
|
+
type="text"
|
|
211
|
+
inputMode="numeric"
|
|
212
|
+
maxLength={6}
|
|
213
|
+
autoComplete="one-time-code"
|
|
214
|
+
autoFocus
|
|
215
|
+
required
|
|
216
|
+
aria-required="true"
|
|
217
|
+
aria-invalid={!!form.error}
|
|
218
|
+
aria-describedby={form.error ? errorId : undefined}
|
|
219
|
+
disabled={form.isLoading && !form.mfaSetup.qrCodeUri}
|
|
220
|
+
value={form.mfaSetup.totpCode}
|
|
221
|
+
onChange={(e) => form.mfaSetup.setTotpCode(e.target.value)}
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
<button
|
|
225
|
+
type="submit"
|
|
226
|
+
disabled={form.isLoading || form.mfaSetup.totpCode.length !== 6}
|
|
227
|
+
aria-busy={form.isLoading}
|
|
228
|
+
>
|
|
229
|
+
{form.isLoading ? (
|
|
230
|
+
<>
|
|
231
|
+
<span aria-hidden="true">...</span>
|
|
232
|
+
<span className="sr-only">Caricamento...</span>
|
|
233
|
+
</>
|
|
234
|
+
) : (
|
|
235
|
+
l.mfaSetupSubmit
|
|
236
|
+
)}
|
|
237
|
+
</button>
|
|
238
|
+
</form>
|
|
239
|
+
</div>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Default: credentials step
|
|
244
|
+
return (
|
|
245
|
+
<form
|
|
246
|
+
onSubmit={form.credentials.onSubmit}
|
|
247
|
+
className={className}
|
|
248
|
+
aria-label="Accedi al tuo account"
|
|
249
|
+
>
|
|
250
|
+
<div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
|
|
251
|
+
{form.error?.message}
|
|
252
|
+
</div>
|
|
253
|
+
<div>
|
|
254
|
+
<label htmlFor={`${uid}-email`}>{l.email}</label>
|
|
255
|
+
<input
|
|
256
|
+
id={`${uid}-email`}
|
|
257
|
+
ref={firstInputRef}
|
|
258
|
+
type="email"
|
|
259
|
+
autoComplete="email"
|
|
260
|
+
autoFocus
|
|
261
|
+
required
|
|
262
|
+
aria-required="true"
|
|
263
|
+
aria-invalid={!!form.error}
|
|
264
|
+
aria-describedby={form.error ? errorId : undefined}
|
|
265
|
+
value={form.credentials.email}
|
|
266
|
+
onChange={(e) => form.credentials.setEmail(e.target.value)}
|
|
267
|
+
/>
|
|
268
|
+
</div>
|
|
269
|
+
<div>
|
|
270
|
+
<label htmlFor={`${uid}-password`}>{l.password}</label>
|
|
271
|
+
<input
|
|
272
|
+
id={`${uid}-password`}
|
|
273
|
+
type="password"
|
|
274
|
+
autoComplete="current-password"
|
|
275
|
+
required
|
|
276
|
+
aria-required="true"
|
|
277
|
+
aria-invalid={!!form.error}
|
|
278
|
+
value={form.credentials.password}
|
|
279
|
+
onChange={(e) => form.credentials.setPassword(e.target.value)}
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
{forgotPasswordLink}
|
|
283
|
+
<button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
|
|
284
|
+
{form.isLoading ? (
|
|
285
|
+
<>
|
|
286
|
+
<span aria-hidden="true">...</span>
|
|
287
|
+
<span className="sr-only">Caricamento...</span>
|
|
288
|
+
</>
|
|
289
|
+
) : (
|
|
290
|
+
l.submit
|
|
291
|
+
)}
|
|
292
|
+
</button>
|
|
293
|
+
{registerLink}
|
|
294
|
+
</form>
|
|
295
|
+
)
|
|
296
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useCallback, useState, type FormEvent } from 'react'
|
|
2
|
+
import { useAuth } from '../../react/hooks/useAuth'
|
|
3
|
+
import type { CognitoAuthError } from '../../core/errors'
|
|
4
|
+
|
|
5
|
+
export interface UseChangePasswordFormOptions {
|
|
6
|
+
onSuccess?: () => void
|
|
7
|
+
onError?: (error: CognitoAuthError) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface UseChangePasswordFormReturn {
|
|
11
|
+
isLoading: boolean
|
|
12
|
+
error: CognitoAuthError | null
|
|
13
|
+
success: boolean
|
|
14
|
+
currentPassword: string
|
|
15
|
+
newPassword: string
|
|
16
|
+
confirmPassword: string
|
|
17
|
+
setCurrentPassword(v: string): void
|
|
18
|
+
setNewPassword(v: string): void
|
|
19
|
+
setConfirmPassword(v: string): void
|
|
20
|
+
onSubmit(e: FormEvent): void
|
|
21
|
+
reset(): void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useChangePasswordForm(
|
|
25
|
+
options: UseChangePasswordFormOptions = {},
|
|
26
|
+
): UseChangePasswordFormReturn {
|
|
27
|
+
const { changePassword } = useAuth()
|
|
28
|
+
|
|
29
|
+
const [isLoading, setLoading] = useState(false)
|
|
30
|
+
const [error, setError] = useState<CognitoAuthError | null>(null)
|
|
31
|
+
const [success, setSuccess] = useState(false)
|
|
32
|
+
const [currentPassword, setCurrentPassword] = useState('')
|
|
33
|
+
const [newPassword, setNewPassword] = useState('')
|
|
34
|
+
const [confirmPassword, setConfirmPassword] = useState('')
|
|
35
|
+
|
|
36
|
+
const handleSubmit = useCallback(
|
|
37
|
+
async (e: FormEvent) => {
|
|
38
|
+
e.preventDefault()
|
|
39
|
+
if (newPassword !== confirmPassword) {
|
|
40
|
+
setError({ message: 'Le nuove password non coincidono', code: 'INVALID_PASSWORD' } as CognitoAuthError)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
setError(null)
|
|
44
|
+
setLoading(true)
|
|
45
|
+
try {
|
|
46
|
+
await changePassword(currentPassword, newPassword)
|
|
47
|
+
setSuccess(true)
|
|
48
|
+
options.onSuccess?.()
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const authErr = err as CognitoAuthError
|
|
51
|
+
setError(authErr)
|
|
52
|
+
options.onError?.(authErr)
|
|
53
|
+
} finally {
|
|
54
|
+
setLoading(false)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[currentPassword, newPassword, confirmPassword, changePassword, options],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const reset = useCallback(() => {
|
|
61
|
+
setCurrentPassword('')
|
|
62
|
+
setNewPassword('')
|
|
63
|
+
setConfirmPassword('')
|
|
64
|
+
setError(null)
|
|
65
|
+
setSuccess(false)
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
isLoading,
|
|
70
|
+
error,
|
|
71
|
+
success,
|
|
72
|
+
currentPassword,
|
|
73
|
+
newPassword,
|
|
74
|
+
confirmPassword,
|
|
75
|
+
setCurrentPassword,
|
|
76
|
+
setNewPassword,
|
|
77
|
+
setConfirmPassword,
|
|
78
|
+
onSubmit: handleSubmit,
|
|
79
|
+
reset,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useCallback, useState, type FormEvent } from 'react'
|
|
2
|
+
import { useAuth } from '../../react/hooks/useAuth'
|
|
3
|
+
import type { CognitoAuthError } from '../../core/errors'
|
|
4
|
+
|
|
5
|
+
export type ForgotPasswordStep = 'request' | 'reset'
|
|
6
|
+
|
|
7
|
+
export interface UseForgotPasswordFormOptions {
|
|
8
|
+
onSuccess?: () => void
|
|
9
|
+
onError?: (error: CognitoAuthError) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseForgotPasswordFormReturn {
|
|
13
|
+
step: ForgotPasswordStep
|
|
14
|
+
isLoading: boolean
|
|
15
|
+
error: CognitoAuthError | null
|
|
16
|
+
|
|
17
|
+
request: {
|
|
18
|
+
email: string
|
|
19
|
+
setEmail(v: string): void
|
|
20
|
+
onSubmit(e: FormEvent): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
reset: {
|
|
24
|
+
email: string
|
|
25
|
+
code: string
|
|
26
|
+
newPassword: string
|
|
27
|
+
confirmPassword: string
|
|
28
|
+
setCode(v: string): void
|
|
29
|
+
setNewPassword(v: string): void
|
|
30
|
+
setConfirmPassword(v: string): void
|
|
31
|
+
onSubmit(e: FormEvent): void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
restart(): void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useForgotPasswordForm(
|
|
38
|
+
options: UseForgotPasswordFormOptions = {},
|
|
39
|
+
): UseForgotPasswordFormReturn {
|
|
40
|
+
const { forgotPassword, confirmForgotPassword } = useAuth()
|
|
41
|
+
|
|
42
|
+
const [step, setStep] = useState<ForgotPasswordStep>('request')
|
|
43
|
+
const [isLoading, setLoading] = useState(false)
|
|
44
|
+
const [error, setError] = useState<CognitoAuthError | null>(null)
|
|
45
|
+
|
|
46
|
+
const [email, setEmail] = useState('')
|
|
47
|
+
const [code, setCode] = useState('')
|
|
48
|
+
const [newPassword, setNewPassword] = useState('')
|
|
49
|
+
const [confirmPassword, setConfirmPassword] = useState('')
|
|
50
|
+
|
|
51
|
+
const handleRequestSubmit = useCallback(
|
|
52
|
+
async (e: FormEvent) => {
|
|
53
|
+
e.preventDefault()
|
|
54
|
+
setError(null)
|
|
55
|
+
setLoading(true)
|
|
56
|
+
try {
|
|
57
|
+
await forgotPassword(email)
|
|
58
|
+
setStep('reset')
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const authErr = err as CognitoAuthError
|
|
61
|
+
setError(authErr)
|
|
62
|
+
options.onError?.(authErr)
|
|
63
|
+
} finally {
|
|
64
|
+
setLoading(false)
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
[email, forgotPassword, options],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const handleResetSubmit = useCallback(
|
|
71
|
+
async (e: FormEvent) => {
|
|
72
|
+
e.preventDefault()
|
|
73
|
+
if (newPassword !== confirmPassword) {
|
|
74
|
+
setError({ message: 'Le password non coincidono', code: 'INVALID_PASSWORD' } as CognitoAuthError)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
setError(null)
|
|
78
|
+
setLoading(true)
|
|
79
|
+
try {
|
|
80
|
+
await confirmForgotPassword(email, code, newPassword)
|
|
81
|
+
options.onSuccess?.()
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const authErr = err as CognitoAuthError
|
|
84
|
+
setError(authErr)
|
|
85
|
+
options.onError?.(authErr)
|
|
86
|
+
} finally {
|
|
87
|
+
setLoading(false)
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
[email, code, newPassword, confirmPassword, confirmForgotPassword, options],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const restart = useCallback(() => {
|
|
94
|
+
setStep('request')
|
|
95
|
+
setCode('')
|
|
96
|
+
setNewPassword('')
|
|
97
|
+
setConfirmPassword('')
|
|
98
|
+
setError(null)
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
step,
|
|
103
|
+
isLoading,
|
|
104
|
+
error,
|
|
105
|
+
request: { email, setEmail, onSubmit: handleRequestSubmit },
|
|
106
|
+
reset: { email, code, newPassword, confirmPassword, setCode, setNewPassword, setConfirmPassword, onSubmit: handleResetSubmit },
|
|
107
|
+
restart,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useCallback, useState, type FormEvent } from 'react'
|
|
2
|
+
import { useMfa } from '../../react/hooks/useMfa'
|
|
3
|
+
import type { CognitoAuthError } from '../../core/errors'
|
|
4
|
+
|
|
5
|
+
export type MfaSetupStep = 'idle' | 'loading' | 'scan' | 'verify' | 'done'
|
|
6
|
+
|
|
7
|
+
export interface UseMfaSetupOptions {
|
|
8
|
+
onSuccess?: () => void
|
|
9
|
+
onError?: (error: CognitoAuthError) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseMfaSetupReturn {
|
|
13
|
+
step: MfaSetupStep
|
|
14
|
+
isLoading: boolean
|
|
15
|
+
error: CognitoAuthError | null
|
|
16
|
+
/** URI otpauth:// da passare a un QR code renderer */
|
|
17
|
+
qrCodeUri: string | null
|
|
18
|
+
/** Secret testuale (per inserimento manuale nell'app) */
|
|
19
|
+
secretCode: string | null
|
|
20
|
+
totpCode: string
|
|
21
|
+
setTotpCode(v: string): void
|
|
22
|
+
/** Avvia il setup TOTP */
|
|
23
|
+
start(): Promise<void>
|
|
24
|
+
/** Verifica il codice inserito dall'app authenticator */
|
|
25
|
+
onVerify(e: FormEvent): void
|
|
26
|
+
reset(): void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useMfaSetup(options: UseMfaSetupOptions = {}): UseMfaSetupReturn {
|
|
30
|
+
const { setup, verifySetup } = useMfa()
|
|
31
|
+
|
|
32
|
+
const [step, setStep] = useState<MfaSetupStep>('idle')
|
|
33
|
+
const [error, setError] = useState<CognitoAuthError | null>(null)
|
|
34
|
+
const [qrCodeUri, setQrCodeUri] = useState<string | null>(null)
|
|
35
|
+
const [secretCode, setSecretCode] = useState<string | null>(null)
|
|
36
|
+
const [totpCode, setTotpCode] = useState('')
|
|
37
|
+
|
|
38
|
+
const start = useCallback(async () => {
|
|
39
|
+
setError(null)
|
|
40
|
+
setStep('loading')
|
|
41
|
+
try {
|
|
42
|
+
const result = await setup()
|
|
43
|
+
setQrCodeUri(result.qrCodeUri)
|
|
44
|
+
setSecretCode(result.secretCode)
|
|
45
|
+
setStep('scan')
|
|
46
|
+
} catch (err) {
|
|
47
|
+
const authErr = err as CognitoAuthError
|
|
48
|
+
setError(authErr)
|
|
49
|
+
setStep('idle')
|
|
50
|
+
options.onError?.(authErr)
|
|
51
|
+
}
|
|
52
|
+
}, [setup, options])
|
|
53
|
+
|
|
54
|
+
const handleVerify = useCallback(
|
|
55
|
+
async (e: FormEvent) => {
|
|
56
|
+
e.preventDefault()
|
|
57
|
+
setError(null)
|
|
58
|
+
setStep('loading')
|
|
59
|
+
try {
|
|
60
|
+
await verifySetup(totpCode)
|
|
61
|
+
setStep('done')
|
|
62
|
+
options.onSuccess?.()
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const authErr = err as CognitoAuthError
|
|
65
|
+
setError(authErr)
|
|
66
|
+
setStep('verify')
|
|
67
|
+
options.onError?.(authErr)
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[totpCode, verifySetup, options],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const reset = useCallback(() => {
|
|
74
|
+
setStep('idle')
|
|
75
|
+
setError(null)
|
|
76
|
+
setQrCodeUri(null)
|
|
77
|
+
setSecretCode(null)
|
|
78
|
+
setTotpCode('')
|
|
79
|
+
}, [])
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
step,
|
|
83
|
+
isLoading: step === 'loading',
|
|
84
|
+
error,
|
|
85
|
+
qrCodeUri,
|
|
86
|
+
secretCode,
|
|
87
|
+
totpCode,
|
|
88
|
+
setTotpCode,
|
|
89
|
+
start,
|
|
90
|
+
onVerify: handleVerify,
|
|
91
|
+
reset,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useCallback, useState, type FormEvent } from 'react'
|
|
2
|
+
import { useAuth } from '../../react/hooks/useAuth'
|
|
3
|
+
import type { CognitoAuthError } from '../../core/errors'
|
|
4
|
+
|
|
5
|
+
export type RegisterStep = 'register' | 'confirm'
|
|
6
|
+
|
|
7
|
+
export interface UseRegisterFormOptions {
|
|
8
|
+
onSuccess?: () => void
|
|
9
|
+
onError?: (error: CognitoAuthError) => void
|
|
10
|
+
/** Attributi aggiuntivi (es. name, phone_number) */
|
|
11
|
+
extraAttributes?: Record<string, string>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseRegisterFormReturn {
|
|
15
|
+
step: RegisterStep
|
|
16
|
+
isLoading: boolean
|
|
17
|
+
error: CognitoAuthError | null
|
|
18
|
+
|
|
19
|
+
register: {
|
|
20
|
+
email: string
|
|
21
|
+
password: string
|
|
22
|
+
confirmPassword: string
|
|
23
|
+
setEmail(v: string): void
|
|
24
|
+
setPassword(v: string): void
|
|
25
|
+
setConfirmPassword(v: string): void
|
|
26
|
+
onSubmit(e: FormEvent): void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
confirm: {
|
|
30
|
+
email: string
|
|
31
|
+
code: string
|
|
32
|
+
setCode(v: string): void
|
|
33
|
+
onSubmit(e: FormEvent): void
|
|
34
|
+
resend(): Promise<void>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
reset(): void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useRegisterForm(options: UseRegisterFormOptions = {}): UseRegisterFormReturn {
|
|
41
|
+
const { signUp, confirmSignUp, resendConfirmationCode } = useAuth()
|
|
42
|
+
|
|
43
|
+
const [step, setStep] = useState<RegisterStep>('register')
|
|
44
|
+
const [isLoading, setLoading] = useState(false)
|
|
45
|
+
const [error, setError] = useState<CognitoAuthError | null>(null)
|
|
46
|
+
|
|
47
|
+
const [email, setEmail] = useState('')
|
|
48
|
+
const [password, setPassword] = useState('')
|
|
49
|
+
const [confirmPassword, setConfirmPassword] = useState('')
|
|
50
|
+
const [code, setCode] = useState('')
|
|
51
|
+
|
|
52
|
+
const handleRegisterSubmit = useCallback(
|
|
53
|
+
async (e: FormEvent) => {
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
if (password !== confirmPassword) {
|
|
56
|
+
setError({ message: 'Le password non coincidono', code: 'INVALID_PASSWORD' } as CognitoAuthError)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
setError(null)
|
|
60
|
+
setLoading(true)
|
|
61
|
+
try {
|
|
62
|
+
await signUp(email, password, options.extraAttributes)
|
|
63
|
+
setStep('confirm')
|
|
64
|
+
} catch (err) {
|
|
65
|
+
const authErr = err as CognitoAuthError
|
|
66
|
+
setError(authErr)
|
|
67
|
+
options.onError?.(authErr)
|
|
68
|
+
} finally {
|
|
69
|
+
setLoading(false)
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[email, password, confirmPassword, signUp, options],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const handleConfirmSubmit = useCallback(
|
|
76
|
+
async (e: FormEvent) => {
|
|
77
|
+
e.preventDefault()
|
|
78
|
+
setError(null)
|
|
79
|
+
setLoading(true)
|
|
80
|
+
try {
|
|
81
|
+
await confirmSignUp(email, code)
|
|
82
|
+
options.onSuccess?.()
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const authErr = err as CognitoAuthError
|
|
85
|
+
setError(authErr)
|
|
86
|
+
options.onError?.(authErr)
|
|
87
|
+
} finally {
|
|
88
|
+
setLoading(false)
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
[email, code, confirmSignUp, options],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
const resend = useCallback(async () => {
|
|
95
|
+
setError(null)
|
|
96
|
+
try {
|
|
97
|
+
await resendConfirmationCode(email)
|
|
98
|
+
} catch (err) {
|
|
99
|
+
setError(err as CognitoAuthError)
|
|
100
|
+
}
|
|
101
|
+
}, [email, resendConfirmationCode])
|
|
102
|
+
|
|
103
|
+
const reset = useCallback(() => {
|
|
104
|
+
setStep('register')
|
|
105
|
+
setEmail('')
|
|
106
|
+
setPassword('')
|
|
107
|
+
setConfirmPassword('')
|
|
108
|
+
setCode('')
|
|
109
|
+
setError(null)
|
|
110
|
+
}, [])
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
step,
|
|
114
|
+
isLoading,
|
|
115
|
+
error,
|
|
116
|
+
register: { email, password, confirmPassword, setEmail, setPassword, setConfirmPassword, onSubmit: handleRegisterSubmit },
|
|
117
|
+
confirm: { email, code, setCode, onSubmit: handleConfirmSubmit, resend },
|
|
118
|
+
reset,
|
|
119
|
+
}
|
|
120
|
+
}
|