mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
fix: resolve telegram auth token expiration and clean up codebase
- Fix stale initData comparison in clearStaleSessionIfNeeded that destroyed valid refresh tokens on mobile WebView reopens - Restrict X-Telegram-Init-Data header to auth endpoints only - Close Mini App on auth retry to force fresh initData from Telegram - Merge Connection page error/not-configured states for better UX - Remove unnecessary comments across 40+ files (section dividers, restating comments, noise catch block comments) - Configure ESLint allowEmptyCatch to properly handle intentional empty catch blocks (62 warnings resolved)
This commit is contained in:
@@ -11,10 +11,8 @@ import { API } from '../config/constants';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
||||
|
||||
// Настраиваем endpoint для refresh
|
||||
tokenRefreshManager.setRefreshEndpoint(`${API_BASE_URL}/cabinet/auth/refresh`);
|
||||
|
||||
// CSRF token management
|
||||
const CSRF_COOKIE_NAME = 'csrf_token';
|
||||
const CSRF_HEADER_NAME = 'X-CSRF-Token';
|
||||
|
||||
@@ -34,7 +32,6 @@ function ensureCsrfToken(): string {
|
||||
let token = getCsrfToken();
|
||||
if (!token) {
|
||||
token = generateCsrfToken();
|
||||
// Set cookie with SameSite=Strict for CSRF protection
|
||||
document.cookie = `${CSRF_COOKIE_NAME}=${token}; path=/; SameSite=Strict; Secure`;
|
||||
}
|
||||
return token;
|
||||
@@ -49,9 +46,7 @@ const getTelegramInitData = (): string | null => {
|
||||
tokenStorage.setTelegramInitData(raw);
|
||||
return raw;
|
||||
}
|
||||
} catch {
|
||||
// Not in Telegram or SDK not initialized
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return tokenStorage.getTelegramInitData();
|
||||
};
|
||||
@@ -64,7 +59,6 @@ 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',
|
||||
@@ -85,15 +79,11 @@ function isAuthEndpoint(url: string | undefined): boolean {
|
||||
return AUTH_ENDPOINTS.some((endpoint) => url.startsWith(endpoint));
|
||||
}
|
||||
|
||||
// Request interceptor - add auth token with expiration check
|
||||
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();
|
||||
|
||||
if (token && isTokenExpired(token)) {
|
||||
// Access token expired — try refresh
|
||||
const newToken = await tokenRefreshManager.refreshAccessToken();
|
||||
if (newToken) {
|
||||
token = newToken;
|
||||
@@ -103,7 +93,6 @@ apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) =>
|
||||
return config;
|
||||
}
|
||||
} else if (!token && tokenStorage.getRefreshToken()) {
|
||||
// No access token (e.g. tab reopen) but refresh token exists — restore session
|
||||
const newToken = await tokenRefreshManager.refreshAccessToken();
|
||||
if (newToken) {
|
||||
token = newToken;
|
||||
@@ -115,12 +104,16 @@ apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) =>
|
||||
}
|
||||
}
|
||||
|
||||
const telegramInitData = getTelegramInitData();
|
||||
if (telegramInitData && config.headers) {
|
||||
config.headers['X-Telegram-Init-Data'] = telegramInitData;
|
||||
const isTelegramAuthEndpoint =
|
||||
config.url?.startsWith('/cabinet/auth/telegram') ||
|
||||
config.url?.startsWith('/cabinet/auth/account/link/telegram');
|
||||
if (isTelegramAuthEndpoint) {
|
||||
const telegramInitData = getTelegramInitData();
|
||||
if (telegramInitData && config.headers) {
|
||||
config.headers['X-Telegram-Init-Data'] = telegramInitData;
|
||||
}
|
||||
}
|
||||
|
||||
// Add CSRF token for state-changing methods
|
||||
const method = config.method?.toUpperCase();
|
||||
if (method && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && config.headers) {
|
||||
config.headers[CSRF_HEADER_NAME] = ensureCsrfToken();
|
||||
@@ -129,7 +122,6 @@ apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) =>
|
||||
return config;
|
||||
});
|
||||
|
||||
// Custom error types for special handling
|
||||
export interface MaintenanceError {
|
||||
code: 'maintenance';
|
||||
message: string;
|
||||
@@ -180,13 +172,11 @@ export function isBlacklistedError(
|
||||
return err.response?.status === 403 && err.response?.data?.detail?.code === 'blacklisted';
|
||||
}
|
||||
|
||||
// Response interceptor - handle 401, 503 (maintenance), 403 (channel subscription)
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
|
||||
|
||||
// Handle maintenance mode (503)
|
||||
if (isMaintenanceError(error)) {
|
||||
const detail = (error.response?.data as { detail: MaintenanceError }).detail;
|
||||
useBlockingStore.getState().setMaintenance({
|
||||
@@ -196,7 +186,6 @@ apiClient.interceptors.response.use(
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Handle channel subscription required (403)
|
||||
if (isChannelSubscriptionError(error)) {
|
||||
const detail = (error.response?.data as { detail: ChannelSubscriptionError }).detail;
|
||||
useBlockingStore.getState().setChannelSubscription({
|
||||
@@ -207,7 +196,6 @@ apiClient.interceptors.response.use(
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Handle blacklisted user (403)
|
||||
if (isBlacklistedError(error)) {
|
||||
const detail = (error.response?.data as { detail: BlacklistedError }).detail;
|
||||
useBlockingStore.getState().setBlacklisted({
|
||||
@@ -216,14 +204,10 @@ apiClient.interceptors.response.use(
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Если получили 401 и ещё не пробовали refresh (на случай если проверка exp не сработала)
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
// Не обрабатываем 401 для авторизационных endpoints - пусть ошибка дойдет до компонента
|
||||
const requestUrl = originalRequest.url || '';
|
||||
const isLoginEndpoint = isAuthEndpoint(requestUrl);
|
||||
|
||||
if (isLoginEndpoint) {
|
||||
// Пробрасываем ошибку в компонент для показа сообщения пользователю
|
||||
if (isAuthEndpoint(requestUrl)) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
@@ -236,7 +220,6 @@ apiClient.interceptors.response.use(
|
||||
}
|
||||
return apiClient(originalRequest);
|
||||
} else {
|
||||
// Refresh не удался
|
||||
tokenStorage.clearTokens();
|
||||
safeRedirectToLogin();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user