Edit README

This commit is contained in:
2026-01-09 20:38:53 +03:00
parent 7a906fa824
commit 57a825b866
16 changed files with 2996 additions and 20 deletions
+59 -5
View File
@@ -10,20 +10,74 @@ const apiClient = axios.create({
},
});
// Interceptor для обработки ошибок
let isRefreshing = false;
let failedQueue: any[] = [];
const processQueue = (error: any, token: string | null = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
// Interceptor для обработки ошибок и автоматического обновления токенов
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Redirect to login if unauthorized, but not if already on public pages
async (error) => {
const originalRequest = error.config;
// Если ошибка 401 и это не запрос на refresh и не повторный запрос
if (error.response?.status === 401 && !originalRequest._retry && originalRequest.url !== '/auth/refresh') {
// Проверяем, находимся ли на публичной странице
const publicPaths = ['/auth', '/verify-email', '/reset-password'];
const currentPath = window.location.pathname;
const isPublicPage = publicPaths.some(path => currentPath.startsWith(path));
if (!isPublicPage) {
if (isPublicPage) {
return Promise.reject(error);
}
if (isRefreshing) {
// Если refresh уже в процессе, добавляем запрос в очередь
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then(() => {
return apiClient(originalRequest);
})
.catch((err) => {
return Promise.reject(err);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
// Пытаемся обновить токен
await apiClient.post('/auth/refresh');
// Если успешно, обрабатываем очередь и повторяем оригинальный запрос
processQueue(null, 'refreshed');
isRefreshing = false;
return apiClient(originalRequest);
} catch (refreshError) {
// Если refresh не удался, очищаем очередь и редиректим на логин
processQueue(refreshError, null);
isRefreshing = false;
window.location.href = '/auth';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
+2 -1
View File
@@ -1,4 +1,5 @@
import apiClient from './api';
import type { User } from './userService';
export interface LoginData {
username: string;
@@ -47,7 +48,7 @@ export const authService = {
return response.data;
},
getCurrentUser: async () => {
getCurrentUser: async (): Promise<User> => {
const response = await apiClient.get('/users/me');
return response.data;
},
+53
View File
@@ -0,0 +1,53 @@
import apiClient from './api';
export interface User {
id: number;
display_name: string;
username: string;
email: string;
birth_day?: string;
description?: string;
avatar_url?: string;
is_active: boolean;
is_verified: boolean;
is_superuser: boolean;
}
export interface UserUpdate {
display_name?: string;
username?: string;
birth_day?: string;
description?: string;
}
export const userService = {
getCurrentUser: async (): Promise<User> => {
const response = await apiClient.get('/users/me');
return response.data;
},
updateProfile: async (data: UserUpdate): Promise<User> => {
const response = await apiClient.put('/users/me', data);
return response.data;
},
uploadAvatar: async (file: File): Promise<User> => {
const formData = new FormData();
formData.append('avatar', file);
const response = await apiClient.post('/users/me/avatar', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
},
deleteAvatar: async (): Promise<void> => {
await apiClient.delete('/users/me/avatar');
},
deleteAccount: async (): Promise<void> => {
await apiClient.delete('/users/me');
},
};