veza/apps/web/src/features/auth/components/TwoFactorVerify.tsx

162 lines
4.7 KiB
TypeScript

import { useState } from 'react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Loader2, Shield, AlertCircle } from 'lucide-react';
import { twoFactorService } from '@/services/2fa-service';
import { useToast } from '@/hooks/useToast';
interface TwoFactorVerifyProps {
onSuccess: (code: string) => void;
onCancel: () => void;
}
export function TwoFactorVerify({ onSuccess, onCancel }: TwoFactorVerifyProps) {
const [code, setCode] = useState('');
const [backupCode, setBackupCode] = useState('');
const [useBackupCode, setUseBackupCode] = useState(false);
const [isVerifying, setIsVerifying] = useState(false);
const [error, setError] = useState('');
const { toast } = useToast();
const handleVerify = async () => {
if (!code && !backupCode) {
setError('Please enter a verification code');
return;
}
try {
setIsVerifying(true);
setError('');
const verificationCode = useBackupCode ? backupCode : code;
const isValid = await twoFactorService.verify(verificationCode);
if (isValid) {
onSuccess(verificationCode);
} else {
setError('Invalid verification code. Please try again.');
}
} catch (error: any) {
setError(error.message);
toast({
message: error.message,
type: 'error',
});
} finally {
setIsVerifying(false);
}
};
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5 text-blue-500" />
Two-Factor Authentication
</CardTitle>
<CardDescription>
Enter the code from your authenticator app
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Enter the 6-digit code from your authenticator app to continue
signing in.
</AlertDescription>
</Alert>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{!useBackupCode ? (
<>
<div className="space-y-2">
<Label htmlFor="2fa-code">Verification Code</Label>
<Input
id="2fa-code"
type="text"
placeholder="000000"
value={code}
onChange={(e) => {
setCode(e.target.value.replace(/\D/g, '').slice(0, 6));
setError('');
}}
maxLength={6}
className="text-center text-2xl tracking-widest"
autoFocus
/>
</div>
<p className="text-sm text-gray-500">
Lost access?{' '}
<button
onClick={() => setUseBackupCode(true)}
className="text-blue-600 hover:underline"
>
Use a backup code
</button>
</p>
</>
) : (
<>
<div className="space-y-2">
<Label htmlFor="backup-code">Backup Code</Label>
<Input
id="backup-code"
type="text"
placeholder="Enter backup code"
value={backupCode}
onChange={(e) => {
setBackupCode(e.target.value);
setError('');
}}
/>
</div>
<p className="text-sm text-gray-500">
<button
onClick={() => setUseBackupCode(false)}
className="text-blue-600 hover:underline"
>
Use authenticator code instead
</button>
</p>
</>
)}
<div className="flex gap-2">
<Button onClick={onCancel} variant="outline" className="flex-1">
Cancel
</Button>
<Button
onClick={handleVerify}
disabled={isVerifying || (!code && !backupCode)}
className="flex-1"
>
{isVerifying ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Verifying...
</>
) : (
'Verify'
)}
</Button>
</div>
</CardContent>
</Card>
);
}