// Auth screens: Email login, Telegram OTP login, Email register const { useState, useEffect } = React; const AuthPage = ({ mode: initialMode, onLogin, onNavigate, botConfig, resetToken }) => { const [mode, setMode] = useState(initialMode || 'login'); // Keep internal mode in sync when parent navigates between auth sub-modes // (login → register, register → login-tg, etc.). Without this, useState's // lazy init means clicks on header "Войти" / "Регистрация" do nothing when // we're already on the auth route. useEffect(() => { setMode(initialMode || 'login'); }, [initialMode]); // Reset transient sub-flow state whenever mode changes (incl. from header nav). useEffect(() => { setError(''); setSuccess(''); setRegStep('form'); setRegCode(''); setOtpSent(false); setOtpCode(''); setNeedsConnect(false); setForgotEmail(''); setForgotSent(false); setResetNew(''); setResetConfirm(''); setResetDone(false); }, [mode]); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [name, setName] = useState(''); const [tgId, setTgId] = useState(() => sessionStorage.getItem('auth_tg_phone') || ''); const [otpSent, setOtpSent] = useState(false); const [otpCode, setOtpCode] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); // Registration: 'form' (name/email/password) → 'code' (email verification) const [regStep, setRegStep] = useState('form'); const [regCode, setRegCode] = useState(''); const [needsConnect, setNeedsConnect] = useState(false); const [forgotEmail, setForgotEmail] = useState(''); const [forgotSent, setForgotSent] = useState(false); const [resetNew, setResetNew] = useState(''); const [resetConfirm, setResetConfirm] = useState(''); const [resetDone, setResetDone] = useState(false); useEffect(() => { if (tgId) sessionStorage.setItem('auth_tg_phone', tgId); else sessionStorage.removeItem('auth_tg_phone'); }, [tgId]); // Validate phone: strip non-digits/plus; accept +XXXXXXXXXX..XXXXX or 10–11 plain digits. const isValidPhone = (raw) => { const cleaned = (raw || '').replace(/[^\d+]/g, ''); if (/^\+\d{10,15}$/.test(cleaned)) return true; if (/^\d{10,11}$/.test(cleaned)) return true; return false; }; const handleEmailLogin = async () => { if (!email || !password) return setError('Заполните все поля'); setLoading(true); setError(''); try { const data = await api.post('/api/auth/email/login', { email, password }); onLogin(data.access_token); } catch (e) { setError(e.status === 401 ? 'Неверный email или пароль' : (e.message || 'Ошибка входа')); } finally { setLoading(false); } }; const handleRegisterRequest = async () => { if (!email || !password) return setError('Заполните все поля'); if (password.length < 8) return setError('Пароль — минимум 8 символов'); setLoading(true); setError(''); setSuccess(''); try { await api.post('/api/auth/email/register-request', { email, password, first_name: name || null }); setRegStep('code'); setRegCode(''); setSuccess('Код отправлен на ' + email); } catch (e) { if (e.status === 409) setError('Email уже зарегистрирован'); else if (e.status === 429) { const sec = e.retry_after; setError(sec ? `Код уже отправлен. Попробуйте через ${sec} секунд.` : 'Код уже отправлен. Попробуйте позже.'); } else if (e.status === 502) { setError('Не удалось отправить код на email. Попробуйте позже или используйте другой email.'); } else if (e.status === 400) { setError(e.message || 'Неверные данные'); } else { setError(e.message || 'Ошибка регистрации'); } } finally { setLoading(false); } }; const handleRegisterVerify = async () => { if (!regCode || regCode.length < 6) return setError('Введите 6-значный код'); setLoading(true); setError(''); try { const data = await api.post('/api/auth/email/register-verify', { email, code: regCode }); onLogin(data.access_token); } catch (e) { if (e.status === 401) setError('Неверный код'); else if (e.status === 410) setError('Код истёк. Запросите новый.'); else setError(e.message || 'Ошибка проверки кода'); } finally { setLoading(false); } }; const handleResendRegisterCode = async () => { setLoading(true); setError(''); setSuccess(''); try { await api.post('/api/auth/email/register-request', { email, password, first_name: name || null }); setSuccess('Код отправлен на ' + email); } catch (e) { if (e.status === 429) { const sec = e.retry_after; setError(sec ? `Код уже отправлен. Попробуйте через ${sec} секунд.` : 'Код уже отправлен. Попробуйте позже.'); } else if (e.status === 502) { setError('Не удалось отправить код на email. Попробуйте позже.'); } else { setError(e.message || 'Ошибка отправки кода'); } } finally { setLoading(false); } }; const handleRequestOtp = async () => { if (!tgId) return setError('Введите номер телефона'); if (!isValidPhone(tgId)) return setError('Введите номер телефона, например +79001234567'); setLoading(true); setError(''); setSuccess(''); setNeedsConnect(false); try { await api.post('/api/auth/telegram/request-code', { identifier: tgId }); setOtpSent(true); setSuccess('Код отправлен в Telegram'); } catch (e) { // 429 = cooldown; 400 = unknown phone or bot can't reach user; 502 = bot network error. if (e.status === 429) { const sec = e.retry_after; setError(sec ? `Слишком частые запросы. Попробуйте через ${sec} секунд.` : 'Слишком частые запросы. Попробуйте через минуту.'); } else if (e.status === 400) { setNeedsConnect(true); } else if (e.status === 502) { setError('Не удалось отправить код через Telegram. Попробуйте позже.'); } else { setError(e.message || 'Ошибка отправки кода. Попробуйте позже.'); } } finally { setLoading(false); } }; const handleVerifyOtp = async () => { if (!otpCode || otpCode.length < 6) return setError('Введите 6-значный код'); setLoading(true); setError(''); try { const data = await api.post('/api/auth/telegram/verify-code', { identifier: tgId, code: otpCode }); onLogin(data.access_token); } catch (e) { if (e.status === 410) setError('Код истёк — запросите новый'); else if (e.status === 401) setError('Неверный код'); else setError(e.message || 'Ошибка проверки кода'); } finally { setLoading(false); } }; const handleForgotPassword = async () => { if (!forgotEmail) return setError('Введите email'); setLoading(true); setError(''); try { await api.post('/api/auth/email/forgot-password', { email: forgotEmail }); } catch (_) { // Always show success to prevent email enumeration } finally { setLoading(false); } setForgotSent(true); }; const handleResetPassword = async () => { if (!resetNew || resetNew.length < 8) return setError('Пароль — минимум 8 символов'); if (resetNew !== resetConfirm) return setError('Пароли не совпадают'); setLoading(true); setError(''); try { await api.post('/api/auth/email/reset-password', { token: resetToken, new_password: resetNew, new_password_confirm: resetConfirm, }); setResetDone(true); window.history.replaceState({}, '', '/'); } catch (e) { if (e.status === 410) setError('Ссылка истекла — запросите новую'); else setError(e.message || 'Ошибка сброса пароля'); } finally { setLoading(false); } }; const logoMark = (
PB
); if (mode === 'reset') return (

Новый пароль

{resetDone ? ( <>
Пароль изменён — войдите с новым паролем
) : ( <> {error &&
{error}
}
setResetNew(e.target.value)} />
setResetConfirm(e.target.value)} />
)}
); if (mode === 'login-tg') return (
{logoMark}

Вход через Telegram

Введите номер телефона — мы отправим код

{error &&
{error}
} {needsConnect && (() => { const botUrl = (botConfig && botConfig.bot_connect_url) || 'https://t.me/AVITOPF_bot?start=connect'; const botName = (botConfig && botConfig.bot_username) || 'AVITOPF_bot'; return (
Номер не привязан к боту
  1. Откройте @{botName} в Telegram
  2. Нажмите «Поделиться контактом»
  3. Вернитесь сюда и нажмите «Получить код»
); })()} {success &&
{success}
} {!otpSent ? ( <>
setTgId(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleRequestOtp()} />
) : ( <>
setOtpCode(e.target.value.replace(/\D/g, ''))} onKeyDown={e => e.key === 'Enter' && handleVerifyOtp()} style={{ textAlign: 'center', fontSize: '1.5rem', letterSpacing: '0.2em', fontWeight: 700 }} autoFocus />
Код действителен 10 минут
)}
setMode('login')} style={{ cursor: 'pointer', color: 'var(--primary)', fontWeight: 600 }}> Войти через Email {' · '} onNavigate('landing')} style={{ cursor: 'pointer' }}>На главную
); if (mode === 'register') return (
{logoMark}

Создать аккаунт

{regStep === 'form' ? 'Email + пароль. Позже можно привязать Telegram.' : 'Подтвердите email — мы отправили вам 6-значный код.'}

{error &&
{error}
} {success && regStep === 'code' &&
{success}
} {regStep === 'form' ? ( <>
setName(e.target.value)} />
setEmail(e.target.value)} />
setPassword(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleRegisterRequest()} />
Минимум 8 символов
или
) : ( <>
setRegCode(e.target.value.replace(/\D/g, ''))} onKeyDown={e => e.key === 'Enter' && handleRegisterVerify()} style={{ textAlign: 'center', fontSize: '1.5rem', letterSpacing: '0.2em', fontWeight: 700 }} autoFocus />
Код отправлен на {email}. Действителен 10 минут.
Не пришёл код?{' '} { if (!loading) handleResendRegisterCode(); }} style={{ color: 'var(--primary)', fontWeight: 600, cursor: loading ? 'default' : 'pointer' }} > Отправить заново
)}
Уже есть аккаунт?{' '} { setRegStep('form'); setRegCode(''); setError(''); setSuccess(''); setMode('login'); }} style={{ color: 'var(--primary)', fontWeight: 600, cursor: 'pointer' }} >Войти
); if (mode === 'forgot') return (

Восстановление пароля

{forgotSent ? ( <>
Если аккаунт существует, письмо с инструкцией отправлено
) : ( <> {error &&
{error}
}
setForgotEmail(e.target.value)} />
)}
); // Default: Email login return (
{logoMark}

Добро пожаловать

Войдите в личный кабинет

{error &&
{error}
}
или через email
setEmail(e.target.value)} />
setPassword(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleEmailLogin()} />
Нет аккаунта?{' '} { setRegStep('form'); setRegCode(''); setError(''); setSuccess(''); setMode('register'); }} style={{ color: 'var(--primary)', fontWeight: 600, cursor: 'pointer' }} >Зарегистрироваться {' · '} onNavigate('landing')} style={{ cursor: 'pointer' }}>На главную
); }; Object.assign(window, { AuthPage });