Add MFA Verification UI during login

Implements MFA verification dialog for login flow:

MFA Verification Features:
- MFAVerificationDialog component for code entry
- TOTP code input (6-digit authenticator app code)
- Email code input with auto-send on dialog open
- Backup code support mentioned in help text
- Resend email code functionality
- Auto-focus on code input field
- Large, centered code input for easy entry
- Real-time validation (6-digit code required)

Login Flow Integration:
- Detect MFA requirement from login API error
- Show MFA dialog when MFA is enabled for user
- Handle MFA verification success with token storage
- Allow cancellation to retry login
- Seamless transition after successful verification

User Experience:
- Email codes sent automatically
- Visual feedback for code sending/verification
- Error alerts for invalid codes
- Loading states for all async operations
- Clean, focused dialog design
- Tip about backup codes

Implementation Details:
- Integrated with existing login page
- Error handling for MFA-required responses
- Token storage after MFA verification
- Navigation after successful MFA
- Support for both TOTP and Email MFA methods

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-01 21:09:42 +00:00
parent e1842f5c1a
commit 48f45f1b04
2 changed files with 236 additions and 1 deletions

View File

@@ -21,6 +21,8 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { motion } from 'framer-motion';
import * as z from 'zod';
import { useAuth } from '@/lib/auth/AuthContext';
import { MFAVerificationDialog } from '@/components/auth/MFAVerificationDialog';
import { tokenStorage } from '@/lib/utils/tokenStorage';
import Link from 'next/link';
const loginSchema = z.object({
@@ -34,6 +36,8 @@ export default function LoginPage() {
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [mfaRequired, setMfaRequired] = useState(false);
const [mfaData, setMfaData] = useState<{ userId: string; mfaMethod: 'totp' | 'email' } | null>(null);
const { login } = useAuth();
const router = useRouter();
@@ -53,12 +57,33 @@ export default function LoginPage() {
await login(data);
// Navigation is handled in the login function
} catch (err: any) {
setError(err.message || 'Failed to login. Please check your credentials.');
// Check if MFA is required
if (err.response?.data?.mfaRequired) {
setMfaRequired(true);
setMfaData({
userId: err.response.data.userId,
mfaMethod: err.response.data.mfaMethod,
});
} else {
setError(err.message || 'Failed to login. Please check your credentials.');
}
} finally {
setIsLoading(false);
}
};
const handleMFAVerified = (tokens: { accessToken: string; refreshToken: string }, user: any) => {
// Store tokens and navigate
tokenStorage.setTokens(tokens.accessToken, tokens.refreshToken);
setMfaRequired(false);
router.push('/');
};
const handleMFACancel = () => {
setMfaRequired(false);
setMfaData(null);
};
return (
<Box
sx={{
@@ -216,6 +241,17 @@ export default function LoginPage() {
</Box>
</Paper>
</motion.div>
{/* MFA Verification Dialog */}
{mfaRequired && mfaData && (
<MFAVerificationDialog
open={mfaRequired}
userId={mfaData.userId}
mfaMethod={mfaData.mfaMethod}
onVerified={handleMFAVerified}
onCancel={handleMFACancel}
/>
)}
</Box>
);
}