Skip to content

Commit 071e5c9

Browse files
authored
fix: superadmin 2fa login (#1544)
1 parent 77a4a62 commit 071e5c9

File tree

12 files changed

+434
-25
lines changed

12 files changed

+434
-25
lines changed

components/guards/SetupMFA.tsx

Lines changed: 132 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
'use client';
22

3-
import { sendVerificationEmail, triggerInitialMFA, verifyMFA } from '@/lib/auth';
3+
import {
4+
reauthenticateUser,
5+
sendVerificationEmail,
6+
triggerInitialMFA,
7+
verifyMFA,
8+
} from '@/lib/auth';
49
import { auth } from '@/lib/firebase';
510
import { useTypedSelector } from '@/lib/hooks/store';
6-
import { Box, Button, TextField, Typography } from '@mui/material';
11+
import { Alert, Box, Button, TextField, Typography } from '@mui/material';
712
import { useRollbar } from '@rollbar/react';
813
import { useTranslations } from 'next-intl';
914
import { useRouter } from 'next/navigation';
10-
import { useState } from 'react';
15+
import { useEffect, useRef, useState } from 'react';
1116
import PhoneInput from '../forms/PhoneInput';
1217

1318
const buttonStyle = {
@@ -27,24 +32,89 @@ const SetupMFA = () => {
2732
const [verificationId, setVerificationId] = useState('');
2833
const [verificationCode, setVerificationCode] = useState('');
2934
const [error, setError] = useState('');
35+
const [showReauth, setShowReauth] = useState(false);
36+
const [password, setPassword] = useState('');
37+
const [isReauthenticating, setIsReauthenticating] = useState(false);
38+
const recaptchaContainerRef = useRef<HTMLDivElement>(null);
39+
const recaptchaCleanupRef = useRef<(() => void) | null>(null);
3040

41+
// Clean up reCAPTCHA on unmount
42+
useEffect(() => {
43+
return () => {
44+
if (recaptchaContainerRef.current) {
45+
recaptchaContainerRef.current.innerHTML = '';
46+
}
47+
if (recaptchaCleanupRef.current) {
48+
recaptchaCleanupRef.current();
49+
recaptchaCleanupRef.current = null;
50+
}
51+
};
52+
}, []);
53+
54+
const handleReauthentication = async () => {
55+
if (!password.trim()) {
56+
setError(t('form.passwordRequired'));
57+
return;
58+
}
59+
60+
setIsReauthenticating(true);
61+
setError('');
62+
63+
try {
64+
await reauthenticateUser(password);
65+
setShowReauth(false);
66+
setPassword('');
67+
// Reset the MFA setup process
68+
setVerificationId('');
69+
setVerificationCode('');
70+
setPhoneNumber('');
71+
} catch (error: any) {
72+
rollbar.error('Reauthentication error:', error);
73+
if (error.code === 'auth/wrong-password') {
74+
setError(t('form.firebase.wrongPassword'));
75+
} else if (error.code === 'auth/too-many-requests') {
76+
setError(t('form.firebase.tooManyAttempts'));
77+
} else {
78+
setError(t('form.reauthenticationError'));
79+
}
80+
} finally {
81+
setIsReauthenticating(false);
82+
}
83+
};
3184
const handleEnrollMFA = async () => {
3285
if (!userVerifiedEmail) {
3386
setError(t('form.emailNotVerified'));
3487
rollbar.error('MFA setup page reached before email is verified');
3588
return;
3689
}
3790

91+
setError('');
92+
93+
// Clean up any existing reCAPTCHA before creating a new one
94+
if (recaptchaContainerRef.current) {
95+
recaptchaContainerRef.current.innerHTML = '';
96+
}
97+
if (recaptchaCleanupRef.current) {
98+
recaptchaCleanupRef.current();
99+
recaptchaCleanupRef.current = null;
100+
}
101+
38102
const { verificationId, error } = await triggerInitialMFA(phoneNumber);
39103
if (error) {
40-
setError(t('form.mfaEnrollError'));
41-
rollbar.error('MFA enrollment trigger error:', error);
104+
if (error.code === 'auth/requires-recent-login') {
105+
setShowReauth(true);
106+
setError('');
107+
} else {
108+
setError(t('form.mfaEnrollError'));
109+
rollbar.error('MFA enrollment trigger error:', error);
110+
}
42111
} else {
43112
setVerificationId(verificationId!);
44113
}
45114
};
46115

47116
const handleFinalizeMFA = async () => {
117+
setError('');
48118
const { success, error } = await verifyMFA(verificationId, verificationCode);
49119
if (success) {
50120
router.push('/admin/dashboard');
@@ -55,18 +125,69 @@ const SetupMFA = () => {
55125
};
56126

57127
const handleSendVerificationEmail = async () => {
128+
setError('');
58129
const user = auth.currentUser;
59130
if (user) {
60131
const { error } = await sendVerificationEmail(user);
61132
if (error) {
133+
rollbar.error('Send verification email error:', error);
62134
setError(t('form.emailVerificationError'));
63135
} else {
64-
rollbar.error('Send verification email error:', error || ' Undefined');
65-
setError(t('form.emailVerificationSent'));
136+
// Show success message instead of error
137+
setError('');
138+
// You might want to show a success state here instead
66139
}
67140
}
68141
};
69142

143+
if (showReauth) {
144+
return (
145+
<Box>
146+
<Typography variant="h3">{t('setupMFA.reauthTitle')}</Typography>
147+
<Typography mb={2}>{t('setupMFA.reauthDescription')}</Typography>
148+
149+
<TextField
150+
id="password"
151+
type="password"
152+
value={password}
153+
onChange={(e) => setPassword(e.target.value)}
154+
label={t('form.passwordLabel')}
155+
fullWidth
156+
variant="standard"
157+
margin="normal"
158+
required
159+
/>
160+
161+
{error && (
162+
<Alert severity="error" sx={{ mt: 2 }}>
163+
{error}
164+
</Alert>
165+
)}
166+
167+
<Box sx={{ mt: 2, display: 'flex', gap: 2 }}>
168+
<Button
169+
variant="outlined"
170+
onClick={() => {
171+
setShowReauth(false);
172+
setPassword('');
173+
setError('');
174+
}}
175+
disabled={isReauthenticating}
176+
>
177+
{t('setupMFA.cancelReauth')}
178+
</Button>
179+
<Button
180+
variant="contained"
181+
color="secondary"
182+
onClick={handleReauthentication}
183+
disabled={isReauthenticating || !password.trim()}
184+
>
185+
{isReauthenticating ? t('setupMFA.reauthenticating') : t('setupMFA.confirmReauth')}
186+
</Button>
187+
</Box>
188+
</Box>
189+
);
190+
}
70191
return (
71192
<Box>
72193
<Typography variant="h3">{t('setupMFA.title')}</Typography>
@@ -93,6 +214,7 @@ const SetupMFA = () => {
93214
<>
94215
<Typography>{t('setupMFA.enterCodeHelperText')}</Typography>
95216
<TextField
217+
id="verificationCode"
96218
value={verificationCode}
97219
onChange={(e) => setVerificationCode(e.target.value)}
98220
label={t('form.verificationCodeLabel')}
@@ -111,11 +233,11 @@ const SetupMFA = () => {
111233
</>
112234
)}
113235
{error && (
114-
<Typography color="error" sx={{ mt: '1rem !important' }}>
236+
<Alert severity="error" sx={{ mt: 2 }}>
115237
{error}
116-
</Typography>
238+
</Alert>
117239
)}
118-
<div id="recaptcha-container"></div>
240+
<div id="recaptcha-container" ref={recaptchaContainerRef}></div>
119241
</Box>
120242
);
121243
};

components/guards/VerifyMFA.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { MultiFactorResolver } from 'firebase/auth';
77
import { useTranslations } from 'next-intl';
88
import { useRouter } from 'next/navigation';
99
import type React from 'react'; // Added import for React
10-
import { useEffect, useState } from 'react';
10+
import { useEffect, useState, useRef } from 'react';
1111

1212
const buttonStyle = {
1313
display: 'block',
@@ -28,7 +28,18 @@ const VerifyMFA: React.FC<VerifyMFAProps> = ({ resolver }) => {
2828
const [error, setError] = useState('');
2929
const router = useRouter();
3030
const rollbar = useRollbar();
31+
const recaptchaContainerRef = useRef<HTMLDivElement>(null);
32+
const hasRecaptchaRendered = useRef(false);
3133

34+
// Clean up reCAPTCHA on unmount
35+
useEffect(() => {
36+
return () => {
37+
if (hasRecaptchaRendered.current && recaptchaContainerRef.current) {
38+
recaptchaContainerRef.current.innerHTML = '';
39+
hasRecaptchaRendered.current = false;
40+
}
41+
};
42+
}, []);
3243
useEffect(() => {
3344
// Get the phone number from the resolver
3445
const hint = resolver.hints[0];
@@ -40,12 +51,20 @@ const VerifyMFA: React.FC<VerifyMFAProps> = ({ resolver }) => {
4051

4152
const handleTriggerMFA = async () => {
4253
setError('');
54+
55+
// Clear any existing reCAPTCHA before creating a new one
56+
if (recaptchaContainerRef.current) {
57+
recaptchaContainerRef.current.innerHTML = '';
58+
hasRecaptchaRendered.current = false;
59+
}
60+
4361
const { verificationId, error } = await triggerVerifyMFA(resolver);
4462
if (error) {
4563
rollbar.error('MFA trigger error:', error);
4664
setError(t('form.mfaTriggerError'));
4765
} else {
4866
setVerificationId(verificationId);
67+
hasRecaptchaRendered.current = true;
4968
}
5069
};
5170

@@ -92,7 +111,7 @@ const VerifyMFA: React.FC<VerifyMFAProps> = ({ resolver }) => {
92111
{error}
93112
</Typography>
94113
)}
95-
<div id="recaptcha-container"></div>
114+
<div id="recaptcha-container" ref={recaptchaContainerRef}></div>
96115
</Box>
97116
);
98117
};

0 commit comments

Comments
 (0)