@ibdop/platform-kit 1.0.11 → 1.0.13
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/README.md +0 -0
- package/dist/components/ErrorBoundary.d.ts +0 -0
- package/dist/components/ErrorBoundary.d.ts.map +0 -0
- package/dist/components/Notification.d.ts +0 -0
- package/dist/components/Notification.d.ts.map +0 -0
- package/dist/components/VersionInfo.d.ts +0 -0
- package/dist/components/VersionInfo.d.ts.map +0 -0
- package/dist/components/index.d.ts +0 -0
- package/dist/components/index.d.ts.map +0 -0
- package/dist/hooks/index.d.ts +3 -2
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useApi.d.ts +22 -8
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useFeatures.d.ts +0 -0
- package/dist/hooks/useFeatures.d.ts.map +0 -0
- package/dist/hooks/useInfoData.d.ts +0 -0
- package/dist/hooks/useInfoData.d.ts.map +0 -0
- package/dist/hooks/usePermissions.d.ts +0 -0
- package/dist/hooks/usePermissions.d.ts.map +0 -0
- package/dist/hooks/useShellAuth.d.ts +2 -5
- package/dist/hooks/useShellAuth.d.ts.map +1 -1
- package/dist/hooks/useV1Config.d.ts +0 -0
- package/dist/hooks/useV1Config.d.ts.map +0 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.d.ts.map +0 -0
- package/dist/index.js +10 -10
- package/dist/index.mjs +931 -866
- package/dist/index.umd.js +10 -10
- package/dist/services/api.d.ts +2 -0
- package/dist/services/api.d.ts.map +1 -1
- package/dist/services/index.d.ts +0 -0
- package/dist/services/index.d.ts.map +0 -0
- package/dist/services/logger.d.ts +0 -0
- package/dist/services/logger.d.ts.map +0 -0
- package/dist/types/index.d.ts +0 -23
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/mfeName.d.ts +0 -0
- package/dist/utils/mfeName.d.ts.map +0 -0
- package/dist/utils/shellAuth.d.ts +44 -0
- package/dist/utils/shellAuth.d.ts.map +1 -0
- package/package.json +3 -5
- package/src/components/ErrorBoundary.tsx +0 -0
- package/src/components/Notification.tsx +0 -0
- package/src/components/VersionInfo.tsx +0 -0
- package/src/components/index.ts +0 -0
- package/src/global.d.ts +0 -0
- package/src/hooks/index.ts +3 -2
- package/src/hooks/useApi.ts +114 -47
- package/src/hooks/useFeatures.ts +0 -0
- package/src/hooks/useInfoData.ts +0 -0
- package/src/hooks/usePermissions.ts +0 -0
- package/src/hooks/useShellAuth.ts +6 -26
- package/src/hooks/useV1Config.ts +0 -0
- package/src/index.ts +0 -0
- package/src/services/api.ts +47 -44
- package/src/services/index.ts +0 -0
- package/src/services/logger.ts +0 -0
- package/src/types/index.ts +1 -24
- package/src/utils/index.ts +10 -0
- package/src/utils/mfeName.ts +0 -0
- package/src/utils/shellAuth.ts +107 -0
package/src/hooks/useApi.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* обработкой ошибок и уведомлениями.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useState, useCallback } from 'react'
|
|
8
|
+
import { useState, useCallback, useRef, useEffect, useMemo } from 'react'
|
|
9
9
|
import type { ApiError, ApiResponse, NotificationPayload } from '../types'
|
|
10
|
+
import { api } from '../services/api'
|
|
10
11
|
|
|
11
12
|
// Development mode flag
|
|
12
13
|
const isDev = import.meta.env?.DEV === true || import.meta.env?.MODE === 'development'
|
|
@@ -104,6 +105,8 @@ export interface UseApiConfig<T = never> {
|
|
|
104
105
|
errorContext?: string
|
|
105
106
|
onSuccess?: (data: T) => void
|
|
106
107
|
onError?: (error: ApiError) => void
|
|
108
|
+
/** Auto-execute on mount (default: false) */
|
|
109
|
+
immediate?: boolean
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
/**
|
|
@@ -117,6 +120,14 @@ export interface UseApiResult<T> {
|
|
|
117
120
|
isSuccess: boolean
|
|
118
121
|
execute: () => Promise<T | null>
|
|
119
122
|
reset: () => void
|
|
123
|
+
abort: () => void
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if error is an abort error
|
|
128
|
+
*/
|
|
129
|
+
function isAbortError(error: unknown): boolean {
|
|
130
|
+
return error instanceof Error && error.name === 'AbortError'
|
|
120
131
|
}
|
|
121
132
|
|
|
122
133
|
/**
|
|
@@ -128,14 +139,14 @@ export interface UseApiResult<T> {
|
|
|
128
139
|
*
|
|
129
140
|
* @example
|
|
130
141
|
* ```tsx
|
|
131
|
-
* const { data, isLoading, isError, execute } = useApi<User[]>(
|
|
132
|
-
* () => fetch('/api/users').then(r => r.json()),
|
|
142
|
+
* const { data, isLoading, isError, execute, abort } = useApi<User[]>(
|
|
143
|
+
* (signal) => fetch('/api/users', { signal }).then(r => r.json()),
|
|
133
144
|
* { notifyOnError: true, errorContext: 'загрузка пользователей' }
|
|
134
145
|
* )
|
|
135
146
|
* ```
|
|
136
147
|
*/
|
|
137
148
|
export function useApi<T>(
|
|
138
|
-
request: () => Promise<ApiResponse<T>>,
|
|
149
|
+
request: (signal?: AbortSignal) => Promise<ApiResponse<T>>,
|
|
139
150
|
config: UseApiConfig<T> = {}
|
|
140
151
|
): UseApiResult<T> {
|
|
141
152
|
const {
|
|
@@ -151,15 +162,50 @@ export function useApi<T>(
|
|
|
151
162
|
const [error, setError] = useState<ApiError | null>(null)
|
|
152
163
|
const [isLoading, setIsLoading] = useState(false)
|
|
153
164
|
|
|
165
|
+
// AbortController ref for request cancellation
|
|
166
|
+
const abortControllerRef = useRef<AbortController | null>(null)
|
|
167
|
+
|
|
154
168
|
const isError = error !== null
|
|
155
169
|
const isSuccess = data !== null && !isLoading && !isError
|
|
156
170
|
|
|
171
|
+
// Cleanup on unmount
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
return () => {
|
|
174
|
+
if (abortControllerRef.current) {
|
|
175
|
+
abortControllerRef.current.abort()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}, [])
|
|
179
|
+
|
|
180
|
+
const abort = useCallback(() => {
|
|
181
|
+
if (abortControllerRef.current) {
|
|
182
|
+
abortControllerRef.current.abort()
|
|
183
|
+
abortControllerRef.current = null
|
|
184
|
+
setIsLoading(false)
|
|
185
|
+
logger.log('Request aborted')
|
|
186
|
+
}
|
|
187
|
+
}, [])
|
|
188
|
+
|
|
157
189
|
const execute = useCallback(async () => {
|
|
190
|
+
// Abort any pending request
|
|
191
|
+
if (abortControllerRef.current) {
|
|
192
|
+
abortControllerRef.current.abort()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Create new AbortController
|
|
196
|
+
abortControllerRef.current = new AbortController()
|
|
197
|
+
const signal = abortControllerRef.current.signal
|
|
198
|
+
|
|
158
199
|
setIsLoading(true)
|
|
159
200
|
setError(null)
|
|
160
201
|
|
|
161
202
|
try {
|
|
162
|
-
const response = await request()
|
|
203
|
+
const response = await request(signal)
|
|
204
|
+
|
|
205
|
+
// Check if aborted during request
|
|
206
|
+
if (signal.aborted) {
|
|
207
|
+
return null
|
|
208
|
+
}
|
|
163
209
|
|
|
164
210
|
if (response.ok) {
|
|
165
211
|
setData(response.data)
|
|
@@ -193,6 +239,12 @@ export function useApi<T>(
|
|
|
193
239
|
return null
|
|
194
240
|
}
|
|
195
241
|
} catch (err) {
|
|
242
|
+
// Ignore abort errors
|
|
243
|
+
if (isAbortError(err)) {
|
|
244
|
+
logger.log('Request was aborted')
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
|
|
196
248
|
// Handle network/server errors
|
|
197
249
|
const apiError = err as ApiError
|
|
198
250
|
setError(apiError)
|
|
@@ -204,7 +256,10 @@ export function useApi<T>(
|
|
|
204
256
|
onError?.(apiError)
|
|
205
257
|
return null
|
|
206
258
|
} finally {
|
|
207
|
-
|
|
259
|
+
if (!signal.aborted) {
|
|
260
|
+
setIsLoading(false)
|
|
261
|
+
}
|
|
262
|
+
abortControllerRef.current = null
|
|
208
263
|
}
|
|
209
264
|
}, [request, notifyOnError, notifyOnSuccess, successMessage, errorContext, onSuccess, onError])
|
|
210
265
|
|
|
@@ -222,91 +277,103 @@ export function useApi<T>(
|
|
|
222
277
|
isSuccess,
|
|
223
278
|
execute,
|
|
224
279
|
reset,
|
|
280
|
+
abort,
|
|
225
281
|
}
|
|
226
282
|
}
|
|
227
283
|
|
|
228
284
|
// Convenience hooks for common HTTP methods
|
|
229
285
|
|
|
230
286
|
/**
|
|
231
|
-
* GET
|
|
287
|
+
* GET request config with auto-execute support
|
|
288
|
+
*/
|
|
289
|
+
export interface UseGetConfig<T> extends UseApiConfig<T> {
|
|
290
|
+
/** Skip request (don't execute) */
|
|
291
|
+
skip?: boolean
|
|
292
|
+
/** Dynamic headers function */
|
|
293
|
+
headers?: () => Record<string, string>
|
|
294
|
+
/** Auto-execute on mount (default: true for useGet) */
|
|
295
|
+
immediate?: boolean
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* GET request with AbortController and auto-execute
|
|
232
300
|
*/
|
|
233
301
|
export function useGet<T>(
|
|
234
302
|
url: string,
|
|
235
|
-
|
|
236
|
-
config: UseApiConfig<T> = {}
|
|
303
|
+
config: UseGetConfig<T> = {}
|
|
237
304
|
): UseApiResult<T> {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
.
|
|
244
|
-
|
|
245
|
-
|
|
305
|
+
const { skip = false, headers, immediate = true, ...apiConfig } = config
|
|
306
|
+
|
|
307
|
+
const request = useMemo(() => {
|
|
308
|
+
return (signal?: AbortSignal) => {
|
|
309
|
+
const headersObj = headers ? headers() : {}
|
|
310
|
+
return api.get(url, {
|
|
311
|
+
headers: { 'Content-Type': 'application/json', ...headersObj },
|
|
312
|
+
signal,
|
|
246
313
|
})
|
|
314
|
+
.then((response) => {
|
|
315
|
+
return { data: response.data, status: response.status, ok: response.status >= 200 && response.status < 300 } as ApiResponse<T>
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
}, [url, headers])
|
|
247
319
|
|
|
248
|
-
|
|
320
|
+
const result = useApi(request, { ...apiConfig, immediate: false })
|
|
321
|
+
|
|
322
|
+
// Auto-execute on mount if not skipped
|
|
323
|
+
useEffect(() => {
|
|
324
|
+
if (immediate && !skip) {
|
|
325
|
+
result.execute()
|
|
326
|
+
}
|
|
327
|
+
}, [immediate, skip, url])
|
|
328
|
+
|
|
329
|
+
return result
|
|
249
330
|
}
|
|
250
331
|
|
|
251
332
|
/**
|
|
252
|
-
* POST запрос
|
|
333
|
+
* POST запрос с AbortController
|
|
253
334
|
*/
|
|
254
335
|
export function usePost<T>(
|
|
255
336
|
url: string,
|
|
256
337
|
data?: unknown,
|
|
257
338
|
config: UseApiConfig<T> = {}
|
|
258
339
|
): UseApiResult<T> {
|
|
259
|
-
const request = () =>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
body: data ? JSON.stringify(data) : undefined,
|
|
264
|
-
})
|
|
265
|
-
.then(async (response) => {
|
|
266
|
-
const responseData = await response.json()
|
|
267
|
-
return { data: responseData, status: response.status, ok: response.ok } as ApiResponse<T>
|
|
340
|
+
const request = (signal?: AbortSignal) =>
|
|
341
|
+
api.post(url, data, { signal })
|
|
342
|
+
.then((response) => {
|
|
343
|
+
return { data: response.data, status: response.status, ok: response.status >= 200 && response.status < 300 } as ApiResponse<T>
|
|
268
344
|
})
|
|
269
345
|
|
|
270
346
|
return useApi(request, config)
|
|
271
347
|
}
|
|
272
348
|
|
|
273
349
|
/**
|
|
274
|
-
* PUT запрос
|
|
350
|
+
* PUT запрос с AbortController
|
|
275
351
|
*/
|
|
276
352
|
export function usePut<T>(
|
|
277
353
|
url: string,
|
|
278
354
|
data?: unknown,
|
|
279
355
|
config: UseApiConfig<T> = {}
|
|
280
356
|
): UseApiResult<T> {
|
|
281
|
-
const request = () =>
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
body: data ? JSON.stringify(data) : undefined,
|
|
286
|
-
})
|
|
287
|
-
.then(async (response) => {
|
|
288
|
-
const responseData = await response.json()
|
|
289
|
-
return { data: responseData, status: response.status, ok: response.ok } as ApiResponse<T>
|
|
357
|
+
const request = (signal?: AbortSignal) =>
|
|
358
|
+
api.put(url, data, { signal })
|
|
359
|
+
.then((response) => {
|
|
360
|
+
return { data: response.data, status: response.status, ok: response.status >= 200 && response.status < 300 } as ApiResponse<T>
|
|
290
361
|
})
|
|
291
362
|
|
|
292
363
|
return useApi(request, config)
|
|
293
364
|
}
|
|
294
365
|
|
|
295
366
|
/**
|
|
296
|
-
* DELETE запрос
|
|
367
|
+
* DELETE запрос с AbortController
|
|
297
368
|
*/
|
|
298
369
|
export function useDel<T>(
|
|
299
370
|
url: string,
|
|
300
371
|
config: UseApiConfig<T> = {}
|
|
301
372
|
): UseApiResult<T> {
|
|
302
|
-
const request = () =>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
})
|
|
307
|
-
.then(async (response) => {
|
|
308
|
-
const responseData = await response.json()
|
|
309
|
-
return { data: responseData, status: response.status, ok: response.ok } as ApiResponse<T>
|
|
373
|
+
const request = (signal?: AbortSignal) =>
|
|
374
|
+
api.delete(url, { signal })
|
|
375
|
+
.then((response) => {
|
|
376
|
+
return { data: response.data, status: response.status, ok: response.status >= 200 && response.status < 300 } as ApiResponse<T>
|
|
310
377
|
})
|
|
311
378
|
|
|
312
379
|
return useApi(request, config)
|
package/src/hooks/useFeatures.ts
CHANGED
|
File without changes
|
package/src/hooks/useInfoData.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { useEffect, useState, useCallback, useRef } from 'react'
|
|
9
9
|
import type { ShellAuth } from '../types'
|
|
10
|
+
import { getShellAuth } from '../utils/shellAuth'
|
|
10
11
|
|
|
11
12
|
// Development mode flag
|
|
12
13
|
const isDev = import.meta.env?.DEV === true || import.meta.env?.MODE === 'development'
|
|
@@ -23,27 +24,6 @@ const logger = {
|
|
|
23
24
|
},
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
* Получить auth из window globals
|
|
28
|
-
*/
|
|
29
|
-
function getAuth(): ShellAuth | null {
|
|
30
|
-
if (typeof window === 'undefined') return null
|
|
31
|
-
|
|
32
|
-
// Try __SHELL_AUTH_INSTANCE__ first
|
|
33
|
-
const win = window as unknown as { __SHELL_AUTH_INSTANCE__?: ShellAuth }
|
|
34
|
-
if (win.__SHELL_AUTH_INSTANCE__) {
|
|
35
|
-
return win.__SHELL_AUTH_INSTANCE__
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Try __SHELL_AUTH__ format
|
|
39
|
-
const win2 = window as unknown as { __SHELL_AUTH__?: { authInstance?: ShellAuth } }
|
|
40
|
-
if (win2.__SHELL_AUTH__?.authInstance) {
|
|
41
|
-
return win2.__SHELL_AUTH__.authInstance
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return null
|
|
45
|
-
}
|
|
46
|
-
|
|
47
27
|
/**
|
|
48
28
|
* Централизованный хук для получения состояния авторизации из shell
|
|
49
29
|
*
|
|
@@ -66,7 +46,7 @@ export function useShellAuth(): ShellAuth {
|
|
|
66
46
|
const maxAttempts = 20 // 10 seconds max wait (20 * 500ms)
|
|
67
47
|
|
|
68
48
|
const getAuthState = useCallback((): ShellAuth | null => {
|
|
69
|
-
return
|
|
49
|
+
return getShellAuth()
|
|
70
50
|
}, [])
|
|
71
51
|
|
|
72
52
|
useEffect(() => {
|
|
@@ -118,7 +98,7 @@ export function useShellAuth(): ShellAuth {
|
|
|
118
98
|
isLoading: isLoading,
|
|
119
99
|
signinRedirect: async () => {
|
|
120
100
|
logger.log('Redirecting to login')
|
|
121
|
-
const shellAuth =
|
|
101
|
+
const shellAuth = getShellAuth()
|
|
122
102
|
if (shellAuth?.signinRedirect) {
|
|
123
103
|
await shellAuth.signinRedirect()
|
|
124
104
|
} else {
|
|
@@ -128,7 +108,7 @@ export function useShellAuth(): ShellAuth {
|
|
|
128
108
|
}
|
|
129
109
|
},
|
|
130
110
|
removeUser: async () => {
|
|
131
|
-
const shellAuth =
|
|
111
|
+
const shellAuth = getShellAuth()
|
|
132
112
|
if (shellAuth?.removeUser) {
|
|
133
113
|
await shellAuth.removeUser()
|
|
134
114
|
}
|
|
@@ -141,5 +121,5 @@ export function useShellAuth(): ShellAuth {
|
|
|
141
121
|
return result
|
|
142
122
|
}
|
|
143
123
|
|
|
144
|
-
//
|
|
145
|
-
export {
|
|
124
|
+
// Re-export shell auth utilities for convenience
|
|
125
|
+
export { getShellAuth }
|
package/src/hooks/useV1Config.ts
CHANGED
|
File without changes
|
package/src/index.ts
CHANGED
|
File without changes
|
package/src/services/api.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'
|
|
8
8
|
import type { ApiError, ApiResponse } from '../types'
|
|
9
|
+
import { getAuthState, redirectToLogin } from '../utils/shellAuth'
|
|
9
10
|
|
|
10
11
|
// MF Name for logging
|
|
11
12
|
const MF_NAME = 'platform-kit'
|
|
@@ -38,6 +39,8 @@ export interface ApiConfig {
|
|
|
38
39
|
timeout?: number
|
|
39
40
|
baseURL?: string
|
|
40
41
|
name?: string
|
|
42
|
+
retries?: number
|
|
43
|
+
retryDelay?: number
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/**
|
|
@@ -49,29 +52,6 @@ export interface ApiErrorResponse {
|
|
|
49
52
|
status?: number
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
/**
|
|
53
|
-
* Получить состояние авторизации из shell
|
|
54
|
-
*/
|
|
55
|
-
function getAuthState(): { isAuthenticated: boolean; user?: { profile?: { access_token?: string } } } | null {
|
|
56
|
-
if (typeof window === 'undefined') {
|
|
57
|
-
return { isAuthenticated: false }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Try __SHELL_AUTH_INSTANCE__
|
|
61
|
-
const shellAuth = (window as unknown as { __SHELL_AUTH_INSTANCE__?: { isAuthenticated: boolean; user?: { profile?: { access_token?: string } } } }).__SHELL_AUTH_INSTANCE__
|
|
62
|
-
if (shellAuth) {
|
|
63
|
-
return shellAuth
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Try __SHELL_AUTH__ format
|
|
67
|
-
const shellAuth2 = (window as unknown as { __SHELL_AUTH__?: { authInstance?: { isAuthenticated: boolean; user?: { profile?: { access_token?: string } } } } }).__SHELL_AUTH__
|
|
68
|
-
if (shellAuth2?.authInstance) {
|
|
69
|
-
return shellAuth2.authInstance
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return { isAuthenticated: false }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
55
|
/**
|
|
76
56
|
* Получить mfeName из window
|
|
77
57
|
*/
|
|
@@ -83,23 +63,6 @@ function getMfeName(): string {
|
|
|
83
63
|
return MF_NAME
|
|
84
64
|
}
|
|
85
65
|
|
|
86
|
-
/**
|
|
87
|
-
* Обработка ошибок авторизации - редирект на логин
|
|
88
|
-
*/
|
|
89
|
-
function handleAuthError(): void {
|
|
90
|
-
if (typeof window === 'undefined') return
|
|
91
|
-
|
|
92
|
-
const shellAuth = (window as unknown as { __SHELL_AUTH_INSTANCE__?: { signinRedirect?: () => Promise<void> } }).__SHELL_AUTH_INSTANCE__
|
|
93
|
-
|
|
94
|
-
if (shellAuth?.signinRedirect) {
|
|
95
|
-
shellAuth.signinRedirect().catch((err) => {
|
|
96
|
-
logger.error('Failed to redirect to login:', err)
|
|
97
|
-
})
|
|
98
|
-
} else {
|
|
99
|
-
window.location.href = '/'
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
66
|
/**
|
|
104
67
|
* Форматирование API ошибки в консистентную структуру
|
|
105
68
|
*/
|
|
@@ -130,6 +93,27 @@ function formatApiError(error: AxiosError<ApiErrorResponse>): ApiError {
|
|
|
130
93
|
}
|
|
131
94
|
}
|
|
132
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Default retry configuration
|
|
98
|
+
*/
|
|
99
|
+
const DEFAULT_RETRIES = 3
|
|
100
|
+
const DEFAULT_RETRY_DELAY = 1000
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if error is retryable
|
|
104
|
+
*/
|
|
105
|
+
function isRetryableError(status?: number): boolean {
|
|
106
|
+
// Retry on network errors, 5xx, and 429 (rate limit)
|
|
107
|
+
return !status || status >= 500 || status === 429
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sleep utility for retry delay
|
|
112
|
+
*/
|
|
113
|
+
function sleep(ms: number): Promise<void> {
|
|
114
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
115
|
+
}
|
|
116
|
+
|
|
133
117
|
/**
|
|
134
118
|
* Создание API клиента с interceptors
|
|
135
119
|
*
|
|
@@ -144,6 +128,8 @@ function formatApiError(error: AxiosError<ApiErrorResponse>): ApiError {
|
|
|
144
128
|
*/
|
|
145
129
|
export function createApiClient(config: ApiConfig = {}): AxiosInstance {
|
|
146
130
|
const clientName = config.name || MF_NAME
|
|
131
|
+
const retries = config.retries ?? DEFAULT_RETRIES
|
|
132
|
+
const retryDelay = config.retryDelay ?? DEFAULT_RETRY_DELAY
|
|
147
133
|
|
|
148
134
|
const clientLogger = {
|
|
149
135
|
log: (...args: unknown[]) => logger.log(`[API:${clientName}]`, ...args),
|
|
@@ -167,10 +153,13 @@ export function createApiClient(config: ApiConfig = {}): AxiosInstance {
|
|
|
167
153
|
|
|
168
154
|
// Attach token ONLY if user is authenticated
|
|
169
155
|
if (authState?.isAuthenticated) {
|
|
170
|
-
|
|
156
|
+
// Try user.access_token first (from react-oidc-context), then user.profile.access_token
|
|
157
|
+
const token = authState.user?.access_token || authState.user?.profile?.access_token
|
|
171
158
|
if (token && config.headers) {
|
|
172
159
|
config.headers.Authorization = `Bearer ${token}`
|
|
173
160
|
clientLogger.info('Auth token attached')
|
|
161
|
+
} else {
|
|
162
|
+
clientLogger.warn('User is authenticated but no token found')
|
|
174
163
|
}
|
|
175
164
|
} else {
|
|
176
165
|
clientLogger.info('User not authenticated - request without token')
|
|
@@ -185,20 +174,34 @@ export function createApiClient(config: ApiConfig = {}): AxiosInstance {
|
|
|
185
174
|
}
|
|
186
175
|
)
|
|
187
176
|
|
|
188
|
-
// Response interceptor - обработка ошибок
|
|
177
|
+
// Response interceptor - обработка ошибок с retry
|
|
189
178
|
client.interceptors.response.use(
|
|
190
179
|
(response) => {
|
|
191
180
|
clientLogger.log(`Response ${response.status}:`, response.config.url)
|
|
192
181
|
return response
|
|
193
182
|
},
|
|
194
|
-
(error: AxiosError<ApiErrorResponse>) => {
|
|
183
|
+
async (error: AxiosError<ApiErrorResponse>) => {
|
|
195
184
|
const status = error.response?.status
|
|
196
185
|
const url = error.config?.url
|
|
186
|
+
const retryCount = (error.config as InternalAxiosRequestConfig & { _retryCount?: number })?._retryCount ?? 0
|
|
187
|
+
|
|
188
|
+
// Retry logic for retryable errors
|
|
189
|
+
if (isRetryableError(status) && retryCount < retries) {
|
|
190
|
+
const configWithRetry = error.config as InternalAxiosRequestConfig & { _retryCount?: number }
|
|
191
|
+
configWithRetry._retryCount = retryCount + 1
|
|
192
|
+
|
|
193
|
+
// Exponential backoff: delay * 2^retryCount
|
|
194
|
+
const backoffDelay = retryDelay * Math.pow(2, retryCount)
|
|
195
|
+
clientLogger.warn(`Retrying (${configWithRetry._retryCount}/${retries}) after ${backoffDelay}ms:`, url)
|
|
196
|
+
|
|
197
|
+
await sleep(backoffDelay)
|
|
198
|
+
return client.request(configWithRetry)
|
|
199
|
+
}
|
|
197
200
|
|
|
198
201
|
// Categorize errors
|
|
199
202
|
if (status === 401) {
|
|
200
203
|
clientLogger.warn('401 Unauthorized - triggering re-auth')
|
|
201
|
-
|
|
204
|
+
redirectToLogin()
|
|
202
205
|
} else if (status === 403) {
|
|
203
206
|
clientLogger.warn('403 Forbidden - insufficient permissions')
|
|
204
207
|
} else if (status === 404) {
|
package/src/services/index.ts
CHANGED
|
File without changes
|
package/src/services/logger.ts
CHANGED
|
File without changes
|
package/src/types/index.ts
CHANGED
|
@@ -75,30 +75,7 @@ export interface ApiResponse<T> {
|
|
|
75
75
|
ok: boolean
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
* Interface для конфигурации useApi
|
|
80
|
-
*/
|
|
81
|
-
export interface UseApiConfig<T = never> {
|
|
82
|
-
notifyOnError?: boolean
|
|
83
|
-
notifyOnSuccess?: boolean
|
|
84
|
-
successMessage?: string
|
|
85
|
-
errorContext?: string
|
|
86
|
-
onSuccess?: (data: T) => void
|
|
87
|
-
onError?: (error: ApiError) => void
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Interface для результата useApi
|
|
92
|
-
*/
|
|
93
|
-
export interface UseApiResult<T> {
|
|
94
|
-
data: T | null
|
|
95
|
-
error: ApiError | null
|
|
96
|
-
isLoading: boolean
|
|
97
|
-
isError: boolean
|
|
98
|
-
isSuccess: boolean
|
|
99
|
-
execute: () => Promise<T | null>
|
|
100
|
-
reset: () => void
|
|
101
|
-
}
|
|
78
|
+
// Note: UseApiConfig and UseApiResult are exported from hooks/useApi.ts
|
|
102
79
|
|
|
103
80
|
// ==================== Info Types ====================
|
|
104
81
|
|
package/src/utils/index.ts
CHANGED
|
@@ -15,5 +15,15 @@ export {
|
|
|
15
15
|
type MfeNameProps,
|
|
16
16
|
} from './mfeName'
|
|
17
17
|
|
|
18
|
+
// Shell Auth utilities
|
|
19
|
+
export {
|
|
20
|
+
getShellAuth,
|
|
21
|
+
getAuthState,
|
|
22
|
+
getAccessToken,
|
|
23
|
+
isAuthenticated,
|
|
24
|
+
redirectToLogin,
|
|
25
|
+
logout,
|
|
26
|
+
} from './shellAuth'
|
|
27
|
+
|
|
18
28
|
// Re-export types
|
|
19
29
|
export type { MfeNameSource } from '../types'
|
package/src/utils/mfeName.ts
CHANGED
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell Auth Utilities - Централизованные утилиты для работы с Shell Auth
|
|
3
|
+
*
|
|
4
|
+
* Используются в hooks/useShellAuth.ts и services/api.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ShellAuth } from '../types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Получить auth instance из window globals
|
|
11
|
+
*
|
|
12
|
+
* Поддерживает два формата:
|
|
13
|
+
* - window.__SHELL_AUTH_INSTANCE__ (прямой доступ)
|
|
14
|
+
* - window.__SHELL_AUTH__.authInstance (вложенный формат)
|
|
15
|
+
*
|
|
16
|
+
* @returns ShellAuth instance или null
|
|
17
|
+
*/
|
|
18
|
+
export function getShellAuth(): ShellAuth | null {
|
|
19
|
+
if (typeof window === 'undefined') return null
|
|
20
|
+
|
|
21
|
+
// Try __SHELL_AUTH_INSTANCE__ first (direct format)
|
|
22
|
+
const win = window as unknown as { __SHELL_AUTH_INSTANCE__?: ShellAuth }
|
|
23
|
+
if (win.__SHELL_AUTH_INSTANCE__) {
|
|
24
|
+
return win.__SHELL_AUTH_INSTANCE__
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Try __SHELL_AUTH__ format (nested format)
|
|
28
|
+
const win2 = window as unknown as { __SHELL_AUTH__?: { authInstance?: ShellAuth } }
|
|
29
|
+
if (win2.__SHELL_AUTH__?.authInstance) {
|
|
30
|
+
return win2.__SHELL_AUTH__.authInstance
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Получить auth state (упрощённый формат для API interceptors)
|
|
38
|
+
*
|
|
39
|
+
* @returns Auth state с isAuthenticated и user
|
|
40
|
+
*/
|
|
41
|
+
export function getAuthState(): {
|
|
42
|
+
isAuthenticated: boolean
|
|
43
|
+
user?: {
|
|
44
|
+
access_token?: string
|
|
45
|
+
profile?: {
|
|
46
|
+
access_token?: string
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} | null {
|
|
50
|
+
const auth = getShellAuth()
|
|
51
|
+
|
|
52
|
+
if (!auth) {
|
|
53
|
+
return { isAuthenticated: false }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
isAuthenticated: auth.isAuthenticated,
|
|
58
|
+
user: auth.user ?? undefined,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Получить access token из shell auth
|
|
64
|
+
*
|
|
65
|
+
* @returns Access token или null
|
|
66
|
+
*/
|
|
67
|
+
export function getAccessToken(): string | null {
|
|
68
|
+
const auth = getShellAuth()
|
|
69
|
+
|
|
70
|
+
if (!auth?.isAuthenticated) {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return auth.user?.profile?.access_token ?? null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Проверить авторизован ли пользователь
|
|
79
|
+
*/
|
|
80
|
+
export function isAuthenticated(): boolean {
|
|
81
|
+
const auth = getShellAuth()
|
|
82
|
+
return auth?.isAuthenticated ?? false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Инициировать редирект на страницу логина
|
|
87
|
+
*/
|
|
88
|
+
export async function redirectToLogin(): Promise<void> {
|
|
89
|
+
const auth = getShellAuth()
|
|
90
|
+
|
|
91
|
+
if (auth?.signinRedirect) {
|
|
92
|
+
await auth.signinRedirect()
|
|
93
|
+
} else if (typeof window !== 'undefined') {
|
|
94
|
+
window.location.href = '/'
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Выход из системы
|
|
100
|
+
*/
|
|
101
|
+
export async function logout(): Promise<void> {
|
|
102
|
+
const auth = getShellAuth()
|
|
103
|
+
|
|
104
|
+
if (auth?.removeUser) {
|
|
105
|
+
await auth.removeUser()
|
|
106
|
+
}
|
|
107
|
+
}
|