@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.
Files changed (63) hide show
  1. package/README.md +0 -0
  2. package/dist/components/ErrorBoundary.d.ts +0 -0
  3. package/dist/components/ErrorBoundary.d.ts.map +0 -0
  4. package/dist/components/Notification.d.ts +0 -0
  5. package/dist/components/Notification.d.ts.map +0 -0
  6. package/dist/components/VersionInfo.d.ts +0 -0
  7. package/dist/components/VersionInfo.d.ts.map +0 -0
  8. package/dist/components/index.d.ts +0 -0
  9. package/dist/components/index.d.ts.map +0 -0
  10. package/dist/hooks/index.d.ts +3 -2
  11. package/dist/hooks/index.d.ts.map +1 -1
  12. package/dist/hooks/useApi.d.ts +22 -8
  13. package/dist/hooks/useApi.d.ts.map +1 -1
  14. package/dist/hooks/useFeatures.d.ts +0 -0
  15. package/dist/hooks/useFeatures.d.ts.map +0 -0
  16. package/dist/hooks/useInfoData.d.ts +0 -0
  17. package/dist/hooks/useInfoData.d.ts.map +0 -0
  18. package/dist/hooks/usePermissions.d.ts +0 -0
  19. package/dist/hooks/usePermissions.d.ts.map +0 -0
  20. package/dist/hooks/useShellAuth.d.ts +2 -5
  21. package/dist/hooks/useShellAuth.d.ts.map +1 -1
  22. package/dist/hooks/useV1Config.d.ts +0 -0
  23. package/dist/hooks/useV1Config.d.ts.map +0 -0
  24. package/dist/index.d.ts +0 -0
  25. package/dist/index.d.ts.map +0 -0
  26. package/dist/index.js +10 -10
  27. package/dist/index.mjs +931 -866
  28. package/dist/index.umd.js +10 -10
  29. package/dist/services/api.d.ts +2 -0
  30. package/dist/services/api.d.ts.map +1 -1
  31. package/dist/services/index.d.ts +0 -0
  32. package/dist/services/index.d.ts.map +0 -0
  33. package/dist/services/logger.d.ts +0 -0
  34. package/dist/services/logger.d.ts.map +0 -0
  35. package/dist/types/index.d.ts +0 -23
  36. package/dist/types/index.d.ts.map +1 -1
  37. package/dist/utils/index.d.ts +1 -0
  38. package/dist/utils/index.d.ts.map +1 -1
  39. package/dist/utils/mfeName.d.ts +0 -0
  40. package/dist/utils/mfeName.d.ts.map +0 -0
  41. package/dist/utils/shellAuth.d.ts +44 -0
  42. package/dist/utils/shellAuth.d.ts.map +1 -0
  43. package/package.json +3 -5
  44. package/src/components/ErrorBoundary.tsx +0 -0
  45. package/src/components/Notification.tsx +0 -0
  46. package/src/components/VersionInfo.tsx +0 -0
  47. package/src/components/index.ts +0 -0
  48. package/src/global.d.ts +0 -0
  49. package/src/hooks/index.ts +3 -2
  50. package/src/hooks/useApi.ts +114 -47
  51. package/src/hooks/useFeatures.ts +0 -0
  52. package/src/hooks/useInfoData.ts +0 -0
  53. package/src/hooks/usePermissions.ts +0 -0
  54. package/src/hooks/useShellAuth.ts +6 -26
  55. package/src/hooks/useV1Config.ts +0 -0
  56. package/src/index.ts +0 -0
  57. package/src/services/api.ts +47 -44
  58. package/src/services/index.ts +0 -0
  59. package/src/services/logger.ts +0 -0
  60. package/src/types/index.ts +1 -24
  61. package/src/utils/index.ts +10 -0
  62. package/src/utils/mfeName.ts +0 -0
  63. package/src/utils/shellAuth.ts +107 -0
@@ -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
- setIsLoading(false)
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
- params?: Record<string, unknown>,
236
- config: UseApiConfig<T> = {}
303
+ config: UseGetConfig<T> = {}
237
304
  ): UseApiResult<T> {
238
- const request = () =>
239
- fetch(url, params ? {
240
- method: 'GET',
241
- headers: { 'Content-Type': 'application/json' }
242
- } : {})
243
- .then(async (response) => {
244
- const data = await response.json()
245
- return { data, status: response.status, ok: response.ok } as ApiResponse<T>
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
- return useApi(request, config)
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
- fetch(url, {
261
- method: 'POST',
262
- headers: { 'Content-Type': 'application/json' },
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
- fetch(url, {
283
- method: 'PUT',
284
- headers: { 'Content-Type': 'application/json' },
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
- fetch(url, {
304
- method: 'DELETE',
305
- headers: { 'Content-Type': 'application/json' },
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)
File without changes
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 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 }
File without changes
package/src/index.ts CHANGED
File without changes
@@ -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
- const token = authState.user?.profile?.access_token
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
- handleAuthError()
204
+ redirectToLogin()
202
205
  } else if (status === 403) {
203
206
  clientLogger.warn('403 Forbidden - insufficient permissions')
204
207
  } else if (status === 404) {
File without changes
File without changes
@@ -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
 
@@ -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'
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
+ }