// 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 (
);
}
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;
})();