import { useState } from 'react'; interface Props { serverUrl: string; /** Called once the backend has confirmed the credentials and set a session cookie. */ onSuccess: () => void; onBack: () => void; } type Stage = 'credentials' | 'totp' | 'webauthn'; interface ApiError { message?: string; } async function postJson(url: string, body: unknown): Promise<{ ok: boolean; data: any }> { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); return { ok: res.ok, data }; } export default function LoginScreen({ serverUrl, onSuccess, onBack }: Props) { const [stage, setStage] = useState('credentials'); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [code, setCode] = useState(''); const [challengeToken, setChallengeToken] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); function handleKey(e: React.KeyboardEvent) { if (e.key !== 'Enter') return; if (stage === 'credentials') handleLogin(); if (stage === 'totp') handleTotp(); } async function handleLogin() { if (!username.trim() || !password) { setError('Enter your username and password.'); return; } setError(''); setLoading(true); try { const { ok, data } = await postJson(`${serverUrl}/api/auth/login`, { username: username.trim(), password, }); if (!ok) { setError((data as ApiError)?.message || 'Invalid username or password.'); return; } if (data.requires_totp) { setChallengeToken(data.challenge_token); setStage('totp'); return; } if (data.requires_webauthn) { setStage('webauthn'); return; } onSuccess(); } catch { setError('Could not reach the server. Check the URL and your connection.'); } finally { setLoading(false); } } async function handleTotp() { if (!code.trim()) { setError('Enter your authenticator code.'); return; } setError(''); setLoading(true); try { const { ok, data } = await postJson(`${serverUrl}/api/auth/totp/challenge`, { challenge_token: challengeToken, code: code.trim(), }); if (!ok) { setError((data as ApiError)?.message || 'Invalid authenticator code.'); return; } onSuccess(); } catch { setError('Could not reach the server. Check the URL and your connection.'); } finally { setLoading(false); } } if (stage === 'webauthn') { return (

Security key required

This account signs in with a security key. Continue to sign in through the server's web interface.

); } if (stage === 'totp') { return (

Two-factor code

Enter the code from your authenticator app.

{ setCode(e.target.value); setError(''); }} onKeyDown={handleKey} disabled={loading} autoFocus /> {error &&

{error}

}
); } return (

Sign in

Sign in to {serverUrl}

{ setUsername(e.target.value); setError(''); }} onKeyDown={handleKey} disabled={loading} autoFocus />
{ setPassword(e.target.value); setError(''); }} onKeyDown={handleKey} disabled={loading} /> {error &&

{error}

}
); }