import { useState } from 'react'; import { login, totpChallenge } from './api'; 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'; 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); // api.login normalizes network/timeout/429/5xx into res.error. const res = await login(serverUrl, username.trim(), password); setLoading(false); if (!res.ok) { setError(res.error || 'Invalid username or password.'); return; } const data = res.data; if (data?.requires_totp) { setChallengeToken(data.challenge_token || ''); setStage('totp'); return; } if (data?.requires_webauthn) { setStage('webauthn'); return; } onSuccess(); } async function handleTotp() { if (!code.trim()) { setError('Enter your authenticator code.'); return; } setError(''); setLoading(true); // totpChallenge fetches + echoes the CSRF token (the challenge endpoint, // unlike /login, is not CSRF-exempt). const res = await totpChallenge(serverUrl, challengeToken, code.trim()); setLoading(false); if (!res.ok) { setError(res.error || 'Invalid authenticator code.'); return; } onSuccess(); } 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.includes('127.0.0.1') ? 'this device' : serverUrl}

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

{error}

}
); }