@emilgroup/account-sdk 1.49.1-beta.20 → 1.49.1-beta.21
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/README.md +2 -2
- package/api/accounts-api.ts +48 -18
- package/api/health-api.ts +1 -1
- package/base.ts +239 -43
- package/common.ts +3 -2
- package/configuration.ts +20 -0
- package/dist/api/accounts-api.d.ts +6 -6
- package/dist/api/accounts-api.js +78 -45
- package/dist/api/health-api.d.ts +1 -1
- package/dist/api/health-api.js +1 -1
- package/dist/base.d.ts +26 -0
- package/dist/base.js +243 -60
- package/dist/common.d.ts +1 -1
- package/dist/common.js +1 -0
- package/dist/configuration.d.ts +17 -0
- package/dist/configuration.js +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ Although this package can be used in both TypeScript and JavaScript, it is inten
|
|
|
17
17
|
Navigate to the folder of your consuming project and run one of the following commands:
|
|
18
18
|
|
|
19
19
|
```
|
|
20
|
-
npm install @emilgroup/account-sdk@1.49.1-beta.
|
|
20
|
+
npm install @emilgroup/account-sdk@1.49.1-beta.21 --save
|
|
21
21
|
```
|
|
22
22
|
or
|
|
23
23
|
```
|
|
24
|
-
yarn add @emilgroup/account-sdk@1.49.1-beta.
|
|
24
|
+
yarn add @emilgroup/account-sdk@1.49.1-beta.21
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
And then you can import `AccountsApi`.
|
package/api/accounts-api.ts
CHANGED
|
@@ -67,8 +67,13 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
|
|
|
67
67
|
// http bearer authentication required
|
|
68
68
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// Only Authorization falls back to the configured token. Without a session it is sent as before, empty when
|
|
71
|
+
// no token is stored, so a refused call still refreshes; a session call (account header) sends none.
|
|
72
|
+
const authorizationIsAuth = 'Authorization'.toLowerCase() === 'authorization';
|
|
73
|
+
const authorizationValue = authorizationIsAuth ? (authorization || baseAccessToken) : authorization;
|
|
74
|
+
const authorizationKeepEmpty = authorizationIsAuth && authorizationValue === '' && !baseOptions?.headers?.['X-Emil-Account'];
|
|
75
|
+
if (authorizationValue || authorizationKeepEmpty) {
|
|
76
|
+
localVarHeaderParameter['Authorization'] = String(authorizationValue);
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
|
|
@@ -115,8 +120,13 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
|
|
|
115
120
|
// http bearer authentication required
|
|
116
121
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
|
117
122
|
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
// Only Authorization falls back to the configured token. Without a session it is sent as before, empty when
|
|
124
|
+
// no token is stored, so a refused call still refreshes; a session call (account header) sends none.
|
|
125
|
+
const authorizationIsAuth = 'Authorization'.toLowerCase() === 'authorization';
|
|
126
|
+
const authorizationValue = authorizationIsAuth ? (authorization || baseAccessToken) : authorization;
|
|
127
|
+
const authorizationKeepEmpty = authorizationIsAuth && authorizationValue === '' && !baseOptions?.headers?.['X-Emil-Account'];
|
|
128
|
+
if (authorizationValue || authorizationKeepEmpty) {
|
|
129
|
+
localVarHeaderParameter['Authorization'] = String(authorizationValue);
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
|
|
@@ -165,8 +175,13 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
|
|
|
165
175
|
localVarQueryParameter['expand'] = expand;
|
|
166
176
|
}
|
|
167
177
|
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
// Only Authorization falls back to the configured token. Without a session it is sent as before, empty when
|
|
179
|
+
// no token is stored, so a refused call still refreshes; a session call (account header) sends none.
|
|
180
|
+
const authorizationIsAuth = 'Authorization'.toLowerCase() === 'authorization';
|
|
181
|
+
const authorizationValue = authorizationIsAuth ? (authorization || baseAccessToken) : authorization;
|
|
182
|
+
const authorizationKeepEmpty = authorizationIsAuth && authorizationValue === '' && !baseOptions?.headers?.['X-Emil-Account'];
|
|
183
|
+
if (authorizationValue || authorizationKeepEmpty) {
|
|
184
|
+
localVarHeaderParameter['Authorization'] = String(authorizationValue);
|
|
170
185
|
}
|
|
171
186
|
|
|
172
187
|
|
|
@@ -215,8 +230,13 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
|
|
|
215
230
|
localVarQueryParameter['expand'] = expand;
|
|
216
231
|
}
|
|
217
232
|
|
|
218
|
-
|
|
219
|
-
|
|
233
|
+
// Only Authorization falls back to the configured token. Without a session it is sent as before, empty when
|
|
234
|
+
// no token is stored, so a refused call still refreshes; a session call (account header) sends none.
|
|
235
|
+
const authorizationIsAuth = 'Authorization'.toLowerCase() === 'authorization';
|
|
236
|
+
const authorizationValue = authorizationIsAuth ? (authorization || baseAccessToken) : authorization;
|
|
237
|
+
const authorizationKeepEmpty = authorizationIsAuth && authorizationValue === '' && !baseOptions?.headers?.['X-Emil-Account'];
|
|
238
|
+
if (authorizationValue || authorizationKeepEmpty) {
|
|
239
|
+
localVarHeaderParameter['Authorization'] = String(authorizationValue);
|
|
220
240
|
}
|
|
221
241
|
|
|
222
242
|
|
|
@@ -291,8 +311,13 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
|
|
|
291
311
|
localVarQueryParameter['filters'] = filters;
|
|
292
312
|
}
|
|
293
313
|
|
|
294
|
-
|
|
295
|
-
|
|
314
|
+
// Only Authorization falls back to the configured token. Without a session it is sent as before, empty when
|
|
315
|
+
// no token is stored, so a refused call still refreshes; a session call (account header) sends none.
|
|
316
|
+
const authorizationIsAuth = 'Authorization'.toLowerCase() === 'authorization';
|
|
317
|
+
const authorizationValue = authorizationIsAuth ? (authorization || baseAccessToken) : authorization;
|
|
318
|
+
const authorizationKeepEmpty = authorizationIsAuth && authorizationValue === '' && !baseOptions?.headers?.['X-Emil-Account'];
|
|
319
|
+
if (authorizationValue || authorizationKeepEmpty) {
|
|
320
|
+
localVarHeaderParameter['Authorization'] = String(authorizationValue);
|
|
296
321
|
}
|
|
297
322
|
|
|
298
323
|
|
|
@@ -339,8 +364,13 @@ export const AccountsApiAxiosParamCreator = function (configuration?: Configurat
|
|
|
339
364
|
// http bearer authentication required
|
|
340
365
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
|
341
366
|
|
|
342
|
-
|
|
343
|
-
|
|
367
|
+
// Only Authorization falls back to the configured token. Without a session it is sent as before, empty when
|
|
368
|
+
// no token is stored, so a refused call still refreshes; a session call (account header) sends none.
|
|
369
|
+
const authorizationIsAuth = 'Authorization'.toLowerCase() === 'authorization';
|
|
370
|
+
const authorizationValue = authorizationIsAuth ? (authorization || baseAccessToken) : authorization;
|
|
371
|
+
const authorizationKeepEmpty = authorizationIsAuth && authorizationValue === '' && !baseOptions?.headers?.['X-Emil-Account'];
|
|
372
|
+
if (authorizationValue || authorizationKeepEmpty) {
|
|
373
|
+
localVarHeaderParameter['Authorization'] = String(authorizationValue);
|
|
344
374
|
}
|
|
345
375
|
|
|
346
376
|
|
|
@@ -741,7 +771,7 @@ export class AccountsApi extends BaseAPI {
|
|
|
741
771
|
* @memberof AccountsApi
|
|
742
772
|
*/
|
|
743
773
|
public createAccount(requestParameters: AccountsApiCreateAccountRequest, options?: AxiosRequestConfig) {
|
|
744
|
-
return AccountsApiFp(this.configuration).createAccount(requestParameters.createAccountRequestDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath));
|
|
774
|
+
return AccountsApiFp(this.configuration).createAccount(requestParameters.createAccountRequestDto, requestParameters.authorization, this.withOwner(options)).then((request) => request(this.axios, this.basePath));
|
|
745
775
|
}
|
|
746
776
|
|
|
747
777
|
/**
|
|
@@ -753,7 +783,7 @@ export class AccountsApi extends BaseAPI {
|
|
|
753
783
|
* @memberof AccountsApi
|
|
754
784
|
*/
|
|
755
785
|
public deleteAccount(requestParameters: AccountsApiDeleteAccountRequest, options?: AxiosRequestConfig) {
|
|
756
|
-
return AccountsApiFp(this.configuration).deleteAccount(requestParameters.code, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath));
|
|
786
|
+
return AccountsApiFp(this.configuration).deleteAccount(requestParameters.code, requestParameters.authorization, this.withOwner(options)).then((request) => request(this.axios, this.basePath));
|
|
757
787
|
}
|
|
758
788
|
|
|
759
789
|
/**
|
|
@@ -765,7 +795,7 @@ export class AccountsApi extends BaseAPI {
|
|
|
765
795
|
* @memberof AccountsApi
|
|
766
796
|
*/
|
|
767
797
|
public getAccount(requestParameters: AccountsApiGetAccountRequest, options?: AxiosRequestConfig) {
|
|
768
|
-
return AccountsApiFp(this.configuration).getAccount(requestParameters.code, requestParameters.authorization, requestParameters.expand, options).then((request) => request(this.axios, this.basePath));
|
|
798
|
+
return AccountsApiFp(this.configuration).getAccount(requestParameters.code, requestParameters.authorization, requestParameters.expand, this.withOwner(options)).then((request) => request(this.axios, this.basePath));
|
|
769
799
|
}
|
|
770
800
|
|
|
771
801
|
/**
|
|
@@ -777,7 +807,7 @@ export class AccountsApi extends BaseAPI {
|
|
|
777
807
|
* @memberof AccountsApi
|
|
778
808
|
*/
|
|
779
809
|
public getAccountByEmail(requestParameters: AccountsApiGetAccountByEmailRequest, options?: AxiosRequestConfig) {
|
|
780
|
-
return AccountsApiFp(this.configuration).getAccountByEmail(requestParameters.email, requestParameters.authorization, requestParameters.expand, options).then((request) => request(this.axios, this.basePath));
|
|
810
|
+
return AccountsApiFp(this.configuration).getAccountByEmail(requestParameters.email, requestParameters.authorization, requestParameters.expand, this.withOwner(options)).then((request) => request(this.axios, this.basePath));
|
|
781
811
|
}
|
|
782
812
|
|
|
783
813
|
/**
|
|
@@ -789,7 +819,7 @@ export class AccountsApi extends BaseAPI {
|
|
|
789
819
|
* @memberof AccountsApi
|
|
790
820
|
*/
|
|
791
821
|
public listAccounts(requestParameters: AccountsApiListAccountsRequest = {}, options?: AxiosRequestConfig) {
|
|
792
|
-
return AccountsApiFp(this.configuration).listAccounts(requestParameters.authorization, requestParameters.pageSize, requestParameters.pageToken, requestParameters.filter, requestParameters.search, requestParameters.order, requestParameters.expand, requestParameters.filters, options).then((request) => request(this.axios, this.basePath));
|
|
822
|
+
return AccountsApiFp(this.configuration).listAccounts(requestParameters.authorization, requestParameters.pageSize, requestParameters.pageToken, requestParameters.filter, requestParameters.search, requestParameters.order, requestParameters.expand, requestParameters.filters, this.withOwner(options)).then((request) => request(this.axios, this.basePath));
|
|
793
823
|
}
|
|
794
824
|
|
|
795
825
|
/**
|
|
@@ -801,6 +831,6 @@ export class AccountsApi extends BaseAPI {
|
|
|
801
831
|
* @memberof AccountsApi
|
|
802
832
|
*/
|
|
803
833
|
public updateAccount(requestParameters: AccountsApiUpdateAccountRequest, options?: AxiosRequestConfig) {
|
|
804
|
-
return AccountsApiFp(this.configuration).updateAccount(requestParameters.code, requestParameters.updateAccountRequestDto, requestParameters.authorization, options).then((request) => request(this.axios, this.basePath));
|
|
834
|
+
return AccountsApiFp(this.configuration).updateAccount(requestParameters.code, requestParameters.updateAccountRequestDto, requestParameters.authorization, this.withOwner(options)).then((request) => request(this.axios, this.basePath));
|
|
805
835
|
}
|
|
806
836
|
}
|
package/api/health-api.ts
CHANGED
|
@@ -119,6 +119,6 @@ export class HealthApi extends BaseAPI {
|
|
|
119
119
|
* @memberof HealthApi
|
|
120
120
|
*/
|
|
121
121
|
public check(options?: AxiosRequestConfig) {
|
|
122
|
-
return HealthApiFp(this.configuration).check(options).then((request) => request(this.axios, this.basePath));
|
|
122
|
+
return HealthApiFp(this.configuration).check(this.withOwner(options)).then((request) => request(this.axios, this.basePath));
|
|
123
123
|
}
|
|
124
124
|
}
|
package/base.ts
CHANGED
|
@@ -32,9 +32,20 @@ export const COLLECTION_FORMATS = {
|
|
|
32
32
|
pipes: "|",
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
export interface SessionClass {
|
|
36
|
+
sub: string;
|
|
37
|
+
username?: string;
|
|
38
|
+
email?: string;
|
|
39
|
+
tenantSlug?: string;
|
|
40
|
+
permissions?: string;
|
|
41
|
+
expiresAt?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
export interface LoginClass {
|
|
36
45
|
accessToken: string;
|
|
37
46
|
permissions: string;
|
|
47
|
+
authMode?: string;
|
|
48
|
+
session?: SessionClass;
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
export interface SwitchWorkspaceRequest {
|
|
@@ -45,6 +56,8 @@ export interface SwitchWorkspaceRequest {
|
|
|
45
56
|
export interface SwitchWorkspaceResponseClass {
|
|
46
57
|
accessToken: string;
|
|
47
58
|
permissions: string;
|
|
59
|
+
authMode?: string;
|
|
60
|
+
session?: SessionClass;
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
export enum Environment {
|
|
@@ -59,6 +72,10 @@ export enum Environment {
|
|
|
59
72
|
let _retry_count = 0
|
|
60
73
|
let _retry = null
|
|
61
74
|
|
|
75
|
+
// Marks each request with the account (username) of the SDK object that sent it: only SDK objects of that
|
|
76
|
+
// account refresh and resend it, never an object of another account open in the same browser.
|
|
77
|
+
const REQUEST_OWNER = '_emilSdkOwner';
|
|
78
|
+
|
|
62
79
|
export function resetRetry() {
|
|
63
80
|
_retry_count = 0
|
|
64
81
|
}
|
|
@@ -77,10 +94,19 @@ interface TokenData {
|
|
|
77
94
|
accessToken?: string;
|
|
78
95
|
username?: string;
|
|
79
96
|
permissions?: string;
|
|
97
|
+
// 'both' or 'session' when authservice keeps the tokens in a server session.
|
|
98
|
+
authMode?: string;
|
|
99
|
+
sub?: string;
|
|
80
100
|
}
|
|
81
101
|
|
|
82
102
|
const NETWORK_ERROR_MESSAGE = "Network Error";
|
|
83
103
|
const TOKEN_DATA = 'APP_TOKEN';
|
|
104
|
+
// Sent only to authservice, to ask for a server session.
|
|
105
|
+
const AUTH_MODE_HEADER = 'X-Emil-Auth-Mode';
|
|
106
|
+
// Names the account on every call; one browser session can hold several.
|
|
107
|
+
const ACCOUNT_HEADER = 'X-Emil-Account';
|
|
108
|
+
const SESSION_MODES = ['both', 'session'];
|
|
109
|
+
const RESEND_SAFE_METHODS = ['get', 'head', 'options'];
|
|
84
110
|
|
|
85
111
|
/**
|
|
86
112
|
*
|
|
@@ -98,14 +124,31 @@ export class BaseAPI {
|
|
|
98
124
|
this.loadTokenData();
|
|
99
125
|
|
|
100
126
|
if (configuration) {
|
|
127
|
+
// Two accounts in one browser share APP_TOKEN: never take the token or session account saved for
|
|
128
|
+
// another account. The mode stays (an app naming its account keeps its session). Saved with the next refresh.
|
|
129
|
+
const ownUser = configuration.username;
|
|
130
|
+
const storedUser = this.tokenData.username;
|
|
131
|
+
if (ownUser && storedUser && ownUser.toLowerCase() !== storedUser.toLowerCase()) {
|
|
132
|
+
delete this.tokenData.accessToken;
|
|
133
|
+
delete this.tokenData.sub;
|
|
134
|
+
this.tokenData.username = ownUser;
|
|
135
|
+
}
|
|
101
136
|
const { accessToken } = this.tokenData;
|
|
102
137
|
this.configuration = configuration;
|
|
103
138
|
this.basePath = configuration.basePath || this.basePath;
|
|
104
139
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
if (configuration.cookieAuth) {
|
|
141
|
+
this.configuration.username = this.configuration.username || this.tokenData.username;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (this.usesSession()) {
|
|
145
|
+
this.useSessionAuth();
|
|
146
|
+
} else {
|
|
147
|
+
// Use config token if provided, otherwise use tokenData token
|
|
148
|
+
const configToken = this.configuration.accessToken;
|
|
149
|
+
const storedToken = accessToken ? `Bearer ${accessToken}` : '';
|
|
150
|
+
this.configuration.accessToken = configToken || storedToken;
|
|
151
|
+
}
|
|
109
152
|
} else {
|
|
110
153
|
const { accessToken, username } = this.tokenData;
|
|
111
154
|
|
|
@@ -119,6 +162,110 @@ export class BaseAPI {
|
|
|
119
162
|
this.attachInterceptor(axios);
|
|
120
163
|
}
|
|
121
164
|
|
|
165
|
+
// The app's own account wins over APP_TOKEN, which holds whichever tab wrote last.
|
|
166
|
+
private sessionAccount(): string | undefined {
|
|
167
|
+
return this.configuration.account || this.tokenData.sub;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Apps may pass the address as the second argument only; authservice is on the same host.
|
|
171
|
+
private authBasePath(): string {
|
|
172
|
+
return this.configuration.basePath || this.basePath;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private usesSession(): boolean {
|
|
176
|
+
return !!this.configuration.cookieAuth && SESSION_MODES.indexOf(this.tokenData.authMode) !== -1 && !!this.sessionAccount();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private useSessionAuth() {
|
|
180
|
+
const { headers, ...baseOptions } = this.configuration.baseOptions || {};
|
|
181
|
+
this.configuration.accessToken = '';
|
|
182
|
+
this.configuration.baseOptions = {
|
|
183
|
+
...baseOptions,
|
|
184
|
+
withCredentials: true,
|
|
185
|
+
headers: { ...headers, [ACCOUNT_HEADER]: this.sessionAccount() },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private useHeaderAuth(accessToken: string, keepSessionData = false) {
|
|
190
|
+
this.configuration.accessToken = accessToken ? `Bearer ${accessToken}` : '';
|
|
191
|
+
this.tokenData.accessToken = accessToken;
|
|
192
|
+
// An app without cookieAuth does not own the session fields: its refresh leaves them for the session apps.
|
|
193
|
+
if (!keepSessionData || this.configuration.cookieAuth) {
|
|
194
|
+
delete this.tokenData.authMode;
|
|
195
|
+
delete this.tokenData.sub;
|
|
196
|
+
}
|
|
197
|
+
if (this.configuration.cookieAuth) {
|
|
198
|
+
const { withCredentials, headers, ...baseOptions } = this.configuration.baseOptions || {};
|
|
199
|
+
const { [ACCOUNT_HEADER]: account, ...otherHeaders } = headers || {};
|
|
200
|
+
this.configuration.baseOptions = { ...baseOptions, headers: otherHeaders };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private applyTokenReply({ accessToken, authMode, session }: LoginClass, fromRefresh = false) {
|
|
205
|
+
if (this.configuration.cookieAuth && SESSION_MODES.indexOf(authMode) !== -1 && session?.sub) {
|
|
206
|
+
this.tokenData.authMode = authMode;
|
|
207
|
+
this.tokenData.sub = session.sub;
|
|
208
|
+
if (this.configuration.account) {
|
|
209
|
+
// A workspace switch moves the app to another account.
|
|
210
|
+
this.configuration.account = session.sub;
|
|
211
|
+
}
|
|
212
|
+
// Both keeps the token for apps not moved yet; session mode keeps no token at all.
|
|
213
|
+
if (authMode === 'both' && accessToken) {
|
|
214
|
+
this.tokenData.accessToken = accessToken;
|
|
215
|
+
} else {
|
|
216
|
+
delete this.tokenData.accessToken;
|
|
217
|
+
}
|
|
218
|
+
this.useSessionAuth();
|
|
219
|
+
} else {
|
|
220
|
+
this.useHeaderAuth(accessToken, fromRefresh);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private sessionHeaders(): Record<string, string> {
|
|
225
|
+
if (!this.configuration.cookieAuth) {
|
|
226
|
+
return {};
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
[AUTH_MODE_HEADER]: 'session',
|
|
230
|
+
...(this.usesSession() && { [ACCOUNT_HEADER]: this.sessionAccount() }),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private withCurrentAuth(config: AxiosRequestConfig): AxiosRequestConfig {
|
|
235
|
+
delete config.headers['Authorization'];
|
|
236
|
+
delete config.headers[ACCOUNT_HEADER];
|
|
237
|
+
if (this.usesSession()) {
|
|
238
|
+
config.headers[ACCOUNT_HEADER] = this.sessionAccount();
|
|
239
|
+
config.withCredentials = true;
|
|
240
|
+
} else if (this.configuration.accessToken) {
|
|
241
|
+
config.headers['Authorization'] = this.configuration.accessToken;
|
|
242
|
+
}
|
|
243
|
+
return config;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Both keeps the token, so a session call the browser could not complete can be sent again with it.
|
|
247
|
+
private canRetryWithToken(config: AxiosRequestConfig): boolean {
|
|
248
|
+
return !!config.withCredentials && !!config.headers?.[ACCOUNT_HEADER]
|
|
249
|
+
&& !!this.tokenData.accessToken && this.tokenData.authMode !== 'session';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// A network error can come after the server ran the call, so only a call that is safe to repeat is sent again.
|
|
253
|
+
private canResend(config: AxiosRequestConfig): boolean {
|
|
254
|
+
return RESEND_SAFE_METHODS.indexOf((config.method || 'get').toLowerCase()) !== -1
|
|
255
|
+
|| Object.keys(config.headers || {}).some((name) => name.toLowerCase() === 'idempotency-key');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private switchToToken() {
|
|
259
|
+
this.useHeaderAuth(this.tokenData.accessToken);
|
|
260
|
+
this.storeTokenData(this.tokenData);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private retryWithToken(axios: AxiosInstance, config: AxiosRequestConfig) {
|
|
264
|
+
this.switchToToken();
|
|
265
|
+
config.withCredentials = false;
|
|
266
|
+
return axios(this.withCurrentAuth(config));
|
|
267
|
+
}
|
|
268
|
+
|
|
122
269
|
selectEnvironment(env: Environment) {
|
|
123
270
|
this.selectBasePath(env);
|
|
124
271
|
}
|
|
@@ -138,8 +285,8 @@ export class BaseAPI {
|
|
|
138
285
|
async authorize(username: string, password: string, targetWorkspace?: string): Promise<void> {
|
|
139
286
|
const options: AxiosRequestConfig = {
|
|
140
287
|
method: 'POST',
|
|
141
|
-
url: `${this.
|
|
142
|
-
headers: { 'Content-Type': 'application/json' },
|
|
288
|
+
url: `${this.authBasePath()}/authservice/v1/login`,
|
|
289
|
+
headers: { 'Content-Type': 'application/json', ...(this.configuration.cookieAuth && { [AUTH_MODE_HEADER]: 'session' }) },
|
|
143
290
|
data: {
|
|
144
291
|
username,
|
|
145
292
|
password,
|
|
@@ -149,13 +296,10 @@ export class BaseAPI {
|
|
|
149
296
|
|
|
150
297
|
const response = await globalAxios.request<LoginClass>(options);
|
|
151
298
|
|
|
152
|
-
const { data: { accessToken, permissions } } = response;
|
|
153
|
-
|
|
154
299
|
this.configuration.username = username;
|
|
155
|
-
this.configuration.accessToken = `Bearer ${accessToken}`;
|
|
156
300
|
this.tokenData.username = username;
|
|
157
|
-
this.tokenData.
|
|
158
|
-
this.
|
|
301
|
+
this.tokenData.permissions = response.data.permissions;
|
|
302
|
+
this.applyTokenReply(response.data);
|
|
159
303
|
|
|
160
304
|
// Switch workspace if provided
|
|
161
305
|
if (targetWorkspace) {
|
|
@@ -175,10 +319,11 @@ export class BaseAPI {
|
|
|
175
319
|
async switchWorkspace(targetWorkspace: string): Promise<void> {
|
|
176
320
|
const options: AxiosRequestConfig = {
|
|
177
321
|
method: 'POST',
|
|
178
|
-
url: `${this.
|
|
322
|
+
url: `${this.authBasePath()}/authservice/v1/workspaces/switch`,
|
|
179
323
|
headers: {
|
|
180
324
|
'Content-Type': 'application/json',
|
|
181
|
-
|
|
325
|
+
...this.sessionHeaders(),
|
|
326
|
+
...(!this.usesSession() && !!this.configuration.accessToken && { 'Authorization': this.configuration.accessToken as string }),
|
|
182
327
|
},
|
|
183
328
|
data: {
|
|
184
329
|
username: this.configuration.username,
|
|
@@ -189,11 +334,8 @@ export class BaseAPI {
|
|
|
189
334
|
|
|
190
335
|
const response = await globalAxios.request<SwitchWorkspaceResponseClass>(options);
|
|
191
336
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this.configuration.accessToken = `Bearer ${accessToken}`;
|
|
195
|
-
this.tokenData.accessToken = accessToken;
|
|
196
|
-
this.tokenData.permissions = permissions;
|
|
337
|
+
this.tokenData.permissions = response.data.permissions;
|
|
338
|
+
this.applyTokenReply(response.data);
|
|
197
339
|
|
|
198
340
|
this.storeTokenData({
|
|
199
341
|
...this.tokenData
|
|
@@ -209,9 +351,34 @@ export class BaseAPI {
|
|
|
209
351
|
|
|
210
352
|
const refreshTokenAxios = globalAxios.create()
|
|
211
353
|
refreshTokenAxios.interceptors.response.use(response => {
|
|
212
|
-
const { permissions } = response.data;
|
|
213
|
-
|
|
354
|
+
const { permissions, accessToken, authMode } = response.data;
|
|
355
|
+
// Decided before the merge below: newer session data must not keep the new token from this object.
|
|
356
|
+
const onSession = this.usesSession();
|
|
357
|
+
|
|
358
|
+
// Newer data saved for this account (e.g. the session mode the shell saved later) wins over the copy
|
|
359
|
+
// read when the object was made; data of another account or another workspace (sub) is never taken.
|
|
360
|
+
const latest = ((typeof window !== 'undefined' && defaultStorage().get<TokenData>(TOKEN_DATA)) || {}) as TokenData;
|
|
361
|
+
const ownSub = this.sessionAccount();
|
|
362
|
+
if (latest.username?.toLowerCase() === username.toLowerCase()
|
|
363
|
+
&& (!ownSub || !latest.sub || latest.sub === ownSub)) {
|
|
364
|
+
// A bare reset ({username}, an app's account check) keeps this copy's other fields; any fuller record
|
|
365
|
+
// is the truth, so fields it lacks were removed on purpose (e.g. a rollback) and stay removed.
|
|
366
|
+
const bareReset = Object.keys(latest).every((key) => key === 'username');
|
|
367
|
+
this.tokenData = bareReset ? { ...this.tokenData, ...latest } : { ...latest };
|
|
368
|
+
}
|
|
369
|
+
// The reply is for this account: label it, or an SDK object of another account would take the token.
|
|
370
|
+
if (this.tokenData.username?.toLowerCase() !== username.toLowerCase()) {
|
|
371
|
+
this.tokenData.username = username;
|
|
372
|
+
}
|
|
214
373
|
this.tokenData.permissions = permissions;
|
|
374
|
+
// Keep the new token for the page's next SDK objects; session data is left as it is.
|
|
375
|
+
if (accessToken && !onSession) {
|
|
376
|
+
this.tokenData.accessToken = accessToken;
|
|
377
|
+
this.configuration.accessToken = `Bearer ${accessToken}`;
|
|
378
|
+
} else if (accessToken && authMode === 'both') {
|
|
379
|
+
// Both: this app stays on the session; the token is kept for apps not moved yet.
|
|
380
|
+
this.tokenData.accessToken = accessToken;
|
|
381
|
+
}
|
|
215
382
|
this.storeTokenData(this.tokenData);
|
|
216
383
|
|
|
217
384
|
return response;
|
|
@@ -219,9 +386,10 @@ export class BaseAPI {
|
|
|
219
386
|
|
|
220
387
|
const options: AxiosRequestConfig = {
|
|
221
388
|
method: 'POST',
|
|
222
|
-
url: `${this.
|
|
389
|
+
url: `${this.authBasePath()}/authservice/v1/refresh-token`,
|
|
223
390
|
headers: {
|
|
224
391
|
'Content-Type': 'application/json',
|
|
392
|
+
...this.sessionHeaders(),
|
|
225
393
|
},
|
|
226
394
|
data: { username: username },
|
|
227
395
|
withCredentials: true,
|
|
@@ -249,6 +417,18 @@ export class BaseAPI {
|
|
|
249
417
|
this.storeTokenData(null);
|
|
250
418
|
}
|
|
251
419
|
|
|
420
|
+
private ownAccount(): string {
|
|
421
|
+
return (this.configuration.username || '').toLowerCase();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
protected withOwner(options?: AxiosRequestConfig): AxiosRequestConfig {
|
|
425
|
+
// An object without a username cannot refresh itself: its requests stay unmarked, as before.
|
|
426
|
+
if (!this.ownAccount()) {
|
|
427
|
+
return { ...options };
|
|
428
|
+
}
|
|
429
|
+
return { ...options, [REQUEST_OWNER]: this.ownAccount() } as AxiosRequestConfig;
|
|
430
|
+
}
|
|
431
|
+
|
|
252
432
|
private attachInterceptor(axios: AxiosInstance) {
|
|
253
433
|
axios.interceptors.response.use(
|
|
254
434
|
(res) => {
|
|
@@ -256,26 +436,33 @@ export class BaseAPI {
|
|
|
256
436
|
},
|
|
257
437
|
async (err) => {
|
|
258
438
|
let originalConfig = err.config;
|
|
439
|
+
// Sent for another account: only that account's SDK objects handle it. Unmarked requests as before.
|
|
440
|
+
const owner = originalConfig?.[REQUEST_OWNER];
|
|
441
|
+
if (owner !== undefined && owner !== this.ownAccount()) {
|
|
442
|
+
return Promise.reject(err);
|
|
443
|
+
}
|
|
259
444
|
if (err.response && !(err.response instanceof XMLHttpRequest)) { // sometimes buggy and is of type request
|
|
260
445
|
// Access Token was expired
|
|
261
446
|
if ((err.response.status === 401 || err.response.status === 403)
|
|
262
447
|
&& !originalConfig._retry) {
|
|
263
448
|
originalConfig._retry = true;
|
|
264
449
|
try {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
this.
|
|
268
|
-
|
|
269
|
-
delete originalConfig.headers['Authorization']
|
|
270
|
-
|
|
271
|
-
originalConfig.headers['Authorization'] = accessToken;
|
|
272
|
-
|
|
273
|
-
this.configuration.accessToken = accessToken;
|
|
274
|
-
this.tokenData.accessToken = tokenString;
|
|
275
|
-
|
|
450
|
+
const reply = await this.refreshTokenInternal();
|
|
451
|
+
this.tokenData.permissions = reply.permissions;
|
|
452
|
+
this.applyTokenReply(reply, true);
|
|
276
453
|
this.storeTokenData(this.tokenData);
|
|
277
454
|
|
|
278
|
-
|
|
455
|
+
if (!this.usesSession()) {
|
|
456
|
+
return axios(this.withCurrentAuth(originalConfig));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Both mode: if the browser does not send the session cookie (another site), use the token.
|
|
460
|
+
return axios(this.withCurrentAuth(originalConfig)).catch((retryError) => {
|
|
461
|
+
if (retryError?.response?.status !== 401 || !this.tokenData.accessToken) {
|
|
462
|
+
return Promise.reject(retryError);
|
|
463
|
+
}
|
|
464
|
+
return this.retryWithToken(axios, originalConfig);
|
|
465
|
+
});
|
|
279
466
|
} catch (_error) {
|
|
280
467
|
if (_error.response && _error.response.data) {
|
|
281
468
|
return Promise.reject(_error.response.data);
|
|
@@ -283,26 +470,35 @@ export class BaseAPI {
|
|
|
283
470
|
return Promise.reject(_error);
|
|
284
471
|
}
|
|
285
472
|
}
|
|
473
|
+
} else if (err.message === NETWORK_ERROR_MESSAGE && this.canRetryWithToken(originalConfig)) {
|
|
474
|
+
// Both mode: the browser blocked the session call or hid its reply; later calls use the token.
|
|
475
|
+
if (!this.canResend(originalConfig)) {
|
|
476
|
+
this.switchToToken();
|
|
477
|
+
return Promise.reject(err);
|
|
478
|
+
}
|
|
479
|
+
return this.retryWithToken(axios, originalConfig);
|
|
286
480
|
} else if (err.message === NETWORK_ERROR_MESSAGE
|
|
287
|
-
&& originalConfig.headers.hasOwnProperty('Authorization')
|
|
481
|
+
&& (originalConfig.headers.hasOwnProperty('Authorization')
|
|
482
|
+
|| (this.configuration.cookieAuth && originalConfig.withCredentials && this.canResend(originalConfig)))
|
|
288
483
|
&& _retry_count < 4
|
|
289
484
|
) {
|
|
290
485
|
_retry_count++;
|
|
291
486
|
try {
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
this.tokenData.permissions = permissions;
|
|
487
|
+
// In session mode an unreadable 401 (no CORS headers on the error reply) also lands here.
|
|
488
|
+
const reply = await this.refreshTokenInternal();
|
|
489
|
+
this.tokenData.permissions = reply.permissions;
|
|
295
490
|
|
|
296
491
|
_retry = true;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
this.configuration.accessToken = accessToken;
|
|
300
|
-
this.tokenData.accessToken = tokenString;
|
|
301
|
-
|
|
492
|
+
this.applyTokenReply(reply, true);
|
|
302
493
|
this.storeTokenData(this.tokenData);
|
|
303
494
|
|
|
495
|
+
// The limit is for refreshes that do not help; one that worked starts the count again.
|
|
496
|
+
// Not awaited, so a failed retry reaches the app as the same axios error as before.
|
|
304
497
|
return axios.request({
|
|
305
|
-
...originalConfig,
|
|
498
|
+
...this.withCurrentAuth(originalConfig),
|
|
499
|
+
}).then((retried) => {
|
|
500
|
+
_retry_count = 0;
|
|
501
|
+
return retried;
|
|
306
502
|
});
|
|
307
503
|
} catch (_error) {
|
|
308
504
|
if (_error.response && _error.response.data) {
|
package/common.ts
CHANGED
|
@@ -130,9 +130,10 @@ export const toPathString = function (url: URL) {
|
|
|
130
130
|
* @export
|
|
131
131
|
*/
|
|
132
132
|
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
|
|
133
|
-
|
|
133
|
+
// Typed by hand: since axios 1.20, request() returns an internal axios type that SDK declarations cannot name.
|
|
134
|
+
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH): Promise<R> => {
|
|
134
135
|
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url};
|
|
135
|
-
return axios.request<T, R>(axiosRequestArgs)
|
|
136
|
+
return axios.request<T, R>(axiosRequestArgs) as Promise<R>;
|
|
136
137
|
};
|
|
137
138
|
}
|
|
138
139
|
|
package/configuration.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface ConfigurationParameters {
|
|
|
21
21
|
basePath?: string;
|
|
22
22
|
baseOptions?: any;
|
|
23
23
|
formDataCtor?: new () => any;
|
|
24
|
+
cookieAuth?: boolean;
|
|
25
|
+
account?: string;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export class Configuration {
|
|
@@ -74,6 +76,22 @@ export class Configuration {
|
|
|
74
76
|
*/
|
|
75
77
|
formDataCtor?: new () => any;
|
|
76
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Use a server session when authservice makes one; otherwise the token, as without this option.
|
|
81
|
+
* Set `account` too, so each SDK object keeps its own account.
|
|
82
|
+
*
|
|
83
|
+
* @type {boolean}
|
|
84
|
+
* @memberof Configuration
|
|
85
|
+
*/
|
|
86
|
+
cookieAuth?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* The session account (Cognito sub) this app shows, so the SDK never takes another tab's account.
|
|
89
|
+
*
|
|
90
|
+
* @type {string}
|
|
91
|
+
* @memberof Configuration
|
|
92
|
+
*/
|
|
93
|
+
account?: string;
|
|
94
|
+
|
|
77
95
|
constructor(param: ConfigurationParameters = {}) {
|
|
78
96
|
this.apiKey = param.apiKey;
|
|
79
97
|
this.username = param.username;
|
|
@@ -82,6 +100,8 @@ export class Configuration {
|
|
|
82
100
|
this.basePath = param.basePath;
|
|
83
101
|
this.baseOptions = param.baseOptions;
|
|
84
102
|
this.formDataCtor = param.formDataCtor;
|
|
103
|
+
this.cookieAuth = param.cookieAuth;
|
|
104
|
+
this.account = param.account;
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
/**
|