mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
Add files via upload
This commit is contained in:
151
src/utils/token.ts
Normal file
151
src/utils/token.ts
Normal 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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user