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:
c0mrade
2026-01-27 17:37:31 +03:00
parent 111ccc4e7a
commit bc90ba3779
133 changed files with 19972 additions and 15523 deletions

View File

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