Add biometric login button to login page
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

- Add biometric authentication button with Face ID/Touch ID/Windows Hello support
- Check WebAuthn support and platform authenticator availability on mount
- Handle biometric login flow with proper error handling
- Show biometric button only when device supports it
- Add loading states and user-friendly error messages

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-02 05:46:57 +00:00
parent 6c8a50b910
commit 5a7202cf5b

View File

@@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import {
Box,
@@ -15,7 +15,7 @@ import {
CircularProgress,
Link as MuiLink,
} from '@mui/material';
import { Visibility, VisibilityOff, Google, Apple } from '@mui/icons-material';
import { Visibility, VisibilityOff, Google, Apple, Fingerprint } from '@mui/icons-material';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { motion } from 'framer-motion';
@@ -23,6 +23,8 @@ import * as z from 'zod';
import { useAuth } from '@/lib/auth/AuthContext';
import { MFAVerificationDialog } from '@/components/auth/MFAVerificationDialog';
import { tokenStorage } from '@/lib/utils/tokenStorage';
import { biometricApi } from '@/lib/api/biometric';
import { startAuthentication } from '@simplewebauthn/browser';
import Link from 'next/link';
const loginSchema = z.object({
@@ -36,6 +38,8 @@ export default function LoginPage() {
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isBiometricLoading, setIsBiometricLoading] = useState(false);
const [isBiometricSupported, setIsBiometricSupported] = useState(false);
const [mfaRequired, setMfaRequired] = useState(false);
const [mfaData, setMfaData] = useState<{ userId: string; mfaMethod: 'totp' | 'email' } | null>(null);
const { login } = useAuth();
@@ -45,10 +49,64 @@ export default function LoginPage() {
register,
handleSubmit,
formState: { errors },
watch,
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
const email = watch('email');
// Check biometric support on mount
useEffect(() => {
checkBiometricSupport();
}, []);
const checkBiometricSupport = async () => {
const supported = biometricApi.isSupported();
if (supported) {
const available = await biometricApi.isPlatformAuthenticatorAvailable();
setIsBiometricSupported(available);
}
};
const handleBiometricLogin = async () => {
setError(null);
setIsBiometricLoading(true);
try {
// Get authentication options from server
const options = await biometricApi.getAuthenticationOptions(email || undefined);
// Start WebAuthn authentication (triggers Face ID/Touch ID/Windows Hello)
const authenticationResponse = await startAuthentication(options);
// Send response to server for verification and get tokens
const result = await biometricApi.verifyAuthentication(
authenticationResponse,
email || undefined,
{
deviceId: authenticationResponse.id.substring(0, 10),
platform: navigator.userAgent,
}
);
// Store tokens and navigate
tokenStorage.setTokens(result.tokens.accessToken, result.tokens.refreshToken);
router.push('/');
} catch (err: any) {
console.error('Biometric login failed:', err);
if (err.name === 'NotAllowedError') {
setError('Biometric authentication was cancelled');
} else if (err.name === 'NotSupportedError') {
setError('Biometric authentication is not supported on this device');
} else {
setError(err.response?.data?.message || err.message || 'Biometric login failed. Please try again.');
}
} finally {
setIsBiometricLoading(false);
}
};
const onSubmit = async (data: LoginFormData) => {
setError(null);
setIsLoading(true);
@@ -229,6 +287,20 @@ export default function LoginPage() {
Continue with Apple
</Button>
{isBiometricSupported && (
<Button
fullWidth
variant="outlined"
startIcon={isBiometricLoading ? <CircularProgress size={20} /> : <Fingerprint />}
size="large"
disabled={isLoading || isBiometricLoading}
onClick={handleBiometricLogin}
sx={{ mt: 2 }}
>
{isBiometricLoading ? 'Authenticating...' : 'Sign in with Biometrics'}
</Button>
)}
<Box sx={{ mt: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
Don't have an account?{' '}