import { useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; import { useAuthStore } from '../store/auth'; import { brandingApi } from '../api/branding'; import { isInTelegramWebApp, getTelegramInitData } from '../hooks/useTelegramSDK'; // Validate redirect URL to prevent open redirect attacks const getSafeRedirectUrl = (url: string | null): string => { if (!url) return '/'; // Only allow relative paths starting with / // Block protocol-relative URLs (//evil.com) and absolute URLs if (!url.startsWith('/') || url.startsWith('//')) { return '/'; } // Additional check for encoded characters that could bypass validation try { const decoded = decodeURIComponent(url); if (!decoded.startsWith('/') || decoded.startsWith('//') || decoded.includes('://')) { return '/'; } } catch { return '/'; } return url; }; const MAX_RETRY_ATTEMPTS = 3; const RETRY_COUNT_KEY = 'telegram_redirect_retry_count'; export default function TelegramRedirect() { const { t } = useTranslation(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { loginWithTelegram, isAuthenticated, isLoading: authLoading } = useAuthStore(); const [status, setStatus] = useState<'loading' | 'success' | 'error' | 'not-telegram'>('loading'); const [errorMessage, setErrorMessage] = useState(''); const [retryCount, setRetryCount] = useState(() => { const stored = sessionStorage.getItem(RETRY_COUNT_KEY); return stored ? parseInt(stored, 10) : 0; }); // Get branding for nice display const { data: branding } = useQuery({ queryKey: ['branding'], queryFn: brandingApi.getBranding, staleTime: 60000, }); const appName = branding ? branding.name : import.meta.env.VITE_APP_NAME || 'VPN'; const logoLetter = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V'; const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null; // Get redirect target from URL params (validated) const redirectTo = getSafeRedirectUrl(searchParams.get('redirect')); useEffect(() => { // If already authenticated, redirect immediately if (isAuthenticated && !authLoading) { setStatus('success'); setTimeout(() => navigate(redirectTo), 500); return; } const initTelegram = async () => { // Check if running in Telegram WebApp const initData = getTelegramInitData(); if (!isInTelegramWebApp() || !initData) { // Not in Telegram, show message and redirect to login setStatus('not-telegram'); setTimeout(() => navigate('/login'), 2000); return; } // Note: ready(), expand(), and theme CSS vars are already handled by SDK init in main.tsx try { await loginWithTelegram(initData); setStatus('success'); // Small delay for nice UX setTimeout(() => { navigate(redirectTo); }, 800); } catch (err: unknown) { console.error('Telegram auth failed:', err); const error = err as { response?: { data?: { detail?: string } } }; setErrorMessage(error.response?.data?.detail || t('auth.telegramRequired')); setStatus('error'); } }; // Small delay to show loading screen setTimeout(initTelegram, 300); }, [loginWithTelegram, navigate, isAuthenticated, authLoading, redirectTo, t]); // Handle retry with limit to prevent infinite loops const handleRetry = () => { if (retryCount >= MAX_RETRY_ATTEMPTS) { setErrorMessage(t('telegramRedirect.maxRetries')); sessionStorage.removeItem(RETRY_COUNT_KEY); return; } const newCount = retryCount + 1; setRetryCount(newCount); sessionStorage.setItem(RETRY_COUNT_KEY, String(newCount)); setStatus('loading'); setErrorMessage(''); window.location.reload(); }; // Clear retry count on successful auth useEffect(() => { if (status === 'success') { sessionStorage.removeItem(RETRY_COUNT_KEY); } }, [status]); return (
{/* Background */}
{/* Logo */}
{branding?.has_custom_logo && logoUrl ? ( {appName} ) : ( {logoLetter} )}

{appName}

{/* Loading State */} {status === 'loading' && (

{t('auth.authenticating')}

{t('common.loading')}

)} {/* Success State */} {status === 'success' && (

{t('auth.loginSuccess')}

{t('telegramRedirect.redirecting')}

)} {/* Error State */} {status === 'error' && (

{t('auth.loginFailed')}

{errorMessage}

)} {/* Not in Telegram State */} {status === 'not-telegram' && (

{t('telegramRedirect.openInTelegram')}

{t('telegramRedirect.openInTelegramDesc')}

{t('telegramRedirect.redirectToLogin')}

)} {/* Telegram branding */}
Telegram Mini App
); }