Skip to content

Commit e2bae26

Browse files
committed
moving the use-two-factor-auth to a hook so it can be re-used
1 parent 4508698 commit e2bae26

File tree

3 files changed

+331
-218
lines changed

3 files changed

+331
-218
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
}

resources/js/pages/auth/two-factor-challenge.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ import { Head, useForm } from '@inertiajs/react';
33
import { Button } from '@/components/ui/button';
44
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp';
55
import { Input } from '@/components/ui/input';
6-
import { Label } from '@/components/ui/label';
76
import InputError from '@/components/input-error';
87
import AuthLayout from '@/layouts/auth-layout';
9-
import TextLink from '@/components/text-link';
108

119
export default function TwoFactorChallenge() {
1210
const [recovery, setRecovery] = useState(false);
@@ -38,12 +36,7 @@ export default function TwoFactorChallenge() {
3836
{!recovery ? (
3937
<form onSubmit={submitCode} className="space-y-4">
4038
<div className="flex flex-col items-center justify-center space-y-3 text-center">
41-
<InputOTP
42-
maxLength={6}
43-
value={data.code}
44-
onChange={(value) => setData('code', value)}
45-
autoFocus
46-
>
39+
<InputOTP maxLength={6} value={data.code} onChange={(value) => setData('code', value)} autoFocus>
4740
<InputOTPGroup>
4841
<InputOTPSlot index={0} />
4942
<InputOTPSlot index={1} />

0 commit comments

Comments
 (0)