Add files via upload

This commit is contained in:
Egor
2026-01-18 05:15:09 +03:00
committed by GitHub
parent a3e53f2cef
commit 050a065017

151
src/utils/token.ts Normal file
View File

@@ -0,0 +1,151 @@
/**
* Утилиты для безопасной работы с JWT токенами
*/
const TOKEN_KEYS = {
ACCESS: 'access_token',
REFRESH: 'refresh_token',
USER: 'user',
TELEGRAM_INIT: 'telegram_init_data',
} as const
interface JWTPayload {
exp?: number
iat?: number
sub?: string
[key: string]: unknown
}
/**
* Декодирует JWT токен без верификации подписи
* Используется только для чтения payload на клиенте
*/
export function decodeJWT(token: string): JWTPayload | null {
try {
const parts = token.split('.')
if (parts.length !== 3) return null
const payload = parts[1]
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
return JSON.parse(decoded)
} catch {
return null
}
}
/**
* Проверяет, истёк ли срок действия токена
* @param token JWT токен
* @param bufferSeconds Буфер в секундах до истечения (по умолчанию 30 сек)
*/
export function isTokenExpired(token: string | null, bufferSeconds = 30): boolean {
if (!token) return true
const payload = decodeJWT(token)
if (!payload?.exp) return true
const now = Math.floor(Date.now() / 1000)
return payload.exp <= now + bufferSeconds
}
/**
* Проверяет, валиден ли токен (не истёк и корректный формат)
*/
export function isTokenValid(token: string | null): boolean {
if (!token) return false
return !isTokenExpired(token)
}
/**
* Безопасное хранилище токенов
* Использует sessionStorage вместо localStorage для защиты от XSS
* Токены не сохраняются между сессиями браузера
*/
export const tokenStorage = {
getAccessToken(): string | null {
try {
return sessionStorage.getItem(TOKEN_KEYS.ACCESS)
} catch {
return null
}
},
getRefreshToken(): string | null {
try {
return sessionStorage.getItem(TOKEN_KEYS.REFRESH)
} catch {
return null
}
},
setTokens(accessToken: string, refreshToken: string): void {
try {
sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken)
sessionStorage.setItem(TOKEN_KEYS.REFRESH, refreshToken)
} catch {
console.error('Failed to save tokens to sessionStorage')
}
},
setAccessToken(accessToken: string): void {
try {
sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken)
} catch {
console.error('Failed to save access token to sessionStorage')
}
},
clearTokens(): void {
try {
sessionStorage.removeItem(TOKEN_KEYS.ACCESS)
sessionStorage.removeItem(TOKEN_KEYS.REFRESH)
sessionStorage.removeItem(TOKEN_KEYS.USER)
// Также очищаем localStorage для миграции со старой версии
localStorage.removeItem(TOKEN_KEYS.ACCESS)
localStorage.removeItem(TOKEN_KEYS.REFRESH)
localStorage.removeItem(TOKEN_KEYS.USER)
} catch {
// ignore
}
},
/**
* Миграция токенов из localStorage в sessionStorage
* Вызывается при инициализации для обратной совместимости
*/
migrateFromLocalStorage(): void {
try {
const accessToken = localStorage.getItem(TOKEN_KEYS.ACCESS)
const refreshToken = localStorage.getItem(TOKEN_KEYS.REFRESH)
if (accessToken && !sessionStorage.getItem(TOKEN_KEYS.ACCESS)) {
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.REFRESH)
} catch {
// ignore
}
},
getTelegramInitData(): string | null {
try {
return sessionStorage.getItem(TOKEN_KEYS.TELEGRAM_INIT)
} catch {
return null
}
},
setTelegramInitData(data: string): void {
try {
sessionStorage.setItem(TOKEN_KEYS.TELEGRAM_INIT, data)
} catch {
// ignore
}
},
}