@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,41 @@
1
+ type Listener<T> = T extends void ? () => void : (payload: T) => void
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export class TypedEventEmitter<TEvents extends Record<keyof TEvents, any>> {
5
+ private readonly _listeners = new Map<keyof TEvents, Set<(...args: any[]) => void>>()
6
+
7
+ on<K extends keyof TEvents>(event: K, listener: Listener<TEvents[K]>): () => void {
8
+ if (!this._listeners.has(event)) {
9
+ this._listeners.set(event, new Set())
10
+ }
11
+ this._listeners.get(event)!.add(listener as (...args: any[]) => void)
12
+ return () => this.off(event, listener)
13
+ }
14
+
15
+ off<K extends keyof TEvents>(event: K, listener: Listener<TEvents[K]>): void {
16
+ this._listeners.get(event)?.delete(listener as (...args: any[]) => void)
17
+ }
18
+
19
+ once<K extends keyof TEvents>(event: K, listener: Listener<TEvents[K]>): () => void {
20
+ const unsub = this.on(event, ((...args: any[]) => {
21
+ unsub()
22
+ ;(listener as any)(...args)
23
+ }) as Listener<TEvents[K]>)
24
+ return unsub
25
+ }
26
+
27
+ protected emit<K extends keyof TEvents>(
28
+ event: K,
29
+ ...args: TEvents[K] extends void ? [] : [TEvents[K]]
30
+ ): void {
31
+ this._listeners.get(event)?.forEach(l => l(...args))
32
+ }
33
+
34
+ removeAllListeners(event?: keyof TEvents): void {
35
+ if (event !== undefined) {
36
+ this._listeners.delete(event)
37
+ } else {
38
+ this._listeners.clear()
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,5 @@
1
+ export * from './types'
2
+ export * from './errors'
3
+ export * from './storage'
4
+ export * from './event-bus'
5
+ export * from './client'
@@ -0,0 +1,32 @@
1
+ import type { CognitoUserSession } from 'amazon-cognito-identity-js'
2
+ import type { AuthSession, AuthUser } from '../types'
3
+
4
+ export function buildAuthSession(session: CognitoUserSession): AuthSession {
5
+ const expiresAt = new Date(session.getAccessToken().getExpiration() * 1000)
6
+ return {
7
+ accessToken: session.getAccessToken().getJwtToken(),
8
+ idToken: session.getIdToken().getJwtToken(),
9
+ refreshToken: session.getRefreshToken().getToken(),
10
+ expiresAt,
11
+ isValid: () => session.isValid(),
12
+ }
13
+ }
14
+
15
+ export function buildAuthUser(session: CognitoUserSession, fallbackUsername: string): AuthUser {
16
+ // Estraiamo tutte le info dall'ID token senza roundtrip API
17
+ const payload = session.getIdToken().decodePayload() as Record<string, unknown>
18
+
19
+ const str = (v: unknown): string => (v != null ? String(v) : '')
20
+ const bool = (v: unknown): boolean => v === true || v === 'true'
21
+
22
+ return {
23
+ sub: str(payload['sub']),
24
+ username: str(payload['cognito:username'] ?? fallbackUsername),
25
+ email: str(payload['email']),
26
+ emailVerified: bool(payload['email_verified']),
27
+ groups: Array.isArray(payload['cognito:groups'])
28
+ ? (payload['cognito:groups'] as string[])
29
+ : [],
30
+ attributes: Object.fromEntries(Object.entries(payload).map(([k, v]) => [k, str(v)])),
31
+ }
32
+ }
@@ -0,0 +1,79 @@
1
+ export interface StorageAdapter {
2
+ getItem(key: string): string | null
3
+ setItem(key: string, value: string): void
4
+ removeItem(key: string): void
5
+ clear?(): void
6
+ }
7
+
8
+ export class LocalStorageAdapter implements StorageAdapter {
9
+ getItem(key: string): string | null {
10
+ return localStorage.getItem(key)
11
+ }
12
+ setItem(key: string, value: string): void {
13
+ localStorage.setItem(key, value)
14
+ }
15
+ removeItem(key: string): void {
16
+ localStorage.removeItem(key)
17
+ }
18
+ clear(): void {
19
+ localStorage.clear()
20
+ }
21
+ }
22
+
23
+ export class SessionStorageAdapter implements StorageAdapter {
24
+ getItem(key: string): string | null {
25
+ return sessionStorage.getItem(key)
26
+ }
27
+ setItem(key: string, value: string): void {
28
+ sessionStorage.setItem(key, value)
29
+ }
30
+ removeItem(key: string): void {
31
+ sessionStorage.removeItem(key)
32
+ }
33
+ clear(): void {
34
+ sessionStorage.clear()
35
+ }
36
+ }
37
+
38
+ // Safe for SSR / Node.js environments
39
+ export class InMemoryStorageAdapter implements StorageAdapter {
40
+ private readonly store = new Map<string, string>()
41
+
42
+ getItem(key: string): string | null {
43
+ return this.store.get(key) ?? null
44
+ }
45
+ setItem(key: string, value: string): void {
46
+ this.store.set(key, value)
47
+ }
48
+ removeItem(key: string): void {
49
+ this.store.delete(key)
50
+ }
51
+ clear(): void {
52
+ this.store.clear()
53
+ }
54
+ }
55
+
56
+ // Uses localStorage when available, falls back to in-memory (e.g. SSR)
57
+ export class AutoStorageAdapter implements StorageAdapter {
58
+ private readonly delegate: StorageAdapter
59
+
60
+ constructor() {
61
+ this.delegate =
62
+ typeof window !== 'undefined' && window.localStorage
63
+ ? new LocalStorageAdapter()
64
+ : new InMemoryStorageAdapter()
65
+ }
66
+
67
+ getItem(key: string): string | null {
68
+ return this.delegate.getItem(key)
69
+ }
70
+ setItem(key: string, value: string): void {
71
+ this.delegate.setItem(key, value)
72
+ }
73
+ removeItem(key: string): void {
74
+ this.delegate.removeItem(key)
75
+ }
76
+ clear(): void {
77
+ this.delegate.clear?.()
78
+ }
79
+ }
@@ -0,0 +1,87 @@
1
+ // ─── State machine ────────────────────────────────────────────────────────────
2
+
3
+ export type AuthState =
4
+ | 'idle'
5
+ | 'loading'
6
+ | 'authenticated'
7
+ | 'unauthenticated'
8
+ | 'mfa_required'
9
+ | 'new_password_required'
10
+ | 'confirm_signup'
11
+
12
+ // ─── MFA ──────────────────────────────────────────────────────────────────────
13
+
14
+ export type MfaType = 'TOTP' | 'SMS' | 'NOMFA'
15
+
16
+ export interface MfaSetupResult {
17
+ secretCode: string
18
+ /** otpauth://totp/<issuer>:<email>?secret=<secret>&issuer=<issuer> */
19
+ qrCodeUri: string
20
+ }
21
+
22
+ export interface MfaPreference {
23
+ enabled: boolean
24
+ preferred: MfaType | null
25
+ totp: boolean
26
+ sms: boolean
27
+ }
28
+
29
+ // ─── User & Session ───────────────────────────────────────────────────────────
30
+
31
+ export interface AuthUser {
32
+ sub: string
33
+ username: string
34
+ email: string
35
+ emailVerified: boolean
36
+ groups: string[]
37
+ attributes: Record<string, string>
38
+ }
39
+
40
+ export interface AuthSession {
41
+ accessToken: string
42
+ idToken: string
43
+ refreshToken: string
44
+ expiresAt: Date
45
+ isValid(): boolean
46
+ }
47
+
48
+ // ─── Sign-in result (discriminated union) ────────────────────────────────────
49
+
50
+ export type SignInResult =
51
+ | { status: 'SUCCESS'; user: AuthUser; session: AuthSession }
52
+ | { status: 'MFA_REQUIRED'; mfaType: MfaType; challengeSession: string }
53
+ | { status: 'NEW_PASSWORD_REQUIRED'; requiredAttributes: string[]; challengeSession: string }
54
+ | { status: 'MFA_SETUP_REQUIRED'; challengeSession: string }
55
+ | { status: 'CONFIRM_SIGNUP' }
56
+
57
+ // ─── Config ───────────────────────────────────────────────────────────────────
58
+
59
+ import type { StorageAdapter } from './storage'
60
+
61
+ export interface AuthConfig {
62
+ userPoolId: string
63
+ clientId: string
64
+ region: string
65
+ storage?: StorageAdapter
66
+ /** Rinnova il token automaticamente prima della scadenza. Default: true */
67
+ autoRefresh?: boolean
68
+ /** Secondi prima della scadenza in cui scatta il refresh. Default: 300 */
69
+ refreshMarginSeconds?: number
70
+ /** Label mostrata nell'app authenticator (es. "MioApp"). Default: clientId */
71
+ totpIssuer?: string
72
+ }
73
+
74
+ export interface ResolvedAuthConfig extends Required<AuthConfig> {}
75
+
76
+ // ─── Events ───────────────────────────────────────────────────────────────────
77
+
78
+ export interface AuthEvents {
79
+ signedIn: AuthUser
80
+ signedOut: void
81
+ tokenRefreshed: AuthSession
82
+ sessionExpired: void
83
+ mfaRequired: { mfaType: MfaType; challengeSession: string }
84
+ newPasswordRequired: { requiredAttributes: string[]; challengeSession: string }
85
+ userUpdated: AuthUser
86
+ stateChanged: AuthState
87
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './core'
@@ -0,0 +1,56 @@
1
+ import type { ReactNode } from 'react'
2
+ import { useAuthContext } from '../context'
3
+
4
+ export interface ProtectedRouteProps {
5
+ children: ReactNode
6
+ /** Mostrato mentre lo stato auth è in inizializzazione (idle/loading). Default: null */
7
+ loading?: ReactNode
8
+ /**
9
+ * Mostrato quando l'utente non è autenticato o non ha i permessi richiesti.
10
+ * Con react-router-dom: <Navigate to="/login" replace />
11
+ */
12
+ fallback?: ReactNode
13
+ /** L'utente deve appartenere ad almeno uno di questi gruppi Cognito */
14
+ requiredGroups?: string[]
15
+ }
16
+
17
+ export function ProtectedRoute({
18
+ children,
19
+ loading = null,
20
+ fallback = null,
21
+ requiredGroups,
22
+ }: ProtectedRouteProps) {
23
+ const { state, user, isLoading, isAuthenticated } = useAuthContext()
24
+
25
+ if (isLoading) return <>{loading}</>
26
+
27
+ if (!isAuthenticated) return <>{fallback}</>
28
+
29
+ if (requiredGroups?.length) {
30
+ const userGroups = user?.groups ?? []
31
+ const hasAccess = requiredGroups.some((g) => userGroups.includes(g))
32
+ if (!hasAccess) return <>{fallback}</>
33
+ }
34
+
35
+ // Unused var from destructuring: kept for clarity
36
+ void state
37
+
38
+ return <>{children}</>
39
+ }
40
+
41
+ // ─── Hook imperativo per logica di redirect inline ──────────────────────────────
42
+
43
+ export interface UseRequireAuthOptions {
44
+ requiredGroups?: string[]
45
+ }
46
+
47
+ export function useRequireAuth(options?: UseRequireAuthOptions) {
48
+ const { state, user, isLoading, isAuthenticated } = useAuthContext()
49
+
50
+ const isAllowed =
51
+ isAuthenticated &&
52
+ (!options?.requiredGroups?.length ||
53
+ options.requiredGroups.some((g) => (user?.groups ?? []).includes(g)))
54
+
55
+ return { isAllowed, isLoading, state, user }
56
+ }
@@ -0,0 +1,126 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ useMemo,
6
+ useState,
7
+ type ReactNode,
8
+ } from 'react'
9
+
10
+ import { CognitoAuthClient } from '../core/client'
11
+ import type { AuthConfig, AuthSession, AuthState, AuthUser } from '../core/types'
12
+
13
+ // ─── Context shape ─────────────────────────────────────────────────────────────
14
+
15
+ export interface AuthContextValue {
16
+ client: CognitoAuthClient
17
+ state: AuthState
18
+ user: AuthUser | null
19
+ session: AuthSession | null
20
+ isLoading: boolean
21
+ isAuthenticated: boolean
22
+ }
23
+
24
+ const AuthContext = createContext<AuthContextValue | null>(null)
25
+
26
+ // ─── Internal hook ──────────────────────────────────────────────────────────────
27
+
28
+ export function useAuthContext(): AuthContextValue {
29
+ const ctx = useContext(AuthContext)
30
+ if (!ctx) {
31
+ throw new Error('useAuthContext must be used inside <AuthProvider>')
32
+ }
33
+ return ctx
34
+ }
35
+
36
+ // ─── Provider ──────────────────────────────────────────────────────────────────
37
+
38
+ export interface AuthProviderProps {
39
+ config: AuthConfig
40
+ children: ReactNode
41
+ }
42
+
43
+ export function AuthProvider({ config, children }: AuthProviderProps) {
44
+ // Client è stabile per tutta la vita del provider — il config viene ignorato dopo il mount
45
+ const [client] = useState(() => new CognitoAuthClient(config))
46
+
47
+ const [authState, setAuthState] = useState<AuthState>('idle')
48
+ const [user, setUser] = useState<AuthUser | null>(null)
49
+ const [session, setSession] = useState<AuthSession | null>(null)
50
+
51
+ useEffect(() => {
52
+ let active = true
53
+
54
+ // Ripristina la sessione dallo storage (localStorage / adapter configurato)
55
+ client
56
+ .getCurrentUser()
57
+ .then(async (currentUser) => {
58
+ if (!active) return
59
+ if (currentUser) {
60
+ const s = await client.getSession().catch(() => null)
61
+ if (active) {
62
+ setUser(currentUser)
63
+ setSession(s)
64
+ }
65
+ } else {
66
+ if (active) setAuthState('unauthenticated')
67
+ }
68
+ })
69
+ .catch(() => {
70
+ if (active) setAuthState('unauthenticated')
71
+ })
72
+
73
+ const unsubs = [
74
+ client.on('stateChanged', (s: AuthState) => {
75
+ if (active) setAuthState(s)
76
+ // Pulisce user/session per tutti gli stati non autenticati
77
+ if (s === 'unauthenticated') {
78
+ setUser(null)
79
+ setSession(null)
80
+ }
81
+ }),
82
+ client.on('signedIn', (u) => {
83
+ if (!active) return
84
+ setUser(u)
85
+ client
86
+ .getSession()
87
+ .then((s) => { if (active) setSession(s) })
88
+ .catch(() => {})
89
+ }),
90
+ client.on('signedOut', () => {
91
+ if (active) {
92
+ setUser(null)
93
+ setSession(null)
94
+ }
95
+ }),
96
+ client.on('tokenRefreshed', (s) => {
97
+ if (active) setSession(s)
98
+ }),
99
+ client.on('sessionExpired', () => {
100
+ if (active) {
101
+ setUser(null)
102
+ setSession(null)
103
+ }
104
+ }),
105
+ ]
106
+
107
+ return () => {
108
+ active = false
109
+ unsubs.forEach((unsub) => unsub())
110
+ }
111
+ }, [client])
112
+
113
+ const value = useMemo<AuthContextValue>(
114
+ () => ({
115
+ client,
116
+ state: authState,
117
+ user,
118
+ session,
119
+ isLoading: authState === 'idle' || authState === 'loading',
120
+ isAuthenticated: authState === 'authenticated',
121
+ }),
122
+ [client, authState, user, session],
123
+ )
124
+
125
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
126
+ }
@@ -0,0 +1,75 @@
1
+ import { useMemo } from 'react'
2
+ import { useAuthContext } from '../context'
3
+ import type { MfaType, SignInResult } from '../../core/types'
4
+
5
+ export function useAuth() {
6
+ const { client, state, user, isLoading, isAuthenticated } = useAuthContext()
7
+
8
+ // useMemo garantisce riferimenti stabili tra i re-render (client non cambia mai)
9
+ const actions = useMemo(
10
+ () => ({
11
+ signIn: (email: string, password: string): Promise<SignInResult> =>
12
+ client.signIn(email, password),
13
+
14
+ signOut: (global?: boolean): Promise<void> =>
15
+ client.signOut(global),
16
+
17
+ signUp: (
18
+ email: string,
19
+ password: string,
20
+ attributes?: Record<string, string>,
21
+ ): Promise<void> => client.signUp(email, password, attributes),
22
+
23
+ confirmSignUp: (email: string, code: string): Promise<void> =>
24
+ client.confirmSignUp(email, code),
25
+
26
+ resendConfirmationCode: (email: string): Promise<void> =>
27
+ client.resendConfirmationCode(email),
28
+
29
+ forgotPassword: (email: string): Promise<void> =>
30
+ client.forgotPassword(email),
31
+
32
+ confirmForgotPassword: (
33
+ email: string,
34
+ code: string,
35
+ newPassword: string,
36
+ ): Promise<void> => client.confirmForgotPassword(email, code, newPassword),
37
+
38
+ changePassword: (current: string, next: string): Promise<void> =>
39
+ client.changePassword(current, next),
40
+
41
+ respondToMfaChallenge: (
42
+ challengeSession: string,
43
+ code: string,
44
+ mfaType: MfaType,
45
+ ): Promise<SignInResult> =>
46
+ client.respondToMfaChallenge(challengeSession, code, mfaType),
47
+
48
+ respondToNewPasswordChallenge: (
49
+ challengeSession: string,
50
+ newPassword: string,
51
+ userAttributes?: Record<string, string>,
52
+ ): Promise<SignInResult> =>
53
+ client.respondToNewPasswordChallenge(challengeSession, newPassword, userAttributes),
54
+
55
+ setupTotpChallenge: (challengeSession: string) =>
56
+ client.setupTotpChallenge(challengeSession),
57
+
58
+ verifyTotpChallenge: (challengeSession: string, code: string): Promise<SignInResult> =>
59
+ client.verifyTotpChallenge(challengeSession, code),
60
+ }),
61
+ [client],
62
+ )
63
+
64
+ return {
65
+ // Stato
66
+ user,
67
+ state,
68
+ isLoading,
69
+ isAuthenticated,
70
+ // Azioni
71
+ ...actions,
72
+ // Client raw per casi avanzati
73
+ client,
74
+ }
75
+ }
@@ -0,0 +1,19 @@
1
+ import { useAuthContext } from '../context'
2
+ import type { MfaType } from '../../core/types'
3
+
4
+ export function useMfa() {
5
+ const { client } = useAuthContext()
6
+
7
+ return {
8
+ /** Avvia il setup TOTP — restituisce secretCode e qrCodeUri per l'app authenticator */
9
+ setup: () => client.setupTotp(),
10
+ /** Conferma il codice TOTP generato dall'app per completare il setup */
11
+ verifySetup: (code: string) => client.verifyTotpSetup(code),
12
+ /** Legge le preferenze MFA correnti (TOTP abilitato, SMS abilitato, preferito) */
13
+ getPreference: () => client.getMfaPreference(),
14
+ /** Imposta il tipo MFA preferito ('TOTP' | 'SMS') */
15
+ setPreference: (type: MfaType) => client.setMfaPreference(type),
16
+ /** Disabilita completamente il secondo fattore */
17
+ disable: () => client.disableMfa(),
18
+ }
19
+ }
@@ -0,0 +1,16 @@
1
+ import { useAuthContext } from '../context'
2
+
3
+ export function useSession() {
4
+ const { session, state, client } = useAuthContext()
5
+
6
+ return {
7
+ accessToken: session?.accessToken ?? null,
8
+ idToken: session?.idToken ?? null,
9
+ refreshToken: session?.refreshToken ?? null,
10
+ expiresAt: session?.expiresAt ?? null,
11
+ isExpired: session ? !session.isValid() : true,
12
+ isAuthenticated: state === 'authenticated',
13
+ /** Forza il rinnovo del token e aggiorna il context */
14
+ refresh: () => client.getSession(),
15
+ }
16
+ }
@@ -0,0 +1,24 @@
1
+ import { useAuthContext } from '../context'
2
+
3
+ export function useUser() {
4
+ const { user, client } = useAuthContext()
5
+
6
+ return {
7
+ user,
8
+ attributes: user?.attributes ?? {},
9
+ groups: user?.groups ?? [],
10
+ /** Aggiorna uno o più attributi utente in Cognito */
11
+ update: (attributes: Record<string, string>) =>
12
+ client.updateUserAttributes(attributes),
13
+ /** Verifica email o phone_number con il codice ricevuto */
14
+ verifyAttribute: (attribute: 'email' | 'phone_number', code: string) =>
15
+ client.verifyUserAttribute(attribute, code),
16
+ /** Invia un nuovo codice di verifica per email o phone_number */
17
+ sendVerificationCode: (attribute: 'email' | 'phone_number') =>
18
+ client.sendAttributeVerificationCode(attribute),
19
+ /** Recupera gli attributi aggiornati direttamente da Cognito */
20
+ refreshAttributes: () => client.getUserAttributes(),
21
+ /** Elimina definitivamente l'account dell'utente corrente */
22
+ deleteAccount: () => client.deleteUser(),
23
+ }
24
+ }
@@ -0,0 +1,10 @@
1
+ export { AuthProvider, useAuthContext } from './context'
2
+ export type { AuthProviderProps, AuthContextValue } from './context'
3
+
4
+ export { useAuth } from './hooks/useAuth'
5
+ export { useSession } from './hooks/useSession'
6
+ export { useUser } from './hooks/useUser'
7
+ export { useMfa } from './hooks/useMfa'
8
+
9
+ export { ProtectedRoute, useRequireAuth } from './components/ProtectedRoute'
10
+ export type { ProtectedRouteProps, UseRequireAuthOptions } from './components/ProtectedRoute'
@@ -0,0 +1,105 @@
1
+ import { useId, useRef } from 'react'
2
+ import {
3
+ useChangePasswordForm,
4
+ type UseChangePasswordFormOptions,
5
+ } from '../hooks/useChangePasswordForm'
6
+
7
+ export interface ChangePasswordFormProps extends UseChangePasswordFormOptions {
8
+ className?: string
9
+ labels?: {
10
+ currentPassword?: string
11
+ newPassword?: string
12
+ confirmPassword?: string
13
+ submit?: string
14
+ successMessage?: string
15
+ }
16
+ }
17
+
18
+ export function ChangePasswordForm({
19
+ className,
20
+ labels = {},
21
+ onSuccess,
22
+ onError,
23
+ }: ChangePasswordFormProps) {
24
+ const uid = useId()
25
+ const form = useChangePasswordForm({ onSuccess, onError })
26
+ const firstInputRef = useRef<HTMLInputElement>(null)
27
+
28
+ const l = {
29
+ currentPassword: labels.currentPassword ?? 'Password attuale',
30
+ newPassword: labels.newPassword ?? 'Nuova password',
31
+ confirmPassword: labels.confirmPassword ?? 'Conferma nuova password',
32
+ submit: labels.submit ?? 'Cambia password',
33
+ successMessage: labels.successMessage ?? 'Password aggiornata con successo.',
34
+ }
35
+
36
+ const errorId = `${uid}-error`
37
+
38
+ if (form.success) {
39
+ return <p role="status">{l.successMessage}</p>
40
+ }
41
+
42
+ return (
43
+ <form
44
+ onSubmit={form.onSubmit}
45
+ className={className}
46
+ aria-label="Cambia password"
47
+ >
48
+ <div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
49
+ {form.error?.message}
50
+ </div>
51
+ <div>
52
+ <label htmlFor={`${uid}-current-password`}>{l.currentPassword}</label>
53
+ <input
54
+ id={`${uid}-current-password`}
55
+ ref={firstInputRef}
56
+ type="password"
57
+ autoComplete="current-password"
58
+ autoFocus
59
+ required
60
+ aria-required="true"
61
+ aria-invalid={!!form.error}
62
+ aria-describedby={form.error ? errorId : undefined}
63
+ value={form.currentPassword}
64
+ onChange={(e) => form.setCurrentPassword(e.target.value)}
65
+ />
66
+ </div>
67
+ <div>
68
+ <label htmlFor={`${uid}-new-password`}>{l.newPassword}</label>
69
+ <input
70
+ id={`${uid}-new-password`}
71
+ type="password"
72
+ autoComplete="new-password"
73
+ required
74
+ aria-required="true"
75
+ aria-invalid={!!form.error}
76
+ value={form.newPassword}
77
+ onChange={(e) => form.setNewPassword(e.target.value)}
78
+ />
79
+ </div>
80
+ <div>
81
+ <label htmlFor={`${uid}-confirm-password`}>{l.confirmPassword}</label>
82
+ <input
83
+ id={`${uid}-confirm-password`}
84
+ type="password"
85
+ autoComplete="new-password"
86
+ required
87
+ aria-required="true"
88
+ aria-invalid={!!form.error}
89
+ value={form.confirmPassword}
90
+ onChange={(e) => form.setConfirmPassword(e.target.value)}
91
+ />
92
+ </div>
93
+ <button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
94
+ {form.isLoading ? (
95
+ <>
96
+ <span aria-hidden="true">...</span>
97
+ <span className="sr-only">Caricamento...</span>
98
+ </>
99
+ ) : (
100
+ l.submit
101
+ )}
102
+ </button>
103
+ </form>
104
+ )
105
+ }