From a2b8094ff7af88f77d767ba31a91946614218711 Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Tue, 14 Jul 2026 23:50:40 +0700 Subject: [PATCH] add client --- src/api/client.ts | 270 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 src/api/client.ts diff --git a/src/api/client.ts b/src/api/client.ts new file mode 100644 index 0000000..1949546 --- /dev/null +++ b/src/api/client.ts @@ -0,0 +1,270 @@ +import axios, { type AxiosInstance, type InternalAxiosRequestConfig, AxiosError } from 'axios'; +import type { + LoginReq, + LoginRsp, + SignupReq, + SignupRsp, + RefreshPasswordReq, + RefreshPasswordRsp, + RefreshReq, + RefreshRsp, + GetUsersReq, + GetUsersRsp, + GetUserByIdReq, + GetUserByIdRsp, + GetMeReq, + GetMeRsp, + AddUserRoleReq, + AddUserRoleRsp, + DeleteUserRoleReq, + DeleteUserRoleRsp, + GetPermissionsReq, + GetPermissionsRsp, + UploadFileReq, + UploadFileRsp, + DownloadFileReq, + AddScenarioReq, + AddScenarioRsp, + GetMyScenariosReq, + GetMyScenariosRsp, + GetScenarioReq, + GetScenarioRsp, + UpdateScenarioReq, + UpdateScenarioRsp, + DeleteScenarioReq, + DeleteScenarioRsp, + AddScenarioPlaceReq, + AddScenarioPlaceRsp, + UpdateScenarioPlaceReq, + UpdateScenarioPlaceRsp, + DeleteScenarioPlaceReq, + DeleteScenarioPlaceRsp, + PingReq, + PingRsp, + EchoReq, + EchoRsp, +} from './generated/main'; + +// Типизированный ответ с error полем +type ApiResponse = T & { error?: string }; + +class ApiClient { + private client: AxiosInstance; + private baseUrl: string; + + constructor() { + this.baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080'; + this.client = axios.create({ + baseURL: this.baseUrl, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Интерсептор для токена + this.client.interceptors.request.use((config) => { + const token = localStorage.getItem('accessToken'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }); + + // Интерсептор для рефреша токена + this.client.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; + + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; + + try { + const refreshToken = localStorage.getItem('refreshToken'); + if (!refreshToken) { + throw new Error('No refresh token'); + } + + const response = await this.refresh({ refreshToken }); + localStorage.setItem('accessToken', response.accessToken!); + localStorage.setItem('refreshToken', response.refreshToken!); + + originalRequest.headers.Authorization = `Bearer ${response.accessToken}`; + return this.client(originalRequest); + } catch (refreshError) { + // Рефреш не удался — разлогиниваем + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + window.location.href = '/login'; + return Promise.reject(refreshError); + } + } + + return Promise.reject(error); + } + ); + } + + // ---------- AUTH ---------- + + async ping(params: PingReq = {}): Promise> { + const { data } = await this.client.get('/api/test/ping'); + return data; + } + + async echo(params: EchoReq): Promise> { + const { data } = await this.client.post('/api/test/echo', params); + return data; + } + + async signup(params: SignupReq): Promise> { + const { data } = await this.client.post('/api/auth/signup', params); + return data; + } + + async refreshPassword(params: RefreshPasswordReq): Promise> { + const { data } = await this.client.post('/api/auth/refresh-password', params); + return data; + } + + async login(params: LoginReq): Promise> { + const { data } = await this.client.post('/api/auth/login', params); + if (data.accessToken) { + localStorage.setItem('accessToken', data.accessToken); + if (data.refreshToken) { + localStorage.setItem('refreshToken', data.refreshToken); + } + } + return data; + } + + async refresh(params: RefreshReq): Promise> { + const { data } = await this.client.post('/api/auth/refresh', params); + return data; + } + + async logout(): Promise { + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + } + + // ---------- USERS ---------- + + async getUsers(params: GetUsersReq = {}): Promise> { + const { data } = await this.client.get('/api/users'); + return data; + } + + async getUserById(params: GetUserByIdReq): Promise> { + const { data } = await this.client.get(`/api/users/${params.id}`); + return data; + } + + async getMe(params: GetMeReq = {}): Promise> { + const { data } = await this.client.get('/api/users/me'); + return data; + } + + async addUserRole(params: AddUserRoleReq): Promise> { + const { id, role } = params; + const { data } = await this.client.post(`/api/users/${id}/role/add`, { role }); + return data; + } + + async deleteUserRole(params: DeleteUserRoleReq): Promise> { + const { id, role } = params; + const { data } = await this.client.post(`/api/users/${id}/role/delete`, { role }); + return data; + } + + // ---------- PERMISSIONS ---------- + + async getPermissions(params: GetPermissionsReq = {}): Promise> { + const { data } = await this.client.get('/api/ui/permissions'); + return data; + } + + // ---------- FILES ---------- + + async uploadFile(params: UploadFileReq): Promise> { + if (!params.data) { + throw new Error('File data is required'); + } + + const formData = new FormData(); + formData.append('filename', params.filename!); + // Преобразуем bytes в Blob + const dataArray = new Uint8Array(params.data); + const blob = new Blob([dataArray]); + formData.append('data', blob); + + const { data } = await this.client.post('/api/files/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + return data; + } + + async downloadFile(params: DownloadFileReq): Promise { + const response = await this.client.get(`/api/files/${params.filename}`, { + responseType: 'blob', + }); + return response.data; + } + + // ---------- SCENARIOS ---------- + + async addScenario(params: AddScenarioReq): Promise> { + const { data } = await this.client.post('/api/scenario', params); + return data; + } + + async getMyScenarios(params: GetMyScenariosReq = {}): Promise> { + const { data } = await this.client.get('/api/my-scenarios'); + return data; + } + + async getScenario(params: GetScenarioReq): Promise> { + const { data } = await this.client.get(`/api/scenarios/${params.id}`); + return data; + } + + async updateScenario(params: UpdateScenarioReq): Promise> { + const { id, ...body } = params; + const { data } = await this.client.put(`/api/scenarios/${id}`, body); + return data; + } + + async deleteScenario(params: DeleteScenarioReq): Promise> { + const { data } = await this.client.delete(`/api/scenarios/${params.id}`); + return data; + } + + // ---------- SCENARIO PLACES ---------- + + async addScenarioPlace(params: AddScenarioPlaceReq): Promise> { + const { id, place } = params; + const { data } = await this.client.post(`/api/scenarios/${id}/places`, place); + return data; + } + + async updateScenarioPlace(params: UpdateScenarioPlaceReq): Promise> { + const { id, code, place } = params; + const { data } = await this.client.put(`/api/scenarios/${id}/places/${code}`, place); + return data; + } + + async deleteScenarioPlace(params: DeleteScenarioPlaceReq): Promise> { + const { id, code } = params; + const { data } = await this.client.delete(`/api/scenarios/${id}/places/${code}`); + return data; + } +} + +// Единый экспорт +export const api = new ApiClient(); + +// Экспорт типов для удобства +export * from './generated/main';