1+ import { useState , useEffect , useCallback } from 'react' ;
2+
3+ interface EnableResponse {
4+ qrCode : string ;
5+ secret : string ;
6+ }
7+
8+ interface RecoveryCodesResponse {
9+ recovery_codes : string [ ] ;
10+ }
11+
12+ export function useTwoFactorAuth ( initialConfirmed : boolean , initialRecoveryCodes : string [ ] ) {
13+ const csrfToken =
14+ document . querySelector ( 'meta[name="csrf-token"]' ) ?. getAttribute ( 'content' ) || '' ;
15+
16+ const headers = {
17+ 'Content-Type' : 'application/json' ,
18+ Accept : 'application/json' ,
19+ 'X-CSRF-TOKEN' : csrfToken ,
20+ 'X-Requested-With' : 'XMLHttpRequest' ,
21+ } ;
22+
23+ const [ confirmed , setConfirmed ] = useState ( initialConfirmed ) ;
24+ const [ qrCodeSvg , setQrCodeSvg ] = useState ( '' ) ;
25+ const [ secretKey , setSecretKey ] = useState ( '' ) ;
26+ const [ recoveryCodesList , setRecoveryCodesList ] = useState ( initialRecoveryCodes ) ;
27+ const [ copied , setCopied ] = useState ( false ) ;
28+ const [ passcode , setPasscode ] = useState ( '' ) ;
29+ const [ error , setError ] = useState ( '' ) ;
30+ const [ verifyStep , setVerifyStep ] = useState ( false ) ;
31+ const [ showingRecoveryCodes , setShowingRecoveryCodes ] = useState ( false ) ;
32+ const [ showModal , setShowModal ] = useState ( false ) ;
33+
34+ // Automatically enable 2FA when modal opens and QR is not yet fetched
35+ useEffect ( ( ) => {
36+ if ( showModal && ! verifyStep && ! qrCodeSvg ) {
37+ enable ( ) ;
38+ }
39+ } , [ showModal , verifyStep , qrCodeSvg ] ) ;
40+
41+ const enable = useCallback ( async ( ) => {
42+ try {
43+ const response = await fetch ( route ( 'two-factor.enable' ) , {
44+ method : 'POST' ,
45+ headers,
46+ } ) ;
47+
48+ if ( response . ok ) {
49+ const data : EnableResponse = await response . json ( ) ;
50+ setQrCodeSvg ( data . qrCode ) ;
51+ setSecretKey ( data . secret ) ;
52+ } else {
53+ console . error ( 'Error enabling 2FA:' , response . statusText ) ;
54+ }
55+ } catch ( error ) {
56+ console . error ( 'Error enabling 2FA:' , error ) ;
57+ }
58+ } , [ headers ] ) ;
59+
60+ const confirm = useCallback ( async ( ) => {
61+ if ( ! passcode || passcode . length !== 6 ) return ;
62+
63+ const formattedCode = passcode . replace ( / \s + / g, '' ) . trim ( ) ;
64+
65+ try {
66+ const response = await fetch ( route ( 'two-factor.confirm' ) , {
67+ method : 'POST' ,
68+ headers,
69+ body : JSON . stringify ( { code : formattedCode } ) ,
70+ } ) ;
71+
72+ if ( response . ok ) {
73+ const responseData = await response . json ( ) ;
74+ if ( responseData . recovery_codes ) {
75+ setRecoveryCodesList ( responseData . recovery_codes ) ;
76+ }
77+
78+ setConfirmed ( true ) ;
79+ setVerifyStep ( false ) ;
80+ setShowModal ( false ) ;
81+ setShowingRecoveryCodes ( true ) ;
82+ setPasscode ( '' ) ;
83+ setError ( '' ) ;
84+ } else {
85+ const errorData = await response . json ( ) ;
86+ console . error ( 'Verification error:' , errorData . message ) ;
87+ setError ( errorData . message || 'Invalid verification code' ) ;
88+ setPasscode ( '' ) ;
89+ }
90+ } catch ( error ) {
91+ console . error ( 'Error confirming 2FA:' , error ) ;
92+ setError ( 'An error occurred while confirming 2FA' ) ;
93+ }
94+ } , [ headers , passcode ] ) ;
95+
96+ const regenerateRecoveryCodes = useCallback ( async ( ) => {
97+ try {
98+ const response = await fetch ( route ( 'two-factor.regenerate-recovery-codes' ) , {
99+ method : 'POST' ,
100+ headers,
101+ } ) ;
102+
103+ if ( response . ok ) {
104+ const data : RecoveryCodesResponse = await response . json ( ) ;
105+ if ( data . recovery_codes ) {
106+ setRecoveryCodesList ( data . recovery_codes ) ;
107+ }
108+ } else {
109+ console . error ( 'Error regenerating codes:' , response . statusText ) ;
110+ }
111+ } catch ( error ) {
112+ console . error ( 'Error regenerating codes:' , error ) ;
113+ }
114+ } , [ headers ] ) ;
115+
116+ const disable = useCallback ( async ( ) => {
117+ try {
118+ const response = await fetch ( route ( 'two-factor.disable' ) , { method : 'DELETE' , headers } ) ;
119+
120+ if ( response . ok ) {
121+ setConfirmed ( false ) ;
122+ setShowingRecoveryCodes ( false ) ;
123+ setRecoveryCodesList ( [ ] ) ;
124+ setQrCodeSvg ( '' ) ;
125+ setSecretKey ( '' ) ;
126+ } else {
127+ console . error ( 'Error disabling 2FA:' , response . statusText ) ;
128+ }
129+ } catch ( error ) {
130+ console . error ( 'Error disabling 2FA:' , error ) ;
131+ }
132+ } , [ headers ] ) ;
133+
134+ const copyToClipboard = useCallback ( ( text : string ) => {
135+ navigator . clipboard . writeText ( text ) ;
136+ setCopied ( true ) ;
137+ setTimeout ( ( ) => setCopied ( false ) , 1500 ) ;
138+ } , [ ] ) ;
139+
140+ return {
141+ confirmed,
142+ qrCodeSvg,
143+ secretKey,
144+ recoveryCodesList,
145+ copied,
146+ passcode,
147+ setPasscode,
148+ error,
149+ setError,
150+ verifyStep,
151+ setVerifyStep,
152+ showingRecoveryCodes,
153+ setShowingRecoveryCodes,
154+ showModal,
155+ setShowModal,
156+ enable,
157+ confirm,
158+ regenerateRecoveryCodes,
159+ disable,
160+ copyToClipboard,
161+ } ;
162+ }
0 commit comments