@ibdop/platform-kit 1.0.10 → 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.
@@ -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 getAuth()
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 = getAuth()
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 = getAuth()
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
- // Named exports for specific helpers
145
- export { getAuth }
124
+ // Re-export shell auth utilities for convenience
125
+ export { getShellAuth }
@@ -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
- handleAuthError()
201
+ redirectToLogin()
202
202
  } else if (status === 403) {
203
203
  clientLogger.warn('403 Forbidden - insufficient permissions')
204
204
  } else if (status === 404) {
@@ -225,3 +225,102 @@ export type MfeNameSource =
225
225
  | 'sessionStorage.mf-config'
226
226
  | 'import.meta.env.VITE_MFE_NAME'
227
227
  | 'import.meta.env.MFE_NAME'
228
+
229
+ // ==================== Feature Types ====================
230
+
231
+ /**
232
+ * Feature toggle for a user
233
+ */
234
+ export interface FeatureToggleInfo {
235
+ name: string
236
+ enabled: boolean
237
+ description?: string
238
+ roles?: string[]
239
+ percentage?: number
240
+ microfrontends?: string[]
241
+ userEnabled: boolean
242
+ mfDependencies?: string[]
243
+ }
244
+
245
+ /**
246
+ * Response from /api/features endpoint
247
+ */
248
+ export interface FeaturesResponse {
249
+ features: FeatureToggleInfo[]
250
+ totalCount: number
251
+ userRoles: string[]
252
+ }
253
+
254
+ /**
255
+ * Admin feature toggle (full info)
256
+ */
257
+ export interface FeatureToggleAdmin {
258
+ name: string
259
+ enabled: boolean
260
+ description: string
261
+ roles: string[]
262
+ percentage: number
263
+ microfrontends: string[]
264
+ mfDependencies: string[]
265
+ }
266
+
267
+ /**
268
+ * Admin response from /api/features/admin endpoint
269
+ */
270
+ export interface FeatureAdminResponse {
271
+ featureToggles: FeatureToggleAdmin[]
272
+ isAdmin: boolean
273
+ microfrontends: string[]
274
+ totalCount: number
275
+ }
276
+
277
+ /**
278
+ * Feature creation request
279
+ */
280
+ export interface FeatureCreateRequest {
281
+ name: string
282
+ enabled?: boolean
283
+ description?: string
284
+ roles?: string[]
285
+ percentage?: number
286
+ microfrontends?: string[]
287
+ }
288
+
289
+ /**
290
+ * Feature update request
291
+ */
292
+ export interface FeatureUpdateRequest {
293
+ enabled?: boolean
294
+ description?: string
295
+ roles?: string[]
296
+ percentage?: number
297
+ microfrontends?: string[]
298
+ }
299
+
300
+ /**
301
+ * Microfrontend with its features
302
+ */
303
+ export interface MicrofrontendWithFeatures {
304
+ name: string
305
+ description?: string
306
+ featureToggles: string[]
307
+ features: FeatureInfo[]
308
+ }
309
+
310
+ /**
311
+ * Feature info for a microfrontend
312
+ */
313
+ export interface FeatureInfo {
314
+ name: string
315
+ enabled: boolean
316
+ global: boolean
317
+ required: boolean
318
+ }
319
+
320
+ /**
321
+ * Response from /api/features/microfrontends endpoint
322
+ */
323
+ export interface MicrofrontendsFeaturesResponse {
324
+ microfrontends: MicrofrontendWithFeatures[]
325
+ totalCount: number
326
+ }
@@ -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
+ }