mbt-api-client 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +561 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +879 -0
- package/dist/index.d.ts +879 -0
- package/dist/index.js +519 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { AxiosRequestConfig, AxiosInstance } from 'axios';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Модуль управления обновлением токенов
|
|
6
|
+
*
|
|
7
|
+
* Реализует механизм обновления JWT токенов с очередью запросов.
|
|
8
|
+
* Предотвращает множественные одновременные запросы на обновление токена,
|
|
9
|
+
* когда несколько параллельных API-запросов получают ошибку 401.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // Создание менеджера с кастомными функциями
|
|
14
|
+
* const tokenManager = createTokenRefreshManager({
|
|
15
|
+
* refreshTokenFn: async () => {
|
|
16
|
+
* const response = await fetch('/api/auth/refresh');
|
|
17
|
+
* const data = await response.json();
|
|
18
|
+
* return { ok: true, accessToken: data.accessToken };
|
|
19
|
+
* },
|
|
20
|
+
* onNavigateToLogin: () => router.push('/login'),
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* // Использование с RTK Query или другими клиентами
|
|
24
|
+
* if (response.status === 401) {
|
|
25
|
+
* const newToken = await tokenManager.handle401();
|
|
26
|
+
* // Повторить запрос с новым токеном
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @module token-refresh
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Конфигурация менеджера обновления токенов
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const config: TokenRefreshConfig = {
|
|
38
|
+
* refreshTokenFn: async () => {
|
|
39
|
+
* // Ваша логика обновления токена
|
|
40
|
+
* return { ok: true, accessToken: 'new-token' };
|
|
41
|
+
* },
|
|
42
|
+
* onNavigateToLogin: () => window.location.href = '/login',
|
|
43
|
+
* getAccessToken: () => localStorage.getItem('accessToken'),
|
|
44
|
+
* getRefreshToken: () => localStorage.getItem('refreshToken'),
|
|
45
|
+
* setTokens: (access, refresh) => {
|
|
46
|
+
* localStorage.setItem('accessToken', access);
|
|
47
|
+
* if (refresh) localStorage.setItem('refreshToken', refresh);
|
|
48
|
+
* },
|
|
49
|
+
* clearTokens: () => {
|
|
50
|
+
* localStorage.removeItem('accessToken');
|
|
51
|
+
* localStorage.removeItem('refreshToken');
|
|
52
|
+
* },
|
|
53
|
+
* };
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
interface TokenRefreshConfig {
|
|
57
|
+
/**
|
|
58
|
+
* Функция для выполнения запроса на обновление токена
|
|
59
|
+
*
|
|
60
|
+
* Должна вернуть объект с `ok: true` и новым `accessToken` при успехе,
|
|
61
|
+
* или `ok: false` при ошибке.
|
|
62
|
+
*
|
|
63
|
+
* @returns Промис с результатом обновления токена
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* refreshTokenFn: async () => {
|
|
68
|
+
* try {
|
|
69
|
+
* const response = await axios.post('/auth/refresh-token', {}, {
|
|
70
|
+
* headers: { Authorization: `Bearer ${refreshToken}` }
|
|
71
|
+
* });
|
|
72
|
+
* return {
|
|
73
|
+
* ok: true,
|
|
74
|
+
* accessToken: response.data.accessToken,
|
|
75
|
+
* refreshToken: response.data.refreshToken,
|
|
76
|
+
* };
|
|
77
|
+
* } catch {
|
|
78
|
+
* return { ok: false };
|
|
79
|
+
* }
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
refreshTokenFn: () => Promise<TokenRefreshResult>;
|
|
84
|
+
/**
|
|
85
|
+
* Колбэк для перенаправления на страницу логина
|
|
86
|
+
*
|
|
87
|
+
* Вызывается когда:
|
|
88
|
+
* - Токен невалиден (message: 'Invalid token')
|
|
89
|
+
* - Обновление токена не удалось
|
|
90
|
+
* - Refresh token истёк
|
|
91
|
+
*
|
|
92
|
+
* @default Очищает localStorage и редиректит на '#/login'
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* onNavigateToLogin: () => {
|
|
97
|
+
* // Для React Router
|
|
98
|
+
* navigate('/login');
|
|
99
|
+
* // Или для Next.js
|
|
100
|
+
* router.push('/login');
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
onNavigateToLogin?: () => void;
|
|
105
|
+
/**
|
|
106
|
+
* Функция получения текущего access токена
|
|
107
|
+
*
|
|
108
|
+
* @default () => localStorage.getItem('accessToken')
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* // Для хранения в Redux
|
|
113
|
+
* getAccessToken: () => store.getState().auth.accessToken
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
getAccessToken?: () => string | null;
|
|
117
|
+
/**
|
|
118
|
+
* Функция получения текущего refresh токена
|
|
119
|
+
*
|
|
120
|
+
* @default () => localStorage.getItem('refreshToken')
|
|
121
|
+
*/
|
|
122
|
+
getRefreshToken?: () => string | null;
|
|
123
|
+
/**
|
|
124
|
+
* Функция сохранения новых токенов после обновления
|
|
125
|
+
*
|
|
126
|
+
* @param accessToken - Новый access токен
|
|
127
|
+
* @param refreshToken - Новый refresh токен (опционально)
|
|
128
|
+
*
|
|
129
|
+
* @default Сохраняет в localStorage
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* // Для хранения в Redux
|
|
134
|
+
* setTokens: (access, refresh) => {
|
|
135
|
+
* store.dispatch(setAuthTokens({ accessToken: access, refreshToken: refresh }));
|
|
136
|
+
* }
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
setTokens?: (accessToken: string, refreshToken?: string) => void;
|
|
140
|
+
/**
|
|
141
|
+
* Функция очистки токенов (при выходе или ошибке авторизации)
|
|
142
|
+
*
|
|
143
|
+
* @default Удаляет 'accessToken' и 'refreshToken' из localStorage
|
|
144
|
+
*/
|
|
145
|
+
clearTokens?: () => void;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Результат операции обновления токена
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* // Успешное обновление
|
|
153
|
+
* const success: TokenRefreshResult = {
|
|
154
|
+
* ok: true,
|
|
155
|
+
* accessToken: 'eyJhbGciOiJIUzI1...',
|
|
156
|
+
* refreshToken: 'dGhpcyBpcyBhIHJl...',
|
|
157
|
+
* };
|
|
158
|
+
*
|
|
159
|
+
* // Ошибка обновления
|
|
160
|
+
* const failure: TokenRefreshResult = { ok: false };
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
interface TokenRefreshResult {
|
|
164
|
+
/**
|
|
165
|
+
* Успешность операции обновления
|
|
166
|
+
*
|
|
167
|
+
* `true` - токен успешно обновлён
|
|
168
|
+
* `false` - ошибка обновления (пользователь будет перенаправлен на логин)
|
|
169
|
+
*/
|
|
170
|
+
ok: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Новый access токен (только при `ok: true`)
|
|
173
|
+
*/
|
|
174
|
+
accessToken?: string;
|
|
175
|
+
/**
|
|
176
|
+
* Новый refresh токен (опционально, только при `ok: true`)
|
|
177
|
+
*/
|
|
178
|
+
refreshToken?: string;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Менеджер обновления токенов с очередью запросов
|
|
182
|
+
*
|
|
183
|
+
* Решает проблему одновременного обновления токена при множественных
|
|
184
|
+
* параллельных запросах, получивших 401 ошибку.
|
|
185
|
+
*
|
|
186
|
+
* **Как это работает:**
|
|
187
|
+
* 1. Первый запрос с 401 инициирует обновление токена
|
|
188
|
+
* 2. Последующие запросы с 401 добавляются в очередь ожидания
|
|
189
|
+
* 3. После успешного обновления все запросы в очереди получают новый токен
|
|
190
|
+
* 4. При ошибке обновления все запросы получают ошибку
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```typescript
|
|
194
|
+
* // Создание менеджера
|
|
195
|
+
* const tokenManager = new TokenRefreshManager({
|
|
196
|
+
* refreshTokenFn: async () => {
|
|
197
|
+
* const res = await fetch('/api/refresh');
|
|
198
|
+
* const data = await res.json();
|
|
199
|
+
* return { ok: true, accessToken: data.token };
|
|
200
|
+
* },
|
|
201
|
+
* });
|
|
202
|
+
*
|
|
203
|
+
* // Использование в axios interceptor
|
|
204
|
+
* axios.interceptors.response.use(
|
|
205
|
+
* response => response,
|
|
206
|
+
* async error => {
|
|
207
|
+
* if (error.response?.status === 401) {
|
|
208
|
+
* const newToken = await tokenManager.handle401();
|
|
209
|
+
* if (newToken) {
|
|
210
|
+
* error.config.headers.Authorization = `Bearer ${newToken}`;
|
|
211
|
+
* return axios(error.config);
|
|
212
|
+
* }
|
|
213
|
+
* }
|
|
214
|
+
* return Promise.reject(error);
|
|
215
|
+
* }
|
|
216
|
+
* );
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
declare class TokenRefreshManager {
|
|
220
|
+
private isRefreshing;
|
|
221
|
+
private refreshPromise;
|
|
222
|
+
private queuedRequests;
|
|
223
|
+
private config;
|
|
224
|
+
/**
|
|
225
|
+
* Создаёт новый экземпляр TokenRefreshManager
|
|
226
|
+
*
|
|
227
|
+
* @param config - Конфигурация менеджера
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```typescript
|
|
231
|
+
* const manager = new TokenRefreshManager({
|
|
232
|
+
* refreshTokenFn: myRefreshFunction,
|
|
233
|
+
* onNavigateToLogin: () => router.push('/login'),
|
|
234
|
+
* });
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
constructor(config: TokenRefreshConfig);
|
|
238
|
+
/**
|
|
239
|
+
* Получает текущий access токен
|
|
240
|
+
*
|
|
241
|
+
* @returns Access токен или null, если токен отсутствует
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* const token = manager.getAccessToken();
|
|
246
|
+
* if (token) {
|
|
247
|
+
* headers.Authorization = `Bearer ${token}`;
|
|
248
|
+
* }
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
getAccessToken(): string | null;
|
|
252
|
+
/**
|
|
253
|
+
* Получает текущий refresh токен
|
|
254
|
+
*
|
|
255
|
+
* @returns Refresh токен или null, если токен отсутствует
|
|
256
|
+
*/
|
|
257
|
+
getRefreshToken(): string | null;
|
|
258
|
+
/**
|
|
259
|
+
* Проверяет, идёт ли в данный момент процесс обновления токена
|
|
260
|
+
*
|
|
261
|
+
* Используйте для определения, нужно ли ставить запрос в очередь
|
|
262
|
+
* или инициировать новое обновление.
|
|
263
|
+
*
|
|
264
|
+
* @returns `true` если обновление в процессе, `false` если нет
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```typescript
|
|
268
|
+
* if (manager.isRefreshInProgress()) {
|
|
269
|
+
* // Ждём завершения текущего обновления
|
|
270
|
+
* const newToken = await manager.waitForTokenRefresh();
|
|
271
|
+
* } else {
|
|
272
|
+
* // Инициируем новое обновление
|
|
273
|
+
* await manager.refreshToken();
|
|
274
|
+
* }
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
isRefreshInProgress(): boolean;
|
|
278
|
+
/**
|
|
279
|
+
* Добавляет запрос в очередь ожидания обновления токена
|
|
280
|
+
*
|
|
281
|
+
* Возвращает промис, который разрешится с новым токеном после
|
|
282
|
+
* успешного обновления или будет отклонён при ошибке.
|
|
283
|
+
*
|
|
284
|
+
* @returns Промис с новым access токеном
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```typescript
|
|
288
|
+
* // В response interceptor при множественных 401
|
|
289
|
+
* if (manager.isRefreshInProgress()) {
|
|
290
|
+
* try {
|
|
291
|
+
* const newToken = await manager.waitForTokenRefresh();
|
|
292
|
+
* // Повторить запрос с новым токеном
|
|
293
|
+
* } catch {
|
|
294
|
+
* // Обновление не удалось
|
|
295
|
+
* }
|
|
296
|
+
* }
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
waitForTokenRefresh(): Promise<string>;
|
|
300
|
+
/**
|
|
301
|
+
* Выполняет обновление токена
|
|
302
|
+
*
|
|
303
|
+
* Если обновление уже в процессе, вернёт существующий промис.
|
|
304
|
+
* После обновления уведомляет все запросы в очереди.
|
|
305
|
+
*
|
|
306
|
+
* @returns Промис с результатом обновления
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```typescript
|
|
310
|
+
* const result = await manager.refreshToken();
|
|
311
|
+
* if (result.ok) {
|
|
312
|
+
* console.log('Новый токен:', result.accessToken);
|
|
313
|
+
* } else {
|
|
314
|
+
* console.log('Ошибка обновления токена');
|
|
315
|
+
* }
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
refreshToken(): Promise<TokenRefreshResult>;
|
|
319
|
+
/**
|
|
320
|
+
* Обрабатывает ошибку 401 — либо обновляет токен, либо перенаправляет на логин
|
|
321
|
+
*
|
|
322
|
+
* Основной метод для использования в interceptors. Автоматически:
|
|
323
|
+
* - Перенаправляет на логин при невалидном токене
|
|
324
|
+
* - Ставит запрос в очередь, если обновление уже идёт
|
|
325
|
+
* - Инициирует обновление, если ещё не запущено
|
|
326
|
+
*
|
|
327
|
+
* @param isInvalidToken - Если true, токен считается невалидным и будет выполнен переход на логин
|
|
328
|
+
* @returns Промис с новым access токеном или null при ошибке
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* ```typescript
|
|
332
|
+
* // В axios response interceptor
|
|
333
|
+
* if (error.response?.status === 401) {
|
|
334
|
+
* const isInvalid = error.response.data?.message === 'Invalid token';
|
|
335
|
+
* const newToken = await manager.handle401(isInvalid);
|
|
336
|
+
*
|
|
337
|
+
* if (newToken) {
|
|
338
|
+
* // Повторить запрос с новым токеном
|
|
339
|
+
* error.config.headers.Authorization = `Bearer ${newToken}`;
|
|
340
|
+
* return axios(error.config);
|
|
341
|
+
* }
|
|
342
|
+
* // newToken === null означает переход на логин
|
|
343
|
+
* }
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
handle401(isInvalidToken?: boolean): Promise<string | null>;
|
|
347
|
+
/**
|
|
348
|
+
* Принудительно перенаправляет на страницу логина
|
|
349
|
+
*
|
|
350
|
+
* Вызывает колбэк onNavigateToLogin из конфигурации.
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```typescript
|
|
354
|
+
* // При необходимости принудительного выхода
|
|
355
|
+
* manager.navigateToLogin();
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
navigateToLogin(): void;
|
|
359
|
+
/**
|
|
360
|
+
* Сбрасывает внутреннее состояние менеджера
|
|
361
|
+
*
|
|
362
|
+
* Полезно для тестирования или при необходимости
|
|
363
|
+
* принудительно сбросить очередь запросов.
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```typescript
|
|
367
|
+
* // В тестах
|
|
368
|
+
* beforeEach(() => {
|
|
369
|
+
* manager.reset();
|
|
370
|
+
* });
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
reset(): void;
|
|
374
|
+
private performRefresh;
|
|
375
|
+
private notifyQueueSuccess;
|
|
376
|
+
private notifyQueueFailure;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Создаёт новый экземпляр TokenRefreshManager
|
|
380
|
+
*
|
|
381
|
+
* Фабричная функция для создания менеджера обновления токенов.
|
|
382
|
+
* Предпочтительный способ создания вместо прямого вызова конструктора.
|
|
383
|
+
*
|
|
384
|
+
* @param config - Конфигурация менеджера
|
|
385
|
+
* @returns Новый экземпляр TokenRefreshManager
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* ```typescript
|
|
389
|
+
* import { createTokenRefreshManager } from 'mbt-api-client';
|
|
390
|
+
*
|
|
391
|
+
* const tokenManager = createTokenRefreshManager({
|
|
392
|
+
* refreshTokenFn: async () => {
|
|
393
|
+
* const response = await fetch('/api/auth/refresh', {
|
|
394
|
+
* method: 'POST',
|
|
395
|
+
* headers: {
|
|
396
|
+
* Authorization: `Bearer ${localStorage.getItem('refreshToken')}`,
|
|
397
|
+
* },
|
|
398
|
+
* });
|
|
399
|
+
*
|
|
400
|
+
* if (!response.ok) {
|
|
401
|
+
* return { ok: false };
|
|
402
|
+
* }
|
|
403
|
+
*
|
|
404
|
+
* const data = await response.json();
|
|
405
|
+
* return {
|
|
406
|
+
* ok: true,
|
|
407
|
+
* accessToken: data.accessToken,
|
|
408
|
+
* refreshToken: data.refreshToken,
|
|
409
|
+
* };
|
|
410
|
+
* },
|
|
411
|
+
* onNavigateToLogin: () => {
|
|
412
|
+
* window.location.href = '/login';
|
|
413
|
+
* },
|
|
414
|
+
* });
|
|
415
|
+
*
|
|
416
|
+
* // Использование
|
|
417
|
+
* const newToken = await tokenManager.handle401();
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
declare const createTokenRefreshManager: (config: TokenRefreshConfig) => TokenRefreshManager;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* URL-адреса микросервисов API
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```typescript
|
|
427
|
+
* const urls: ApiClientUrls = {
|
|
428
|
+
* authService: 'https://auth.example.com',
|
|
429
|
+
* userProgressService: 'https://progress.example.com',
|
|
430
|
+
* recommendationService: 'https://rec.example.com',
|
|
431
|
+
* partnerManager: 'https://partners.example.com',
|
|
432
|
+
* puzzleController: 'https://puzzles.example.com',
|
|
433
|
+
* monolith: 'https://api.example.com',
|
|
434
|
+
* };
|
|
435
|
+
* ```
|
|
436
|
+
*/
|
|
437
|
+
interface ApiClientUrls {
|
|
438
|
+
/** URL сервиса авторизации (логин, регистрация, refresh токенов) */
|
|
439
|
+
authService: string;
|
|
440
|
+
/** URL сервиса прогресса пользователя */
|
|
441
|
+
userProgressService: string;
|
|
442
|
+
/** URL сервиса рекомендаций */
|
|
443
|
+
recommendationService: string;
|
|
444
|
+
/** URL сервиса управления партнёрами */
|
|
445
|
+
partnerManager: string;
|
|
446
|
+
/** URL контроллера пазлов/задач */
|
|
447
|
+
puzzleController: string;
|
|
448
|
+
/** URL основного монолитного сервиса */
|
|
449
|
+
monolith: string;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Конфигурация API клиента
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* ```typescript
|
|
456
|
+
* const config: ApiClientConfig = {
|
|
457
|
+
* urls: {
|
|
458
|
+
* authService: 'https://auth.example.com',
|
|
459
|
+
* userProgressService: 'https://progress.example.com',
|
|
460
|
+
* // ... остальные URL
|
|
461
|
+
* },
|
|
462
|
+
* onNavigateToLogin: () => {
|
|
463
|
+
* // Для React Router
|
|
464
|
+
* navigate('/login');
|
|
465
|
+
* },
|
|
466
|
+
* timeout: 15000,
|
|
467
|
+
* decodeJwt: (token) => {
|
|
468
|
+
* // Кастомный декодер JWT
|
|
469
|
+
* return jwtDecode(token);
|
|
470
|
+
* },
|
|
471
|
+
* };
|
|
472
|
+
* ```
|
|
473
|
+
*/
|
|
474
|
+
interface ApiClientConfig {
|
|
475
|
+
/**
|
|
476
|
+
* URL-адреса микросервисов
|
|
477
|
+
*/
|
|
478
|
+
urls: ApiClientUrls;
|
|
479
|
+
/**
|
|
480
|
+
* Колбэк для перенаправления на страницу логина
|
|
481
|
+
*
|
|
482
|
+
* Вызывается при:
|
|
483
|
+
* - Ошибке 401 с невалидным токеном
|
|
484
|
+
* - Ошибке 403 (доступ запрещён)
|
|
485
|
+
* - Неудачном обновлении токена
|
|
486
|
+
*
|
|
487
|
+
* @default Очищает localStorage и редиректит на '#/login'
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```typescript
|
|
491
|
+
* // React Router
|
|
492
|
+
* onNavigateToLogin: () => navigate('/login')
|
|
493
|
+
*
|
|
494
|
+
* // Next.js
|
|
495
|
+
* onNavigateToLogin: () => router.push('/login')
|
|
496
|
+
*
|
|
497
|
+
* // Vanilla JS
|
|
498
|
+
* onNavigateToLogin: () => { window.location.href = '/login' }
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
onNavigateToLogin?: () => void;
|
|
502
|
+
/**
|
|
503
|
+
* Таймаут запросов в миллисекундах
|
|
504
|
+
*
|
|
505
|
+
* @default 10000 (10 секунд)
|
|
506
|
+
*/
|
|
507
|
+
timeout?: number;
|
|
508
|
+
/**
|
|
509
|
+
* Функция декодирования JWT токена для извлечения userId
|
|
510
|
+
*
|
|
511
|
+
* Используется для добавления заголовка X-User-Id к запросам.
|
|
512
|
+
* Если не указана, используется встроенный base64 декодер.
|
|
513
|
+
*
|
|
514
|
+
* @param token - JWT токен для декодирования
|
|
515
|
+
* @returns Объект с userId или null при ошибке декодирования
|
|
516
|
+
*
|
|
517
|
+
* @default Встроенный base64 декодер
|
|
518
|
+
*
|
|
519
|
+
* @example
|
|
520
|
+
* ```typescript
|
|
521
|
+
* // С библиотекой jwt-decode
|
|
522
|
+
* import { jwtDecode } from 'jwt-decode';
|
|
523
|
+
*
|
|
524
|
+
* decodeJwt: (token) => {
|
|
525
|
+
* try {
|
|
526
|
+
* return jwtDecode<{ userId: string }>(token);
|
|
527
|
+
* } catch {
|
|
528
|
+
* return null;
|
|
529
|
+
* }
|
|
530
|
+
* }
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
533
|
+
decodeJwt?: (token: string) => {
|
|
534
|
+
userId?: string;
|
|
535
|
+
} | null;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Обёртка над axios для выполнения HTTP-запросов
|
|
539
|
+
*
|
|
540
|
+
* Предоставляет типизированные методы для всех HTTP-операций.
|
|
541
|
+
* Автоматически извлекает `data` из ответа axios.
|
|
542
|
+
*
|
|
543
|
+
* @example
|
|
544
|
+
* ```typescript
|
|
545
|
+
* interface User {
|
|
546
|
+
* id: string;
|
|
547
|
+
* name: string;
|
|
548
|
+
* email: string;
|
|
549
|
+
* }
|
|
550
|
+
*
|
|
551
|
+
* // GET запрос с типизацией
|
|
552
|
+
* const user = await apiService.get<User>('/users/me');
|
|
553
|
+
* console.log(user.name);
|
|
554
|
+
*
|
|
555
|
+
* // POST запрос
|
|
556
|
+
* const newUser = await apiService.post<User>('/users', {
|
|
557
|
+
* name: 'John',
|
|
558
|
+
* email: 'john@example.com',
|
|
559
|
+
* });
|
|
560
|
+
*
|
|
561
|
+
* // С дополнительными параметрами axios
|
|
562
|
+
* const data = await apiService.get<Data>('/endpoint', {
|
|
563
|
+
* params: { page: 1, limit: 10 },
|
|
564
|
+
* headers: { 'X-Custom-Header': 'value' },
|
|
565
|
+
* });
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
568
|
+
interface ApiService {
|
|
569
|
+
/**
|
|
570
|
+
* Выполняет GET-запрос
|
|
571
|
+
*
|
|
572
|
+
* Автоматически добавляет cache-buster параметр `_t` для предотвращения кэширования.
|
|
573
|
+
*
|
|
574
|
+
* @typeParam T - Тип данных ответа
|
|
575
|
+
* @param url - URL эндпоинта (относительно baseURL сервиса)
|
|
576
|
+
* @param config - Дополнительные параметры axios (headers, params и т.д.)
|
|
577
|
+
* @returns Промис с данными ответа
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* ```typescript
|
|
581
|
+
* // Простой GET
|
|
582
|
+
* const users = await service.get<User[]>('/users');
|
|
583
|
+
*
|
|
584
|
+
* // С query параметрами
|
|
585
|
+
* const filtered = await service.get<User[]>('/users', {
|
|
586
|
+
* params: { role: 'admin', active: true }
|
|
587
|
+
* });
|
|
588
|
+
* ```
|
|
589
|
+
*/
|
|
590
|
+
get: <T>(url: string, config?: AxiosRequestConfig) => Promise<T>;
|
|
591
|
+
/**
|
|
592
|
+
* Выполняет POST-запрос
|
|
593
|
+
*
|
|
594
|
+
* @typeParam T - Тип данных ответа
|
|
595
|
+
* @param url - URL эндпоинта (относительно baseURL сервиса)
|
|
596
|
+
* @param data - Тело запроса (будет сериализовано в JSON)
|
|
597
|
+
* @param config - Дополнительные параметры axios
|
|
598
|
+
* @returns Промис с данными ответа
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* ```typescript
|
|
602
|
+
* const newUser = await service.post<User>('/users', {
|
|
603
|
+
* name: 'John Doe',
|
|
604
|
+
* email: 'john@example.com',
|
|
605
|
+
* });
|
|
606
|
+
* ```
|
|
607
|
+
*/
|
|
608
|
+
post: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => Promise<T>;
|
|
609
|
+
/**
|
|
610
|
+
* Выполняет PUT-запрос
|
|
611
|
+
*
|
|
612
|
+
* @typeParam T - Тип данных ответа
|
|
613
|
+
* @param url - URL эндпоинта
|
|
614
|
+
* @param data - Тело запроса для обновления
|
|
615
|
+
* @param config - Дополнительные параметры axios
|
|
616
|
+
* @returns Промис с данными ответа
|
|
617
|
+
*
|
|
618
|
+
* @example
|
|
619
|
+
* ```typescript
|
|
620
|
+
* const updated = await service.put<User>('/users/123', {
|
|
621
|
+
* name: 'Jane Doe',
|
|
622
|
+
* });
|
|
623
|
+
* ```
|
|
624
|
+
*/
|
|
625
|
+
put: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => Promise<T>;
|
|
626
|
+
/**
|
|
627
|
+
* Выполняет DELETE-запрос
|
|
628
|
+
*
|
|
629
|
+
* @typeParam T - Тип данных ответа
|
|
630
|
+
* @param url - URL эндпоинта
|
|
631
|
+
* @param config - Дополнительные параметры axios
|
|
632
|
+
* @returns Промис с данными ответа
|
|
633
|
+
*
|
|
634
|
+
* @example
|
|
635
|
+
* ```typescript
|
|
636
|
+
* await service.delete<void>('/users/123');
|
|
637
|
+
* ```
|
|
638
|
+
*/
|
|
639
|
+
delete: <T>(url: string, config?: AxiosRequestConfig) => Promise<T>;
|
|
640
|
+
/**
|
|
641
|
+
* Доступ к нативному экземпляру axios
|
|
642
|
+
*
|
|
643
|
+
* Используйте для добавления кастомных interceptors или
|
|
644
|
+
* выполнения нестандартных запросов.
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* ```typescript
|
|
648
|
+
* // Добавление кастомного interceptor
|
|
649
|
+
* service.axiosInstance.interceptors.request.use(config => {
|
|
650
|
+
* config.headers['X-Request-Id'] = generateId();
|
|
651
|
+
* return config;
|
|
652
|
+
* });
|
|
653
|
+
*
|
|
654
|
+
* // Выполнение PATCH запроса
|
|
655
|
+
* await service.axiosInstance.patch('/users/123', { name: 'New Name' });
|
|
656
|
+
* ```
|
|
657
|
+
*/
|
|
658
|
+
axiosInstance: AxiosInstance;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Главный объект API клиента
|
|
662
|
+
*
|
|
663
|
+
* Содержит настроенные сервисы для каждого микросервиса
|
|
664
|
+
* и менеджер обновления токенов.
|
|
665
|
+
*
|
|
666
|
+
* @example
|
|
667
|
+
* ```typescript
|
|
668
|
+
* const apiClient = createApiClient({ urls: {...} });
|
|
669
|
+
*
|
|
670
|
+
* // Использование сервисов
|
|
671
|
+
* const user = await apiClient.userProgressService.get('/me');
|
|
672
|
+
* const puzzles = await apiClient.puzzleController.get('/puzzles');
|
|
673
|
+
*
|
|
674
|
+
* // Работа с токенами
|
|
675
|
+
* const currentToken = apiClient.tokenRefreshManager.getAccessToken();
|
|
676
|
+
* ```
|
|
677
|
+
*/
|
|
678
|
+
interface ApiClient {
|
|
679
|
+
/**
|
|
680
|
+
* Сервис авторизации
|
|
681
|
+
*
|
|
682
|
+
* Используется для логина, регистрации, обновления токенов.
|
|
683
|
+
*/
|
|
684
|
+
authService: ApiService;
|
|
685
|
+
/**
|
|
686
|
+
* Сервис прогресса пользователя
|
|
687
|
+
*
|
|
688
|
+
* Используется для получения и обновления прогресса обучения.
|
|
689
|
+
*/
|
|
690
|
+
userProgressService: ApiService;
|
|
691
|
+
/**
|
|
692
|
+
* Сервис рекомендаций
|
|
693
|
+
*
|
|
694
|
+
* Используется для получения персонализированных рекомендаций.
|
|
695
|
+
*/
|
|
696
|
+
recommendationService: ApiService;
|
|
697
|
+
/**
|
|
698
|
+
* Сервис управления партнёрами
|
|
699
|
+
*/
|
|
700
|
+
partnerManager: ApiService;
|
|
701
|
+
/**
|
|
702
|
+
* Контроллер пазлов/задач
|
|
703
|
+
*
|
|
704
|
+
* Используется для работы с учебными задачами и пазлами.
|
|
705
|
+
*/
|
|
706
|
+
puzzleController: ApiService;
|
|
707
|
+
/**
|
|
708
|
+
* Основной монолитный сервис
|
|
709
|
+
*
|
|
710
|
+
* Используется для запросов, не выделенных в отдельные микросервисы.
|
|
711
|
+
*/
|
|
712
|
+
monolith: ApiService;
|
|
713
|
+
/**
|
|
714
|
+
* Менеджер обновления токенов
|
|
715
|
+
*
|
|
716
|
+
* Предоставляет доступ к функциям работы с токенами.
|
|
717
|
+
* Можно использовать с RTK Query или другими HTTP-клиентами.
|
|
718
|
+
*
|
|
719
|
+
* @example
|
|
720
|
+
* ```typescript
|
|
721
|
+
* // Получение текущего токена
|
|
722
|
+
* const token = apiClient.tokenRefreshManager.getAccessToken();
|
|
723
|
+
*
|
|
724
|
+
* // Обработка 401 в кастомном клиенте
|
|
725
|
+
* if (response.status === 401) {
|
|
726
|
+
* const newToken = await apiClient.tokenRefreshManager.handle401();
|
|
727
|
+
* }
|
|
728
|
+
* ```
|
|
729
|
+
*/
|
|
730
|
+
tokenRefreshManager: TokenRefreshManager;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Создаёт настроенный API клиент
|
|
734
|
+
*
|
|
735
|
+
* Фабричная функция, которая создаёт полностью настроенный API клиент
|
|
736
|
+
* с поддержкой автоматического обновления токенов, обработкой ошибок
|
|
737
|
+
* авторизации и типизированными методами запросов.
|
|
738
|
+
*
|
|
739
|
+
* **Особенности:**
|
|
740
|
+
* - Автоматическое добавление Authorization заголовка
|
|
741
|
+
* - Автоматическое обновление токена при 401 ошибке
|
|
742
|
+
* - Очередь запросов при одновременном обновлении токена
|
|
743
|
+
* - Добавление X-User-Id заголовка из JWT
|
|
744
|
+
* - Cache-busting для GET запросов
|
|
745
|
+
*
|
|
746
|
+
* @param config - Конфигурация клиента
|
|
747
|
+
* @returns Настроенный API клиент
|
|
748
|
+
*
|
|
749
|
+
* @example
|
|
750
|
+
* ```typescript
|
|
751
|
+
* import { createApiClient } from 'mbt-api-client';
|
|
752
|
+
*
|
|
753
|
+
* const apiClient = createApiClient({
|
|
754
|
+
* urls: {
|
|
755
|
+
* authService: 'https://auth.example.com',
|
|
756
|
+
* userProgressService: 'https://progress.example.com',
|
|
757
|
+
* recommendationService: 'https://rec.example.com',
|
|
758
|
+
* partnerManager: 'https://partners.example.com',
|
|
759
|
+
* puzzleController: 'https://puzzles.example.com',
|
|
760
|
+
* monolith: 'https://api.example.com',
|
|
761
|
+
* },
|
|
762
|
+
* onNavigateToLogin: () => {
|
|
763
|
+
* // Очистка состояния и редирект
|
|
764
|
+
* store.dispatch(logout());
|
|
765
|
+
* navigate('/login');
|
|
766
|
+
* },
|
|
767
|
+
* timeout: 15000, // 15 секунд
|
|
768
|
+
* });
|
|
769
|
+
*
|
|
770
|
+
* // Использование
|
|
771
|
+
* try {
|
|
772
|
+
* const user = await apiClient.userProgressService.get<User>('/me');
|
|
773
|
+
* console.log('Пользователь:', user);
|
|
774
|
+
* } catch (error) {
|
|
775
|
+
* console.error('Ошибка:', error);
|
|
776
|
+
* }
|
|
777
|
+
* ```
|
|
778
|
+
*/
|
|
779
|
+
declare const createApiClient: (config: ApiClientConfig) => ApiClient;
|
|
780
|
+
/**
|
|
781
|
+
* React Context для API клиента
|
|
782
|
+
*
|
|
783
|
+
* Используется внутри ApiClientProvider для передачи клиента
|
|
784
|
+
* через дерево компонентов.
|
|
785
|
+
*
|
|
786
|
+
* @example
|
|
787
|
+
* ```typescript
|
|
788
|
+
* // Для кастомных случаев (обычно используйте ApiClientProvider)
|
|
789
|
+
* <ApiClientContext.Provider value={apiClient}>
|
|
790
|
+
* <App />
|
|
791
|
+
* </ApiClientContext.Provider>
|
|
792
|
+
* ```
|
|
793
|
+
*/
|
|
794
|
+
declare const ApiClientContext: react.Context<ApiClient | null>;
|
|
795
|
+
/**
|
|
796
|
+
* React хук для доступа к API клиенту
|
|
797
|
+
*
|
|
798
|
+
* Получает API клиент из контекста. Должен использоваться внутри
|
|
799
|
+
* компонента, обёрнутого в ApiClientProvider.
|
|
800
|
+
*
|
|
801
|
+
* @returns API клиент с типизированными сервисами
|
|
802
|
+
* @throws Error если используется вне ApiClientProvider
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* ```typescript
|
|
806
|
+
* import { useApiClient } from 'mbt-api-client';
|
|
807
|
+
*
|
|
808
|
+
* function UserProfile() {
|
|
809
|
+
* const { userProgressService } = useApiClient();
|
|
810
|
+
* const [user, setUser] = useState<User | null>(null);
|
|
811
|
+
*
|
|
812
|
+
* useEffect(() => {
|
|
813
|
+
* userProgressService.get<User>('/me')
|
|
814
|
+
* .then(setUser)
|
|
815
|
+
* .catch(console.error);
|
|
816
|
+
* }, []);
|
|
817
|
+
*
|
|
818
|
+
* return <div>{user?.name}</div>;
|
|
819
|
+
* }
|
|
820
|
+
*
|
|
821
|
+
* // С деструктуризацией нескольких сервисов
|
|
822
|
+
* function Dashboard() {
|
|
823
|
+
* const {
|
|
824
|
+
* userProgressService,
|
|
825
|
+
* recommendationService,
|
|
826
|
+
* puzzleController,
|
|
827
|
+
* } = useApiClient();
|
|
828
|
+
*
|
|
829
|
+
* // Параллельные запросы
|
|
830
|
+
* const [progress, recommendations, puzzles] = await Promise.all([
|
|
831
|
+
* userProgressService.get('/progress'),
|
|
832
|
+
* recommendationService.get('/for-me'),
|
|
833
|
+
* puzzleController.get('/puzzles'),
|
|
834
|
+
* ]);
|
|
835
|
+
* }
|
|
836
|
+
* ```
|
|
837
|
+
*/
|
|
838
|
+
declare const useApiClient: () => ApiClient;
|
|
839
|
+
/**
|
|
840
|
+
* React Provider для API клиента
|
|
841
|
+
*
|
|
842
|
+
* Оборачивает приложение для предоставления доступа к API клиенту
|
|
843
|
+
* через хук useApiClient.
|
|
844
|
+
*
|
|
845
|
+
* @example
|
|
846
|
+
* ```typescript
|
|
847
|
+
* import { createApiClient, ApiClientProvider } from 'mbt-api-client';
|
|
848
|
+
*
|
|
849
|
+
* // Создание клиента (обычно в отдельном файле)
|
|
850
|
+
* const apiClient = createApiClient({
|
|
851
|
+
* urls: { ... },
|
|
852
|
+
* });
|
|
853
|
+
*
|
|
854
|
+
* // В корне приложения
|
|
855
|
+
* function App() {
|
|
856
|
+
* return (
|
|
857
|
+
* <ApiClientProvider value={apiClient}>
|
|
858
|
+
* <Router>
|
|
859
|
+
* <Routes />
|
|
860
|
+
* </Router>
|
|
861
|
+
* </ApiClientProvider>
|
|
862
|
+
* );
|
|
863
|
+
* }
|
|
864
|
+
*
|
|
865
|
+
* // С React Query
|
|
866
|
+
* function App() {
|
|
867
|
+
* return (
|
|
868
|
+
* <QueryClientProvider client={queryClient}>
|
|
869
|
+
* <ApiClientProvider value={apiClient}>
|
|
870
|
+
* <Routes />
|
|
871
|
+
* </ApiClientProvider>
|
|
872
|
+
* </QueryClientProvider>
|
|
873
|
+
* );
|
|
874
|
+
* }
|
|
875
|
+
* ```
|
|
876
|
+
*/
|
|
877
|
+
declare const ApiClientProvider: react.Provider<ApiClient | null>;
|
|
878
|
+
|
|
879
|
+
export { type ApiClient, type ApiClientConfig, ApiClientContext, ApiClientProvider, type ApiClientUrls, type ApiService, type TokenRefreshConfig, TokenRefreshManager, type TokenRefreshResult, createApiClient, createTokenRefreshManager, useApiClient };
|