fix: resolve Telegram Mini App auth failures on all platforms

- Skip token refresh/Bearer header for auth endpoints in request interceptor
- Wait for auth store initialization before attempting Telegram login
- Clear stale session tokens when init data changes (WebView persistence)
- Show backend error details instead of generic messages
This commit is contained in:
c0mrade
2026-02-06 23:14:08 +03:00
parent a1c0ceba19
commit 7df751ea35
4 changed files with 87 additions and 23 deletions

View File

@@ -64,8 +64,28 @@ 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 // Request interceptor - add auth token with expiration check
apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => { apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
// 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(); let token = tokenStorage.getAccessToken();
// Проверяем срок действия токена перед запросом // Проверяем срок действия токена перед запросом
@@ -85,6 +105,7 @@ apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) =>
if (token && config.headers) { if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${token}`;
} }
}
const telegramInitData = getTelegramInitData(); const telegramInitData = getTelegramInitData();
if (telegramInitData && config.headers) { if (telegramInitData && config.headers) {

View File

@@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { import {
init, init,
restoreInitData, restoreInitData,
retrieveRawInitData,
mountMiniApp, mountMiniApp,
miniAppReady, miniAppReady,
mountThemeParams, mountThemeParams,
@@ -19,6 +20,7 @@ import {
requestFullscreen, requestFullscreen,
isFullscreen, isFullscreen,
} from '@telegram-apps/sdk-react'; } from '@telegram-apps/sdk-react';
import { clearStaleSessionIfNeeded } from './utils/token';
import { AppWithNavigator } from './AppWithNavigator'; import { AppWithNavigator } from './AppWithNavigator';
import { ErrorBoundary } from './components/ErrorBoundary'; import { ErrorBoundary } from './components/ErrorBoundary';
import { initLogoPreload } from './api/branding'; import { initLogoPreload } from './api/branding';
@@ -37,6 +39,9 @@ if (!alreadyInitialized) {
init(); init();
restoreInitData(); restoreInitData();
// Сбрасываем старые токены если init data изменился (новая сессия Telegram)
clearStaleSessionIfNeeded(retrieveRawInitData() || null);
// Mount components — each in its own try/catch so one failure doesn't block others // Mount components — each in its own try/catch so one failure doesn't block others
try { try {
mountMiniApp(); mountMiniApp();

View File

@@ -24,7 +24,13 @@ export default function Login() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { isAuthenticated, loginWithTelegram, loginWithEmail, registerWithEmail } = useAuthStore(); const {
isAuthenticated,
isLoading: isAuthInitializing,
loginWithTelegram,
loginWithEmail,
registerWithEmail,
} = useAuthStore();
// Extract referral code from URL // Extract referral code from URL
const referralCode = searchParams.get('ref') || ''; const referralCode = searchParams.get('ref') || '';
@@ -111,7 +117,12 @@ export default function Login() {
}, [isAuthenticated, navigate, getReturnUrl]); }, [isAuthenticated, navigate, getReturnUrl]);
// Try Telegram WebApp authentication on mount (with auto-retry on 401) // 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(() => { useEffect(() => {
// Don't attempt Telegram auth until store initialization is done
if (isAuthInitializing) return;
const tryTelegramAuth = async () => { const tryTelegramAuth = async () => {
const initData = getTelegramInitData(); const initData = getTelegramInitData();
if (!isInTelegramWebApp() || !initData) return; if (!isInTelegramWebApp() || !initData) return;
@@ -126,15 +137,18 @@ export default function Login() {
navigate(getReturnUrl(), { replace: true }); navigate(getReturnUrl(), { replace: true });
return; return;
} catch (err) { } catch (err) {
const status = (err as { response?: { status?: number } })?.response?.status; const error = err as { response?: { status?: number; data?: { detail?: string } } };
console.warn(`Telegram auth attempt ${attempt + 1} failed with status:`, status); 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) { if (status === 401 && attempt < MAX_RETRIES) {
await new Promise((r) => setTimeout(r, 1500)); await new Promise((r) => setTimeout(r, 1500));
continue; 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(); tryTelegramAuth();
}, [loginWithTelegram, navigate, t, getReturnUrl]); }, [isAuthInitializing, loginWithTelegram, navigate, t, getReturnUrl]);
// Manual retry for Telegram Mini App auth // Manual retry for Telegram Mini App auth
const handleRetryTelegramAuth = async () => { const handleRetryTelegramAuth = async () => {
@@ -158,9 +172,14 @@ export default function Login() {
await loginWithTelegram(initData); await loginWithTelegram(initData);
navigate(getReturnUrl(), { replace: true }); navigate(getReturnUrl(), { replace: true });
} catch (err) { } catch (err) {
const status = (err as { response?: { status?: number } })?.response?.status; const error = err as { response?: { status?: number; data?: { detail?: string } } };
console.warn('Telegram auth retry failed with status:', status); const status = error.response?.status;
setError(t('auth.telegramRetryFailed', 'Authorization failed. Close the app and try again.')); 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 { } finally {
setIsLoading(false); setIsLoading(false);
} }

View File

@@ -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 запросы * Предотвращает множественные параллельные refresh запросы