@pushwoosh/http-client 2.12.0 → 2.13.1
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/http-client.errors.d.ts → http-client.errors.d.ts} +0 -1
- package/http-client.errors.js +285 -0
- package/{dist/http-client.helpers.d.ts → http-client.helpers.d.ts} +0 -1
- package/http-client.helpers.js +14 -0
- package/{dist/http-client.main.d.ts → http-client.main.d.ts} +0 -1
- package/http-client.main.js +524 -0
- package/{dist/http-client.types.d.ts → http-client.types.d.ts} +0 -1
- package/http-client.types.js +8 -0
- package/{dist/http-client.utilities.d.ts → http-client.utilities.d.ts} +0 -1
- package/http-client.utilities.js +63 -0
- package/{dist/index.d.ts → index.d.ts} +0 -1
- package/index.js +4 -0
- package/package.json +11 -13
- package/README.md +0 -302
- package/dist/http-client.errors.d.ts.map +0 -1
- package/dist/http-client.helpers.d.ts.map +0 -1
- package/dist/http-client.main.d.ts.map +0 -1
- package/dist/http-client.types.d.ts.map +0 -1
- package/dist/http-client.utilities.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.mjs +0 -3
- package/dist/index.mjs.LICENSE.txt +0 -1
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import Cookies from 'js-cookie';
|
|
2
|
+
import { jwtDecode } from 'jwt-decode';
|
|
3
|
+
import { LogicError, UnauthorizedError, UnprocessableEntityError } from './http-client.errors';
|
|
4
|
+
import { updatePath } from './http-client.helpers';
|
|
5
|
+
import { Method } from './http-client.types';
|
|
6
|
+
export class HttpClient {
|
|
7
|
+
/**
|
|
8
|
+
* Cookie name used to save the authorization state
|
|
9
|
+
* For more information see https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.1
|
|
10
|
+
*/
|
|
11
|
+
static AUTHORIZATION_STATE_COOKIE_NAME = '__PW_AUTHORIZATION_STATE';
|
|
12
|
+
static AUTHORIZATION_STATE_SESSION_KEY = '__PW_SESSION_AUTHORIZATION_STATE';
|
|
13
|
+
static URL_BEFORE_AUTHORIZATION_SESSION_KEY = '__PW_SESSION_URL_BEFORE_AUTHORIZATION';
|
|
14
|
+
/**
|
|
15
|
+
* Cookie name used to save the page address before authorization
|
|
16
|
+
*/
|
|
17
|
+
static URL_BEFORE_AUTHORIZATION_COOKIE_NAME = '__PW_URL_BEFORE_AUTHORIZATION';
|
|
18
|
+
/**
|
|
19
|
+
* Base URL for requests
|
|
20
|
+
*/
|
|
21
|
+
baseURL;
|
|
22
|
+
/**
|
|
23
|
+
* API for working with navigation
|
|
24
|
+
*/
|
|
25
|
+
history;
|
|
26
|
+
/**
|
|
27
|
+
* Http Client configuration
|
|
28
|
+
*/
|
|
29
|
+
config;
|
|
30
|
+
/**
|
|
31
|
+
* Access token used to access protected resources
|
|
32
|
+
* For more information see https://www.rfc-editor.org/rfc/rfc6749.html#section-1.4
|
|
33
|
+
*/
|
|
34
|
+
accessToken;
|
|
35
|
+
/**
|
|
36
|
+
* Refresh token used to refresh the access token.
|
|
37
|
+
* For more information see https://www.rfc-editor.org/rfc/rfc6749.html#section-1.5
|
|
38
|
+
*/
|
|
39
|
+
refreshToken;
|
|
40
|
+
/**
|
|
41
|
+
* Is token refresh in progress
|
|
42
|
+
*/
|
|
43
|
+
isRefreshing;
|
|
44
|
+
/**
|
|
45
|
+
* Email or username of the logged in user.
|
|
46
|
+
* Taken from jwt (claims).
|
|
47
|
+
*/
|
|
48
|
+
userName;
|
|
49
|
+
/**
|
|
50
|
+
* Account ID under which the user is logged in.
|
|
51
|
+
* Taken from jwt (claims).
|
|
52
|
+
*/
|
|
53
|
+
accountID;
|
|
54
|
+
/**
|
|
55
|
+
* Create an instance of the class
|
|
56
|
+
*
|
|
57
|
+
* @param history - API for working with navigation
|
|
58
|
+
* @param config - Http Client configuration
|
|
59
|
+
*/
|
|
60
|
+
constructor(history, config) {
|
|
61
|
+
const {
|
|
62
|
+
baseURL
|
|
63
|
+
} = config;
|
|
64
|
+
this.baseURL = baseURL;
|
|
65
|
+
this.history = history;
|
|
66
|
+
this.config = config;
|
|
67
|
+
this.accessToken = null;
|
|
68
|
+
this.refreshToken = null;
|
|
69
|
+
this.isRefreshing = false;
|
|
70
|
+
this.userName = '';
|
|
71
|
+
this.accountID = '';
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Check for authorization
|
|
75
|
+
*
|
|
76
|
+
* @param details - request details to protected resources
|
|
77
|
+
*/
|
|
78
|
+
checkAuthorization = async details => {
|
|
79
|
+
// if the token refresh process is in progress, wait 100 ms and try to check the authorization again
|
|
80
|
+
if (this.isRefreshing) {
|
|
81
|
+
await new Promise(resolve => {
|
|
82
|
+
setTimeout(resolve, 100);
|
|
83
|
+
});
|
|
84
|
+
await this.checkAuthorization(details);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// if there is no access token, then the client is definitely not authorized
|
|
88
|
+
if (!this.accessToken) {
|
|
89
|
+
throw new UnauthorizedError('Unauthorized!', details);
|
|
90
|
+
}
|
|
91
|
+
// if the token lifetime has expired, you need to try to update it using the refresh token
|
|
92
|
+
const currentTime = new Date().getTime() / 1000;
|
|
93
|
+
const introspect = jwtDecode(this.accessToken);
|
|
94
|
+
const isTokenExpired = introspect.expired_at < currentTime;
|
|
95
|
+
if (isTokenExpired) {
|
|
96
|
+
await this.updateTokenByRefreshToken(details);
|
|
97
|
+
await this.checkAuthorization(details);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Update access token
|
|
102
|
+
* For more information see:
|
|
103
|
+
* https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.3
|
|
104
|
+
* https://www.rfc-editor.org/rfc/rfc6749.html#section-6
|
|
105
|
+
*
|
|
106
|
+
* @param query - query parameters involved in the update
|
|
107
|
+
*/
|
|
108
|
+
updateToken = async query => {
|
|
109
|
+
// client identification is done using the header
|
|
110
|
+
const clientIdInBase64 = btoa(`${this.config.clientId}:`);
|
|
111
|
+
const authorizationHeader = `Basic ${clientIdInBase64}`;
|
|
112
|
+
try {
|
|
113
|
+
this.isRefreshing = true;
|
|
114
|
+
const tokens = await this.post(this.config.urls.token, {
|
|
115
|
+
headers: {
|
|
116
|
+
Authorization: authorizationHeader
|
|
117
|
+
},
|
|
118
|
+
query,
|
|
119
|
+
withAuthorization: false,
|
|
120
|
+
withCredentials: true
|
|
121
|
+
});
|
|
122
|
+
this.accessToken = tokens.access_token;
|
|
123
|
+
this.refreshToken = tokens.refresh_token;
|
|
124
|
+
this.isRefreshing = false;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
this.accessToken = null;
|
|
127
|
+
this.refreshToken = null;
|
|
128
|
+
this.isRefreshing = false;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Update access token using authorization code
|
|
134
|
+
* For more information see: https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.3
|
|
135
|
+
*
|
|
136
|
+
* @param authorizationCode - authorization code received from the authorization server
|
|
137
|
+
*/
|
|
138
|
+
updateTokenByAuthorizationCode = async authorizationCode => {
|
|
139
|
+
await this.updateToken({
|
|
140
|
+
grant_type: 'authorization_code',
|
|
141
|
+
code: authorizationCode,
|
|
142
|
+
redirect_uri: `${window.location.origin}${window.location.pathname}`
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Update access token using refresh token
|
|
147
|
+
* For more information see: https://www.rfc-editor.org/rfc/rfc6749.html#section-6
|
|
148
|
+
*
|
|
149
|
+
* @param details - request details to protected resources
|
|
150
|
+
*/
|
|
151
|
+
updateTokenByRefreshToken = async details => {
|
|
152
|
+
if (!this.refreshToken) {
|
|
153
|
+
this.accessToken = null;
|
|
154
|
+
throw new UnauthorizedError('Unauthorized!', details);
|
|
155
|
+
}
|
|
156
|
+
await this.updateToken({
|
|
157
|
+
grant_type: 'refresh_token',
|
|
158
|
+
refresh_token: this.refreshToken,
|
|
159
|
+
username: this.userName,
|
|
160
|
+
account_id: this.accountID
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Process response from fetch request
|
|
165
|
+
*
|
|
166
|
+
* @param response - fetch response
|
|
167
|
+
* @param details - request details
|
|
168
|
+
* @param responseMiddleware - middleware to process response
|
|
169
|
+
*/
|
|
170
|
+
processFetchResponse = async (response, details, responseMiddleware) => {
|
|
171
|
+
let data;
|
|
172
|
+
if (response.headers.get('Content-Type')?.includes('application/json')) {
|
|
173
|
+
data = await response.json().catch(() => ({}));
|
|
174
|
+
} else {
|
|
175
|
+
data = await response.text().then(str => {
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(str);
|
|
178
|
+
} catch {
|
|
179
|
+
return str;
|
|
180
|
+
}
|
|
181
|
+
}).catch(() => '');
|
|
182
|
+
}
|
|
183
|
+
return responseMiddleware({
|
|
184
|
+
result: data,
|
|
185
|
+
status: response.status,
|
|
186
|
+
message: response.statusText,
|
|
187
|
+
details
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Build fetch request URL, headers, and options from the given parameters.
|
|
192
|
+
* Shared by both `request()` and `stream()`.
|
|
193
|
+
*/
|
|
194
|
+
buildFetchRequest = async (url, method, options) => {
|
|
195
|
+
// Initialize fetch request
|
|
196
|
+
let fetchUrl = url;
|
|
197
|
+
// Apply base URL if present
|
|
198
|
+
if (this.baseURL && !fetchUrl.startsWith('http')) {
|
|
199
|
+
fetchUrl = `${this.baseURL}${fetchUrl.startsWith('/') ? '' : '/'}${fetchUrl}`;
|
|
200
|
+
}
|
|
201
|
+
// Apply URL parameters
|
|
202
|
+
if (options.params) {
|
|
203
|
+
fetchUrl = updatePath(fetchUrl, options.params);
|
|
204
|
+
}
|
|
205
|
+
// Apply query parameters
|
|
206
|
+
if (options.query) {
|
|
207
|
+
const queryParams = options.query;
|
|
208
|
+
let queryString;
|
|
209
|
+
if (typeof queryParams === 'string') {
|
|
210
|
+
queryString = queryParams;
|
|
211
|
+
} else if (queryParams instanceof URLSearchParams) {
|
|
212
|
+
queryString = queryParams.toString();
|
|
213
|
+
} else {
|
|
214
|
+
queryString = new URLSearchParams(queryParams).toString();
|
|
215
|
+
}
|
|
216
|
+
fetchUrl = `${fetchUrl}${fetchUrl.includes('?') ? '&' : '?'}${queryString}`;
|
|
217
|
+
}
|
|
218
|
+
// Initialize request headers
|
|
219
|
+
const headers = options.headers || {};
|
|
220
|
+
const headersInit = {};
|
|
221
|
+
// Convert headers object to proper format
|
|
222
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
223
|
+
headersInit[key] = String(value);
|
|
224
|
+
});
|
|
225
|
+
// Add authorization header if needed
|
|
226
|
+
if (options.withAuthorization) {
|
|
227
|
+
await this.checkAuthorization(null);
|
|
228
|
+
headersInit.Authorization = `Bearer ${this.accessToken}`;
|
|
229
|
+
}
|
|
230
|
+
// Initialize request options
|
|
231
|
+
const fetchOptions = {
|
|
232
|
+
method,
|
|
233
|
+
headers: headersInit,
|
|
234
|
+
credentials: options.withCredentials ? 'include' : 'same-origin',
|
|
235
|
+
signal: options.signal
|
|
236
|
+
};
|
|
237
|
+
// Add body if needed
|
|
238
|
+
if (options.body !== undefined) {
|
|
239
|
+
if (options.body instanceof FormData) {
|
|
240
|
+
fetchOptions.body = options.body;
|
|
241
|
+
} else {
|
|
242
|
+
headersInit['Content-Type'] = 'application/json';
|
|
243
|
+
if (method !== Method.GET) {
|
|
244
|
+
fetchOptions.body = JSON.stringify(options.body);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
fetchUrl,
|
|
250
|
+
fetchOptions,
|
|
251
|
+
headersInit
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* Make a request
|
|
256
|
+
*
|
|
257
|
+
* @param url - request address
|
|
258
|
+
* @param method - request method
|
|
259
|
+
* @param options - request configuration
|
|
260
|
+
*/
|
|
261
|
+
request = async (url, method, options) => {
|
|
262
|
+
const {
|
|
263
|
+
basicErrorMiddleware,
|
|
264
|
+
basicResponseMiddleware
|
|
265
|
+
} = this.config;
|
|
266
|
+
const {
|
|
267
|
+
errorMiddleware = basicErrorMiddleware,
|
|
268
|
+
responseMiddleware = basicResponseMiddleware
|
|
269
|
+
} = options;
|
|
270
|
+
// form request details
|
|
271
|
+
const details = {
|
|
272
|
+
url,
|
|
273
|
+
method,
|
|
274
|
+
headers: options.headers,
|
|
275
|
+
params: options.params,
|
|
276
|
+
query: options.query,
|
|
277
|
+
body: options.body,
|
|
278
|
+
withCredentials: options.withCredentials,
|
|
279
|
+
withAuthorization: options.withAuthorization
|
|
280
|
+
};
|
|
281
|
+
// the cast is needed only because RequestOptions is a conditional type over
|
|
282
|
+
// generics that are still unresolved here
|
|
283
|
+
const {
|
|
284
|
+
fetchUrl,
|
|
285
|
+
fetchOptions,
|
|
286
|
+
headersInit
|
|
287
|
+
} = await this.buildFetchRequest(url, method, options);
|
|
288
|
+
// the request can be made twice if an UnauthorizedError occurred,
|
|
289
|
+
// to have the opportunity to automatically update the access token using the refresh token
|
|
290
|
+
try {
|
|
291
|
+
const response = await fetch(fetchUrl, fetchOptions);
|
|
292
|
+
return await this.processFetchResponse(response, details, responseMiddleware);
|
|
293
|
+
} catch (firstError) {
|
|
294
|
+
// if the error that occurred during the first request is not Unauthorized, then pass it further
|
|
295
|
+
if (!(firstError instanceof UnauthorizedError)) {
|
|
296
|
+
await errorMiddleware(firstError);
|
|
297
|
+
// if for some reason an exception is not thrown in errorMiddleware, we do it for it
|
|
298
|
+
throw firstError;
|
|
299
|
+
}
|
|
300
|
+
// second attempt to make the request, but now with token update
|
|
301
|
+
try {
|
|
302
|
+
if (options.withAuthorization) {
|
|
303
|
+
await this.updateTokenByRefreshToken(details);
|
|
304
|
+
await this.checkAuthorization(details);
|
|
305
|
+
headersInit.Authorization = `Bearer ${this.accessToken}`;
|
|
306
|
+
}
|
|
307
|
+
const response = await fetch(fetchUrl, fetchOptions);
|
|
308
|
+
return await this.processFetchResponse(response, details, responseMiddleware);
|
|
309
|
+
} catch (secondError) {
|
|
310
|
+
await errorMiddleware(secondError);
|
|
311
|
+
// if for some reason an exception is not thrown in errorMiddleware, we do it for it
|
|
312
|
+
throw secondError;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
/**
|
|
317
|
+
* Make a streaming request that returns the raw Response.
|
|
318
|
+
* Use this for NDJSON, SSE, or other streaming protocols where
|
|
319
|
+
* you need access to response.body (ReadableStream).
|
|
320
|
+
*
|
|
321
|
+
* @param url - request address
|
|
322
|
+
* @param method - request method
|
|
323
|
+
* @param options - stream request configuration
|
|
324
|
+
*/
|
|
325
|
+
stream = async (url, method, options) => {
|
|
326
|
+
const {
|
|
327
|
+
basicErrorMiddleware
|
|
328
|
+
} = this.config;
|
|
329
|
+
const {
|
|
330
|
+
errorMiddleware = basicErrorMiddleware
|
|
331
|
+
} = options;
|
|
332
|
+
const {
|
|
333
|
+
fetchUrl,
|
|
334
|
+
fetchOptions,
|
|
335
|
+
headersInit
|
|
336
|
+
} = await this.buildFetchRequest(url, method, options);
|
|
337
|
+
const details = {
|
|
338
|
+
url,
|
|
339
|
+
method,
|
|
340
|
+
params: options.params,
|
|
341
|
+
query: options.query,
|
|
342
|
+
body: options.body,
|
|
343
|
+
withCredentials: options.withCredentials,
|
|
344
|
+
withAuthorization: options.withAuthorization
|
|
345
|
+
};
|
|
346
|
+
const {
|
|
347
|
+
basicResponseMiddleware
|
|
348
|
+
} = this.config;
|
|
349
|
+
try {
|
|
350
|
+
const response = await fetch(fetchUrl, fetchOptions);
|
|
351
|
+
if (!response.ok) {
|
|
352
|
+
// Delegate to processFetchResponse to get proper typed errors (BadRequestError, etc.)
|
|
353
|
+
await this.processFetchResponse(response, details, basicResponseMiddleware);
|
|
354
|
+
}
|
|
355
|
+
return response;
|
|
356
|
+
} catch (firstError) {
|
|
357
|
+
if (!(firstError instanceof UnauthorizedError)) {
|
|
358
|
+
await errorMiddleware(firstError);
|
|
359
|
+
throw firstError;
|
|
360
|
+
}
|
|
361
|
+
// Retry with token refresh
|
|
362
|
+
try {
|
|
363
|
+
if (options.withAuthorization) {
|
|
364
|
+
await this.updateTokenByRefreshToken(details);
|
|
365
|
+
await this.checkAuthorization(details);
|
|
366
|
+
headersInit.Authorization = `Bearer ${this.accessToken}`;
|
|
367
|
+
}
|
|
368
|
+
const response = await fetch(fetchUrl, fetchOptions);
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
await this.processFetchResponse(response, details, basicResponseMiddleware);
|
|
371
|
+
}
|
|
372
|
+
return response;
|
|
373
|
+
} catch (secondError) {
|
|
374
|
+
await errorMiddleware(secondError);
|
|
375
|
+
throw secondError;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
/**
|
|
380
|
+
* Perform GET request
|
|
381
|
+
*
|
|
382
|
+
* @param url - request address
|
|
383
|
+
* @param options - GET request configuration
|
|
384
|
+
*/
|
|
385
|
+
get = (url, options) => this.request(url, Method.GET, options);
|
|
386
|
+
/**
|
|
387
|
+
* Perform POST request
|
|
388
|
+
*
|
|
389
|
+
* @param url - request address
|
|
390
|
+
* @param options - POST request configuration
|
|
391
|
+
*/
|
|
392
|
+
post = (url, options) => this.request(url, Method.POST, options);
|
|
393
|
+
/**
|
|
394
|
+
* Perform PUT request
|
|
395
|
+
*
|
|
396
|
+
* @param url - request address
|
|
397
|
+
* @param options - PUT request configuration
|
|
398
|
+
*/
|
|
399
|
+
put = (url, options) => this.request(url, Method.PUT, options);
|
|
400
|
+
/**
|
|
401
|
+
* Perform PATCH request
|
|
402
|
+
*
|
|
403
|
+
* @param url - request address
|
|
404
|
+
* @param options - PATCH request configuration
|
|
405
|
+
*/
|
|
406
|
+
patch = (url, options) => this.request(url, Method.PATCH, options);
|
|
407
|
+
/**
|
|
408
|
+
* Perform DELETE request
|
|
409
|
+
*
|
|
410
|
+
* @param url - request address
|
|
411
|
+
* @param options - DELETE request configuration
|
|
412
|
+
*/
|
|
413
|
+
delete = (url, options) => this.request(url, Method.DELETE, options);
|
|
414
|
+
/**
|
|
415
|
+
* Address to return to once authorization completes
|
|
416
|
+
*/
|
|
417
|
+
static resolveUrlBeforeAuthorization() {
|
|
418
|
+
const stored = sessionStorage.getItem(HttpClient.URL_BEFORE_AUTHORIZATION_SESSION_KEY);
|
|
419
|
+
// SSO can bounce the browser back to `origin + pathname` to re-issue /authorize, and by then
|
|
420
|
+
// the query string is already gone: reuse the stored address, but only for that same page
|
|
421
|
+
if (stored && HttpClient.isCurrentDocument(stored)) {
|
|
422
|
+
return stored;
|
|
423
|
+
}
|
|
424
|
+
return window.location.href;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Does the URL address the page the browser is on right now, query and hash aside
|
|
428
|
+
*/
|
|
429
|
+
static isCurrentDocument(url) {
|
|
430
|
+
try {
|
|
431
|
+
const parsed = new URL(url);
|
|
432
|
+
return parsed.origin === window.location.origin && parsed.pathname === window.location.pathname;
|
|
433
|
+
} catch {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Perform the first step of client authorization
|
|
439
|
+
* For more information see https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.1
|
|
440
|
+
*/
|
|
441
|
+
login = async () => {
|
|
442
|
+
const newAuthorizationState = Math.random().toString().slice(2, 12);
|
|
443
|
+
// Store in sessionStorage (per-tab)
|
|
444
|
+
sessionStorage.setItem(HttpClient.AUTHORIZATION_STATE_SESSION_KEY, newAuthorizationState);
|
|
445
|
+
const urlBeforeAuthorization = HttpClient.resolveUrlBeforeAuthorization();
|
|
446
|
+
sessionStorage.setItem(HttpClient.URL_BEFORE_AUTHORIZATION_SESSION_KEY, urlBeforeAuthorization);
|
|
447
|
+
// Also store in cookie as fallback for cross-domain scenarios
|
|
448
|
+
Cookies.set(HttpClient.AUTHORIZATION_STATE_COOKIE_NAME, newAuthorizationState);
|
|
449
|
+
// URL in cookie - it gets overwritten by other tabs!!!
|
|
450
|
+
Cookies.set(HttpClient.URL_BEFORE_AUTHORIZATION_COOKIE_NAME, urlBeforeAuthorization);
|
|
451
|
+
const searchParams = new URLSearchParams({
|
|
452
|
+
response_type: 'code',
|
|
453
|
+
client_id: this.config.clientId,
|
|
454
|
+
redirect_uri: `${window.location.origin}${window.location.pathname}`,
|
|
455
|
+
state: newAuthorizationState
|
|
456
|
+
});
|
|
457
|
+
window.location.href = `${this.config.urls.authorize}?${searchParams.toString()}`;
|
|
458
|
+
};
|
|
459
|
+
/**
|
|
460
|
+
* Perform the second step of client authorization
|
|
461
|
+
* For more information see https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.3
|
|
462
|
+
*
|
|
463
|
+
* @param authorization - client authorization data on the server
|
|
464
|
+
*/
|
|
465
|
+
authorize = async authorization => {
|
|
466
|
+
// Try sessionStorage first (per-tab), then fallback to cookie
|
|
467
|
+
const storedAuthorizationState = sessionStorage.getItem(HttpClient.AUTHORIZATION_STATE_SESSION_KEY) || Cookies.get(HttpClient.AUTHORIZATION_STATE_COOKIE_NAME);
|
|
468
|
+
// Read from sessionStorage first (per-tab), then cookie
|
|
469
|
+
const storedAuthorizationUrl = sessionStorage.getItem(HttpClient.URL_BEFORE_AUTHORIZATION_SESSION_KEY) || Cookies.get(HttpClient.URL_BEFORE_AUTHORIZATION_COOKIE_NAME);
|
|
470
|
+
// Clean up
|
|
471
|
+
sessionStorage.removeItem(HttpClient.AUTHORIZATION_STATE_SESSION_KEY);
|
|
472
|
+
sessionStorage.removeItem(HttpClient.URL_BEFORE_AUTHORIZATION_SESSION_KEY);
|
|
473
|
+
Cookies.remove(HttpClient.AUTHORIZATION_STATE_COOKIE_NAME);
|
|
474
|
+
Cookies.remove(HttpClient.URL_BEFORE_AUTHORIZATION_COOKIE_NAME);
|
|
475
|
+
if (storedAuthorizationUrl) {
|
|
476
|
+
const url = new URL(storedAuthorizationUrl);
|
|
477
|
+
this.history.replace({
|
|
478
|
+
pathname: url.pathname,
|
|
479
|
+
search: url.search,
|
|
480
|
+
hash: url.hash
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
if (storedAuthorizationState !== authorization.state) {
|
|
484
|
+
throw new UnprocessableEntityError('Stored authorization state and received authorization state are not equal!');
|
|
485
|
+
}
|
|
486
|
+
await this.updateTokenByAuthorizationCode(authorization.code);
|
|
487
|
+
if (!this.accessToken) {
|
|
488
|
+
throw new LogicError('Access token is null after tokens update!');
|
|
489
|
+
}
|
|
490
|
+
const introspect = jwtDecode(this.accessToken);
|
|
491
|
+
this.userName = introspect.username;
|
|
492
|
+
this.accountID = introspect.account_id.toString();
|
|
493
|
+
return introspect;
|
|
494
|
+
};
|
|
495
|
+
/**
|
|
496
|
+
* End the session
|
|
497
|
+
*/
|
|
498
|
+
logout = async () => {
|
|
499
|
+
this.accessToken = null;
|
|
500
|
+
this.refreshToken = null;
|
|
501
|
+
await this.get(this.config.urls.logout, {
|
|
502
|
+
withAuthorization: false,
|
|
503
|
+
withCredentials: true
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
/**
|
|
507
|
+
* Get access token
|
|
508
|
+
*/
|
|
509
|
+
getAccessToken = async () => {
|
|
510
|
+
await this.checkAuthorization(null);
|
|
511
|
+
if (!this.accessToken) {
|
|
512
|
+
throw new LogicError('Access token is null after authorization check!');
|
|
513
|
+
}
|
|
514
|
+
return this.accessToken;
|
|
515
|
+
};
|
|
516
|
+
/**
|
|
517
|
+
* Manually set the access token
|
|
518
|
+
*
|
|
519
|
+
* @param accessToken - access token
|
|
520
|
+
*/
|
|
521
|
+
setAccessToken = accessToken => {
|
|
522
|
+
this.accessToken = accessToken;
|
|
523
|
+
};
|
|
524
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { BadGatewayError, BadRequestError, ForbiddenError, GatewayTimeoutError, InternalServerError, MethodNotAllowedError, NotFoundError, NotImplementedError, PayloadTooLargeError, ServiceUnavailableError, TooManyRequestsError, UnauthorizedError, UnprocessableEntityError, UnsupportedMediaTypeError, UntiledError } from './http-client.errors';
|
|
2
|
+
/**
|
|
3
|
+
* Check if the request was successful.
|
|
4
|
+
*
|
|
5
|
+
* @param response - response from the server.
|
|
6
|
+
*/
|
|
7
|
+
export function checkIsResponseSuccess(response) {
|
|
8
|
+
return response.status >= 200 && response.status < 300;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Default server response handler.
|
|
12
|
+
*
|
|
13
|
+
* @param response - response from the server.
|
|
14
|
+
*/
|
|
15
|
+
export function defaultResponseMiddleware(response) {
|
|
16
|
+
if (checkIsResponseSuccess(response)) {
|
|
17
|
+
return response.result;
|
|
18
|
+
}
|
|
19
|
+
// The response may contain an empty string as message, for example from nginx.
|
|
20
|
+
// In errors we expect undefined as an empty value to replace it with status.
|
|
21
|
+
// Therefore, we leave undefined as the last value in the logical operator when response.message is empty.
|
|
22
|
+
const message = response.result?.message || response.message || undefined;
|
|
23
|
+
switch (response.status) {
|
|
24
|
+
case 400:
|
|
25
|
+
throw new BadRequestError(message, response.details);
|
|
26
|
+
case 401:
|
|
27
|
+
throw new UnauthorizedError(message, response.details);
|
|
28
|
+
case 403:
|
|
29
|
+
throw new ForbiddenError(message, response.details);
|
|
30
|
+
case 404:
|
|
31
|
+
throw new NotFoundError(message, response.details);
|
|
32
|
+
case 405:
|
|
33
|
+
throw new MethodNotAllowedError(message, response.details);
|
|
34
|
+
case 413:
|
|
35
|
+
throw new PayloadTooLargeError(message, response.details);
|
|
36
|
+
case 415:
|
|
37
|
+
throw new UnsupportedMediaTypeError(message, response.details);
|
|
38
|
+
case 422:
|
|
39
|
+
throw new UnprocessableEntityError(message, response.details);
|
|
40
|
+
case 429:
|
|
41
|
+
throw new TooManyRequestsError(message, response.details);
|
|
42
|
+
case 500:
|
|
43
|
+
throw new InternalServerError(message, response.details);
|
|
44
|
+
case 501:
|
|
45
|
+
throw new NotImplementedError(message, response.details);
|
|
46
|
+
case 502:
|
|
47
|
+
throw new BadGatewayError(message, response.details);
|
|
48
|
+
case 503:
|
|
49
|
+
throw new ServiceUnavailableError(message, response.details);
|
|
50
|
+
case 504:
|
|
51
|
+
throw new GatewayTimeoutError(message, response.details);
|
|
52
|
+
default:
|
|
53
|
+
throw new UntiledError(message || 'Untiled Error!', response.status, response.details);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Default error handler.
|
|
58
|
+
*
|
|
59
|
+
* @param error - error.
|
|
60
|
+
*/
|
|
61
|
+
export function defaultErrorMiddleware(error) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
package/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pushwoosh/http-client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.1",
|
|
4
4
|
"description": "HTTP client with OAuth2 for Pushwoosh micro-frontends",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://gitlab.corp.pushwoosh.com/frontend/frontend-monorepo.git",
|
|
8
8
|
"directory": "packages/http-client"
|
|
9
9
|
},
|
|
10
|
-
"main": "
|
|
11
|
-
"module": "
|
|
12
|
-
"types": "
|
|
10
|
+
"main": "index.js",
|
|
11
|
+
"module": "index.js",
|
|
12
|
+
"types": "index.d.ts",
|
|
13
|
+
"sideEffects": false,
|
|
13
14
|
"publishConfig": {
|
|
14
|
-
"access": "public"
|
|
15
|
+
"access": "public",
|
|
16
|
+
"directory": "dist"
|
|
15
17
|
},
|
|
16
|
-
"files": [
|
|
17
|
-
"dist"
|
|
18
|
-
],
|
|
19
18
|
"pushwoosh": {
|
|
20
19
|
"cdn": {
|
|
21
20
|
"path": "http-client",
|
|
21
|
+
"dir": "cdn-dist",
|
|
22
22
|
"importMap": [
|
|
23
23
|
"@frontend/http-client",
|
|
24
24
|
"@pushwoosh/http-client"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"path-to-regexp": "^6.2.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@pushwoosh/frontend-builder-engine": "^
|
|
34
|
+
"@pushwoosh/frontend-builder-engine": "^5.5.0",
|
|
35
35
|
"@types/history": "^4.7.11",
|
|
36
36
|
"@types/js-cookie": "^3.0.1",
|
|
37
37
|
"typescript": "^6.0.3"
|
|
@@ -40,13 +40,11 @@
|
|
|
40
40
|
"node": ">= 24"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
|
-
"
|
|
44
|
-
"build": "pnpm run clean && pnpm run build:types && `pushwoosh-engine app:command:build`",
|
|
45
|
-
"build:types": "tsc --project tsconfig.json",
|
|
43
|
+
"build": "pushwoosh-engine lib:build && `pushwoosh-engine app:command:build`",
|
|
46
44
|
"analyze": "`pushwoosh-engine app:command:analyze`",
|
|
47
45
|
"check": "pnpm run check:lint && pnpm run check:types",
|
|
48
46
|
"check:lint": "echo \"Run check:lint\" && NODE_ENV=production eslint ./src",
|
|
49
47
|
"check:lint-fix": "echo \"Run check:lint\" && NODE_ENV=production eslint ./src --fix",
|
|
50
|
-
"check:types": "echo \"Run check:types\" &&
|
|
48
|
+
"check:types": "echo \"Run check:types\" && pushwoosh-engine lib:check-types"
|
|
51
49
|
}
|
|
52
50
|
}
|