diff --git a/src/api/client.ts b/src/api/client.ts index cc2b423..9dcdc85 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -64,26 +64,47 @@ export const apiClient = axios.create({ }, }); +// Auth endpoints that don't need Bearer token or token refresh +const AUTH_ENDPOINTS = [ + '/cabinet/auth/telegram', + '/cabinet/auth/telegram/widget', + '/cabinet/auth/email/login', + '/cabinet/auth/email/register', + '/cabinet/auth/email/verify', + '/cabinet/auth/refresh', + '/cabinet/auth/password/forgot', + '/cabinet/auth/password/reset', +]; + +function isAuthEndpoint(url: string | undefined): boolean { + if (!url) return false; + return AUTH_ENDPOINTS.some((endpoint) => url.includes(endpoint)); +} + // Request interceptor - add auth token with expiration check apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => { - let token = tokenStorage.getAccessToken(); + // Skip token refresh and Bearer header for auth endpoints + // These endpoints authenticate via init_data/credentials, not Bearer tokens + if (!isAuthEndpoint(config.url)) { + let token = tokenStorage.getAccessToken(); - // Проверяем срок действия токена перед запросом - if (token && isTokenExpired(token)) { - // Используем централизованный менеджер для refresh - const newToken = await tokenRefreshManager.refreshAccessToken(); - if (newToken) { - token = newToken; - } else { - // Refresh не удался - редирект на логин - tokenStorage.clearTokens(); - safeRedirectToLogin(); - return config; + // Проверяем срок действия токена перед запросом + if (token && isTokenExpired(token)) { + // Используем централизованный менеджер для refresh + const newToken = await tokenRefreshManager.refreshAccessToken(); + if (newToken) { + token = newToken; + } else { + // Refresh не удался - редирект на логин + tokenStorage.clearTokens(); + safeRedirectToLogin(); + return config; + } } - } - if (token && config.headers) { - config.headers.Authorization = `Bearer ${token}`; + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } } const telegramInitData = getTelegramInitData(); diff --git a/src/main.tsx b/src/main.tsx index 05c79a7..6d79dbd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { init, restoreInitData, + retrieveRawInitData, mountMiniApp, miniAppReady, mountThemeParams, @@ -19,6 +20,7 @@ import { requestFullscreen, isFullscreen, } from '@telegram-apps/sdk-react'; +import { clearStaleSessionIfNeeded } from './utils/token'; import { AppWithNavigator } from './AppWithNavigator'; import { ErrorBoundary } from './components/ErrorBoundary'; import { initLogoPreload } from './api/branding'; @@ -37,6 +39,9 @@ if (!alreadyInitialized) { init(); restoreInitData(); + // Сбрасываем старые токены если init data изменился (новая сессия Telegram) + clearStaleSessionIfNeeded(retrieveRawInitData() || null); + // Mount components — each in its own try/catch so one failure doesn't block others try { mountMiniApp(); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 3a3669a..76b9f3b 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -24,7 +24,13 @@ export default function Login() { const navigate = useNavigate(); const location = useLocation(); const [searchParams] = useSearchParams(); - const { isAuthenticated, loginWithTelegram, loginWithEmail, registerWithEmail } = useAuthStore(); + const { + isAuthenticated, + isLoading: isAuthInitializing, + loginWithTelegram, + loginWithEmail, + registerWithEmail, + } = useAuthStore(); // Extract referral code from URL const referralCode = searchParams.get('ref') || ''; @@ -111,7 +117,12 @@ export default function Login() { }, [isAuthenticated, navigate, getReturnUrl]); // Try Telegram WebApp authentication on mount (with auto-retry on 401) + // Wait for auth store initialization to complete to avoid race conditions + // with stale tokens triggering interceptor refresh/redirect loops useEffect(() => { + // Don't attempt Telegram auth until store initialization is done + if (isAuthInitializing) return; + const tryTelegramAuth = async () => { const initData = getTelegramInitData(); if (!isInTelegramWebApp() || !initData) return; @@ -126,15 +137,18 @@ export default function Login() { navigate(getReturnUrl(), { replace: true }); return; } catch (err) { - const status = (err as { response?: { status?: number } })?.response?.status; - console.warn(`Telegram auth attempt ${attempt + 1} failed with status:`, status); + const error = err as { response?: { status?: number; data?: { detail?: string } } }; + const status = error.response?.status; + const detail = error.response?.data?.detail; + console.warn(`Telegram auth attempt ${attempt + 1} failed:`, status, detail); if (status === 401 && attempt < MAX_RETRIES) { await new Promise((r) => setTimeout(r, 1500)); continue; } - setError(t('auth.telegramRequired')); + // Show backend error detail if available, otherwise generic message + setError(detail || t('auth.telegramRequired')); } } @@ -142,7 +156,7 @@ export default function Login() { }; tryTelegramAuth(); - }, [loginWithTelegram, navigate, t, getReturnUrl]); + }, [isAuthInitializing, loginWithTelegram, navigate, t, getReturnUrl]); // Manual retry for Telegram Mini App auth const handleRetryTelegramAuth = async () => { @@ -158,9 +172,14 @@ export default function Login() { await loginWithTelegram(initData); navigate(getReturnUrl(), { replace: true }); } catch (err) { - const status = (err as { response?: { status?: number } })?.response?.status; - console.warn('Telegram auth retry failed with status:', status); - setError(t('auth.telegramRetryFailed', 'Authorization failed. Close the app and try again.')); + const error = err as { response?: { status?: number; data?: { detail?: string } } }; + const status = error.response?.status; + const detail = error.response?.data?.detail; + console.warn('Telegram auth retry failed:', status, detail); + setError( + detail || + t('auth.telegramRetryFailed', 'Authorization failed. Close the app and try again.'), + ); } finally { setIsLoading(false); } diff --git a/src/utils/token.ts b/src/utils/token.ts index cf5e483..9ce503a 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -152,6 +152,25 @@ export const tokenStorage = { }, }; +export function clearStaleSessionIfNeeded(freshInitData: string | null): void { + if (!freshInitData) return; + + try { + const stored = sessionStorage.getItem(TOKEN_KEYS.TELEGRAM_INIT); + + if (stored && stored !== freshInitData) { + sessionStorage.removeItem(TOKEN_KEYS.ACCESS); + sessionStorage.removeItem(TOKEN_KEYS.REFRESH); + sessionStorage.removeItem(TOKEN_KEYS.USER); + } + + sessionStorage.setItem(TOKEN_KEYS.TELEGRAM_INIT, freshInitData); + localStorage.removeItem(TOKEN_KEYS.TELEGRAM_INIT); + } catch { + // Storage недоступен + } +} + /** * Централизованный менеджер обновления токенов * Предотвращает множественные параллельные refresh запросы