import { useState, useRef, useEffect, type KeyboardEvent } from 'react'; import { Lock, LoaderCircle, ArrowRight, ArrowLeft, Shield, Check } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; const LOGIN_STORAGE_KEY = 'bloby_login_totp'; interface Props { onLogin: (token: string) => void; totpEnabled?: boolean; } export default function LoginScreen({ onLogin, totpEnabled }: Props) { const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); // TOTP state const [phase, setPhase] = useState<'password' | 'totp'>('password'); const [totpCode, setTotpCode] = useState(''); const [pendingToken, setPendingToken] = useState(''); const [trustDevice, setTrustDevice] = useState(true); const [useRecovery, setUseRecovery] = useState(false); const totpInputRef = useRef(null); // Restore TOTP phase after page reload (mobile: OS suspends PWA on app-switch) useEffect(() => { try { const raw = sessionStorage.getItem(LOGIN_STORAGE_KEY); if (!raw) return; const saved = JSON.parse(raw); if (saved.pendingToken && saved.expiresAt > Date.now()) { setPendingToken(saved.pendingToken); setPhase('totp'); } else { sessionStorage.removeItem(LOGIN_STORAGE_KEY); } } catch {} }, []); // Auto-focus TOTP input when switching to TOTP phase useEffect(() => { if (phase === 'totp') { setTimeout(() => totpInputRef.current?.focus(), 100); } }, [phase]); function saveLoginState(token: string) { try { // Pending token has 5min server expiry — save with matching client expiry sessionStorage.setItem(LOGIN_STORAGE_KEY, JSON.stringify({ pendingToken: token, expiresAt: Date.now() + 4.5 * 60 * 1000, })); } catch {} } function clearLoginState() { try { sessionStorage.removeItem(LOGIN_STORAGE_KEY); } catch {} } const handleSubmit = async () => { if (!password.trim() || loading) return; setLoading(true); setError(''); try { const credentials = btoa(`admin:${password}`); const res = await fetch('/api/portal/login', { headers: { 'Authorization': `Basic ${credentials}` }, credentials: 'include', }); const data = await res.json(); if (res.ok && data.token) { clearLoginState(); onLogin(data.token); } else if (res.ok && data.requiresTOTP) { setPendingToken(data.pendingToken); saveLoginState(data.pendingToken); setPhase('totp'); setError(''); } else { setError(data.error || 'Invalid password'); } } catch { setError('Could not reach server'); } finally { setLoading(false); } }; const handleTotpSubmit = async () => { if (!totpCode.trim() || loading) return; setLoading(true); setError(''); try { const res = await fetch( `/api/portal/login/totp?pending=${encodeURIComponent(pendingToken)}&code=${encodeURIComponent(totpCode)}&trust=${trustDevice ? '1' : '0'}`, { credentials: 'include' }, ); const data = await res.json(); if (res.ok && data.token) { clearLoginState(); onLogin(data.token); } else { setError(data.error || 'Invalid code'); } } catch { setError('Could not reach server'); } finally { setLoading(false); } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter') { if (phase === 'password') handleSubmit(); else handleTotpSubmit(); } }; const handleBackToPassword = () => { setPhase('password'); setError(''); setTotpCode(''); setUseRecovery(false); clearLoginState(); }; const inputCls = 'w-full bg-white/[0.05] border border-white/[0.08] text-white rounded-xl px-4 py-3 text-base outline-none input-glow placeholder:text-white/20 transition-all'; return (
{phase === 'password' ? (

Welcome back

Enter your password to continue.

{error && (

{error}

)} setPassword(e.target.value)} onKeyDown={handleKeyDown} placeholder="Password" autoFocus autoComplete="current-password" className={inputCls} />
) : (

{useRecovery ? 'Recovery code' : 'Enter your 2FA code'}

{useRecovery ? 'Enter one of your recovery codes.' : 'Open your authenticator app and enter the 6-digit code.'}

{error && (

{error}

)} { setTotpCode(useRecovery ? e.target.value : e.target.value.replace(/\D/g, '')); setError(''); }} onKeyDown={handleKeyDown} placeholder={useRecovery ? 'Recovery code' : '000000'} autoFocus className={inputCls + (useRecovery ? '' : ' tracking-[0.3em] text-center font-mono')} /> {/* Trust device checkbox */}
)}
); }