// Admin Login Gate — same visual language as CodeGate but amber accent. // Handles three states: // 1. login — email + password (+ optional TOTP) form // 2. enroll-show — show TOTP QR code after first login attempt // 3. enroll-done — confirm TOTP enrollment then back to login const AdminGate = ({ onSuccess, onBack }) => { const [step, setStep] = React.useState('login'); // login | enroll-show | enroll-done const [email, setEmail] = React.useState(''); const [password, setPassword] = React.useState(''); const [totp, setTotp] = React.useState(''); const [error, setError] = React.useState(''); const [submitting, setSubmitting] = React.useState(false); const [rateLimited, setRateLimited] = React.useState(false); const [qrData, setQrData] = React.useState(null); // { secret, keyUri, qrDataUrl } const emailRef = React.useRef(null); React.useEffect(() => { emailRef.current?.focus(); }, []); const submit = async () => { if (submitting || rateLimited) return; setError(''); if (!email.includes('@') || password.length < 8) { setError('Enter a valid email and password (min 8 chars).'); return; } setSubmitting(true); try { const res = await tmApi.admin.login(email.trim().toLowerCase(), password, totp || undefined); // Success onSuccess({ email: res.admin?.email || email, role: res.admin?.role || 'super' }); } catch (e) { const code = e.code || 'http_error'; if (code === 'rate_limited') { setRateLimited(true); setError('Too many login attempts. Wait 15 minutes and try again.'); setTimeout(() => setRateLimited(false), 15 * 60 * 1000); } else if (code === 'totp_enrollment_required') { // First-time login — kick off enrollment try { const start = await tmApi.admin.start2fa(email.trim().toLowerCase(), password); setQrData(start); setStep('enroll-show'); setTotp(''); setError(''); } catch (e2) { setError(e2.message || 'Could not start 2FA enrollment.'); } } else if (code === 'totp_required') { setError('Enter your 6-digit TOTP code.'); } else if (code === 'invalid_totp') { setError('Invalid TOTP code — try again.'); setTotp(''); } else if (code === 'invalid_credentials') { setError('Invalid email or password.'); } else { setError(e.message || 'Login failed.'); } } finally { setSubmitting(false); } }; const confirmEnroll = async () => { if (submitting) return; if (!/^\d{6}$/.test(totp)) { setError('TOTP must be exactly 6 digits.'); return; } setSubmitting(true); setError(''); try { await tmApi.admin.confirm2fa(email.trim().toLowerCase(), password, totp); // Now log in for real const res = await tmApi.admin.login(email.trim().toLowerCase(), password, totp); onSuccess({ email: res.admin?.email || email, role: res.admin?.role || 'super' }); } catch (e) { setError(e.message || 'Enrollment failed.'); } finally { setSubmitting(false); } }; return (
Email + password + 6-digit TOTP code from your authenticator app.
First login — scan with Google Authenticator / Authy / 1Password,
then enter the 6-digit code below.