mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
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:
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 запросы
|
||||||
|
|||||||
Reference in New Issue
Block a user