@ibdop/platform-kit 1.0.11 → 1.0.12
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/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useApi.d.ts +10 -7
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useShellAuth.d.ts +2 -5
- package/dist/hooks/useShellAuth.d.ts.map +1 -1
- package/dist/index.js +10 -10
- package/dist/index.mjs +870 -830
- 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/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/shellAuth.d.ts +43 -0
- package/dist/utils/shellAuth.d.ts.map +1 -0
- package/package.json +3 -5
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useApi.ts +76 -17
- package/src/hooks/useShellAuth.ts +6 -26
- package/src/services/api.ts +43 -43
- package/src/utils/index.ts +10 -0
- package/src/utils/shellAuth.ts +106 -0
|
@@ -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/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),
|
|
@@ -185,20 +171,34 @@ export function createApiClient(config: ApiConfig = {}): AxiosInstance {
|
|
|
185
171
|
}
|
|
186
172
|
)
|
|
187
173
|
|
|
188
|
-
// Response interceptor - обработка ошибок
|
|
174
|
+
// Response interceptor - обработка ошибок с retry
|
|
189
175
|
client.interceptors.response.use(
|
|
190
176
|
(response) => {
|
|
191
177
|
clientLogger.log(`Response ${response.status}:`, response.config.url)
|
|
192
178
|
return response
|
|
193
179
|
},
|
|
194
|
-
(error: AxiosError<ApiErrorResponse>) => {
|
|
180
|
+
async (error: AxiosError<ApiErrorResponse>) => {
|
|
195
181
|
const status = error.response?.status
|
|
196
182
|
const url = error.config?.url
|
|
183
|
+
const retryCount = (error.config as InternalAxiosRequestConfig & { _retryCount?: number })?._retryCount ?? 0
|
|
184
|
+
|
|
185
|
+
// Retry logic for retryable errors
|
|
186
|
+
if (isRetryableError(status) && retryCount < retries) {
|
|
187
|
+
const configWithRetry = error.config as InternalAxiosRequestConfig & { _retryCount?: number }
|
|
188
|
+
configWithRetry._retryCount = retryCount + 1
|
|
189
|
+
|
|
190
|
+
// Exponential backoff: delay * 2^retryCount
|
|
191
|
+
const backoffDelay = retryDelay * Math.pow(2, retryCount)
|
|
192
|
+
clientLogger.warn(`Retrying (${configWithRetry._retryCount}/${retries}) after ${backoffDelay}ms:`, url)
|
|
193
|
+
|
|
194
|
+
await sleep(backoffDelay)
|
|
195
|
+
return client.request(configWithRetry)
|
|
196
|
+
}
|
|
197
197
|
|
|
198
198
|
// Categorize errors
|
|
199
199
|
if (status === 401) {
|
|
200
200
|
clientLogger.warn('401 Unauthorized - triggering re-auth')
|
|
201
|
-
|
|
201
|
+
redirectToLogin()
|
|
202
202
|
} else if (status === 403) {
|
|
203
203
|
clientLogger.warn('403 Forbidden - insufficient permissions')
|
|
204
204
|
} else if (status === 404) {
|
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'
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
profile?: {
|
|
45
|
+
access_token?: string
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} | null {
|
|
49
|
+
const auth = getShellAuth()
|
|
50
|
+
|
|
51
|
+
if (!auth) {
|
|
52
|
+
return { isAuthenticated: false }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
isAuthenticated: auth.isAuthenticated,
|
|
57
|
+
user: auth.user ?? undefined,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Получить access token из shell auth
|
|
63
|
+
*
|
|
64
|
+
* @returns Access token или null
|
|
65
|
+
*/
|
|
66
|
+
export function getAccessToken(): string | null {
|
|
67
|
+
const auth = getShellAuth()
|
|
68
|
+
|
|
69
|
+
if (!auth?.isAuthenticated) {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return auth.user?.profile?.access_token ?? null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Проверить авторизован ли пользователь
|
|
78
|
+
*/
|
|
79
|
+
export function isAuthenticated(): boolean {
|
|
80
|
+
const auth = getShellAuth()
|
|
81
|
+
return auth?.isAuthenticated ?? false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Инициировать редирект на страницу логина
|
|
86
|
+
*/
|
|
87
|
+
export async function redirectToLogin(): Promise<void> {
|
|
88
|
+
const auth = getShellAuth()
|
|
89
|
+
|
|
90
|
+
if (auth?.signinRedirect) {
|
|
91
|
+
await auth.signinRedirect()
|
|
92
|
+
} else if (typeof window !== 'undefined') {
|
|
93
|
+
window.location.href = '/'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Выход из системы
|
|
99
|
+
*/
|
|
100
|
+
export async function logout(): Promise<void> {
|
|
101
|
+
const auth = getShellAuth()
|
|
102
|
+
|
|
103
|
+
if (auth?.removeUser) {
|
|
104
|
+
await auth.removeUser()
|
|
105
|
+
}
|
|
106
|
+
}
|