// LoginScreen + SetupScreen - layout B from the auth brainstorm spec: // 22px wordmark + "WILD DRAGON BROADCAST" tagline above a --bg-1 card. // Matches DESIGN.md tokens; no decoration, dense, ops register. (function () { const API_BASE = '/api/v1'; async function postJson(path, body) { return fetch(API_BASE + path, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'dragonflight-ui', }, body: JSON.stringify(body), }); } function Brand() { return (
Dragonflight
Wild Dragon Broadcast
); } function Card({ children }) { return (
{children}
); } function Field({ label, type = 'text', value, onChange, autoComplete, autoFocus }) { return (
onChange(e.target.value)} style={{ width: '100%', background: 'var(--bg-3)', border: '1px solid var(--border)', borderRadius: 4, padding: '8px 10px', fontSize: 12.5, color: 'var(--text-1)', fontFamily: 'var(--font-mono)', boxSizing: 'border-box', }} />
); } function Button({ children, disabled, onClick, type = 'button' }) { return ( ); } function ErrorRow({ text }) { if (!text) return null; return (
{text}
); } function Screen({ children }) { return (
e.preventDefault()} style={{ width: 300 }}> {children}
); } function LoginScreen({ onDone }) { const [username, setUsername] = React.useState(''); const [password, setPassword] = React.useState(''); const [error, setError] = React.useState(''); const [busy, setBusy] = React.useState(false); // Second factor: when the server returns { mfa_required, ticket }, we switch // to the code step instead of completing login. const [ticket, setTicket] = React.useState(null); const [code, setCode] = React.useState(''); const submit = async () => { setError(''); setBusy(true); try { const r = await postJson('/auth/login', { username, password }); if (r.status === 200) { const body = await r.json().catch(() => ({})); if (body.mfa_required) { setTicket(body.ticket); setBusy(false); return; } onDone(); return; } const body = await r.json().catch(() => ({})); setError(body.error || ('Login failed: ' + r.status)); } catch (e) { setError(e.message || 'Login failed'); } finally { setBusy(false); } }; const submitCode = async () => { setError(''); setBusy(true); try { const r = await postJson('/auth/login/totp', { ticket, code: code.trim() }); if (r.status === 200) { onDone(); return; } const body = await r.json().catch(() => ({})); // An expired/used ticket means the user must start over. if (r.status === 401 && /ticket/.test(body.error || '')) { setTicket(null); setCode(''); setPassword(''); setError('Session timed out — please sign in again.'); } else { setError(body.error || ('Verification failed: ' + r.status)); } } catch (e) { setError(e.message || 'Verification failed'); } finally { setBusy(false); } }; if (ticket) { return (
Two-factor authentication
Enter the 6-digit code from your authenticator app, or a recovery code.
); } return ( ); } function SetupScreen({ onDone }) { const [username, setUsername] = React.useState(''); const [password, setPassword] = React.useState(''); const [confirm, setConfirm] = React.useState(''); const [error, setError] = React.useState(''); const [busy, setBusy] = React.useState(false); const submit = async () => { setError(''); if (password !== confirm) { setError('Passwords do not match'); return; } if (password.length < 12) { setError('Password must be at least 12 characters'); return; } setBusy(true); try { const r = await postJson('/auth/setup', { username, password }); if (r.status === 200) { onDone(); return; } const body = await r.json().catch(() => ({})); setError(body.error || ('Setup failed: ' + r.status)); } catch (e) { setError(e.message || 'Setup failed'); } finally { setBusy(false); } }; return (
First-run setup: create the first admin
); } window.LoginScreen = LoginScreen; window.SetupScreen = SetupScreen; })();