// CalGrillz — Login (email + password OR email + OTP code). // // Customers without a password fall back to the code path automatically. // Mode toggle lives at the top of the right-hand form column. function CGWLoginPage() { // mode: 'password' | 'code'. step: 'enter' | 'otp' | 'twofa' const [mode, setMode] = useState('password'); const [step, setStep] = useState('enter'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [otp, setOtp] = useState(['','','','','','']); const [error, setError] = useState(''); const [info, setInfo] = useState(''); const [busy, setBusy] = useState(false); const [resendIn, setResendIn] = useState(0); const refs = useRef([]); // 2FA challenge state (set when the server asks for a second factor). const [twoFa, setTwoFa] = useState(null); // { method, label, emailHint } const [twoFaCode, setTwoFaCode] = useState(''); const URLS = window.CG_URLS || {}; const CSRF = URLS.csrf || ''; // Optional return-to path after login (e.g. arriving from the public live // draw as ?next=/draw/38). Same-origin ROOT-relative paths only — never an // absolute or //host URL — so it can't be used as an open redirect. const safeNext = () => { try { const n = new URLSearchParams(window.location.search).get('next'); // Must be a plain root-relative path: starts '/', not '//' or '/\', and no // backslash / control chars (browsers normalise those into '//host'). Then // confirm it still resolves to THIS origin. Blocks open-redirect payloads // like /%5Cevil.com or /%09/evil.com. if (!n || n.charAt(0) !== '/' || n.charAt(1) === '/' || /[\\\t\n\r\x00]/.test(n)) return null; const u = new URL(n, window.location.origin); return u.origin === window.location.origin ? (u.pathname + u.search + u.hash) : null; } catch (e) {} return null; }; useEffect(() => { if (resendIn > 0) { const t = setInterval(() => setResendIn(s => Math.max(0, s - 1)), 1000); return () => clearInterval(t); } }, [resendIn]); const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()); async function signInWithPassword() { setError(''); if (! emailValid) { setError('Enter your email.'); return; } if (! password) { setError('Enter your password.'); return; } setBusy(true); try { const res = await fetch(URLS.loginPassword || '/login/password', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF }, body: JSON.stringify({ email: email.trim(), password }), }); const data = await res.json().catch(() => ({})); if (! res.ok || ! data.ok) { setError(data.error || 'Email and password do not match.'); return; } if (data.two_factor_required) { enterTwoFa(data); return; } window.location.href = safeNext() || data.redirect_to || (URLS.rewards || '/'); } catch (e) { setError('Network hiccup. Try once more.'); } finally { setBusy(false); } } // A second factor is required — switch to the 2FA challenge step. function enterTwoFa(data) { setTwoFa({ method: data.method, label: data.method_label || 'code', emailHint: data.email_hint || null }); setTwoFaCode(''); setError(''); setInfo(''); setStep('twofa'); if (data.method === 'email') setResendIn(30); } async function submitTwoFa() { const code = (twoFaCode || '').trim(); if (! code) { setError('Enter your code.'); return; } setError(''); setBusy(true); try { const res = await fetch(URLS.login2fa || '/login/2fa', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF }, body: JSON.stringify({ code }), }); const data = await res.json().catch(() => ({})); if (! res.ok || ! data.ok) { setError(data.error || 'That code is not correct.'); if (data.restart) { setStep('enter'); setTwoFa(null); } return; } window.location.href = safeNext() || data.redirect_to || (URLS.rewards || '/'); } catch (e) { setError('Network hiccup. Try once more.'); } finally { setBusy(false); } } async function resendTwoFa() { setError(''); setInfo(''); try { const res = await fetch(URLS.login2faResend || '/login/2fa/resend', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF }, body: JSON.stringify({}), }); const data = await res.json().catch(() => ({})); if (data.restart) { setStep('enter'); setTwoFa(null); setError(data.error || 'Please sign in again.'); return; } setResendIn(30); setInfo('New code sent to your email.'); } catch (e) { setError('Could not resend. Try again.'); } } async function sendCode() { setError(''); setInfo(''); if (! emailValid) { setError('Enter a valid email.'); return; } setBusy(true); try { const res = await fetch(URLS.loginRequest || '/login/request', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF }, body: JSON.stringify({ email: email.trim() }), }); const data = await res.json().catch(() => ({})); // New / unregistered email → registration is required (name + phone). // Hand off to the signup page, carrying the typed email for prefill. if (data.needs_registration && data.redirect_to) { try { sessionStorage.setItem('cg_signup_email', email.trim()); } catch (e) {} window.location.href = data.redirect_to; return; } if (! res.ok || ! data.ok) { setError(data.error || 'Could not send the code.'); return; } setMode('code'); setStep('otp'); setResendIn(30); if (data.dev_code) setInfo('Dev mode code: ' + data.dev_code); else setInfo('Code sent. Check your inbox (and spam folder).'); setTimeout(() => refs.current[0]?.focus(), 50); } catch (e) { setError('Network hiccup. Try once more.'); } finally { setBusy(false); } } const setOtpChar = (i, ch) => { const v = ch.replace(/\D/g, '').slice(-1); const next = [...otp]; next[i] = v; setOtp(next); if (v && i < 5) refs.current[i + 1]?.focus(); }; const onOtpKey = (i, e) => { if (e.key === 'Backspace' && !otp[i] && i > 0) refs.current[i - 1]?.focus(); }; const onOtpPaste = (e) => { const v = (e.clipboardData.getData('text') || '').replace(/\D/g, '').slice(0, 6).split(''); if (v.length) { const next = ['','','','','','']; v.forEach((c, i) => next[i] = c); setOtp(next); refs.current[Math.min(v.length, 5)]?.focus(); e.preventDefault(); } }; async function verify() { const code = otp.join(''); if (code.length !== 6) { setError('Enter all 6 digits.'); return; } setError(''); setBusy(true); try { const res = await fetch(URLS.loginVerify || '/login/verify', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF }, body: JSON.stringify({ email: email.trim(), code }), }); const data = await res.json().catch(() => ({})); if (data.needs_registration && data.redirect_to) { try { sessionStorage.setItem('cg_signup_email', email.trim()); } catch (e) {} window.location.href = data.redirect_to; return; } if (! res.ok || ! data.ok) { setError(data.error || 'Wrong code or expired.'); return; } if (data.two_factor_required) { enterTwoFa(data); return; } window.location.href = safeNext() || data.redirect_to || (URLS.rewards || '/'); } catch (e) { setError('Network hiccup. Try once more.'); } finally { setBusy(false); } } // ─── Mode toggle (segmented control) ──────────────────────────────────── const Toggle = (
Sign in with your password, or skip it and we'll email you a 6-digit code.
Use your password or get a one-time code.
{Toggle} {/* Real > )} {step === 'otp' && ( <>We emailed {email} a 6-digit code.
{info && ({twoFa.method === 'pin' && <>Enter your account PIN to finish signing in.>} {twoFa.method === 'totp' && <>Open your authenticator app and enter the 6-digit code.>} {twoFa.method === 'email' && <>We sent a 6-digit code to {twoFa.emailHint || 'your email'}.>}
{info && (