mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix: persist refresh token across Telegram Mini App reopens
Telegram Mini App creates a new WebView on each open, clearing sessionStorage. Combined with 24h auth_date validation on backend, returning users couldn't re-authenticate after closing the app. - Move refresh token to localStorage for persistence across sessions - Keep access token in sessionStorage (short-lived, OK to lose) - Fix auth store initialize() to recover when access token is missing but refresh token exists in localStorage - Remove separate mountThemeParams() call to fix ConcurrentCallError (mountMiniApp() handles it internally in SDK v3) - Update token migration logic for new storage strategy
This commit is contained in:
@@ -7,7 +7,6 @@ import {
|
|||||||
retrieveRawInitData,
|
retrieveRawInitData,
|
||||||
mountMiniApp,
|
mountMiniApp,
|
||||||
miniAppReady,
|
miniAppReady,
|
||||||
mountThemeParams,
|
|
||||||
mountViewport,
|
mountViewport,
|
||||||
expandViewport,
|
expandViewport,
|
||||||
mountSwipeBehavior,
|
mountSwipeBehavior,
|
||||||
@@ -43,16 +42,17 @@ if (!alreadyInitialized) {
|
|||||||
clearStaleSessionIfNeeded(retrieveRawInitData() || null);
|
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
|
||||||
|
// Note: mountMiniApp() internally mounts themeParams in SDK v3,
|
||||||
|
// so we don't call mountThemeParams() separately to avoid ConcurrentCallError
|
||||||
try {
|
try {
|
||||||
mountMiniApp();
|
mountMiniApp();
|
||||||
} catch {
|
} catch {
|
||||||
/* already mounted */
|
/* already mounted */
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
mountThemeParams();
|
|
||||||
bindThemeParamsCssVars();
|
bindThemeParamsCssVars();
|
||||||
} catch {
|
} catch {
|
||||||
/* already mounted */
|
/* theme params not yet available */
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
mountSwipeBehavior();
|
mountSwipeBehavior();
|
||||||
|
|||||||
@@ -141,12 +141,14 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
const accessToken = tokenStorage.getAccessToken();
|
const accessToken = tokenStorage.getAccessToken();
|
||||||
const refreshToken = tokenStorage.getRefreshToken();
|
const refreshToken = tokenStorage.getRefreshToken();
|
||||||
|
|
||||||
if (!accessToken || !refreshToken) {
|
if (!refreshToken) {
|
||||||
set({ isLoading: false, isAuthenticated: false });
|
set({ isLoading: false, isAuthenticated: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем валидность токена перед использованием
|
// No access token or it's expired — try refresh
|
||||||
|
// This handles Mini App reopens where sessionStorage was cleared
|
||||||
|
// but refresh token persists in localStorage
|
||||||
if (!isTokenValid(accessToken)) {
|
if (!isTokenValid(accessToken)) {
|
||||||
// Используем централизованный менеджер для refresh
|
// Используем централизованный менеджер для refresh
|
||||||
const newToken = await tokenRefreshManager.refreshAccessToken();
|
const newToken = await tokenRefreshManager.refreshAccessToken();
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ export function isTokenValid(token: string | null): boolean {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Безопасное хранилище токенов
|
* Безопасное хранилище токенов
|
||||||
* Использует sessionStorage вместо localStorage для защиты от XSS
|
* Access token: sessionStorage (short-lived, cleared on tab close)
|
||||||
* Токены не сохраняются между сессиями браузера
|
* Refresh token: localStorage (persistent, survives Mini App reopens, server-validated)
|
||||||
*/
|
*/
|
||||||
export const tokenStorage = {
|
export const tokenStorage = {
|
||||||
getAccessToken(): string | null {
|
getAccessToken(): string | null {
|
||||||
@@ -74,7 +74,8 @@ export const tokenStorage = {
|
|||||||
|
|
||||||
getRefreshToken(): string | null {
|
getRefreshToken(): string | null {
|
||||||
try {
|
try {
|
||||||
return sessionStorage.getItem(TOKEN_KEYS.REFRESH);
|
// Refresh token in localStorage for persistence across Mini App reopens
|
||||||
|
return localStorage.getItem(TOKEN_KEYS.REFRESH) || sessionStorage.getItem(TOKEN_KEYS.REFRESH);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -83,9 +84,12 @@ export const tokenStorage = {
|
|||||||
setTokens(accessToken: string, refreshToken: string): void {
|
setTokens(accessToken: string, refreshToken: string): void {
|
||||||
try {
|
try {
|
||||||
sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken);
|
sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken);
|
||||||
sessionStorage.setItem(TOKEN_KEYS.REFRESH, refreshToken);
|
// Refresh token in localStorage — survives Mini App tab close/reopen
|
||||||
|
localStorage.setItem(TOKEN_KEYS.REFRESH, refreshToken);
|
||||||
|
// Clean up old sessionStorage refresh token (migration)
|
||||||
|
sessionStorage.removeItem(TOKEN_KEYS.REFRESH);
|
||||||
} catch {
|
} catch {
|
||||||
console.error('Failed to save tokens to sessionStorage');
|
// Storage unavailable
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -112,24 +116,26 @@ export const tokenStorage = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Миграция токенов из localStorage в sessionStorage
|
* Миграция токенов для обратной совместимости.
|
||||||
* Вызывается при инициализации для обратной совместимости
|
* Access token: sessionStorage (short-lived, OK to lose on tab close)
|
||||||
|
* Refresh token: localStorage (persistent, survives Mini App reopens)
|
||||||
*/
|
*/
|
||||||
migrateFromLocalStorage(): void {
|
migrateFromLocalStorage(): void {
|
||||||
try {
|
try {
|
||||||
const accessToken = localStorage.getItem(TOKEN_KEYS.ACCESS);
|
const accessToken = localStorage.getItem(TOKEN_KEYS.ACCESS);
|
||||||
const refreshToken = localStorage.getItem(TOKEN_KEYS.REFRESH);
|
|
||||||
|
|
||||||
|
// Migrate access token to sessionStorage
|
||||||
if (accessToken && !sessionStorage.getItem(TOKEN_KEYS.ACCESS)) {
|
if (accessToken && !sessionStorage.getItem(TOKEN_KEYS.ACCESS)) {
|
||||||
sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken);
|
sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken);
|
||||||
}
|
}
|
||||||
if (refreshToken && !sessionStorage.getItem(TOKEN_KEYS.REFRESH)) {
|
|
||||||
sessionStorage.setItem(TOKEN_KEYS.REFRESH, refreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаляем из localStorage после миграции
|
|
||||||
localStorage.removeItem(TOKEN_KEYS.ACCESS);
|
localStorage.removeItem(TOKEN_KEYS.ACCESS);
|
||||||
localStorage.removeItem(TOKEN_KEYS.REFRESH);
|
|
||||||
|
// Migrate refresh token from sessionStorage to localStorage
|
||||||
|
const refreshInSession = sessionStorage.getItem(TOKEN_KEYS.REFRESH);
|
||||||
|
if (refreshInSession && !localStorage.getItem(TOKEN_KEYS.REFRESH)) {
|
||||||
|
localStorage.setItem(TOKEN_KEYS.REFRESH, refreshInSession);
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem(TOKEN_KEYS.REFRESH);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@@ -159,9 +165,11 @@ export function clearStaleSessionIfNeeded(freshInitData: string | null): void {
|
|||||||
const stored = sessionStorage.getItem(TOKEN_KEYS.TELEGRAM_INIT);
|
const stored = sessionStorage.getItem(TOKEN_KEYS.TELEGRAM_INIT);
|
||||||
|
|
||||||
if (stored && stored !== freshInitData) {
|
if (stored && stored !== freshInitData) {
|
||||||
|
// New Telegram session (different user) — clear all auth tokens
|
||||||
sessionStorage.removeItem(TOKEN_KEYS.ACCESS);
|
sessionStorage.removeItem(TOKEN_KEYS.ACCESS);
|
||||||
sessionStorage.removeItem(TOKEN_KEYS.REFRESH);
|
sessionStorage.removeItem(TOKEN_KEYS.REFRESH);
|
||||||
sessionStorage.removeItem(TOKEN_KEYS.USER);
|
sessionStorage.removeItem(TOKEN_KEYS.USER);
|
||||||
|
localStorage.removeItem(TOKEN_KEYS.REFRESH);
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionStorage.setItem(TOKEN_KEYS.TELEGRAM_INIT, freshInitData);
|
sessionStorage.setItem(TOKEN_KEYS.TELEGRAM_INIT, freshInitData);
|
||||||
|
|||||||
Reference in New Issue
Block a user