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:
c0mrade
2026-03-13 17:50:49 +03:00
parent 682b6b70dc
commit 2dab25c5a0
43 changed files with 72 additions and 647 deletions

View File

@@ -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();
}