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 ADDED
@@ -0,0 +1,561 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ApiClientContext: () => ApiClientContext,
34
+ ApiClientProvider: () => ApiClientProvider,
35
+ TokenRefreshManager: () => TokenRefreshManager,
36
+ createApiClient: () => createApiClient,
37
+ createTokenRefreshManager: () => createTokenRefreshManager,
38
+ useApiClient: () => useApiClient
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/api-client.ts
43
+ var import_axios = __toESM(require("axios"), 1);
44
+ var import_react = require("react");
45
+
46
+ // src/token-refresh.ts
47
+ var defaultGetAccessToken = () => localStorage.getItem("accessToken");
48
+ var defaultGetRefreshToken = () => localStorage.getItem("refreshToken");
49
+ var defaultSetTokens = (accessToken, refreshToken) => {
50
+ localStorage.setItem("accessToken", accessToken);
51
+ if (refreshToken) {
52
+ localStorage.setItem("refreshToken", refreshToken);
53
+ }
54
+ };
55
+ var defaultClearTokens = () => {
56
+ localStorage.removeItem("accessToken");
57
+ localStorage.removeItem("refreshToken");
58
+ };
59
+ var defaultNavigateToLogin = () => {
60
+ defaultClearTokens();
61
+ window.location.href = "#/login";
62
+ };
63
+ var TokenRefreshManager = class {
64
+ /**
65
+ * Создаёт новый экземпляр TokenRefreshManager
66
+ *
67
+ * @param config - Конфигурация менеджера
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * const manager = new TokenRefreshManager({
72
+ * refreshTokenFn: myRefreshFunction,
73
+ * onNavigateToLogin: () => router.push('/login'),
74
+ * });
75
+ * ```
76
+ */
77
+ constructor(config) {
78
+ this.isRefreshing = false;
79
+ this.refreshPromise = null;
80
+ this.queuedRequests = [];
81
+ this.config = {
82
+ refreshTokenFn: config.refreshTokenFn,
83
+ onNavigateToLogin: config.onNavigateToLogin ?? defaultNavigateToLogin,
84
+ getAccessToken: config.getAccessToken ?? defaultGetAccessToken,
85
+ getRefreshToken: config.getRefreshToken ?? defaultGetRefreshToken,
86
+ setTokens: config.setTokens ?? defaultSetTokens,
87
+ clearTokens: config.clearTokens ?? defaultClearTokens
88
+ };
89
+ }
90
+ /**
91
+ * Получает текущий access токен
92
+ *
93
+ * @returns Access токен или null, если токен отсутствует
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * const token = manager.getAccessToken();
98
+ * if (token) {
99
+ * headers.Authorization = `Bearer ${token}`;
100
+ * }
101
+ * ```
102
+ */
103
+ getAccessToken() {
104
+ return this.config.getAccessToken();
105
+ }
106
+ /**
107
+ * Получает текущий refresh токен
108
+ *
109
+ * @returns Refresh токен или null, если токен отсутствует
110
+ */
111
+ getRefreshToken() {
112
+ return this.config.getRefreshToken();
113
+ }
114
+ /**
115
+ * Проверяет, идёт ли в данный момент процесс обновления токена
116
+ *
117
+ * Используйте для определения, нужно ли ставить запрос в очередь
118
+ * или инициировать новое обновление.
119
+ *
120
+ * @returns `true` если обновление в процессе, `false` если нет
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * if (manager.isRefreshInProgress()) {
125
+ * // Ждём завершения текущего обновления
126
+ * const newToken = await manager.waitForTokenRefresh();
127
+ * } else {
128
+ * // Инициируем новое обновление
129
+ * await manager.refreshToken();
130
+ * }
131
+ * ```
132
+ */
133
+ isRefreshInProgress() {
134
+ return this.isRefreshing;
135
+ }
136
+ /**
137
+ * Добавляет запрос в очередь ожидания обновления токена
138
+ *
139
+ * Возвращает промис, который разрешится с новым токеном после
140
+ * успешного обновления или будет отклонён при ошибке.
141
+ *
142
+ * @returns Промис с новым access токеном
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * // В response interceptor при множественных 401
147
+ * if (manager.isRefreshInProgress()) {
148
+ * try {
149
+ * const newToken = await manager.waitForTokenRefresh();
150
+ * // Повторить запрос с новым токеном
151
+ * } catch {
152
+ * // Обновление не удалось
153
+ * }
154
+ * }
155
+ * ```
156
+ */
157
+ waitForTokenRefresh() {
158
+ return new Promise((resolve, reject) => {
159
+ this.queuedRequests.push({ resolve, reject });
160
+ });
161
+ }
162
+ /**
163
+ * Выполняет обновление токена
164
+ *
165
+ * Если обновление уже в процессе, вернёт существующий промис.
166
+ * После обновления уведомляет все запросы в очереди.
167
+ *
168
+ * @returns Промис с результатом обновления
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * const result = await manager.refreshToken();
173
+ * if (result.ok) {
174
+ * console.log('Новый токен:', result.accessToken);
175
+ * } else {
176
+ * console.log('Ошибка обновления токена');
177
+ * }
178
+ * ```
179
+ */
180
+ async refreshToken() {
181
+ if (this.refreshPromise) {
182
+ return this.refreshPromise;
183
+ }
184
+ this.isRefreshing = true;
185
+ this.refreshPromise = this.performRefresh().finally(() => {
186
+ setTimeout(() => {
187
+ this.refreshPromise = null;
188
+ }, 100);
189
+ });
190
+ return this.refreshPromise;
191
+ }
192
+ /**
193
+ * Обрабатывает ошибку 401 — либо обновляет токен, либо перенаправляет на логин
194
+ *
195
+ * Основной метод для использования в interceptors. Автоматически:
196
+ * - Перенаправляет на логин при невалидном токене
197
+ * - Ставит запрос в очередь, если обновление уже идёт
198
+ * - Инициирует обновление, если ещё не запущено
199
+ *
200
+ * @param isInvalidToken - Если true, токен считается невалидным и будет выполнен переход на логин
201
+ * @returns Промис с новым access токеном или null при ошибке
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * // В axios response interceptor
206
+ * if (error.response?.status === 401) {
207
+ * const isInvalid = error.response.data?.message === 'Invalid token';
208
+ * const newToken = await manager.handle401(isInvalid);
209
+ *
210
+ * if (newToken) {
211
+ * // Повторить запрос с новым токеном
212
+ * error.config.headers.Authorization = `Bearer ${newToken}`;
213
+ * return axios(error.config);
214
+ * }
215
+ * // newToken === null означает переход на логин
216
+ * }
217
+ * ```
218
+ */
219
+ async handle401(isInvalidToken = false) {
220
+ if (isInvalidToken) {
221
+ this.config.onNavigateToLogin();
222
+ return null;
223
+ }
224
+ if (this.isRefreshing) {
225
+ return this.waitForTokenRefresh();
226
+ }
227
+ const result = await this.refreshToken();
228
+ if (!result.ok) {
229
+ this.config.onNavigateToLogin();
230
+ return null;
231
+ }
232
+ return result.accessToken || this.config.getAccessToken();
233
+ }
234
+ /**
235
+ * Принудительно перенаправляет на страницу логина
236
+ *
237
+ * Вызывает колбэк onNavigateToLogin из конфигурации.
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * // При необходимости принудительного выхода
242
+ * manager.navigateToLogin();
243
+ * ```
244
+ */
245
+ navigateToLogin() {
246
+ this.config.onNavigateToLogin();
247
+ }
248
+ /**
249
+ * Сбрасывает внутреннее состояние менеджера
250
+ *
251
+ * Полезно для тестирования или при необходимости
252
+ * принудительно сбросить очередь запросов.
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * // В тестах
257
+ * beforeEach(() => {
258
+ * manager.reset();
259
+ * });
260
+ * ```
261
+ */
262
+ reset() {
263
+ this.isRefreshing = false;
264
+ this.refreshPromise = null;
265
+ this.queuedRequests = [];
266
+ }
267
+ async performRefresh() {
268
+ try {
269
+ const result = await this.config.refreshTokenFn();
270
+ if (result.ok && result.accessToken) {
271
+ this.config.setTokens(result.accessToken, result.refreshToken);
272
+ this.notifyQueueSuccess(result.accessToken);
273
+ } else {
274
+ this.notifyQueueFailure(new Error("Token refresh failed"));
275
+ }
276
+ this.isRefreshing = false;
277
+ return result;
278
+ } catch (error) {
279
+ this.isRefreshing = false;
280
+ this.notifyQueueFailure(error);
281
+ return { ok: false };
282
+ }
283
+ }
284
+ notifyQueueSuccess(newToken) {
285
+ this.queuedRequests.forEach(({ resolve }) => resolve(newToken));
286
+ this.queuedRequests = [];
287
+ }
288
+ notifyQueueFailure(error) {
289
+ this.queuedRequests.forEach(({ reject }) => reject(error));
290
+ this.queuedRequests = [];
291
+ }
292
+ };
293
+ var createTokenRefreshManager = (config) => {
294
+ return new TokenRefreshManager(config);
295
+ };
296
+
297
+ // src/api-client.ts
298
+ var RETRY_KEY = /* @__PURE__ */ Symbol("retry");
299
+ var defaultNavigateToLogin2 = () => {
300
+ localStorage.removeItem("accessToken");
301
+ localStorage.removeItem("refreshToken");
302
+ window.location.href = "#/login";
303
+ };
304
+ var defaultDecodeJwt = (token) => {
305
+ try {
306
+ const base64Url = token.split(".")[1];
307
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
308
+ const jsonPayload = decodeURIComponent(
309
+ atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
310
+ );
311
+ return JSON.parse(jsonPayload);
312
+ } catch {
313
+ return null;
314
+ }
315
+ };
316
+ var createAxiosInstance = (baseUrl, tokenRefreshManager, onNavigateToLogin, timeout, decodeJwt) => {
317
+ const instance = import_axios.default.create({
318
+ baseURL: baseUrl,
319
+ timeout,
320
+ headers: {
321
+ "Content-Type": "application/json"
322
+ }
323
+ });
324
+ instance.interceptors.request.use(
325
+ (requestConfig) => {
326
+ const isRefreshRequest = requestConfig.url?.includes("auth/refresh-token");
327
+ const accessToken = tokenRefreshManager.getAccessToken();
328
+ const refreshToken = tokenRefreshManager.getRefreshToken();
329
+ const token = isRefreshRequest ? refreshToken : accessToken;
330
+ if (token) {
331
+ Object.assign(requestConfig.headers, {
332
+ Authorization: `Bearer ${token}`
333
+ });
334
+ }
335
+ const decodedToken = decodeJwt(accessToken || "");
336
+ if (decodedToken?.userId) {
337
+ Object.assign(requestConfig.headers, {
338
+ "X-User-Id": decodedToken.userId
339
+ });
340
+ }
341
+ return requestConfig;
342
+ },
343
+ (error) => Promise.reject(error)
344
+ );
345
+ instance.interceptors.response.use(
346
+ (response) => response,
347
+ async (error) => {
348
+ const originalRequest = error.config;
349
+ if (!originalRequest) {
350
+ return Promise.reject(error);
351
+ }
352
+ const isRefreshRequest = originalRequest.url?.includes("auth/refresh-token");
353
+ if (error.response?.status === 401) {
354
+ const responseData = error.response?.data;
355
+ const isInvalidToken = responseData?.message === "Invalid token";
356
+ if (isInvalidToken) {
357
+ onNavigateToLogin();
358
+ return Promise.reject(error);
359
+ }
360
+ if (isRefreshRequest) {
361
+ onNavigateToLogin();
362
+ return Promise.reject(error);
363
+ }
364
+ if (originalRequest[RETRY_KEY]) {
365
+ onNavigateToLogin();
366
+ return Promise.reject(error);
367
+ }
368
+ if (tokenRefreshManager.isRefreshInProgress()) {
369
+ try {
370
+ const newToken = await tokenRefreshManager.waitForTokenRefresh();
371
+ const retryConfig2 = { ...originalRequest };
372
+ Object.assign(retryConfig2.headers, {
373
+ Authorization: `Bearer ${newToken}`
374
+ });
375
+ return instance(retryConfig2);
376
+ } catch {
377
+ return Promise.reject(error);
378
+ }
379
+ }
380
+ const retryConfig = {
381
+ ...originalRequest,
382
+ [RETRY_KEY]: true
383
+ };
384
+ try {
385
+ const newToken = await tokenRefreshManager.handle401(false);
386
+ if (!newToken) {
387
+ return Promise.reject(new Error("Failed to refresh token"));
388
+ }
389
+ Object.assign(retryConfig.headers, {
390
+ Authorization: `Bearer ${newToken}`
391
+ });
392
+ return instance(retryConfig);
393
+ } catch (refreshError) {
394
+ onNavigateToLogin();
395
+ return Promise.reject(refreshError);
396
+ }
397
+ }
398
+ if (error.response?.status === 403) {
399
+ onNavigateToLogin();
400
+ return Promise.reject(error);
401
+ }
402
+ if (error.response?.status === 400 && isRefreshRequest) {
403
+ await new Promise((resolve) => {
404
+ setTimeout(resolve, 1e3);
405
+ });
406
+ const newToken = tokenRefreshManager.getAccessToken();
407
+ if (newToken) {
408
+ const retryConfig = { ...originalRequest };
409
+ Object.assign(retryConfig.headers, {
410
+ Authorization: `Bearer ${newToken}`
411
+ });
412
+ return instance(retryConfig);
413
+ }
414
+ onNavigateToLogin();
415
+ return Promise.reject(new Error("Failed to refresh token"));
416
+ }
417
+ return Promise.reject(error);
418
+ }
419
+ );
420
+ return instance;
421
+ };
422
+ var createApiService = (axiosInstance) => {
423
+ return {
424
+ get: (url, config) => {
425
+ const cacheBuster = `${url.includes("?") ? "&" : "?"}_t=${Date.now()}`;
426
+ return axiosInstance.get(url + cacheBuster, config).then((response) => response.data);
427
+ },
428
+ post: (url, data, config) => axiosInstance.post(url, data, config).then((response) => response.data),
429
+ put: (url, data, config) => axiosInstance.put(url, data, config).then((response) => response.data),
430
+ delete: (url, config) => axiosInstance.delete(url, config).then((response) => response.data),
431
+ axiosInstance
432
+ };
433
+ };
434
+ var createApiClient = (config) => {
435
+ const {
436
+ urls,
437
+ onNavigateToLogin = defaultNavigateToLogin2,
438
+ timeout = 1e4,
439
+ decodeJwt = defaultDecodeJwt
440
+ } = config;
441
+ const authAxiosInstance = import_axios.default.create({
442
+ baseURL: urls.authService,
443
+ timeout,
444
+ headers: {
445
+ "Content-Type": "application/json"
446
+ }
447
+ });
448
+ const refreshTokenFn = async () => {
449
+ try {
450
+ const refreshToken = localStorage.getItem("refreshToken");
451
+ if (!refreshToken) {
452
+ return { ok: false };
453
+ }
454
+ const response = await authAxiosInstance.post(
455
+ "/auth/refresh-token",
456
+ {},
457
+ {
458
+ headers: {
459
+ Authorization: `Bearer ${refreshToken}`
460
+ }
461
+ }
462
+ );
463
+ const { accessToken, refreshToken: newRefreshToken } = response.data;
464
+ return {
465
+ ok: true,
466
+ accessToken,
467
+ refreshToken: newRefreshToken
468
+ };
469
+ } catch {
470
+ return { ok: false };
471
+ }
472
+ };
473
+ const tokenRefreshManager = createTokenRefreshManager({
474
+ refreshTokenFn,
475
+ onNavigateToLogin
476
+ });
477
+ authAxiosInstance.interceptors.request.use(
478
+ (requestConfig) => {
479
+ const isRefreshRequest = requestConfig.url?.includes("auth/refresh-token");
480
+ const accessToken = tokenRefreshManager.getAccessToken();
481
+ const refreshToken = tokenRefreshManager.getRefreshToken();
482
+ const token = isRefreshRequest ? refreshToken : accessToken;
483
+ if (token && !requestConfig.headers.Authorization) {
484
+ Object.assign(requestConfig.headers, {
485
+ Authorization: `Bearer ${token}`
486
+ });
487
+ }
488
+ const decodedToken = decodeJwt(accessToken || "");
489
+ if (decodedToken?.userId) {
490
+ Object.assign(requestConfig.headers, {
491
+ "X-User-Id": decodedToken.userId
492
+ });
493
+ }
494
+ return requestConfig;
495
+ },
496
+ (error) => Promise.reject(error)
497
+ );
498
+ const userProgressAxiosInstance = createAxiosInstance(
499
+ urls.userProgressService,
500
+ tokenRefreshManager,
501
+ onNavigateToLogin,
502
+ timeout,
503
+ decodeJwt
504
+ );
505
+ const recommendationAxiosInstance = createAxiosInstance(
506
+ urls.recommendationService,
507
+ tokenRefreshManager,
508
+ onNavigateToLogin,
509
+ timeout,
510
+ decodeJwt
511
+ );
512
+ const partnerManagerAxiosInstance = createAxiosInstance(
513
+ urls.partnerManager,
514
+ tokenRefreshManager,
515
+ onNavigateToLogin,
516
+ timeout,
517
+ decodeJwt
518
+ );
519
+ const puzzleControllerAxiosInstance = createAxiosInstance(
520
+ urls.puzzleController,
521
+ tokenRefreshManager,
522
+ onNavigateToLogin,
523
+ timeout,
524
+ decodeJwt
525
+ );
526
+ const monolithAxiosInstance = createAxiosInstance(
527
+ urls.monolith,
528
+ tokenRefreshManager,
529
+ onNavigateToLogin,
530
+ timeout,
531
+ decodeJwt
532
+ );
533
+ return {
534
+ authService: createApiService(authAxiosInstance),
535
+ userProgressService: createApiService(userProgressAxiosInstance),
536
+ recommendationService: createApiService(recommendationAxiosInstance),
537
+ partnerManager: createApiService(partnerManagerAxiosInstance),
538
+ puzzleController: createApiService(puzzleControllerAxiosInstance),
539
+ monolith: createApiService(monolithAxiosInstance),
540
+ tokenRefreshManager
541
+ };
542
+ };
543
+ var ApiClientContext = (0, import_react.createContext)(null);
544
+ var useApiClient = () => {
545
+ const context = (0, import_react.useContext)(ApiClientContext);
546
+ if (!context) {
547
+ throw new Error("useApiClient must be used within ApiClientProvider");
548
+ }
549
+ return context;
550
+ };
551
+ var ApiClientProvider = ApiClientContext.Provider;
552
+ // Annotate the CommonJS export names for ESM import in node:
553
+ 0 && (module.exports = {
554
+ ApiClientContext,
555
+ ApiClientProvider,
556
+ TokenRefreshManager,
557
+ createApiClient,
558
+ createTokenRefreshManager,
559
+ useApiClient
560
+ });
561
+ //# sourceMappingURL=index.cjs.map