// 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 (
A
ADMIN·CTL
RESTRICTED · ADMINS ONLY
{step === 'login' && ( <>

Admin sign-in

Email + password + 6-digit TOTP code from your authenticator app.

{ setEmail(e.target.value); setError(''); }} onKeyDown={(e) => e.key === 'Enter' && submit()} placeholder="admin@example.com" disabled={submitting || rateLimited} autoComplete="email" style={inputStyle(error && !rateLimited && !email.includes('@'))} /> { setPassword(e.target.value); setError(''); }} onKeyDown={(e) => e.key === 'Enter' && submit()} placeholder="password" disabled={submitting || rateLimited} autoComplete="current-password" style={inputStyle(false)} /> { setTotp(e.target.value.replace(/\D/g, '').slice(0, 6)); setError(''); }} onKeyDown={(e) => e.key === 'Enter' && submit()} placeholder="6-digit TOTP (leave blank on first login)" disabled={submitting || rateLimited} autoComplete="one-time-code" style={{ ...inputStyle(false), letterSpacing: totp ? '0.4em' : 'normal', textAlign: totp ? 'center' : 'left' }} />
{error && (
{error}
)}
)} {step === 'enroll-show' && qrData && ( <>

Enroll TOTP

First login — scan with Google Authenticator / Authy / 1Password,
then enter the 6-digit code below.

TOTP QR
Can't scan? Show secret manually
{qrData.secret}
{ setTotp(e.target.value.replace(/\D/g, '').slice(0, 6)); setError(''); }} onKeyDown={(e) => e.key === 'Enter' && confirmEnroll()} placeholder="6-digit code" autoFocus style={{ ...inputStyle(false), letterSpacing: totp ? '0.4em' : 'normal', textAlign: 'center', fontSize: 22 }} /> {error && (
{error}
)} )}
v0.4.2-admin · 2FA mandatory · rate-limited
); }; const inputStyle = (hasErr) => ({ width: '100%', background: '#141414', border: `1px solid ${hasErr ? '#DC2626' : '#262626'}`, color: '#EDEDED', fontFamily: "'JetBrains Mono', monospace", fontSize: 14, padding: '13px 16px', borderRadius: 6, outline: 'none', boxSizing: 'border-box', transition: 'border-color 150ms', }); const amberBtn = (disabled) => ({ width: '100%', marginTop: 12, background: '#F59E0B', color: '#0A0A0A', border: 'none', padding: '13px 18px', borderRadius: 6, fontFamily: "'Geist', system-ui, sans-serif", fontSize: 14, fontWeight: 600, letterSpacing: '0.02em', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, transition: 'background 150ms ease', }); window.AdminGate = AdminGate;