import { useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { Mail, Lock, ArrowRight, Fingerprint, ArrowLeft, ShieldCheck, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Checkbox } from "@/components/ui/checkbox"; import { useAuth } from "@/contexts/AuthContext"; import { api, ApiError, tokenManager } from "@/lib/api"; import { useToast } from "@/hooks/use-toast"; import { InputOTP, InputOTPGroup, InputOTPSlot, } from "@/components/ui/input-otp"; import { isWebAuthnSupported, createLoginAssertion, formatLoginAssertion, WebAuthnLoginOptions, } from "@/lib/webauthn"; type LoginStep = 'credentials' | 'totp' | 'passkey-email'; export default function LoginPage() { const { login, verifyTotp, refreshUser } = useAuth(); const navigate = useNavigate(); const { toast } = useToast(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [rememberMe, setRememberMe] = useState(false); const [isLoading, setIsLoading] = useState(false); const [step, setStep] = useState('credentials'); const [totpCode, setTotpCode] = useState(""); const [useBackupCode, setUseBackupCode] = useState(false); const [passkeyEmail, setPasskeyEmail] = useState(""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); try { const result = await login(email, password, rememberMe); if (result.requiresTotp) { setStep('totp'); setTotpCode(""); } // If no TOTP required, navigation happens in AuthContext } catch (error) { if (import.meta.env.DEV) { console.error("[Gatehouse] Login failed:", error); } const message = error instanceof ApiError ? error.message : import.meta.env.DEV && error instanceof Error ? error.message : "An unexpected error occurred"; toast({ variant: "destructive", title: "Sign in failed", description: message, }); } finally { setIsLoading(false); } }; const handleTotpSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (totpCode.length < 6) { toast({ variant: "destructive", title: "Invalid code", description: "Please enter your complete verification code.", }); return; } setIsLoading(true); try { await verifyTotp(totpCode, useBackupCode); // Navigation happens in AuthContext } catch (error) { if (import.meta.env.DEV) { console.error("[Gatehouse] TOTP verification failed:", error); } const message = error instanceof ApiError ? error.message : import.meta.env.DEV && error instanceof Error ? error.message : "Invalid verification code"; toast({ variant: "destructive", title: "Verification failed", description: message, }); setTotpCode(""); } finally { setIsLoading(false); } }; const handlePasskeyLogin = async () => { if (!isWebAuthnSupported()) { toast({ variant: "destructive", title: "Not supported", description: "Passkeys are not supported in this browser.", }); return; } // If we have an email from the form, use it directly const emailToUse = email || passkeyEmail; if (!emailToUse) { setStep('passkey-email'); return; } setIsLoading(true); try { // Step 1: Get login options from server const options = await api.webauthn.beginLogin(emailToUse) as unknown as WebAuthnLoginOptions; // Step 2: Create assertion using browser WebAuthn API const assertion = await createLoginAssertion(options); // Step 3: Complete login with server const formattedAssertion = formatLoginAssertion(assertion); const result = await api.webauthn.completeLogin(formattedAssertion); // Token is stored by completeLogin, refresh user and navigate await refreshUser(); navigate('/profile'); toast({ title: "Welcome back", description: `Signed in as ${result.user.email}`, }); } catch (error) { if (import.meta.env.DEV) { console.error("[Gatehouse] Passkey login failed:", error); } let message = "Failed to sign in with passkey"; if (error instanceof ApiError) { message = error.message; } else if (error instanceof DOMException) { switch (error.name) { case "NotAllowedError": message = "Authentication was cancelled or timed out."; break; case "InvalidStateError": message = "No passkey found for this account."; break; default: message = error.message || message; } } else if (error instanceof Error) { message = error.message; } toast({ variant: "destructive", title: "Passkey sign in failed", description: message, }); } finally { setIsLoading(false); } }; const handlePasskeyEmailSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!passkeyEmail) return; await handlePasskeyLogin(); }; const handleBackToCredentials = () => { setStep('credentials'); setTotpCode(""); setUseBackupCode(false); setPasskeyEmail(""); }; // Auto-submit when OTP is complete const handleOtpChange = (value: string) => { setTotpCode(value); if (value.length === 6 && !useBackupCode) { setTimeout(() => { const form = document.getElementById('totp-form') as HTMLFormElement; if (form) form.requestSubmit(); }, 100); } }; // Passkey email entry step if (step === 'passkey-email') { return (

Sign in with passkey

Enter your email to continue with passkey authentication

setPasskeyEmail(e.target.value)} className="pl-10" required autoFocus />
); } // TOTP verification step if (step === 'totp') { return (

Two-factor authentication

Enter the 6-digit code from your authenticator app

{useBackupCode ? (
setTotpCode(e.target.value.toUpperCase())} className="text-center font-mono tracking-widest" maxLength={16} autoFocus />
) : (
)}
); } // Credentials step (default) return (

Welcome back

Sign in to your account to continue

setEmail(e.target.value)} className="pl-10" required />
setPassword(e.target.value)} className="pl-10" required />
setRememberMe(checked === true)} />
Forgot password?
or continue with
{/* Alternative login methods */}

Don't have an account?{" "} Create one

); }