@feardread/feature-factory 3.0.2 → 4.0.2
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/bundle.min.js +2 -2
- package/dist/index.esm.js +3 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/factory/api.js +521 -56
- package/src/factory/cache.js +479 -27
- package/src/factory/index.js +171 -100
- package/src/factory/service.js +412 -18
- package/src/factory/state.js +430 -5
- package/src/factory/thunk.js +264 -38
- package/src/factories/api.js +0 -72
- package/src/factories/api.ts +0 -72
- package/src/factories/cache.js +0 -71
- package/src/factories/cache.ts +0 -71
- package/src/factories/factory.js +0 -149
- package/src/factories/factory.ts +0 -158
- package/src/factories/service.js +0 -28
- package/src/factories/service.ts +0 -28
- package/src/factories/state.js +0 -10
- package/src/factories/state.ts +0 -10
- package/src/factories/thunk.js +0 -53
- package/src/factories/thunk.ts +0 -53
- package/src/index.ts +0 -7
- package/tsconfig.json +0 -11
package/package.json
CHANGED
package/src/factory/api.js
CHANGED
|
@@ -1,72 +1,537 @@
|
|
|
1
|
-
import axios from
|
|
2
|
-
import qs from
|
|
3
|
-
import CacheFactory from
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import qs from 'qs';
|
|
3
|
+
import CacheFactory from './cache';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Environment-based configuration
|
|
7
|
+
*/
|
|
8
|
+
const CONFIG = {
|
|
9
|
+
// API Base URLs
|
|
10
|
+
baseUrls: {
|
|
11
|
+
production: process.env.REACT_APP_API_BASE_URL_PROD || 'https://fear.master.com/fear/api/',
|
|
12
|
+
development: process.env.REACT_APP_API_BASE_URL_DEV || 'http://localhost:4000/fear/api/',
|
|
13
|
+
test: process.env.REACT_APP_API_BASE_URL_TEST || 'http://localhost:3001/fear/api/',
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
// Token configuration
|
|
17
|
+
tokenNames: {
|
|
18
|
+
bearer: 'Authorization',
|
|
19
|
+
custom: process.env.REACT_APP_JWT_TOKEN_HEADER || 'x-token',
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// Cache keys
|
|
23
|
+
cacheKeys: {
|
|
24
|
+
auth: 'auth',
|
|
25
|
+
refreshToken: 'refresh_token',
|
|
26
|
+
userPrefs: 'user_preferences',
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Request configuration
|
|
30
|
+
timeout: parseInt(process.env.REACT_APP_API_TIMEOUT) || 30000,
|
|
31
|
+
retryAttempts: parseInt(process.env.REACT_APP_API_RETRY_ATTEMPTS) || 3,
|
|
32
|
+
retryDelay: parseInt(process.env.REACT_APP_API_RETRY_DELAY) || 1000,
|
|
33
|
+
};
|
|
5
34
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Gets the appropriate base URL for the current environment
|
|
37
|
+
* @returns {string} Base URL
|
|
38
|
+
*/
|
|
39
|
+
const getBaseUrl = () => {
|
|
40
|
+
const env = process.env.NODE_ENV || 'development';
|
|
41
|
+
return CONFIG.baseUrls[env] || CONFIG.baseUrls.development;
|
|
42
|
+
};
|
|
9
43
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Gets authentication data from cache
|
|
46
|
+
* @returns {Object|null} Auth data or null
|
|
47
|
+
*/
|
|
48
|
+
const getAuthData = async () => {
|
|
49
|
+
try {
|
|
50
|
+
const authData = await CacheFactory.local.get(CONFIG.cacheKeys.auth);
|
|
51
|
+
return authData && typeof authData === 'object' ? authData : null;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn('Failed to retrieve auth data:', error);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
13
57
|
|
|
14
|
-
|
|
15
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Checks if token is expired
|
|
60
|
+
* @param {string} token - JWT token
|
|
61
|
+
* @returns {boolean} Whether token is expired
|
|
62
|
+
*/
|
|
63
|
+
const isTokenExpired = (token) => {
|
|
64
|
+
if (!token) return true;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
68
|
+
const currentTime = Date.now() / 1000;
|
|
69
|
+
return payload.exp < currentTime;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Refreshes the authentication token
|
|
77
|
+
* @returns {Promise<string|null>} New token or null if refresh failed
|
|
78
|
+
*/
|
|
79
|
+
const refreshAuthToken = async () => {
|
|
80
|
+
try {
|
|
81
|
+
const authData = await getAuthData();
|
|
82
|
+
const refreshToken = authData?.refreshToken || await CacheFactory.local.get(CONFIG.cacheKeys.refreshToken);
|
|
83
|
+
|
|
84
|
+
if (!refreshToken) {
|
|
85
|
+
throw new Error('No refresh token available');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const response = await axios.post(`${getBaseUrl()}auth/refresh`, {
|
|
89
|
+
refreshToken,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const newAuthData = {
|
|
93
|
+
token: response.data.token,
|
|
94
|
+
refreshToken: response.data.refreshToken || refreshToken,
|
|
95
|
+
user: response.data.user,
|
|
96
|
+
expiresAt: response.data.expiresAt,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await CacheFactory.local.set(CONFIG.cacheKeys.auth, newAuthData);
|
|
100
|
+
return newAuthData.token;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error('Token refresh failed:', error);
|
|
103
|
+
await clearAuthData();
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Clears authentication data from cache
|
|
110
|
+
*/
|
|
111
|
+
const clearAuthData = async () => {
|
|
112
|
+
try {
|
|
113
|
+
await Promise.all([
|
|
114
|
+
CacheFactory.local.remove(CONFIG.cacheKeys.auth),
|
|
115
|
+
CacheFactory.local.remove(CONFIG.cacheKeys.refreshToken),
|
|
116
|
+
]);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.warn('Failed to clear auth data:', error);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Creates retry delay with exponential backoff
|
|
124
|
+
* @param {number} attempt - Current attempt number
|
|
125
|
+
* @returns {number} Delay in milliseconds
|
|
126
|
+
*/
|
|
127
|
+
const getRetryDelay = (attempt) => {
|
|
128
|
+
return CONFIG.retryDelay * Math.pow(2, attempt - 1);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Standardized error formatter
|
|
133
|
+
* @param {Object} error - Axios error object
|
|
134
|
+
* @returns {Object} Formatted error
|
|
135
|
+
*/
|
|
136
|
+
const formatError = (error) => {
|
|
137
|
+
const baseError = {
|
|
138
|
+
message: 'An unknown error occurred',
|
|
139
|
+
status: null,
|
|
140
|
+
code: null,
|
|
141
|
+
data: null,
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (error.response) {
|
|
146
|
+
// Server responded with error status
|
|
147
|
+
return {
|
|
148
|
+
...baseError,
|
|
149
|
+
message: error.response.data?.message || `HTTP Error ${error.response.status}`,
|
|
150
|
+
status: error.response.status,
|
|
151
|
+
code: error.response.data?.code || error.code,
|
|
152
|
+
data: error.response.data,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (error.request) {
|
|
157
|
+
// Request made but no response received
|
|
158
|
+
return {
|
|
159
|
+
...baseError,
|
|
160
|
+
message: 'Network error - no response received',
|
|
161
|
+
code: 'NETWORK_ERROR',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Something else happened
|
|
166
|
+
return {
|
|
167
|
+
...baseError,
|
|
168
|
+
message: error.message || 'Request configuration error',
|
|
169
|
+
code: error.code || 'CONFIG_ERROR',
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Request interceptor with retry logic
|
|
175
|
+
* @param {Object} config - Axios config
|
|
176
|
+
* @returns {Promise<Object>} Modified config
|
|
177
|
+
*/
|
|
178
|
+
const requestInterceptor = async (config) => {
|
|
179
|
+
try {
|
|
180
|
+
// Set retry metadata
|
|
181
|
+
config.metadata = { startTime: Date.now() };
|
|
182
|
+
config._retry = config._retry || 0;
|
|
183
|
+
|
|
184
|
+
// Get authentication data
|
|
185
|
+
const authData = await getAuthData();
|
|
186
|
+
let token = authData?.token;
|
|
187
|
+
|
|
188
|
+
// Check if token needs refresh
|
|
189
|
+
if (token && isTokenExpired(token) && config._retry === 0) {
|
|
190
|
+
token = await refreshAuthToken();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Set authorization headers
|
|
194
|
+
if (token) {
|
|
195
|
+
config.headers[CONFIG.tokenNames.bearer] = `Bearer ${token}`;
|
|
196
|
+
config.headers[CONFIG.tokenNames.custom] = token;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Add request ID for tracking
|
|
200
|
+
config.headers['X-Request-ID'] = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
201
|
+
|
|
202
|
+
// Add client info
|
|
203
|
+
config.headers['X-Client-Version'] = process.env.REACT_APP_VERSION || '1.0.0';
|
|
204
|
+
config.headers['X-Client-Platform'] = 'web';
|
|
205
|
+
|
|
206
|
+
return config;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error('Request interceptor error:', error);
|
|
209
|
+
return config;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Request error interceptor
|
|
215
|
+
* @param {Object} error - Request error
|
|
216
|
+
* @returns {Promise<Object>} Rejected promise
|
|
217
|
+
*/
|
|
218
|
+
const requestErrorInterceptor = (error) => {
|
|
219
|
+
console.error('Request setup error:', error);
|
|
220
|
+
return Promise.reject(formatError(error));
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Response interceptor with success handling
|
|
225
|
+
* @param {Object} response - Axios response
|
|
226
|
+
* @returns {Object} Processed response
|
|
227
|
+
*/
|
|
228
|
+
const responseInterceptor = (response) => {
|
|
229
|
+
// Add response metadata
|
|
230
|
+
response.metadata = {
|
|
231
|
+
responseTime: Date.now() - (response.config.metadata?.startTime || Date.now()),
|
|
232
|
+
requestId: response.config.headers['X-Request-ID'],
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// Log successful responses in development
|
|
236
|
+
if (process.env.NODE_ENV === 'development') {
|
|
237
|
+
console.log(`✅ API Success [${response.status}]:`, {
|
|
238
|
+
url: response.config.url,
|
|
239
|
+
method: response.config.method?.toUpperCase(),
|
|
240
|
+
responseTime: response.metadata.responseTime,
|
|
241
|
+
requestId: response.metadata.requestId,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Validate response structure
|
|
246
|
+
if (response.data && typeof response.data === 'object') {
|
|
247
|
+
// Handle different success status codes
|
|
248
|
+
if ([200, 201, 202, 204].includes(response.status)) {
|
|
249
|
+
return response;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Handle edge cases
|
|
254
|
+
if (response.status >= 200 && response.status < 300) {
|
|
255
|
+
return response;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Unexpected status code
|
|
259
|
+
return Promise.reject(formatError({
|
|
260
|
+
response,
|
|
261
|
+
message: `Unexpected response status: ${response.status}`,
|
|
262
|
+
}));
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Response error interceptor with retry and auth handling
|
|
267
|
+
* @param {Object} error - Response error
|
|
268
|
+
* @returns {Promise<Object>} Retry attempt or rejected promise
|
|
269
|
+
*/
|
|
270
|
+
const responseErrorInterceptor = async (error) => {
|
|
271
|
+
const originalRequest = error.config;
|
|
272
|
+
|
|
273
|
+
// Format error for consistent handling
|
|
274
|
+
const formattedError = formatError(error);
|
|
275
|
+
|
|
276
|
+
// Log errors in development
|
|
277
|
+
if (process.env.NODE_ENV === 'development') {
|
|
278
|
+
console.error(`❌ API Error [${formattedError.status}]:`, {
|
|
279
|
+
url: originalRequest?.url,
|
|
280
|
+
method: originalRequest?.method?.toUpperCase(),
|
|
281
|
+
message: formattedError.message,
|
|
282
|
+
requestId: originalRequest?.headers['X-Request-ID'],
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Handle authentication errors
|
|
287
|
+
if (formattedError.status === 401 && !originalRequest._isRetryingAuth) {
|
|
288
|
+
originalRequest._isRetryingAuth = true;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const newToken = await refreshAuthToken();
|
|
292
|
+
if (newToken) {
|
|
293
|
+
// Retry original request with new token
|
|
294
|
+
originalRequest.headers[CONFIG.tokenNames.bearer] = `Bearer ${newToken}`;
|
|
295
|
+
originalRequest.headers[CONFIG.tokenNames.custom] = newToken;
|
|
296
|
+
return apiInstance(originalRequest);
|
|
297
|
+
}
|
|
298
|
+
} catch (refreshError) {
|
|
299
|
+
console.error('Auth refresh failed:', refreshError);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Clear auth data and redirect to login
|
|
303
|
+
await clearAuthData();
|
|
304
|
+
|
|
305
|
+
// Dispatch auth failure event
|
|
306
|
+
if (typeof window !== 'undefined') {
|
|
307
|
+
window.dispatchEvent(new CustomEvent('auth:failure', {
|
|
308
|
+
detail: { error: formattedError }
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Handle retryable errors
|
|
314
|
+
const isRetryable = [408, 429, 500, 502, 503, 504].includes(formattedError.status) ||
|
|
315
|
+
formattedError.code === 'NETWORK_ERROR';
|
|
316
|
+
|
|
317
|
+
if (isRetryable && originalRequest._retry < CONFIG.retryAttempts) {
|
|
318
|
+
originalRequest._retry += 1;
|
|
319
|
+
|
|
320
|
+
const delay = getRetryDelay(originalRequest._retry);
|
|
321
|
+
console.log(`⏳ Retrying request (${originalRequest._retry}/${CONFIG.retryAttempts}) in ${delay}ms`);
|
|
322
|
+
|
|
323
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
324
|
+
return apiInstance(originalRequest);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Handle rate limiting
|
|
328
|
+
if (formattedError.status === 429) {
|
|
329
|
+
const retryAfter = error.response?.headers['retry-after'];
|
|
330
|
+
if (retryAfter) {
|
|
331
|
+
formattedError.retryAfter = parseInt(retryAfter) * 1000;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return Promise.reject(formattedError);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Creates the main API instance
|
|
340
|
+
*/
|
|
341
|
+
const createApiInstance = () => {
|
|
342
|
+
const instance = axios.create({
|
|
343
|
+
baseURL: getBaseUrl(),
|
|
344
|
+
timeout: CONFIG.timeout,
|
|
16
345
|
headers: {
|
|
17
|
-
|
|
18
|
-
|
|
346
|
+
'Accept': 'application/json',
|
|
347
|
+
'Content-Type': 'application/json',
|
|
19
348
|
},
|
|
20
|
-
paramsSerializer:
|
|
21
|
-
|
|
349
|
+
paramsSerializer: {
|
|
350
|
+
serialize: (params) => qs.stringify(params, {
|
|
351
|
+
indices: false,
|
|
352
|
+
skipNulls: true,
|
|
353
|
+
arrayFormat: 'brackets',
|
|
354
|
+
}),
|
|
22
355
|
},
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
356
|
+
withCredentials: true,
|
|
357
|
+
// Disable automatic JSON parsing for better error handling
|
|
358
|
+
transformResponse: [
|
|
359
|
+
(data) => {
|
|
360
|
+
try {
|
|
361
|
+
return JSON.parse(data);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return data;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
],
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Add interceptors
|
|
370
|
+
instance.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
|
|
371
|
+
instance.interceptors.response.use(responseInterceptor, responseErrorInterceptor);
|
|
372
|
+
|
|
373
|
+
return instance;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// Create the main API instance
|
|
377
|
+
const apiInstance = createApiInstance();
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* API utility methods
|
|
381
|
+
*/
|
|
382
|
+
const API = {
|
|
383
|
+
// Main axios instance
|
|
384
|
+
instance: apiInstance,
|
|
385
|
+
|
|
386
|
+
// Direct access to axios methods
|
|
387
|
+
get: apiInstance.get.bind(apiInstance),
|
|
388
|
+
post: apiInstance.post.bind(apiInstance),
|
|
389
|
+
put: apiInstance.put.bind(apiInstance),
|
|
390
|
+
patch: apiInstance.patch.bind(apiInstance),
|
|
391
|
+
delete: apiInstance.delete.bind(apiInstance),
|
|
392
|
+
head: apiInstance.head.bind(apiInstance),
|
|
393
|
+
options: apiInstance.options.bind(apiInstance),
|
|
394
|
+
|
|
395
|
+
// Utility methods
|
|
396
|
+
/**
|
|
397
|
+
* Makes a request with custom configuration
|
|
398
|
+
* @param {Object} config - Request configuration
|
|
399
|
+
* @returns {Promise<Object>} Response
|
|
400
|
+
*/
|
|
401
|
+
request: (config) => apiInstance.request(config),
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Creates a new API instance with custom configuration
|
|
405
|
+
* @param {Object} customConfig - Custom axios configuration
|
|
406
|
+
* @returns {Object} New API instance
|
|
407
|
+
*/
|
|
408
|
+
create: (customConfig = {}) => {
|
|
409
|
+
const customInstance = axios.create({
|
|
410
|
+
...apiInstance.defaults,
|
|
411
|
+
...customConfig,
|
|
412
|
+
});
|
|
31
413
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
414
|
+
customInstance.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
|
|
415
|
+
customInstance.interceptors.response.use(responseInterceptor, responseErrorInterceptor);
|
|
416
|
+
|
|
417
|
+
return customInstance;
|
|
418
|
+
},
|
|
36
419
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
420
|
+
/**
|
|
421
|
+
* Sets authentication token
|
|
422
|
+
* @param {string} token - Auth token
|
|
423
|
+
* @param {Object} userData - User data
|
|
424
|
+
* @returns {Promise<boolean>} Success status
|
|
425
|
+
*/
|
|
426
|
+
setAuth: async (token, userData = {}) => {
|
|
427
|
+
try {
|
|
428
|
+
const authData = {
|
|
429
|
+
token,
|
|
430
|
+
user: userData,
|
|
431
|
+
timestamp: Date.now(),
|
|
432
|
+
expiresAt: userData.expiresAt,
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
await CacheFactory.local.set(CONFIG.cacheKeys.auth, authData);
|
|
436
|
+
return true;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.error('Failed to set auth:', error);
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
},
|
|
41
442
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
443
|
+
/**
|
|
444
|
+
* Clears authentication
|
|
445
|
+
* @returns {Promise<boolean>} Success status
|
|
446
|
+
*/
|
|
447
|
+
clearAuth: async () => {
|
|
448
|
+
try {
|
|
449
|
+
await clearAuthData();
|
|
450
|
+
return true;
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error('Failed to clear auth:', error);
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
},
|
|
46
456
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
457
|
+
/**
|
|
458
|
+
* Gets current auth status
|
|
459
|
+
* @returns {Promise<Object>} Auth status
|
|
460
|
+
*/
|
|
461
|
+
getAuthStatus: async () => {
|
|
462
|
+
const authData = await getAuthData();
|
|
463
|
+
const isAuthenticated = !!(authData?.token && !isTokenExpired(authData.token));
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
isAuthenticated,
|
|
467
|
+
user: authData?.user || null,
|
|
468
|
+
token: authData?.token || null,
|
|
469
|
+
expiresAt: authData?.expiresAt || null,
|
|
470
|
+
};
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Configures global request defaults
|
|
475
|
+
* @param {Object} config - Configuration object
|
|
476
|
+
*/
|
|
477
|
+
configure: (config) => {
|
|
478
|
+
Object.assign(apiInstance.defaults, config);
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Health check endpoint
|
|
483
|
+
* @returns {Promise<Object>} Health status
|
|
484
|
+
*/
|
|
485
|
+
healthCheck: async () => {
|
|
486
|
+
try {
|
|
487
|
+
const response = await apiInstance.get('health', { timeout: 5000 });
|
|
488
|
+
return {
|
|
489
|
+
status: 'healthy',
|
|
490
|
+
response: response.data,
|
|
491
|
+
timestamp: Date.now(),
|
|
492
|
+
};
|
|
493
|
+
} catch (error) {
|
|
494
|
+
return {
|
|
495
|
+
status: 'unhealthy',
|
|
496
|
+
error: formatError(error),
|
|
497
|
+
timestamp: Date.now(),
|
|
498
|
+
};
|
|
66
499
|
}
|
|
67
|
-
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Upload file with progress tracking
|
|
504
|
+
* @param {string} url - Upload URL
|
|
505
|
+
* @param {FormData} formData - Form data with file
|
|
506
|
+
* @param {Function} onProgress - Progress callback
|
|
507
|
+
* @returns {Promise<Object>} Upload response
|
|
508
|
+
*/
|
|
509
|
+
uploadFile: (url, formData, onProgress) => {
|
|
510
|
+
return apiInstance.post(url, formData, {
|
|
511
|
+
headers: {
|
|
512
|
+
'Content-Type': 'multipart/form-data',
|
|
513
|
+
},
|
|
514
|
+
onUploadProgress: (progressEvent) => {
|
|
515
|
+
if (onProgress && progressEvent.total) {
|
|
516
|
+
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
|
517
|
+
onProgress(progress, progressEvent);
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
},
|
|
68
522
|
|
|
523
|
+
// Configuration access
|
|
524
|
+
config: CONFIG,
|
|
525
|
+
getBaseUrl,
|
|
526
|
+
};
|
|
69
527
|
|
|
70
|
-
|
|
528
|
+
// Add event listener for auth failures (optional)
|
|
529
|
+
if (typeof window !== 'undefined') {
|
|
530
|
+
window.addEventListener('auth:failure', (event) => {
|
|
531
|
+
console.warn('Authentication failed:', event.detail.error);
|
|
532
|
+
// Could trigger a redirect to login page here
|
|
533
|
+
});
|
|
534
|
+
}
|
|
71
535
|
|
|
536
|
+
export { API };
|
|
72
537
|
export default API;
|