2025-12-22 21:56:37 +00:00
|
|
|
import { useState } from 'react';
|
2025-12-13 02:34:34 +00:00
|
|
|
import {
|
|
|
|
|
Card,
|
|
|
|
|
CardContent,
|
|
|
|
|
CardDescription,
|
|
|
|
|
CardHeader,
|
|
|
|
|
CardTitle,
|
|
|
|
|
} from '@/components/ui/card';
|
2025-12-03 21:56:50 +00:00
|
|
|
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';
|
2025-12-22 21:56:37 +00:00
|
|
|
import { useToast } from '@/hooks/useToast';
|
2026-01-07 18:39:21 +00:00
|
|
|
import { parseApiError } from '@/utils/apiErrorHandler';
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
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;
|
2026-01-16 11:31:40 +00:00
|
|
|
// TODO: Fix 2FA login verification
|
|
|
|
|
// Issue: twoFactorService.verify(secret, code) is for 2FA setup, not login verification.
|
|
|
|
|
// During login, we don't have the secret. The 2FA code should be verified as part of
|
|
|
|
|
// the login flow (either via a separate endpoint like /auth/login/2fa or by including
|
|
|
|
|
// the code in the login request when requires_2fa is true).
|
|
|
|
|
// Current workaround: Pass code to parent via onSuccess() - parent should handle verification.
|
|
|
|
|
// Action required: Check backend API contract for 2FA login verification endpoint.
|
2025-12-27 15:23:53 +00:00
|
|
|
try {
|
2026-01-16 11:31:40 +00:00
|
|
|
// FIXME: This is incorrect - verify() is for setup, not login
|
|
|
|
|
// Remove this call once proper login 2FA verification is implemented
|
2025-12-27 15:23:53 +00:00
|
|
|
await twoFactorService.verify('', verificationCode);
|
|
|
|
|
onSuccess(verificationCode);
|
2026-01-07 18:39:21 +00:00
|
|
|
} catch (verifyError: unknown) {
|
2026-01-07 09:32:53 +00:00
|
|
|
// If verification fails, show error message
|
2026-01-07 18:39:21 +00:00
|
|
|
const apiError = parseApiError(verifyError);
|
|
|
|
|
const errorMessage = apiError.message;
|
2026-01-07 09:32:53 +00:00
|
|
|
setError(errorMessage);
|
|
|
|
|
toast({
|
|
|
|
|
message: errorMessage,
|
|
|
|
|
type: 'error',
|
|
|
|
|
});
|
|
|
|
|
throw verifyError; // Re-throw to be caught by outer catch if needed
|
2025-12-03 21:56:50 +00:00
|
|
|
}
|
2026-01-07 18:39:21 +00:00
|
|
|
} catch (error: unknown) {
|
|
|
|
|
const apiError = parseApiError(error);
|
|
|
|
|
setError(apiError.message);
|
2025-12-03 21:56:50 +00:00
|
|
|
toast({
|
2026-01-07 18:39:21 +00:00
|
|
|
message: apiError.message,
|
2025-12-22 21:56:37 +00:00
|
|
|
type: 'error',
|
2025-12-03 21:56:50 +00:00
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setIsVerifying(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card className="w-full max-w-md">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
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 10:32:55 +00:00
|
|
|
<Shield className="h-5 w-5 text-kodo-steel" />
|
2025-12-03 21:56:50 +00:00
|
|
|
Two-Factor Authentication
|
|
|
|
|
</CardTitle>
|
2025-12-13 02:34:34 +00:00
|
|
|
<CardDescription>
|
|
|
|
|
Enter the code from your authenticator app
|
|
|
|
|
</CardDescription>
|
2025-12-03 21:56:50 +00:00
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
<Alert>
|
|
|
|
|
<AlertCircle className="h-4 w-4" />
|
|
|
|
|
<AlertDescription>
|
2025-12-13 02:34:34 +00:00
|
|
|
Enter the 6-digit code from your authenticator app to continue
|
|
|
|
|
signing in.
|
2025-12-03 21:56:50 +00:00
|
|
|
</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>
|
2026-01-16 00:56:47 +00:00
|
|
|
<p className="text-sm text-kodo-content-dim">
|
2025-12-03 21:56:50 +00:00
|
|
|
Lost access?{' '}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setUseBackupCode(true)}
|
2026-01-16 00:56:47 +00:00
|
|
|
className="text-kodo-cyan hover:underline"
|
2025-12-03 21:56:50 +00:00
|
|
|
>
|
|
|
|
|
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>
|
2026-01-16 00:56:47 +00:00
|
|
|
<p className="text-sm text-kodo-content-dim">
|
2025-12-03 21:56:50 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => setUseBackupCode(false)}
|
2026-01-16 00:56:47 +00:00
|
|
|
className="text-kodo-cyan hover:underline"
|
2025-12-03 21:56:50 +00:00
|
|
|
>
|
|
|
|
|
Use authenticator code instead
|
|
|
|
|
</button>
|
|
|
|
|
</p>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button onClick={onCancel} variant="outline" className="flex-1">
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
2025-12-13 02:34:34 +00:00
|
|
|
<Button
|
|
|
|
|
onClick={handleVerify}
|
|
|
|
|
disabled={isVerifying || (!code && !backupCode)}
|
|
|
|
|
className="flex-1"
|
|
|
|
|
>
|
2025-12-03 21:56:50 +00:00
|
|
|
{isVerifying ? (
|
|
|
|
|
<>
|
|
|
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
Verifying...
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
'Verify'
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
}
|