veza/apps/web/src/features/auth/components/TwoFactorVerify.tsx
senke 3749aa2394 aesthetic-improvements: reduce decorative cyan in chat, auth, player, streaming, and dashboard (80/20 rule, batch 13)
- Chat: ChatSidebar loading spinner and decorative icon, VirtualizedChatMessages decorative attachment badge, ChatPage decorative icon and loading spinner border/text, ChatMessage decorative username indicator and icon (7 instances)
- Auth: TwoFactorVerify decorative icon (1 instance)
- Player: PlayerLoading decorative spinner (1 instance)
- Streaming: PlaybackSummary decorative icon (1 instance)
- Dashboard: DashboardPage decorative chart color and gradient and icon (3 instances)
- Total: ~13 files, ~13 instances replaced
- Preserved: Active/selected states (ChatSidebar selected conversation, ChatMessage isMe message bubble and highlighted message, DashboardPage selected button 30J, ChatInput drag active overlay and emoji picker active, TrackFilters active filter badge, TrackHistory current track, TrackGridDensitySelector selected density, PlaybackSpeedControl selected speed, ViewToggle selected view mode, TrackList selected tracks, TrackListRow selected state, PlaylistList selected view mode, QualitySelector selected quality, SettingsPage selected tab and theme, LoginForm checkbox accent - focus/interaction, RegisterPage checkbox accent - focus/interaction), functional links (ForgotPasswordPage link, TwoFactorVerify links, RegisterPage links, AuthLayout link, ProfileForm links, LoginPage link, RegisterPage link), design system variants, semantic status indicators, interactive states, functional loading indicators, informational alerts/toasts
- Action 11.3.1.3 in progress (thirteenth batch: chat, auth, player, streaming, and dashboard components)
2026-01-16 11:32:55 +01:00

177 lines
5.5 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';
import { parseApiError } from '@/utils/apiErrorHandler';
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;
// Note: twoFactorService.verify requires secret and code, but this component
// is for verification during login, not setup. We need to check if there's
// a different method or if we need to pass a secret here.
// For now, we'll assume the verification happens differently during login
// and just call onSuccess if we have a code
// TODO: Fix this - twoFactorService.verify is for setup, not login verification
try {
await twoFactorService.verify('', verificationCode);
onSuccess(verificationCode);
} catch (verifyError: unknown) {
// If verification fails, show error message
const apiError = parseApiError(verifyError);
const errorMessage = apiError.message;
setError(errorMessage);
toast({
message: errorMessage,
type: 'error',
});
throw verifyError; // Re-throw to be caught by outer catch if needed
}
} catch (error: unknown) {
const apiError = parseApiError(error);
setError(apiError.message);
toast({
message: apiError.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-kodo-steel" />
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-kodo-content-dim">
Lost access?{' '}
<button
onClick={() => setUseBackupCode(true)}
className="text-kodo-cyan 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-kodo-content-dim">
<button
onClick={() => setUseBackupCode(false)}
className="text-kodo-cyan 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>
);
}