mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
refactor: migrate to eslint flat config and format codebase with prettier
- Remove legacy .eslintrc.cjs and .eslintignore - Add eslint.config.js with flat config, security rules (no-eval, no-implied-eval, no-new-func, no-script-url) - Add .prettierrc and .prettierignore - Format entire codebase with prettier
This commit is contained in:
@@ -1,23 +1,23 @@
|
||||
export interface HSLColor {
|
||||
h: number
|
||||
s: number
|
||||
l: number
|
||||
h: number;
|
||||
s: number;
|
||||
l: number;
|
||||
}
|
||||
|
||||
export interface RGBColor {
|
||||
r: number
|
||||
g: number
|
||||
b: number
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
export function hexToRgb(hex: string): RGBColor {
|
||||
if (hex.length === 4) {
|
||||
hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]
|
||||
hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
|
||||
}
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
return { r, g, b }
|
||||
const r = parseInt(hex.slice(1, 3), 16);
|
||||
const g = parseInt(hex.slice(3, 5), 16);
|
||||
const b = parseInt(hex.slice(5, 7), 16);
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
export function rgbToHex(r: number, g: number, b: number): string {
|
||||
@@ -25,39 +25,39 @@ export function rgbToHex(r: number, g: number, b: number): string {
|
||||
'#' +
|
||||
[r, g, b]
|
||||
.map((x) => {
|
||||
const hex = Math.round(Math.max(0, Math.min(255, x))).toString(16)
|
||||
return hex.length === 1 ? '0' + hex : hex
|
||||
const hex = Math.round(Math.max(0, Math.min(255, x))).toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
})
|
||||
.join('')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function hexToHsl(hex: string): HSLColor {
|
||||
const { r, g, b } = hexToRgb(hex)
|
||||
const rNorm = r / 255
|
||||
const gNorm = g / 255
|
||||
const bNorm = b / 255
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
const rNorm = r / 255;
|
||||
const gNorm = g / 255;
|
||||
const bNorm = b / 255;
|
||||
|
||||
const max = Math.max(rNorm, gNorm, bNorm)
|
||||
const min = Math.min(rNorm, gNorm, bNorm)
|
||||
let h = 0
|
||||
let s = 0
|
||||
const l = (max + min) / 2
|
||||
const max = Math.max(rNorm, gNorm, bNorm);
|
||||
const min = Math.min(rNorm, gNorm, bNorm);
|
||||
let h = 0;
|
||||
let s = 0;
|
||||
const l = (max + min) / 2;
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
switch (max) {
|
||||
case rNorm:
|
||||
h = ((gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0)) / 6
|
||||
break
|
||||
h = ((gNorm - bNorm) / d + (gNorm < bNorm ? 6 : 0)) / 6;
|
||||
break;
|
||||
case gNorm:
|
||||
h = ((bNorm - rNorm) / d + 2) / 6
|
||||
break
|
||||
h = ((bNorm - rNorm) / d + 2) / 6;
|
||||
break;
|
||||
case bNorm:
|
||||
h = ((rNorm - gNorm) / d + 4) / 6
|
||||
break
|
||||
h = ((rNorm - gNorm) / d + 4) / 6;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,38 +65,38 @@ export function hexToHsl(hex: string): HSLColor {
|
||||
h: Math.round(h * 360),
|
||||
s: Math.round(s * 100),
|
||||
l: Math.round(l * 100),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function hslToRgb(h: number, s: number, l: number): RGBColor {
|
||||
const sNorm = s / 100
|
||||
const lNorm = l / 100
|
||||
const sNorm = s / 100;
|
||||
const lNorm = l / 100;
|
||||
|
||||
const a = sNorm * Math.min(lNorm, 1 - lNorm)
|
||||
const a = sNorm * Math.min(lNorm, 1 - lNorm);
|
||||
const f = (n: number) => {
|
||||
const k = (n + h / 30) % 12
|
||||
const color = lNorm - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)
|
||||
return Math.round(255 * color)
|
||||
}
|
||||
const k = (n + h / 30) % 12;
|
||||
const color = lNorm - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||
return Math.round(255 * color);
|
||||
};
|
||||
|
||||
return { r: f(0), g: f(8), b: f(4) }
|
||||
return { r: f(0), g: f(8), b: f(4) };
|
||||
}
|
||||
|
||||
export function hslToHex(h: number, s: number, l: number): string {
|
||||
const { r, g, b } = hslToRgb(h, s, l)
|
||||
return rgbToHex(r, g, b)
|
||||
const { r, g, b } = hslToRgb(h, s, l);
|
||||
return rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
export function isValidHex(hex: string): boolean {
|
||||
return /^#[0-9A-Fa-f]{6}$/.test(hex)
|
||||
return /^#[0-9A-Fa-f]{6}$/.test(hex);
|
||||
}
|
||||
|
||||
export function normalizeHex(hex: string): string {
|
||||
if (!hex.startsWith('#')) {
|
||||
hex = '#' + hex
|
||||
hex = '#' + hex;
|
||||
}
|
||||
if (hex.length === 4) {
|
||||
hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]
|
||||
hex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
|
||||
}
|
||||
return hex.toLowerCase()
|
||||
return hex.toLowerCase();
|
||||
}
|
||||
|
||||
@@ -3,29 +3,29 @@
|
||||
* В production логи не выводятся
|
||||
*/
|
||||
|
||||
const isDev = import.meta.env.DEV
|
||||
const isDev = import.meta.env.DEV;
|
||||
|
||||
export const logger = {
|
||||
log: (...args: unknown[]): void => {
|
||||
if (isDev) {
|
||||
console.log(...args)
|
||||
console.log(...args);
|
||||
}
|
||||
},
|
||||
|
||||
warn: (...args: unknown[]): void => {
|
||||
if (isDev) {
|
||||
console.warn(...args)
|
||||
console.warn(...args);
|
||||
}
|
||||
},
|
||||
|
||||
error: (...args: unknown[]): void => {
|
||||
// Ошибки логируем всегда (важно для отладки в production)
|
||||
console.error(...args)
|
||||
console.error(...args);
|
||||
},
|
||||
|
||||
debug: (...args: unknown[]): void => {
|
||||
if (isDev) {
|
||||
console.debug(...args)
|
||||
console.debug(...args);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -35,23 +35,23 @@ export const logger = {
|
||||
createLogger: (prefix: string) => ({
|
||||
log: (...args: unknown[]): void => {
|
||||
if (isDev) {
|
||||
console.log(`[${prefix}]`, ...args)
|
||||
console.log(`[${prefix}]`, ...args);
|
||||
}
|
||||
},
|
||||
warn: (...args: unknown[]): void => {
|
||||
if (isDev) {
|
||||
console.warn(`[${prefix}]`, ...args)
|
||||
console.warn(`[${prefix}]`, ...args);
|
||||
}
|
||||
},
|
||||
error: (...args: unknown[]): void => {
|
||||
console.error(`[${prefix}]`, ...args)
|
||||
console.error(`[${prefix}]`, ...args);
|
||||
},
|
||||
debug: (...args: unknown[]): void => {
|
||||
if (isDev) {
|
||||
console.debug(`[${prefix}]`, ...args)
|
||||
console.debug(`[${prefix}]`, ...args);
|
||||
}
|
||||
},
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
export default logger
|
||||
export default logger;
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
*/
|
||||
|
||||
interface RateLimitEntry {
|
||||
count: number
|
||||
resetTime: number
|
||||
count: number;
|
||||
resetTime: number;
|
||||
}
|
||||
|
||||
const rateLimitStore = new Map<string, RateLimitEntry>()
|
||||
const rateLimitStore = new Map<string, RateLimitEntry>();
|
||||
|
||||
/**
|
||||
* Check if action is rate limited
|
||||
@@ -19,52 +19,52 @@ const rateLimitStore = new Map<string, RateLimitEntry>()
|
||||
export function checkRateLimit(
|
||||
key: string,
|
||||
maxRequests: number = 3,
|
||||
windowMs: number = 10000
|
||||
windowMs: number = 10000,
|
||||
): boolean {
|
||||
const now = Date.now()
|
||||
const entry = rateLimitStore.get(key)
|
||||
const now = Date.now();
|
||||
const entry = rateLimitStore.get(key);
|
||||
|
||||
// Clean up expired entry
|
||||
if (entry && now >= entry.resetTime) {
|
||||
rateLimitStore.delete(key)
|
||||
rateLimitStore.delete(key);
|
||||
}
|
||||
|
||||
const currentEntry = rateLimitStore.get(key)
|
||||
const currentEntry = rateLimitStore.get(key);
|
||||
|
||||
if (!currentEntry) {
|
||||
// First request - allow and start tracking
|
||||
rateLimitStore.set(key, {
|
||||
count: 1,
|
||||
resetTime: now + windowMs,
|
||||
})
|
||||
return true
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentEntry.count >= maxRequests) {
|
||||
// Rate limit exceeded
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
// Increment counter and allow
|
||||
currentEntry.count++
|
||||
return true
|
||||
currentEntry.count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining time until rate limit resets (in seconds)
|
||||
*/
|
||||
export function getRateLimitResetTime(key: string): number {
|
||||
const entry = rateLimitStore.get(key)
|
||||
if (!entry) return 0
|
||||
const remaining = entry.resetTime - Date.now()
|
||||
return remaining > 0 ? Math.ceil(remaining / 1000) : 0
|
||||
const entry = rateLimitStore.get(key);
|
||||
if (!entry) return 0;
|
||||
const remaining = entry.resetTime - Date.now();
|
||||
return remaining > 0 ? Math.ceil(remaining / 1000) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset rate limit for a key (e.g., after successful action)
|
||||
*/
|
||||
export function resetRateLimit(key: string): void {
|
||||
rateLimitStore.delete(key)
|
||||
rateLimitStore.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,19 +72,19 @@ export function resetRateLimit(key: string): void {
|
||||
*/
|
||||
export function debounce<T extends (...args: unknown[]) => unknown>(
|
||||
func: T,
|
||||
wait: number
|
||||
wait: number,
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
timeoutId = setTimeout(() => {
|
||||
func(...args)
|
||||
timeoutId = null
|
||||
}, wait)
|
||||
}
|
||||
func(...args);
|
||||
timeoutId = null;
|
||||
}, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,19 +92,19 @@ export function debounce<T extends (...args: unknown[]) => unknown>(
|
||||
*/
|
||||
export function throttle<T extends (...args: unknown[]) => unknown>(
|
||||
func: T,
|
||||
limit: number
|
||||
limit: number,
|
||||
): (...args: Parameters<T>) => void {
|
||||
let inThrottle = false
|
||||
let inThrottle = false;
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (!inThrottle) {
|
||||
func(...args)
|
||||
inThrottle = true
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => {
|
||||
inThrottle = false
|
||||
}, limit)
|
||||
inThrottle = false;
|
||||
}, limit);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Rate limit keys for different actions
|
||||
@@ -114,4 +114,4 @@ export const RATE_LIMIT_KEYS = {
|
||||
TICKET_REPLY: 'ticket-reply',
|
||||
PROMO_ACTIVATE: 'promo-activate',
|
||||
WHEEL_SPIN: 'wheel-spin',
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
* Утилиты для безопасной работы с JWT токенами
|
||||
*/
|
||||
|
||||
import axios from 'axios'
|
||||
import axios from 'axios';
|
||||
|
||||
const TOKEN_KEYS = {
|
||||
ACCESS: 'access_token',
|
||||
REFRESH: 'refresh_token',
|
||||
USER: 'user',
|
||||
TELEGRAM_INIT: 'telegram_init_data',
|
||||
} as const
|
||||
} as const;
|
||||
|
||||
interface JWTPayload {
|
||||
exp?: number
|
||||
iat?: number
|
||||
sub?: string
|
||||
[key: string]: unknown
|
||||
exp?: number;
|
||||
iat?: number;
|
||||
sub?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,14 +24,14 @@ interface JWTPayload {
|
||||
*/
|
||||
export function decodeJWT(token: string): JWTPayload | null {
|
||||
try {
|
||||
const parts = token.split('.')
|
||||
if (parts.length !== 3) return null
|
||||
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)
|
||||
const payload = parts[1];
|
||||
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
return JSON.parse(decoded);
|
||||
} catch {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,21 +41,21 @@ export function decodeJWT(token: string): JWTPayload | null {
|
||||
* @param bufferSeconds Буфер в секундах до истечения (по умолчанию 30 сек)
|
||||
*/
|
||||
export function isTokenExpired(token: string | null, bufferSeconds = 30): boolean {
|
||||
if (!token) return true
|
||||
if (!token) return true;
|
||||
|
||||
const payload = decodeJWT(token)
|
||||
if (!payload?.exp) return true
|
||||
const payload = decodeJWT(token);
|
||||
if (!payload?.exp) return true;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
return payload.exp <= now + bufferSeconds
|
||||
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)
|
||||
if (!token) return false;
|
||||
return !isTokenExpired(token);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,46 +66,46 @@ export function isTokenValid(token: string | null): boolean {
|
||||
export const tokenStorage = {
|
||||
getAccessToken(): string | null {
|
||||
try {
|
||||
return sessionStorage.getItem(TOKEN_KEYS.ACCESS)
|
||||
return sessionStorage.getItem(TOKEN_KEYS.ACCESS);
|
||||
} catch {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getRefreshToken(): string | null {
|
||||
try {
|
||||
return sessionStorage.getItem(TOKEN_KEYS.REFRESH)
|
||||
return sessionStorage.getItem(TOKEN_KEYS.REFRESH);
|
||||
} catch {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
setTokens(accessToken: string, refreshToken: string): void {
|
||||
try {
|
||||
sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken)
|
||||
sessionStorage.setItem(TOKEN_KEYS.REFRESH, refreshToken)
|
||||
sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken);
|
||||
sessionStorage.setItem(TOKEN_KEYS.REFRESH, refreshToken);
|
||||
} catch {
|
||||
console.error('Failed to save tokens to sessionStorage')
|
||||
console.error('Failed to save tokens to sessionStorage');
|
||||
}
|
||||
},
|
||||
|
||||
setAccessToken(accessToken: string): void {
|
||||
try {
|
||||
sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken)
|
||||
sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken);
|
||||
} catch {
|
||||
console.error('Failed to save access token to sessionStorage')
|
||||
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)
|
||||
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)
|
||||
localStorage.removeItem(TOKEN_KEYS.ACCESS);
|
||||
localStorage.removeItem(TOKEN_KEYS.REFRESH);
|
||||
localStorage.removeItem(TOKEN_KEYS.USER);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -117,19 +117,19 @@ export const tokenStorage = {
|
||||
*/
|
||||
migrateFromLocalStorage(): void {
|
||||
try {
|
||||
const accessToken = localStorage.getItem(TOKEN_KEYS.ACCESS)
|
||||
const refreshToken = localStorage.getItem(TOKEN_KEYS.REFRESH)
|
||||
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)
|
||||
sessionStorage.setItem(TOKEN_KEYS.ACCESS, accessToken);
|
||||
}
|
||||
if (refreshToken && !sessionStorage.getItem(TOKEN_KEYS.REFRESH)) {
|
||||
sessionStorage.setItem(TOKEN_KEYS.REFRESH, refreshToken)
|
||||
sessionStorage.setItem(TOKEN_KEYS.REFRESH, refreshToken);
|
||||
}
|
||||
|
||||
// Удаляем из localStorage после миграции
|
||||
localStorage.removeItem(TOKEN_KEYS.ACCESS)
|
||||
localStorage.removeItem(TOKEN_KEYS.REFRESH)
|
||||
localStorage.removeItem(TOKEN_KEYS.ACCESS);
|
||||
localStorage.removeItem(TOKEN_KEYS.REFRESH);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -137,33 +137,33 @@ export const tokenStorage = {
|
||||
|
||||
getTelegramInitData(): string | null {
|
||||
try {
|
||||
return sessionStorage.getItem(TOKEN_KEYS.TELEGRAM_INIT)
|
||||
return sessionStorage.getItem(TOKEN_KEYS.TELEGRAM_INIT);
|
||||
} catch {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
setTelegramInitData(data: string): void {
|
||||
try {
|
||||
sessionStorage.setItem(TOKEN_KEYS.TELEGRAM_INIT, data)
|
||||
sessionStorage.setItem(TOKEN_KEYS.TELEGRAM_INIT, data);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Централизованный менеджер обновления токенов
|
||||
* Предотвращает множественные параллельные refresh запросы
|
||||
*/
|
||||
class TokenRefreshManager {
|
||||
private isRefreshing = false
|
||||
private refreshPromise: Promise<string | null> | null = null
|
||||
private subscribers: ((token: string | null) => void)[] = []
|
||||
private refreshEndpoint = '/api/cabinet/auth/refresh'
|
||||
private isRefreshing = false;
|
||||
private refreshPromise: Promise<string | null> | null = null;
|
||||
private subscribers: ((token: string | null) => void)[] = [];
|
||||
private refreshEndpoint = '/api/cabinet/auth/refresh';
|
||||
|
||||
setRefreshEndpoint(endpoint: string): void {
|
||||
this.refreshEndpoint = endpoint
|
||||
this.refreshEndpoint = endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,24 +173,24 @@ class TokenRefreshManager {
|
||||
async refreshAccessToken(): Promise<string | null> {
|
||||
// Если уже идёт refresh - возвращаем существующий Promise
|
||||
if (this.isRefreshing && this.refreshPromise) {
|
||||
return this.refreshPromise
|
||||
return this.refreshPromise;
|
||||
}
|
||||
|
||||
const refreshToken = tokenStorage.getRefreshToken()
|
||||
const refreshToken = tokenStorage.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
this.isRefreshing = true
|
||||
this.refreshPromise = this.doRefresh(refreshToken)
|
||||
this.isRefreshing = true;
|
||||
this.refreshPromise = this.doRefresh(refreshToken);
|
||||
|
||||
try {
|
||||
const result = await this.refreshPromise
|
||||
this.notifySubscribers(result)
|
||||
return result
|
||||
const result = await this.refreshPromise;
|
||||
this.notifySubscribers(result);
|
||||
return result;
|
||||
} finally {
|
||||
this.isRefreshing = false
|
||||
this.refreshPromise = null
|
||||
this.isRefreshing = false;
|
||||
this.refreshPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,20 +200,20 @@ class TokenRefreshManager {
|
||||
const response = await axios.post<{ access_token?: string }>(
|
||||
this.refreshEndpoint,
|
||||
{ refresh_token: refreshToken },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
)
|
||||
{ headers: { 'Content-Type': 'application/json' } },
|
||||
);
|
||||
|
||||
const newAccessToken = response.data.access_token
|
||||
const newAccessToken = response.data.access_token;
|
||||
|
||||
if (newAccessToken) {
|
||||
tokenStorage.setAccessToken(newAccessToken)
|
||||
return newAccessToken
|
||||
tokenStorage.setAccessToken(newAccessToken);
|
||||
return newAccessToken;
|
||||
}
|
||||
|
||||
return null
|
||||
return null;
|
||||
} catch {
|
||||
// Token refresh failed - don't log sensitive error details
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,22 +221,22 @@ class TokenRefreshManager {
|
||||
* Подписка на результат refresh (для ожидающих запросов)
|
||||
*/
|
||||
subscribe(callback: (token: string | null) => void): () => void {
|
||||
this.subscribers.push(callback)
|
||||
this.subscribers.push(callback);
|
||||
return () => {
|
||||
this.subscribers = this.subscribers.filter((cb) => cb !== callback)
|
||||
}
|
||||
this.subscribers = this.subscribers.filter((cb) => cb !== callback);
|
||||
};
|
||||
}
|
||||
|
||||
private notifySubscribers(token: string | null): void {
|
||||
this.subscribers.forEach((cb) => cb(token))
|
||||
this.subscribers = []
|
||||
this.subscribers.forEach((cb) => cb(token));
|
||||
this.subscribers = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, идёт ли сейчас refresh
|
||||
*/
|
||||
get isRefreshInProgress(): boolean {
|
||||
return this.isRefreshing
|
||||
return this.isRefreshing;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,28 +244,28 @@ class TokenRefreshManager {
|
||||
*/
|
||||
async waitForRefresh(): Promise<string | null> {
|
||||
if (this.refreshPromise) {
|
||||
return this.refreshPromise
|
||||
return this.refreshPromise;
|
||||
}
|
||||
return tokenStorage.getAccessToken()
|
||||
return tokenStorage.getAccessToken();
|
||||
}
|
||||
}
|
||||
|
||||
export const tokenRefreshManager = new TokenRefreshManager()
|
||||
export const tokenRefreshManager = new TokenRefreshManager();
|
||||
|
||||
/**
|
||||
* Ключ для сохранения URL для возврата после логина
|
||||
*/
|
||||
const RETURN_URL_KEY = 'auth_return_url'
|
||||
const RETURN_URL_KEY = 'auth_return_url';
|
||||
|
||||
/**
|
||||
* Сохраняет URL для возврата после авторизации
|
||||
*/
|
||||
export function saveReturnUrl(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
const currentPath = window.location.pathname + window.location.search
|
||||
const currentPath = window.location.pathname + window.location.search;
|
||||
// Не сохраняем /login как return URL
|
||||
if (currentPath && currentPath !== '/login') {
|
||||
sessionStorage.setItem(RETURN_URL_KEY, currentPath)
|
||||
sessionStorage.setItem(RETURN_URL_KEY, currentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,11 +275,11 @@ export function saveReturnUrl(): void {
|
||||
*/
|
||||
export function getAndClearReturnUrl(): string | null {
|
||||
if (typeof window !== 'undefined') {
|
||||
const url = sessionStorage.getItem(RETURN_URL_KEY)
|
||||
sessionStorage.removeItem(RETURN_URL_KEY)
|
||||
return url
|
||||
const url = sessionStorage.getItem(RETURN_URL_KEY);
|
||||
sessionStorage.removeItem(RETURN_URL_KEY);
|
||||
return url;
|
||||
}
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,14 +289,14 @@ export function getAndClearReturnUrl(): string | null {
|
||||
*/
|
||||
export function safeRedirectToLogin(): void {
|
||||
// Разрешённые пути для редиректа (относительные пути нашего приложения)
|
||||
const loginPath = '/login'
|
||||
const loginPath = '/login';
|
||||
|
||||
// Проверяем, что мы на том же origin
|
||||
if (typeof window !== 'undefined') {
|
||||
// Сохраняем текущий URL для возврата после логина
|
||||
saveReturnUrl()
|
||||
saveReturnUrl();
|
||||
// Используем только относительный путь для безопасности
|
||||
window.location.href = loginPath
|
||||
window.location.href = loginPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,18 +306,18 @@ export function safeRedirectToLogin(): void {
|
||||
*/
|
||||
export function isValidRedirectUrl(url: string): boolean {
|
||||
// Пустой URL - небезопасен
|
||||
if (!url) return false
|
||||
if (!url) return false;
|
||||
|
||||
// Относительные пути всегда безопасны
|
||||
if (url.startsWith('/') && !url.startsWith('//')) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(url, window.location.origin)
|
||||
const parsed = new URL(url, window.location.origin);
|
||||
// Разрешаем только тот же origin
|
||||
return parsed.origin === window.location.origin
|
||||
return parsed.origin === window.location.origin;
|
||||
} catch {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user