/** * Login — the only page that renders outside AppShell. * * The daemon may be running with `auth.mode: none`, `apikey` or `basic`, so the * form is built from what `/api/whoami` reports rather than hard-coded. A 401 * from that probe is expected (that is why the visitor is here) and is not * shown as an error. */ import { useEffect, useState } from 'react' import type { FormEvent } from 'react' import { Globe, KeyRound, LogIn, ShieldCheck, Terminal } from 'lucide-react' import { useSearchParams } from 'react-router-dom' import { ApiError, login, whoami } from '../lib/api' import type { WhoamiResponse } from '../lib/api' import { Button, Card, CardBody, Input } from '../components/ui' import { Spinner } from '../components/ui' /** "apikey" -> one field, "basic" -> two, anything else -> one field. */ function wantsPassword(mode: string): boolean { return mode === 'basic' } export default function Login() { const [params] = useSearchParams() const next = params.get('next') ?? '/' const [mode, setMode] = useState(null) const [apiKey, setApiKey] = useState('') const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [pending, setPending] = useState(false) const [error, setError] = useState(null) useEffect(() => { let live = true whoami() .then((who: WhoamiResponse) => { if (live) setMode(who.auth_mode ?? 'apikey') }) .catch(() => { // Unauthenticated probes and older daemons both land here: show the // form rather than an error, the submit will say what is wrong. if (live) setMode('apikey') }) return () => { live = false } }, []) const onSubmit = async (event: FormEvent): Promise => { event.preventDefault() if (pending) return setError(null) setPending(true) try { if (wantsPassword(mode ?? 'apikey')) { await login({ username, password }) } else { await login({ api_key: apiKey }) } // A full navigation: the shell reads whoami and the token on boot. window.location.assign(next.startsWith('/') ? next : '/') } catch (err) { setError( err instanceof ApiError ? err.message : err instanceof Error ? err.message : 'login failed', ) } finally { setPending(false) } } const passwordMode = wantsPassword(mode ?? 'apikey') return (
fancy-dns
void onSubmit(event)}> {passwordMode ? ( <> setUsername(event.target.value)} autoComplete="username" autoFocus required /> setPassword(event.target.value)} autoComplete="current-password" required /> ) : ( setApiKey(event.target.value)} autoComplete="current-password" spellCheck={false} autoFocus required /> )} {error !== null && (

{error}

)}

{pending && (

waiting for the node

)}
) }