Токены, правильная работа

This commit is contained in:
Халимов Рустам
2026-04-19 22:13:20 +03:00
parent 70acad56fb
commit 9e715fe3ab
18 changed files with 422 additions and 45 deletions
@@ -3,11 +3,74 @@ const API_BASE = '/api';
export class HttpClient {
private token: string | null = null;
private isRefreshing = false;
private refreshSubscribers: Array<(token: string) => void> = [];
setToken(token: string | null) {
this.token = token;
}
private subscribeTokenRefresh(cb: (token: string) => void) {
this.refreshSubscribers.push(cb);
}
private onRefreshed(token: string) {
this.refreshSubscribers.forEach(cb => cb(token));
this.refreshSubscribers = [];
}
private async handle401(): Promise<string | null> {
if (this.isRefreshing) {
return new Promise(resolve => {
this.subscribeTokenRefresh(token => {
resolve(token);
});
});
}
const refreshToken = localStorage.getItem('knot_refresh_token');
if (!refreshToken) {
return null;
}
this.isRefreshing = true;
try {
const response = await fetch(`${API_BASE}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) {
localStorage.removeItem('knot_token');
localStorage.removeItem('knot_refresh_token');
this.token = null;
return null;
}
const data = await response.json();
const newToken = data.accessToken;
const newRefreshToken = data.refreshToken;
localStorage.setItem('knot_token', newToken);
if (newRefreshToken) {
localStorage.setItem('knot_refresh_token', newRefreshToken);
}
this.token = newToken;
this.onRefreshed(newToken);
return newToken;
} catch (err) {
localStorage.removeItem('knot_token');
localStorage.removeItem('knot_refresh_token');
this.token = null;
return null;
} finally {
this.isRefreshing = false;
}
}
async request<T>(endpoint: string, options: RequestInit & { timeout?: number } = {}): Promise<T> {
const { timeout = 30_000, ...fetchOptions } = options;
const controller = new AbortController();
@@ -41,6 +104,36 @@ export class HttpClient {
}
clearTimeout(timer);
// Handle 401 Unauthorized
if (response.status === 401 && endpoint !== '/auth/refresh') {
const newToken = await this.handle401();
if (newToken) {
// Retry the original request with new token
const retryHeaders: Record<string, string> = {
...computedHeaders,
Authorization: `Bearer ${newToken}`,
};
const retryController = new AbortController();
const retryTimer = timeout > 0 ? setTimeout(() => retryController.abort(), timeout) : undefined;
try {
response = await fetch(`${API_BASE}${endpoint}`, {
...fetchOptions,
headers: retryHeaders,
signal: retryController.signal,
});
} catch (err) {
clearTimeout(retryTimer);
if (err instanceof DOMException && err.name === 'AbortError') {
throw new Error('Время ожидания запроса истекло');
}
throw err;
}
clearTimeout(retryTimer);
}
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData.error || errorData.message || `Request failed with status ${response.status}`;