@massimo.mazzoleni/cognito-max 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +2410 -0
  2. package/dist/chunk-AD7T42HJ.js +3 -0
  3. package/dist/chunk-AD7T42HJ.js.map +1 -0
  4. package/dist/chunk-DKPFVGTY.js +683 -0
  5. package/dist/chunk-DKPFVGTY.js.map +1 -0
  6. package/dist/chunk-N4OQLBV6.js +135 -0
  7. package/dist/chunk-N4OQLBV6.js.map +1 -0
  8. package/dist/client-63FraVdm.d.ts +69 -0
  9. package/dist/client-BAoL8h4E.d.cts +69 -0
  10. package/dist/core/index.cjs +696 -0
  11. package/dist/core/index.cjs.map +1 -0
  12. package/dist/core/index.d.cts +3 -0
  13. package/dist/core/index.d.ts +3 -0
  14. package/dist/core/index.js +4 -0
  15. package/dist/core/index.js.map +1 -0
  16. package/dist/errors-BkUDHleb.d.cts +22 -0
  17. package/dist/errors-BkUDHleb.d.ts +22 -0
  18. package/dist/index.cjs +696 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.cts +3 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.js +4 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/react/index.cjs +844 -0
  25. package/dist/react/index.cjs.map +1 -0
  26. package/dist/react/index.d.cts +104 -0
  27. package/dist/react/index.d.ts +104 -0
  28. package/dist/react/index.js +64 -0
  29. package/dist/react/index.js.map +1 -0
  30. package/dist/types-bxA1vonL.d.cts +113 -0
  31. package/dist/types-bxA1vonL.d.ts +113 -0
  32. package/dist/ui/index.cjs +1183 -0
  33. package/dist/ui/index.cjs.map +1 -0
  34. package/dist/ui/index.d.cts +241 -0
  35. package/dist/ui/index.d.ts +241 -0
  36. package/dist/ui/index.js +1109 -0
  37. package/dist/ui/index.js.map +1 -0
  38. package/package.json +81 -0
  39. package/src/core/client.ts +604 -0
  40. package/src/core/errors.ts +91 -0
  41. package/src/core/event-bus.ts +41 -0
  42. package/src/core/index.ts +5 -0
  43. package/src/core/internal/converters.ts +32 -0
  44. package/src/core/storage.ts +79 -0
  45. package/src/core/types.ts +87 -0
  46. package/src/index.ts +1 -0
  47. package/src/react/components/ProtectedRoute.tsx +56 -0
  48. package/src/react/context.tsx +126 -0
  49. package/src/react/hooks/useAuth.ts +75 -0
  50. package/src/react/hooks/useMfa.ts +19 -0
  51. package/src/react/hooks/useSession.ts +16 -0
  52. package/src/react/hooks/useUser.ts +24 -0
  53. package/src/react/index.ts +10 -0
  54. package/src/ui/components/ChangePasswordForm.tsx +105 -0
  55. package/src/ui/components/ForgotPasswordForm.tsx +159 -0
  56. package/src/ui/components/MfaSetupWizard.tsx +136 -0
  57. package/src/ui/components/RegisterForm.tsx +159 -0
  58. package/src/ui/components/SignInForm.tsx +296 -0
  59. package/src/ui/hooks/useChangePasswordForm.ts +81 -0
  60. package/src/ui/hooks/useForgotPasswordForm.ts +109 -0
  61. package/src/ui/hooks/useMfaSetup.ts +93 -0
  62. package/src/ui/hooks/useRegisterForm.ts +120 -0
  63. package/src/ui/hooks/useSignInForm.ts +245 -0
  64. package/src/ui/index.ts +31 -0
@@ -0,0 +1,159 @@
1
+ import { useEffect, useId, useRef, type ReactNode } from 'react'
2
+ import {
3
+ useForgotPasswordForm,
4
+ type UseForgotPasswordFormOptions,
5
+ } from '../hooks/useForgotPasswordForm'
6
+
7
+ export interface ForgotPasswordFormProps extends UseForgotPasswordFormOptions {
8
+ className?: string
9
+ labels?: {
10
+ email?: string
11
+ requestSubmit?: string
12
+ code?: string
13
+ newPassword?: string
14
+ confirmPassword?: string
15
+ resetSubmit?: string
16
+ }
17
+ signInLink?: ReactNode
18
+ }
19
+
20
+ export function ForgotPasswordForm({
21
+ className,
22
+ labels = {},
23
+ signInLink,
24
+ onSuccess,
25
+ onError,
26
+ }: ForgotPasswordFormProps) {
27
+ const uid = useId()
28
+ const form = useForgotPasswordForm({ onSuccess, onError })
29
+ const firstInputRef = useRef<HTMLInputElement>(null)
30
+
31
+ useEffect(() => {
32
+ firstInputRef.current?.focus()
33
+ }, [form.step])
34
+
35
+ const l = {
36
+ email: labels.email ?? 'Email',
37
+ requestSubmit: labels.requestSubmit ?? 'Invia codice',
38
+ code: labels.code ?? 'Codice di verifica',
39
+ newPassword: labels.newPassword ?? 'Nuova password',
40
+ confirmPassword: labels.confirmPassword ?? 'Conferma password',
41
+ resetSubmit: labels.resetSubmit ?? 'Reimposta password',
42
+ }
43
+
44
+ const errorId = `${uid}-error`
45
+
46
+ if (form.step === 'reset') {
47
+ return (
48
+ <form
49
+ onSubmit={form.reset.onSubmit}
50
+ className={className}
51
+ aria-label="Reimposta password"
52
+ >
53
+ <p>
54
+ Abbiamo inviato un codice a <strong>{form.reset.email}</strong>.
55
+ </p>
56
+ <div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
57
+ {form.error?.message}
58
+ </div>
59
+ <div>
60
+ <label htmlFor={`${uid}-code`}>{l.code}</label>
61
+ <input
62
+ id={`${uid}-code`}
63
+ ref={firstInputRef}
64
+ type="text"
65
+ inputMode="numeric"
66
+ maxLength={6}
67
+ autoComplete="one-time-code"
68
+ autoFocus
69
+ required
70
+ aria-required="true"
71
+ aria-invalid={!!form.error}
72
+ aria-describedby={form.error ? errorId : undefined}
73
+ value={form.reset.code}
74
+ onChange={(e) => form.reset.setCode(e.target.value)}
75
+ />
76
+ </div>
77
+ <div>
78
+ <label htmlFor={`${uid}-new-password`}>{l.newPassword}</label>
79
+ <input
80
+ id={`${uid}-new-password`}
81
+ type="password"
82
+ autoComplete="new-password"
83
+ required
84
+ aria-required="true"
85
+ aria-invalid={!!form.error}
86
+ value={form.reset.newPassword}
87
+ onChange={(e) => form.reset.setNewPassword(e.target.value)}
88
+ />
89
+ </div>
90
+ <div>
91
+ <label htmlFor={`${uid}-confirm-password`}>{l.confirmPassword}</label>
92
+ <input
93
+ id={`${uid}-confirm-password`}
94
+ type="password"
95
+ autoComplete="new-password"
96
+ required
97
+ aria-required="true"
98
+ aria-invalid={!!form.error}
99
+ value={form.reset.confirmPassword}
100
+ onChange={(e) => form.reset.setConfirmPassword(e.target.value)}
101
+ />
102
+ </div>
103
+ <button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
104
+ {form.isLoading ? (
105
+ <>
106
+ <span aria-hidden="true">...</span>
107
+ <span className="sr-only">Caricamento...</span>
108
+ </>
109
+ ) : (
110
+ l.resetSubmit
111
+ )}
112
+ </button>
113
+ <button type="button" onClick={form.restart}>
114
+ Torna all&apos;inizio
115
+ </button>
116
+ </form>
117
+ )
118
+ }
119
+
120
+ return (
121
+ <form
122
+ onSubmit={form.request.onSubmit}
123
+ className={className}
124
+ aria-label="Richiedi codice di reimpostazione password"
125
+ >
126
+ <p>Inserisci la tua email per ricevere il codice di reimpostazione.</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}-email`}>{l.email}</label>
132
+ <input
133
+ id={`${uid}-email`}
134
+ ref={firstInputRef}
135
+ type="email"
136
+ autoComplete="email"
137
+ autoFocus
138
+ required
139
+ aria-required="true"
140
+ aria-invalid={!!form.error}
141
+ aria-describedby={form.error ? errorId : undefined}
142
+ value={form.request.email}
143
+ onChange={(e) => form.request.setEmail(e.target.value)}
144
+ />
145
+ </div>
146
+ <button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
147
+ {form.isLoading ? (
148
+ <>
149
+ <span aria-hidden="true">...</span>
150
+ <span className="sr-only">Caricamento...</span>
151
+ </>
152
+ ) : (
153
+ l.requestSubmit
154
+ )}
155
+ </button>
156
+ {signInLink}
157
+ </form>
158
+ )
159
+ }
@@ -0,0 +1,136 @@
1
+ import { useEffect, useId, useRef, type ReactNode } from 'react'
2
+ import { useMfaSetup, type UseMfaSetupOptions } from '../hooks/useMfaSetup'
3
+
4
+ export interface MfaSetupWizardProps extends UseMfaSetupOptions {
5
+ className?: string
6
+ labels?: {
7
+ start?: string
8
+ scanInstruction?: string
9
+ manualEntry?: string
10
+ codeLabel?: string
11
+ verifySubmit?: string
12
+ successMessage?: string
13
+ }
14
+ /**
15
+ * Render del QR code. Riceve il URI otpauth:// — usa react-qr-code o simile.
16
+ * Default: mostra il URI come testo copiabile.
17
+ */
18
+ renderQrCode?: (uri: string) => ReactNode
19
+ }
20
+
21
+ export function MfaSetupWizard({
22
+ className,
23
+ labels = {},
24
+ renderQrCode,
25
+ onSuccess,
26
+ onError,
27
+ }: MfaSetupWizardProps) {
28
+ const uid = useId()
29
+ const wizard = useMfaSetup({ onSuccess, onError })
30
+ const firstInputRef = useRef<HTMLInputElement>(null)
31
+
32
+ useEffect(() => {
33
+ firstInputRef.current?.focus()
34
+ }, [wizard.step])
35
+
36
+ const l = {
37
+ start: labels.start ?? 'Configura autenticazione a due fattori (TOTP)',
38
+ scanInstruction:
39
+ labels.scanInstruction ??
40
+ 'Scansiona il QR code con la tua app authenticator (Google Authenticator, Authy, 1Password…)',
41
+ manualEntry: labels.manualEntry ?? 'Oppure inserisci il codice manualmente:',
42
+ codeLabel: labels.codeLabel ?? 'Codice a 6 cifre',
43
+ verifySubmit: labels.verifySubmit ?? 'Verifica e attiva',
44
+ successMessage: labels.successMessage ?? 'Autenticazione a due fattori attivata con successo.',
45
+ }
46
+
47
+ const errorId = `${uid}-error`
48
+
49
+ if (wizard.step === 'done') {
50
+ return <p role="status">{l.successMessage}</p>
51
+ }
52
+
53
+ if (wizard.step === 'idle') {
54
+ return (
55
+ <div className={className}>
56
+ <div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
57
+ {wizard.error?.message}
58
+ </div>
59
+ {/* autoFocus handles focus on initial render; firstInputRef targets inputs in later steps */}
60
+ <button type="button" autoFocus onClick={wizard.start}>
61
+ {l.start}
62
+ </button>
63
+ </div>
64
+ )
65
+ }
66
+
67
+ if (wizard.step === 'loading') {
68
+ return (
69
+ <div className={className} aria-live="polite" aria-atomic="true">
70
+ ...
71
+ </div>
72
+ )
73
+ }
74
+
75
+ // step === 'scan' | 'verify'
76
+ return (
77
+ <div className={className}>
78
+ <div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
79
+ {wizard.error?.message}
80
+ </div>
81
+
82
+ {wizard.qrCodeUri && (
83
+ <div>
84
+ <p>{l.scanInstruction}</p>
85
+ {renderQrCode ? (
86
+ renderQrCode(wizard.qrCodeUri)
87
+ ) : (
88
+ <code style={{ wordBreak: 'break-all', fontSize: '0.75em' }}>
89
+ {wizard.qrCodeUri}
90
+ </code>
91
+ )}
92
+ {wizard.secretCode && (
93
+ <p>
94
+ {l.manualEntry} <code>{wizard.secretCode}</code>
95
+ </p>
96
+ )}
97
+ </div>
98
+ )}
99
+
100
+ <form onSubmit={wizard.onVerify} aria-label="Verifica dispositivo TOTP">
101
+ <div>
102
+ <label htmlFor={`${uid}-totp-code`}>{l.codeLabel}</label>
103
+ <input
104
+ id={`${uid}-totp-code`}
105
+ ref={firstInputRef}
106
+ type="text"
107
+ inputMode="numeric"
108
+ maxLength={6}
109
+ autoComplete="one-time-code"
110
+ autoFocus
111
+ required
112
+ aria-required="true"
113
+ aria-invalid={!!wizard.error}
114
+ aria-describedby={wizard.error ? errorId : undefined}
115
+ value={wizard.totpCode}
116
+ onChange={(e) => wizard.setTotpCode(e.target.value)}
117
+ />
118
+ </div>
119
+ <button
120
+ type="submit"
121
+ disabled={wizard.isLoading || wizard.totpCode.length !== 6}
122
+ aria-busy={wizard.isLoading}
123
+ >
124
+ {wizard.isLoading ? (
125
+ <>
126
+ <span aria-hidden="true">...</span>
127
+ <span className="sr-only">Caricamento...</span>
128
+ </>
129
+ ) : (
130
+ l.verifySubmit
131
+ )}
132
+ </button>
133
+ </form>
134
+ </div>
135
+ )
136
+ }
@@ -0,0 +1,159 @@
1
+ import { useEffect, useId, useRef, type ReactNode } from 'react'
2
+ import { useRegisterForm, type UseRegisterFormOptions } from '../hooks/useRegisterForm'
3
+
4
+ export interface RegisterFormProps extends UseRegisterFormOptions {
5
+ className?: string
6
+ labels?: {
7
+ email?: string
8
+ password?: string
9
+ confirmPassword?: string
10
+ submit?: string
11
+ code?: string
12
+ confirmSubmit?: string
13
+ resend?: string
14
+ }
15
+ signInLink?: ReactNode
16
+ }
17
+
18
+ export function RegisterForm({
19
+ className,
20
+ labels = {},
21
+ signInLink,
22
+ onSuccess,
23
+ onError,
24
+ extraAttributes,
25
+ }: RegisterFormProps) {
26
+ const uid = useId()
27
+ const form = useRegisterForm({ onSuccess, onError, extraAttributes })
28
+ const firstInputRef = useRef<HTMLInputElement>(null)
29
+
30
+ useEffect(() => {
31
+ firstInputRef.current?.focus()
32
+ }, [form.step])
33
+
34
+ const l = {
35
+ email: labels.email ?? 'Email',
36
+ password: labels.password ?? 'Password',
37
+ confirmPassword: labels.confirmPassword ?? 'Conferma password',
38
+ submit: labels.submit ?? 'Registrati',
39
+ code: labels.code ?? 'Codice di verifica',
40
+ confirmSubmit: labels.confirmSubmit ?? 'Conferma',
41
+ resend: labels.resend ?? 'Invia di nuovo',
42
+ }
43
+
44
+ const errorId = `${uid}-error`
45
+
46
+ if (form.step === 'confirm') {
47
+ return (
48
+ <form
49
+ onSubmit={form.confirm.onSubmit}
50
+ className={className}
51
+ aria-label="Conferma registrazione"
52
+ >
53
+ <p>
54
+ Controlla la tua email <strong>{form.confirm.email}</strong> e inserisci il codice
55
+ ricevuto.
56
+ </p>
57
+ <div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
58
+ {form.error?.message}
59
+ </div>
60
+ <div>
61
+ <label htmlFor={`${uid}-code`}>{l.code}</label>
62
+ <input
63
+ id={`${uid}-code`}
64
+ ref={firstInputRef}
65
+ type="text"
66
+ inputMode="numeric"
67
+ maxLength={6}
68
+ autoComplete="one-time-code"
69
+ autoFocus
70
+ required
71
+ aria-required="true"
72
+ aria-invalid={!!form.error}
73
+ aria-describedby={form.error ? errorId : undefined}
74
+ value={form.confirm.code}
75
+ onChange={(e) => form.confirm.setCode(e.target.value)}
76
+ />
77
+ </div>
78
+ <button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
79
+ {form.isLoading ? (
80
+ <>
81
+ <span aria-hidden="true">...</span>
82
+ <span className="sr-only">Caricamento...</span>
83
+ </>
84
+ ) : (
85
+ l.confirmSubmit
86
+ )}
87
+ </button>
88
+ <button type="button" onClick={form.confirm.resend}>
89
+ {l.resend}
90
+ </button>
91
+ </form>
92
+ )
93
+ }
94
+
95
+ return (
96
+ <form
97
+ onSubmit={form.register.onSubmit}
98
+ className={className}
99
+ aria-label="Crea un nuovo account"
100
+ >
101
+ <div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
102
+ {form.error?.message}
103
+ </div>
104
+ <div>
105
+ <label htmlFor={`${uid}-email`}>{l.email}</label>
106
+ <input
107
+ id={`${uid}-email`}
108
+ ref={firstInputRef}
109
+ type="email"
110
+ autoComplete="email"
111
+ autoFocus
112
+ required
113
+ aria-required="true"
114
+ aria-invalid={!!form.error}
115
+ aria-describedby={form.error ? errorId : undefined}
116
+ value={form.register.email}
117
+ onChange={(e) => form.register.setEmail(e.target.value)}
118
+ />
119
+ </div>
120
+ <div>
121
+ <label htmlFor={`${uid}-password`}>{l.password}</label>
122
+ <input
123
+ id={`${uid}-password`}
124
+ type="password"
125
+ autoComplete="new-password"
126
+ required
127
+ aria-required="true"
128
+ aria-invalid={!!form.error}
129
+ value={form.register.password}
130
+ onChange={(e) => form.register.setPassword(e.target.value)}
131
+ />
132
+ </div>
133
+ <div>
134
+ <label htmlFor={`${uid}-confirm-password`}>{l.confirmPassword}</label>
135
+ <input
136
+ id={`${uid}-confirm-password`}
137
+ type="password"
138
+ autoComplete="new-password"
139
+ required
140
+ aria-required="true"
141
+ aria-invalid={!!form.error}
142
+ value={form.register.confirmPassword}
143
+ onChange={(e) => form.register.setConfirmPassword(e.target.value)}
144
+ />
145
+ </div>
146
+ <button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
147
+ {form.isLoading ? (
148
+ <>
149
+ <span aria-hidden="true">...</span>
150
+ <span className="sr-only">Caricamento...</span>
151
+ </>
152
+ ) : (
153
+ l.submit
154
+ )}
155
+ </button>
156
+ {signInLink}
157
+ </form>
158
+ )
159
+ }