@orsetra/shared-ui 1.1.41 → 1.2.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.
Files changed (2) hide show
  1. package/lib/http-client.ts +157 -133
  2. package/package.json +6 -5
@@ -1,7 +1,21 @@
1
1
  // Simplified HTTP client without AWS Amplify dependencies
2
2
 
3
+ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
4
+
3
5
  export type AuthHeadersProvider = () => Promise<Record<string, string>> | Record<string, string>;
4
6
 
7
+ export type RequestInterceptor = (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
8
+ export type RequestErrorInterceptor = (error: any) => any;
9
+ export type ResponseInterceptor = (response: AxiosResponse) => AxiosResponse | Promise<AxiosResponse>;
10
+ export type ResponseErrorInterceptor = (error: any) => any;
11
+
12
+ export type Interceptors = {
13
+ onRequest?: RequestInterceptor;
14
+ onRequestError?: RequestErrorInterceptor;
15
+ onResponse?: ResponseInterceptor;
16
+ onResponseError?: ResponseErrorInterceptor;
17
+ };
18
+
5
19
  export class ApiError extends Error {
6
20
  status: number;
7
21
  code?: number;
@@ -21,32 +35,58 @@ export class ApiError extends Error {
21
35
  class HttpClient {
22
36
  private baseUrl: string;
23
37
  private authHeadersProvider?: AuthHeadersProvider;
38
+ private axiosInstance: AxiosInstance;
24
39
 
25
- private constructor(baseUrl: string, authHeadersProvider?: AuthHeadersProvider) {
40
+ private constructor(baseUrl: string, authHeadersProvider?: AuthHeadersProvider, interceptors?: Interceptors) {
26
41
  this.baseUrl = baseUrl || '';
27
42
  this.authHeadersProvider = authHeadersProvider;
43
+
44
+ // Create axios instance with base configuration
45
+ this.axiosInstance = axios.create({
46
+ baseURL: this.baseUrl,
47
+ timeout: 30000, // 30 seconds timeout
48
+ headers: {
49
+ 'Content-Type': 'application/json'
50
+ }
51
+ });
52
+
53
+ // Setup interceptors if provided
54
+ if (interceptors) {
55
+ this.setupInterceptors(interceptors);
56
+ }
57
+ }
58
+
59
+ private setupInterceptors(interceptors: Interceptors): void {
60
+ // Setup request interceptor
61
+ if (interceptors.onRequest || interceptors.onRequestError) {
62
+ this.axiosInstance.interceptors.request.use(
63
+ interceptors.onRequest || ((config) => config),
64
+ interceptors.onRequestError || ((error) => Promise.reject(error))
65
+ );
66
+ }
67
+
68
+ // Setup response interceptor
69
+ if (interceptors.onResponse || interceptors.onResponseError) {
70
+ this.axiosInstance.interceptors.response.use(
71
+ interceptors.onResponse || ((response) => response),
72
+ interceptors.onResponseError || ((error) => Promise.reject(error))
73
+ );
74
+ }
28
75
  }
29
76
 
30
- public static getInstance(baseUrl: string, authHeadersProvider?: AuthHeadersProvider): HttpClient {
31
- return new HttpClient(baseUrl, authHeadersProvider);
77
+ public static getInstance(baseUrl: string, authHeadersProvider?: AuthHeadersProvider, interceptors?: Interceptors): HttpClient {
78
+ return new HttpClient(baseUrl, authHeadersProvider, interceptors);
32
79
  }
33
80
 
34
- private getFullUrl(path: string): string {
35
- const cleanPath = path.startsWith('/') ? path.slice(1) : path;
36
- return `${this.baseUrl}/${cleanPath}`;
37
- }
38
81
 
39
- private async getAuthHeaders(): Promise<Headers> {
40
- const headers = new Headers();
41
- headers.set('Content-Type', 'application/json');
82
+ private async getAuthHeaders(): Promise<Record<string, string>> {
83
+ const headers: Record<string, string> = {};
42
84
 
43
85
  if (this.authHeadersProvider) {
44
86
  try {
45
87
  const authHeaders = await this.authHeadersProvider();
46
- Object.entries(authHeaders).forEach(([key, value]) => {
47
- headers.set(key, value);
48
- });
49
- } catch (error) {
88
+ Object.assign(headers, authHeaders);
89
+ } catch (error: unknown) {
50
90
  console.warn('Failed to get auth headers:', error);
51
91
  }
52
92
  }
@@ -54,184 +94,168 @@ class HttpClient {
54
94
  return headers;
55
95
  }
56
96
 
57
- async request<T>(url: string, options: RequestInit = {}): Promise<T | null> {
97
+ async request<T>(url: string, options: AxiosRequestConfig = {}): Promise<T | null> {
58
98
  const headers = await this.getAuthHeaders();
59
-
60
- // Fusionner avec les headers existants
61
99
  if (options.headers) {
62
- Object.entries(options.headers).forEach(([key, value]) => {
63
- headers.set(key, value as string);
64
- });
100
+ Object.assign(headers, options.headers);
65
101
  }
66
102
 
67
- const fullUrl = this.getFullUrl(url);
68
-
69
- const response = await fetch(fullUrl, {
70
- ...options,
71
- headers,
72
- });
103
+ try {
104
+ const response: AxiosResponse<T> = await this.axiosInstance.request<T>({
105
+ url,
106
+ ...options,
107
+ headers,
108
+ });
73
109
 
74
- if (!response.ok) {
75
- // Try to extract error message from API response
76
- let errorMessage = `Request failed: ${response.status}`;
77
- let errorBody: any = undefined;
78
- try {
79
- errorBody = await response.json();
80
- // Zitadel API returns { code, message, details } on error
110
+ // Handle 204 No Content
111
+ if (response.status === 204) {
112
+ return null;
113
+ }
114
+
115
+ return response.data;
116
+ } catch (error: unknown) {
117
+ if (axios.isAxiosError(error) && error.response) {
118
+ const response = error.response;
119
+
120
+ // Try to extract error message from API response
121
+ let errorMessage = `Request failed: ${response.status}`;
122
+ let errorBody: any = response.data;
123
+
81
124
  if (errorBody?.message) {
82
125
  errorMessage = errorBody.message;
83
126
  } else if (errorBody?.error) {
84
127
  errorMessage = errorBody.error;
85
128
  }
86
- } catch {
87
- // If we can't parse JSON, use the default message
88
- }
89
-
90
- const apiError = new ApiError(errorMessage, {
91
- status: response.status,
92
- code: typeof errorBody?.code === 'number' ? errorBody.code : undefined,
93
- details: errorBody?.details,
94
- raw: errorBody,
95
- });
96
129
 
97
- // Zitadel step-up requirement
98
- const isMfaRequired =
99
- apiError.code === 7 ||
100
- (typeof errorMessage === 'string' && errorMessage.toLowerCase().includes('mfa required'));
101
-
102
- if (isMfaRequired && typeof window !== 'undefined') {
103
- try {
104
- window.dispatchEvent(
105
- new CustomEvent('zitadel:mfa-required', {
106
- detail: { url, status: apiError.status, code: apiError.code, error: apiError.raw },
107
- })
108
- );
109
- } catch {
110
- // ignore event dispatch issues
111
- }
130
+ const apiError = new ApiError(errorMessage, {
131
+ status: response.status,
132
+ code: typeof errorBody?.code === 'number' ? errorBody.code : undefined,
133
+ details: errorBody?.details,
134
+ raw: errorBody,
135
+ });
136
+ throw apiError;
137
+ } else {
138
+ // For non-axios errors or network errors
139
+ throw new Error(error instanceof Error ? error.message : 'Unknown error occurred');
112
140
  }
113
-
114
- throw apiError;
115
- }
116
-
117
- if (response.status === 204) {
118
- return null;
119
141
  }
120
-
121
- return response.json();
122
142
  }
123
143
 
124
- async get<T>(url: string, options: RequestInit = {}): Promise<T | null> {
144
+ async get<T>(url: string, options: AxiosRequestConfig = {}): Promise<T | null> {
125
145
  return this.request<T>(url, {
126
146
  ...options,
127
147
  method: 'GET'
128
148
  });
129
149
  }
130
150
 
131
- async post<T>(url: string, body?: any, options: RequestInit = {}): Promise<T | null> {
151
+ async post<T>(url: string, body?: any, options: AxiosRequestConfig = {}): Promise<T | null> {
132
152
  return this.request<T>(url, {
133
153
  ...options,
134
154
  method: 'POST',
135
- body: body ? JSON.stringify(body) : undefined
155
+ data: body
136
156
  });
137
157
  }
138
158
 
139
- async put<T>(url: string, body?: any, options: RequestInit = {}): Promise<T | null> {
159
+ async put<T>(url: string, body?: any, options: AxiosRequestConfig = {}): Promise<T | null> {
140
160
  return this.request<T>(url, {
141
161
  ...options,
142
162
  method: 'PUT',
143
- body: body ? JSON.stringify(body) : undefined
163
+ data: body
144
164
  });
145
165
  }
146
166
 
147
- async patch<T>(url: string, body?: any, options: RequestInit = {}): Promise<T | null> {
167
+ async patch<T>(url: string, body?: any, options: AxiosRequestConfig = {}): Promise<T | null> {
148
168
  return this.request<T>(url, {
149
169
  ...options,
150
170
  method: 'PATCH',
151
- body: body ? JSON.stringify(body) : undefined
171
+ data: body
152
172
  });
153
173
  }
154
174
 
155
- async delete<T>(url: string, options: RequestInit = {}): Promise<T | null> {
175
+ async delete<T>(url: string, options: AxiosRequestConfig = {}): Promise<T | null> {
156
176
  return this.request<T>(url, {
157
177
  ...options,
158
178
  method: 'DELETE'
159
179
  });
160
180
  }
161
181
 
162
- async upload<T>(url: string, formData: FormData, options: RequestInit = {}): Promise<T> {
182
+ async upload<T>(url: string, formData: FormData, options: AxiosRequestConfig = {}): Promise<T> {
163
183
  const headers = await this.getAuthHeaders();
164
-
165
- if (headers.has('Content-Type')) {
166
- headers.delete('Content-Type');
167
- }
168
-
169
- const fullUrl = this.getFullUrl(url);
170
-
171
- const response = await fetch(fullUrl, {
172
- ...options,
173
- method: 'POST',
174
- body: formData,
175
- headers
176
- });
177
-
178
- if (!response.ok) {
179
- // Try to extract error message from API response
180
- let errorMessage = `Upload failed: ${response.status} ${response.statusText}`;
181
- try {
182
- const errorBody = await response.json();
183
- if (errorBody?.message) {
184
- errorMessage = errorBody.message;
185
- } else if (errorBody?.error) {
186
- errorMessage = errorBody.error;
184
+ try {
185
+ const response: AxiosResponse<T> = await this.axiosInstance.request<T>({
186
+ url,
187
+ method: 'POST',
188
+ data: formData,
189
+ headers,
190
+ ...options
191
+ });
192
+
193
+ if (response.status === 204) {
194
+ return null as unknown as T;
195
+ }
196
+
197
+ return response.data;
198
+ } catch (error: unknown) {
199
+ if (axios.isAxiosError(error) && error.response) {
200
+ const response = error.response;
201
+ let errorMessage = `Upload failed: ${response.status} ${response.statusText}`;
202
+
203
+ if (response.data) {
204
+ if (typeof response.data === 'object' && response.data?.message) {
205
+ errorMessage = response.data.message;
206
+ } else if (typeof response.data === 'object' && response.data?.error) {
207
+ errorMessage = response.data.error;
208
+ }
187
209
  }
188
- } catch {
189
- // Try text if JSON fails
190
- try {
191
- const errorText = await response.text();
192
- if (errorText) errorMessage = errorText;
193
- } catch { /* ignore */ }
210
+
211
+ throw new Error(errorMessage);
212
+ } else {
213
+ throw new Error(error instanceof Error ? error.message : 'Unknown upload error');
194
214
  }
195
- throw new Error(errorMessage);
196
- }
197
-
198
- if (response.status === 204) {
199
- return null as unknown as T;
200
215
  }
201
-
202
- return response.json();
203
216
  }
204
217
 
205
218
  async download(url: string): Promise<Blob> {
206
219
  const headers = await this.getAuthHeaders();
207
- const fullUrl = this.getFullUrl(url);
208
-
209
- const response = await fetch(fullUrl, {
210
- method: 'GET',
211
- headers,
212
- credentials: 'include'
213
- });
214
220
 
215
- if (!response.ok) {
216
- // Try to extract error message from API response
217
- let errorMessage = `Download failed: ${response.status} ${response.statusText}`;
218
- try {
219
- const errorBody = await response.json();
220
- if (errorBody?.message) {
221
- errorMessage = errorBody.message;
222
- } else if (errorBody?.error) {
223
- errorMessage = errorBody.error;
221
+ try {
222
+ const response = await this.axiosInstance.request({
223
+ url,
224
+ method: 'GET',
225
+ headers,
226
+ responseType: 'blob'
227
+ });
228
+
229
+ return response.data;
230
+ } catch (error: unknown) {
231
+ if (axios.isAxiosError(error) && error.response) {
232
+ const response = error.response;
233
+ let errorMessage = `Download failed: ${response.status} ${response.statusText}`;
234
+
235
+ if (response.data) {
236
+ try {
237
+ const errorText = await (response.data as Blob).text();
238
+ const errorBody = JSON.parse(errorText);
239
+ if (errorBody?.message) {
240
+ errorMessage = errorBody.message;
241
+ } else if (errorBody?.error) {
242
+ errorMessage = errorBody.error;
243
+ }
244
+ } catch {
245
+ // If JSON parsing fails, use default message
246
+ }
224
247
  }
225
- } catch { /* ignore */ }
226
- throw new Error(errorMessage);
248
+
249
+ throw new Error(errorMessage);
250
+ } else {
251
+ throw new Error(error instanceof Error ? error.message : 'Unknown download error');
252
+ }
227
253
  }
228
-
229
- return await response.blob();
230
254
  }
231
255
  }
232
256
 
233
- export function useHttpClient(baseUrl: string, authHeadersProvider?: AuthHeadersProvider) {
234
- return HttpClient.getInstance(baseUrl, authHeadersProvider);
257
+ export function useHttpClient(baseUrl: string, authHeadersProvider?: AuthHeadersProvider, interceptors?: Interceptors) {
258
+ return HttpClient.getInstance(baseUrl, authHeadersProvider, interceptors);
235
259
  }
236
260
 
237
261
  export default HttpClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.1.41",
3
+ "version": "1.2.0",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",
@@ -75,6 +75,8 @@
75
75
  "@radix-ui/react-toggle": "1.1.1",
76
76
  "@radix-ui/react-toggle-group": "1.1.1",
77
77
  "@radix-ui/react-tooltip": "1.1.6",
78
+ "@types/js-cookie": "^3.0.6",
79
+ "axios": "^1.13.2",
78
80
  "class-variance-authority": "^0.7.1",
79
81
  "clsx": "^2.1.1",
80
82
  "cmdk": "1.0.4",
@@ -82,15 +84,14 @@
82
84
  "embla-carousel-react": "8.5.1",
83
85
  "i18next": "^23.15.0",
84
86
  "input-otp": "1.4.1",
87
+ "js-cookie": "^3.0.5",
88
+ "js-yaml": "^4.1.1",
85
89
  "lodash": "^4.17.21",
86
90
  "lucide-react": "^0.454.0",
87
91
  "moment": "^2.30.1",
92
+ "monaco-editor": "^0.55.1",
88
93
  "next-themes": "^0.4.4",
89
94
  "react-avatar": "^5.0.3",
90
- "js-cookie": "^3.0.5",
91
- "js-yaml": "^4.1.1",
92
- "monaco-editor": "^0.55.1",
93
- "@types/js-cookie": "^3.0.6",
94
95
  "react-day-picker": "8.10.1",
95
96
  "react-easy-crop": "^5.0.8",
96
97
  "react-hook-form": "^7.54.0",