import { useState, useEffect, useMemo, useCallback } from 'react'; import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { useAuthStore } from '../store/auth'; import { authApi } from '../api/auth'; import { brandingApi, getCachedBranding, setCachedBranding, preloadLogo, isLogoPreloaded, type BrandingInfo, type EmailAuthEnabled, } from '../api/branding'; import { getAndClearReturnUrl } from '../utils/token'; import LanguageSwitcher from '../components/LanguageSwitcher'; import TelegramLoginButton from '../components/TelegramLoginButton'; export default function Login() { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); const [searchParams] = useSearchParams(); const { isAuthenticated, loginWithTelegram, loginWithEmail, registerWithEmail } = useAuthStore(); // Extract referral code from URL const referralCode = searchParams.get('ref') || ''; const [authMode, setAuthMode] = useState<'login' | 'register'>(() => referralCode ? 'register' : 'login', ); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [firstName, setFirstName] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isTelegramWebApp, setIsTelegramWebApp] = useState(false); const [logoLoaded, setLogoLoaded] = useState(() => isLogoPreloaded()); const [registeredEmail, setRegisteredEmail] = useState(null); const [showForgotPassword, setShowForgotPassword] = useState(false); const [forgotPasswordEmail, setForgotPasswordEmail] = useState(''); const [forgotPasswordSent, setForgotPasswordSent] = useState(false); const [forgotPasswordLoading, setForgotPasswordLoading] = useState(false); const [forgotPasswordError, setForgotPasswordError] = useState(''); // Получаем URL для возврата после авторизации const getReturnUrl = useCallback(() => { // Сначала проверяем state от React Router const stateFrom = (location.state as { from?: string })?.from; if (stateFrom && stateFrom !== '/login') { return stateFrom; } // Затем проверяем сохранённый URL в sessionStorage (от safeRedirectToLogin) const savedUrl = getAndClearReturnUrl(); if (savedUrl && savedUrl !== '/login') { return savedUrl; } // По умолчанию на главную return '/'; }, [location.state]); // Fetch branding with unified cache const cachedBranding = useMemo(() => getCachedBranding(), []); const { data: branding } = useQuery({ queryKey: ['branding'], queryFn: async () => { const data = await brandingApi.getBranding(); setCachedBranding(data); preloadLogo(data); return data; }, staleTime: 60000, initialData: cachedBranding ?? undefined, }); // Check if email auth is enabled const { data: emailAuthConfig } = useQuery({ queryKey: ['email-auth-enabled'], queryFn: brandingApi.getEmailAuthEnabled, staleTime: 60000, }); const isEmailAuthEnabled = emailAuthConfig?.enabled ?? true; const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME || ''; // If email auth is disabled but user came with ref param, redirect to bot useEffect(() => { if (referralCode && emailAuthConfig?.enabled === false && botUsername) { window.location.href = `https://t.me/${botUsername}?start=${referralCode}`; } }, [referralCode, emailAuthConfig, botUsername]); const appName = branding ? branding.name : import.meta.env.VITE_APP_NAME || 'VPN'; const appLogo = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V'; const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null; // Set document title useEffect(() => { document.title = appName || 'VPN'; }, [appName]); useEffect(() => { if (isAuthenticated) { navigate(getReturnUrl(), { replace: true }); } }, [isAuthenticated, navigate, getReturnUrl]); // Try Telegram WebApp authentication on mount useEffect(() => { const tryTelegramAuth = async () => { const tg = window.Telegram?.WebApp; if (tg?.initData) { setIsTelegramWebApp(true); tg.ready(); tg.expand(); setIsLoading(true); try { await loginWithTelegram(tg.initData); navigate(getReturnUrl(), { replace: true }); } catch (err) { // Log only status code to avoid leaking sensitive data const status = (err as { response?: { status?: number } })?.response?.status; console.warn('Telegram auth failed with status:', status); setError(t('auth.telegramRequired')); } finally { setIsLoading(false); } } }; tryTelegramAuth(); }, [loginWithTelegram, navigate, t, getReturnUrl]); const handleEmailSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); // Валидация email const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!email.trim() || !emailRegex.test(email.trim())) { setError(t('auth.invalidEmail', 'Please enter a valid email address')); return; } if (authMode === 'register') { // Валидация для регистрации if (password !== confirmPassword) { setError(t('auth.passwordMismatch', 'Passwords do not match')); return; } if (password.length < 8) { setError(t('auth.passwordTooShort', 'Password must be at least 8 characters')); return; } } setIsLoading(true); try { if (authMode === 'login') { await loginWithEmail(email, password); navigate(getReturnUrl(), { replace: true }); } else { const result = await registerWithEmail( email, password, firstName || undefined, referralCode || undefined, ); // Show "check your email" screen setRegisteredEmail(result.email); } } catch (err: unknown) { const error = err as { response?: { status?: number; data?: { detail?: string } } }; const status = error.response?.status; const detail = error.response?.data?.detail; if (status === 400 && detail?.includes('already registered')) { setError(t('auth.emailAlreadyRegistered', 'This email is already registered')); } else if (status === 401 || status === 403) { if (detail?.includes('verify your email')) { setError(t('auth.emailNotVerified', 'Please verify your email first')); } else { setError(t('auth.invalidCredentials', 'Invalid email or password')); } } else if (status === 429) { setError(t('auth.tooManyAttempts', 'Too many attempts. Please try again later')); } else { setError(detail || t('common.error')); } } finally { setIsLoading(false); } }; const handleForgotPassword = async (e: React.FormEvent) => { e.preventDefault(); setForgotPasswordError(''); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!forgotPasswordEmail.trim() || !emailRegex.test(forgotPasswordEmail.trim())) { setForgotPasswordError(t('auth.invalidEmail', 'Please enter a valid email address')); return; } setForgotPasswordLoading(true); try { await authApi.forgotPassword(forgotPasswordEmail.trim()); setForgotPasswordSent(true); } catch (err: unknown) { const error = err as { response?: { status?: number; data?: { detail?: string } } }; const detail = error.response?.data?.detail; setForgotPasswordError(detail || t('common.error')); } finally { setForgotPasswordLoading(false); } }; const closeForgotPasswordModal = () => { setShowForgotPassword(false); setForgotPasswordEmail(''); setForgotPasswordSent(false); setForgotPasswordError(''); }; return (
{/* Background gradient */}
{/* Language switcher */}
{/* Logo */}
{/* Always show letter as fallback */} {appLogo} {/* Logo image with smooth fade-in */} {branding?.has_custom_logo && logoUrl && ( {appName setLogoLoaded(true)} /> )}
{appName &&

{appName}

}

{t('auth.loginSubtitle')}

{/* Referral Banner - only show when email auth is enabled */} {referralCode && isEmailAuthEnabled && (
{t('auth.referralInvite')}
)}
{/* Check Email Screen */} {registeredEmail ? (

{t('auth.checkEmail', 'Check your email')}

{t('auth.verificationSent', 'We sent a verification link to:')}

{registeredEmail}

{t( 'auth.clickLinkToVerify', 'Click the link in the email to verify your account and log in.', )}

) : ( /* Card */
{error && (
{error}
)} {/* Telegram auth section */}

{t('auth.registerHint')}

{isLoading && isTelegramWebApp ? (

{t('auth.authenticating')}

) : ( )}
{/* Email auth section - only when enabled */} {isEmailAuthEnabled && ( <> {/* Divider */}
{t('auth.or', 'or')}
{/* Login / Register toggle */}
{/* First name field - only for registration */} {authMode === 'register' && (
setFirstName(e.target.value)} />
)}
setEmail(e.target.value)} />
setPassword(e.target.value)} />
{/* Confirm password - only for registration */} {authMode === 'register' && (
setConfirmPassword(e.target.value)} />
)}
{/* Verification notice for registration */} {authMode === 'register' && (

{t( 'auth.verificationEmailNotice', 'After registration, a verification email will be sent to your address', )}

)} {/* Forgot password link - only for login */} {authMode === 'login' && (
)}
)}
)}
{/* Forgot Password Modal */} {showForgotPassword && (
{forgotPasswordSent ? (

{t('auth.checkEmail', 'Check your email')}

{t( 'auth.passwordResetSent', 'If an account exists with this email, we sent password reset instructions.', )}

) : ( <>

{t('auth.forgotPassword', 'Forgot password?')}

{t( 'auth.forgotPasswordHint', 'Enter your email and we will send you instructions to reset your password.', )}

setForgotPasswordEmail(e.target.value)} placeholder="you@example.com" className="input" autoFocus />
{forgotPasswordError && (
{forgotPasswordError}
)}
)}
)}
); }