import React, { useState, useRef } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Turnstile } from '@marsidev/react-turnstile'; import { startAuthentication } from '@simplewebauthn/browser'; import { Fingerprint, AlertCircle } from 'lucide-react'; import api from '../../api'; import { useAuth } from '../../App'; const LoginPage = () => { const { t } = useTranslation(); const [form, setForm] = useState({ email: '', password: '', website: '' }); const [error, setError] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [emailTouched, setEmailTouched] = useState(false); const [emailValid, setEmailValid] = useState(null); const [captchaToken, setCaptchaToken] = useState(null); const turnstileRef = useRef(null); // 2FA login state const [requires2fa, setRequires2fa] = useState(false); const [tempToken, setTempToken] = useState(null); const [totpCode, setTotpCode] = useState(''); const [useRecoveryCode, setUseRecoveryCode] = useState(false); const [recoveryCode, setRecoveryCode] = useState(''); const [codeError, setCodeError] = useState(''); const navigate = useNavigate(); const { login } = useAuth(); const validateEmail = (email: string) => Boolean(String(email).toLowerCase().match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)); const handleEmailChange = (e: React.ChangeEvent) => { const val = e.target.value; setForm({ ...form, email: val }); if (emailTouched) { setEmailValid(val === '' ? null : validateEmail(val)); } }; const handleEmailBlur = () => { setEmailTouched(true); setEmailValid(form.email === '' ? null : validateEmail(form.email)); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (form.website) return; if (requires2fa) { setIsSubmitting(true); setError(''); setCodeError(''); try { if (useRecoveryCode) { const res = await api.post('/auth/login/recovery-code', { tempToken, code: recoveryCode.trim(), }); login(res.data.token); navigate('/dashboard'); } else { const res = await api.post('/auth/login/2fa', { temp_token: tempToken, code: totpCode, }); login(res.data.token); navigate('/dashboard'); } } catch (err: any) { setCodeError(err.response?.data?.error || (useRecoveryCode ? 'Invalid recovery code' : 'Invalid 2FA code')); } finally { setIsSubmitting(false); } return; } setEmailTouched(true); const isEmailValid = validateEmail(form.email); setEmailValid(isEmailValid); if (!isEmailValid) { setError('Please enter a valid email address.'); return; } setIsSubmitting(true); setError(''); let currentToken = captchaToken; if (!currentToken && turnstileRef.current) { try { currentToken = await turnstileRef.current.getResponsePromise(2000); } catch (err) { try { turnstileRef.current.reset(); currentToken = await turnstileRef.current.getResponsePromise(4000); } catch (resetErr) { console.error('Turnstile verification failed:', resetErr); } } } if (!currentToken) { setError('Please complete the security check.'); setIsSubmitting(false); return; } try { const res = await api.post('/auth/login', { email: form.email, password: form.password, captchaToken: currentToken, }); if (res.data.requires_2fa) { setRequires2fa(true); setTempToken(res.data.temp_token); } else { login(res.data.token); navigate('/dashboard'); } } catch (err: any) { setError(err.response?.data?.error || 'Invalid credentials.'); } finally { setIsSubmitting(false); } }; const handlePasskeyLogin = async () => { setError(''); setIsSubmitting(true); try { const startRes = await api.post('/auth/passkey/login/start', { email: form.email.trim() || undefined, }); const options = startRes.data?.options?.publicKey || startRes.data?.options || startRes.data?.publicKey || startRes.data; const credential = await startAuthentication({ optionsJSON: options, }); const finishRes = await api.post('/auth/passkey/login/finish', { challengeId: startRes.data.challengeId, credential, }); if (finishRes.data.token) { login(finishRes.data.token); navigate('/dashboard'); } } catch (err: any) { if (err.name !== 'NotAllowedError') { setError(err.response?.data?.error || err.message || 'Passkey authentication failed.'); } } finally { setIsSubmitting(false); } }; const emailIsInvalid = emailTouched && emailValid === false; const emailIsValid = emailTouched && emailValid === true; return ( <>

Welcome back

Sign in to your PumpkinMC account.

{!requires2fa && error && (
{error}
)} {/* Honeypot */} setForm({ ...form, website: e.target.value })} /> {requires2fa ? (
{!useRecoveryCode ? ( <>

Enter the 6-digit code from your authenticator app.

{ setCodeError(''); setTotpCode(e.target.value.replace(/\D/g, '').slice(0, 6)); }} autoComplete="one-time-code" inputMode="numeric" pattern="[0-9]*" required disabled={isSubmitting} autoFocus style={{ letterSpacing: '0.2em', textAlign: 'center', fontSize: '1.2rem', fontFamily: 'var(--font-mono, monospace)', borderColor: codeError ? '#ef4444' : undefined, boxShadow: codeError ? '0 0 0 3px rgba(239, 68, 68, 0.2)' : undefined, }} />
) : ( <>

Enter one of your 12-character backup recovery codes.

{ setCodeError(''); setRecoveryCode(e.target.value); }} autoComplete="off" required disabled={isSubmitting} autoFocus style={{ letterSpacing: '0.08em', textAlign: 'center', fontSize: '1.05rem', fontFamily: 'var(--font-mono, monospace)', borderColor: codeError ? '#ef4444' : undefined, boxShadow: codeError ? '0 0 0 3px rgba(239, 68, 68, 0.2)' : undefined, }} />
)}
) : ( <>
or email & password
{emailIsInvalid && ( )} {emailIsValid && ( )}
{emailIsInvalid && (

Please enter a valid email address

)}
Forgot password?
setForm({ ...form, password: e.target.value })} required disabled={isSubmitting} autoComplete="current-password" />
)} {!requires2fa && (
setCaptchaToken(token)} onExpire={() => setCaptchaToken(null)} onError={() => setCaptchaToken(null)} options={{ theme: 'dark', appearance: 'interaction-only' } as any} />
)}
{t('auth.no_account')} {t('auth.create_account')}
); }; export default LoginPage;