@etsoo/appscript 1.5.19 → 1.5.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/__tests__/app/CoreApp.ts +17 -0
- package/lib/cjs/app/CoreApp.d.ts +60 -14
- package/lib/cjs/app/CoreApp.js +212 -54
- package/lib/cjs/app/ExternalSettings.d.ts +15 -14
- package/lib/cjs/app/ExternalSettings.js +3 -0
- package/lib/cjs/app/IApp.d.ts +35 -2
- package/lib/cjs/erp/AuthApi.js +1 -1
- package/lib/cjs/erp/dto/ApiRefreshTokenDto.d.ts +21 -0
- package/lib/cjs/erp/dto/ApiRefreshTokenDto.js +2 -0
- package/lib/mjs/app/CoreApp.d.ts +60 -14
- package/lib/mjs/app/CoreApp.js +214 -56
- package/lib/mjs/app/ExternalSettings.d.ts +15 -14
- package/lib/mjs/app/ExternalSettings.js +3 -0
- package/lib/mjs/app/IApp.d.ts +35 -2
- package/lib/mjs/erp/AuthApi.js +1 -1
- package/lib/mjs/erp/dto/ApiRefreshTokenDto.d.ts +21 -0
- package/lib/mjs/erp/dto/ApiRefreshTokenDto.js +1 -0
- package/package.json +9 -9
- package/src/app/CoreApp.ts +304 -65
- package/src/app/ExternalSettings.ts +21 -16
- package/src/app/IApp.ts +47 -2
- package/src/erp/AuthApi.ts +1 -1
- package/src/erp/dto/ApiRefreshTokenDto.ts +24 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API refresh token data
|
|
3
|
+
*/
|
|
4
|
+
export type ApiRefreshTokenDto = {
|
|
5
|
+
/**
|
|
6
|
+
* Refresh token
|
|
7
|
+
*/
|
|
8
|
+
readonly refreshToken: string;
|
|
9
|
+
/**
|
|
10
|
+
* Access token
|
|
11
|
+
*/
|
|
12
|
+
readonly accessToken: string;
|
|
13
|
+
/**
|
|
14
|
+
* Token type
|
|
15
|
+
*/
|
|
16
|
+
readonly tokenType: string;
|
|
17
|
+
/**
|
|
18
|
+
* Expires in
|
|
19
|
+
*/
|
|
20
|
+
readonly expiresIn: number;
|
|
21
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etsoo/appscript",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.21",
|
|
4
4
|
"description": "Applications shared TypeScript framework",
|
|
5
5
|
"main": "lib/cjs/index.js",
|
|
6
6
|
"module": "lib/mjs/index.js",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
},
|
|
53
53
|
"homepage": "https://github.com/ETSOO/AppScript#readme",
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@etsoo/notificationbase": "^1.1.
|
|
56
|
-
"@etsoo/restclient": "^1.1.
|
|
57
|
-
"@etsoo/shared": "^1.2.
|
|
55
|
+
"@etsoo/notificationbase": "^1.1.48",
|
|
56
|
+
"@etsoo/restclient": "^1.1.10",
|
|
57
|
+
"@etsoo/shared": "^1.2.46",
|
|
58
58
|
"crypto-js": "^4.2.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@babel/cli": "^7.25.
|
|
62
|
-
"@babel/core": "^7.25.
|
|
63
|
-
"@babel/plugin-transform-runtime": "^7.25.
|
|
64
|
-
"@babel/preset-env": "^7.25.
|
|
65
|
-
"@babel/runtime-corejs3": "^7.25.
|
|
61
|
+
"@babel/cli": "^7.25.7",
|
|
62
|
+
"@babel/core": "^7.25.7",
|
|
63
|
+
"@babel/plugin-transform-runtime": "^7.25.7",
|
|
64
|
+
"@babel/preset-env": "^7.25.7",
|
|
65
|
+
"@babel/runtime-corejs3": "^7.25.7",
|
|
66
66
|
"@types/crypto-js": "^4.2.2",
|
|
67
67
|
"@types/jest": "^29.5.13",
|
|
68
68
|
"jest": "^29.7.0",
|
package/src/app/CoreApp.ts
CHANGED
|
@@ -7,13 +7,14 @@ import {
|
|
|
7
7
|
NotificationMessageType,
|
|
8
8
|
NotificationReturn
|
|
9
9
|
} from '@etsoo/notificationbase';
|
|
10
|
-
import { ApiDataError, IApi, IPData } from '@etsoo/restclient';
|
|
10
|
+
import { ApiDataError, createClient, IApi, IPData } from '@etsoo/restclient';
|
|
11
11
|
import {
|
|
12
12
|
DataTypes,
|
|
13
13
|
DateUtils,
|
|
14
14
|
DomUtils,
|
|
15
15
|
ErrorData,
|
|
16
16
|
ErrorType,
|
|
17
|
+
ExtendUtils,
|
|
17
18
|
IActionResult,
|
|
18
19
|
IStorage,
|
|
19
20
|
ListType,
|
|
@@ -43,12 +44,23 @@ import {
|
|
|
43
44
|
import { UserRole } from './UserRole';
|
|
44
45
|
import type CryptoJS from 'crypto-js';
|
|
45
46
|
import { Currency } from '../business/Currency';
|
|
47
|
+
import { ExternalEndpoint } from './ExternalSettings';
|
|
48
|
+
import { ApiRefreshTokenDto } from '../erp/dto/ApiRefreshTokenDto';
|
|
46
49
|
|
|
47
50
|
type CJType = typeof CryptoJS;
|
|
48
51
|
let CJ: CJType;
|
|
49
52
|
|
|
50
53
|
const loadCrypto = () => import('crypto-js');
|
|
51
54
|
|
|
55
|
+
// API refresh token function interface
|
|
56
|
+
type ApiRefreshTokenFunction = (
|
|
57
|
+
api: IApi,
|
|
58
|
+
token: string
|
|
59
|
+
) => Promise<[string, number] | undefined>;
|
|
60
|
+
|
|
61
|
+
// API task data
|
|
62
|
+
type ApiTaskData = [IApi, number, number, ApiRefreshTokenFunction, string?];
|
|
63
|
+
|
|
52
64
|
/**
|
|
53
65
|
* Core application interface
|
|
54
66
|
*/
|
|
@@ -229,11 +241,6 @@ export abstract class CoreApp<
|
|
|
229
241
|
*/
|
|
230
242
|
protected lastCalled = false;
|
|
231
243
|
|
|
232
|
-
/**
|
|
233
|
-
* Token refresh count down seed
|
|
234
|
-
*/
|
|
235
|
-
protected refreshCountdownSeed = 0;
|
|
236
|
-
|
|
237
244
|
/**
|
|
238
245
|
* Init call Api URL
|
|
239
246
|
*/
|
|
@@ -246,6 +253,10 @@ export abstract class CoreApp<
|
|
|
246
253
|
|
|
247
254
|
private cachedRefreshToken?: string;
|
|
248
255
|
|
|
256
|
+
private apis: Record<string, ApiTaskData> = {};
|
|
257
|
+
|
|
258
|
+
private tasks: [() => PromiseLike<void | false>, number, number][] = [];
|
|
259
|
+
|
|
249
260
|
/**
|
|
250
261
|
* Get persisted fields
|
|
251
262
|
*/
|
|
@@ -269,7 +280,7 @@ export abstract class CoreApp<
|
|
|
269
280
|
*/
|
|
270
281
|
protected constructor(
|
|
271
282
|
settings: S,
|
|
272
|
-
api: IApi,
|
|
283
|
+
api: IApi | undefined | null,
|
|
273
284
|
notifier: INotifier<N, C>,
|
|
274
285
|
storage: IStorage,
|
|
275
286
|
name: string,
|
|
@@ -286,7 +297,34 @@ export abstract class CoreApp<
|
|
|
286
297
|
}
|
|
287
298
|
this.defaultRegion = region;
|
|
288
299
|
|
|
289
|
-
|
|
300
|
+
const refresh: ApiRefreshTokenFunction = async (api, token) => {
|
|
301
|
+
if (this.lastCalled) {
|
|
302
|
+
// Call refreshToken to update access token
|
|
303
|
+
await this.refreshToken();
|
|
304
|
+
} else {
|
|
305
|
+
// Popup countdown for user action
|
|
306
|
+
this.freshCountdownUI();
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
if (api) {
|
|
312
|
+
// Base URL of the API
|
|
313
|
+
api.baseUrl = this.settings.endpoint;
|
|
314
|
+
api.name = 'system';
|
|
315
|
+
this.setApi(api, refresh);
|
|
316
|
+
this.api = api;
|
|
317
|
+
} else {
|
|
318
|
+
this.api = this.createApi(
|
|
319
|
+
'system',
|
|
320
|
+
{
|
|
321
|
+
endpoint: settings.endpoint,
|
|
322
|
+
webUrl: settings.webUrl
|
|
323
|
+
},
|
|
324
|
+
refresh
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
290
328
|
this.notifier = notifier;
|
|
291
329
|
this.storage = storage;
|
|
292
330
|
this.name = name;
|
|
@@ -301,8 +339,6 @@ export abstract class CoreApp<
|
|
|
301
339
|
// Device id
|
|
302
340
|
this._deviceId = storage.getData(this.fields.deviceId, '');
|
|
303
341
|
|
|
304
|
-
this.setApi(api);
|
|
305
|
-
|
|
306
342
|
const { currentCulture, currentRegion } = settings;
|
|
307
343
|
|
|
308
344
|
// Load resources
|
|
@@ -464,19 +500,86 @@ export abstract class CoreApp<
|
|
|
464
500
|
this.storage.copyTo(this.persistedFields);
|
|
465
501
|
}
|
|
466
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Add scheduled task
|
|
505
|
+
* @param task Task, return false to stop
|
|
506
|
+
* @param seconds Interval in seconds
|
|
507
|
+
*/
|
|
508
|
+
addTask(task: () => PromiseLike<void | false>, seconds: number) {
|
|
509
|
+
this.tasks.push([task, seconds, seconds]);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Create API client, override to implement custom client creation by name
|
|
514
|
+
* @param name Client name
|
|
515
|
+
* @param item External endpoint item
|
|
516
|
+
* @returns Result
|
|
517
|
+
*/
|
|
518
|
+
createApi(
|
|
519
|
+
name: string,
|
|
520
|
+
item: ExternalEndpoint,
|
|
521
|
+
refresh?: (
|
|
522
|
+
api: IApi,
|
|
523
|
+
token: string
|
|
524
|
+
) => Promise<[string, number] | undefined>
|
|
525
|
+
) {
|
|
526
|
+
if (this.apis[name] != null) {
|
|
527
|
+
throw new Error(`API ${name} already exists`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const api = createClient();
|
|
531
|
+
api.name = name;
|
|
532
|
+
api.baseUrl = item.endpoint;
|
|
533
|
+
this.setApi(api, refresh);
|
|
534
|
+
return api;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Update API token and expires
|
|
539
|
+
* @param name Api name
|
|
540
|
+
* @param token Refresh token
|
|
541
|
+
* @param seconds Access token expires in seconds
|
|
542
|
+
*/
|
|
543
|
+
updateApi(name: string, token: string | undefined, seconds: number): void;
|
|
544
|
+
updateApi(
|
|
545
|
+
data: ApiTaskData,
|
|
546
|
+
token: string | undefined,
|
|
547
|
+
seconds: number
|
|
548
|
+
): void;
|
|
549
|
+
updateApi(
|
|
550
|
+
nameOrData: string | ApiTaskData,
|
|
551
|
+
token: string | undefined,
|
|
552
|
+
seconds: number
|
|
553
|
+
) {
|
|
554
|
+
const api =
|
|
555
|
+
typeof nameOrData === 'string' ? this.apis[nameOrData] : nameOrData;
|
|
556
|
+
if (api == null) return;
|
|
557
|
+
|
|
558
|
+
// Consider the API call delay
|
|
559
|
+
if (seconds > 0) {
|
|
560
|
+
seconds -= 30;
|
|
561
|
+
if (seconds < 10) seconds = 10;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
api[1] = seconds;
|
|
565
|
+
api[2] = seconds;
|
|
566
|
+
api[4] = token;
|
|
567
|
+
}
|
|
568
|
+
|
|
467
569
|
/**
|
|
468
570
|
* Setup Api
|
|
469
571
|
* @param api Api
|
|
470
572
|
*/
|
|
471
|
-
protected setApi(api: IApi) {
|
|
472
|
-
// Base URL of the API
|
|
473
|
-
api.baseUrl = this.settings.endpoint;
|
|
474
|
-
|
|
573
|
+
protected setApi(api: IApi, refresh?: ApiRefreshTokenFunction) {
|
|
475
574
|
// onRequest, show loading or not, rewrite the property to override default action
|
|
476
575
|
this.setApiLoading(api);
|
|
477
576
|
|
|
478
577
|
// Global API error handler
|
|
479
578
|
this.setApiErrorHandler(api);
|
|
579
|
+
|
|
580
|
+
// Setup API countdown
|
|
581
|
+
refresh ??= this.apiRefreshToken.bind(this);
|
|
582
|
+
this.apis[api.name] = [api, -1, -1, refresh];
|
|
480
583
|
}
|
|
481
584
|
|
|
482
585
|
/**
|
|
@@ -484,7 +587,7 @@ export abstract class CoreApp<
|
|
|
484
587
|
* @param api Api
|
|
485
588
|
* @param handlerFor401 Handler for 401 error
|
|
486
589
|
*/
|
|
487
|
-
|
|
590
|
+
setApiErrorHandler(
|
|
488
591
|
api: IApi,
|
|
489
592
|
handlerFor401?: boolean | (() => Promise<void>)
|
|
490
593
|
) {
|
|
@@ -492,7 +595,7 @@ export abstract class CoreApp<
|
|
|
492
595
|
// Debug
|
|
493
596
|
if (this.debug) {
|
|
494
597
|
console.debug(
|
|
495
|
-
|
|
598
|
+
`CoreApp.${this.name}.setApiErrorHandler`,
|
|
496
599
|
api,
|
|
497
600
|
error,
|
|
498
601
|
handlerFor401
|
|
@@ -519,11 +622,13 @@ export abstract class CoreApp<
|
|
|
519
622
|
error.message === 'Failed to fetch')
|
|
520
623
|
) {
|
|
521
624
|
// Network error
|
|
522
|
-
this.notifier.alert(
|
|
625
|
+
this.notifier.alert(
|
|
626
|
+
this.get('networkError') + ` [${this.name}]`
|
|
627
|
+
);
|
|
523
628
|
return;
|
|
524
629
|
} else {
|
|
525
630
|
// Log
|
|
526
|
-
console.error(
|
|
631
|
+
console.error(`${this.name} API error`, error);
|
|
527
632
|
}
|
|
528
633
|
|
|
529
634
|
// Report the error
|
|
@@ -535,13 +640,13 @@ export abstract class CoreApp<
|
|
|
535
640
|
* Setup Api loading
|
|
536
641
|
* @param api Api
|
|
537
642
|
*/
|
|
538
|
-
|
|
643
|
+
setApiLoading(api: IApi) {
|
|
539
644
|
// onRequest, show loading or not, rewrite the property to override default action
|
|
540
645
|
api.onRequest = (data) => {
|
|
541
646
|
// Debug
|
|
542
647
|
if (this.debug) {
|
|
543
648
|
console.debug(
|
|
544
|
-
|
|
649
|
+
`CoreApp.${this.name}.setApiLoading.onRequest`,
|
|
545
650
|
api,
|
|
546
651
|
data,
|
|
547
652
|
this.notifier.loadingCount
|
|
@@ -558,7 +663,7 @@ export abstract class CoreApp<
|
|
|
558
663
|
// Debug
|
|
559
664
|
if (this.debug) {
|
|
560
665
|
console.debug(
|
|
561
|
-
|
|
666
|
+
`CoreApp.${this.name}.setApiLoading.onComplete`,
|
|
562
667
|
api,
|
|
563
668
|
data,
|
|
564
669
|
this.notifier.loadingCount,
|
|
@@ -572,7 +677,7 @@ export abstract class CoreApp<
|
|
|
572
677
|
// Debug
|
|
573
678
|
if (this.debug) {
|
|
574
679
|
console.debug(
|
|
575
|
-
|
|
680
|
+
`CoreApp.${this.name}.setApiLoading.onComplete.showLoading`,
|
|
576
681
|
api,
|
|
577
682
|
this.notifier.loadingCount
|
|
578
683
|
);
|
|
@@ -587,7 +692,7 @@ export abstract class CoreApp<
|
|
|
587
692
|
* @param action Custom action
|
|
588
693
|
* @param preventDefault Is prevent default action
|
|
589
694
|
*/
|
|
590
|
-
|
|
695
|
+
setupLogging(
|
|
591
696
|
action?: (data: ErrorData) => void | Promise<void>,
|
|
592
697
|
preventDefault?: ((type: ErrorType) => boolean) | boolean
|
|
593
698
|
) {
|
|
@@ -869,10 +974,18 @@ export abstract class CoreApp<
|
|
|
869
974
|
this._isTryingLogin = false;
|
|
870
975
|
|
|
871
976
|
// Token countdown
|
|
872
|
-
if (this.authorized)
|
|
873
|
-
|
|
977
|
+
if (this.authorized) {
|
|
978
|
+
this.lastCalled = false;
|
|
979
|
+
if (refreshToken) {
|
|
980
|
+
this.updateApi(
|
|
981
|
+
this.api.name,
|
|
982
|
+
refreshToken,
|
|
983
|
+
this.userData!.seconds
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
} else {
|
|
874
987
|
this.cachedRefreshToken = undefined;
|
|
875
|
-
this.
|
|
988
|
+
this.updateApi(this.api.name, undefined, -1);
|
|
876
989
|
}
|
|
877
990
|
|
|
878
991
|
// Host notice
|
|
@@ -1766,43 +1879,6 @@ export abstract class CoreApp<
|
|
|
1766
1879
|
this.notifier.hideLoading(true);
|
|
1767
1880
|
}
|
|
1768
1881
|
|
|
1769
|
-
/**
|
|
1770
|
-
* Refresh countdown
|
|
1771
|
-
* @param seconds Seconds
|
|
1772
|
-
*/
|
|
1773
|
-
protected refreshCountdown(seconds: number) {
|
|
1774
|
-
// Make sure is big than 60 seconds
|
|
1775
|
-
// Take action 60 seconds before expiry
|
|
1776
|
-
seconds -= 60;
|
|
1777
|
-
if (seconds <= 0) return;
|
|
1778
|
-
|
|
1779
|
-
// Clear the current timeout seed
|
|
1780
|
-
this.refreshCountdownClear();
|
|
1781
|
-
|
|
1782
|
-
// Reset last call flag
|
|
1783
|
-
// Any success call will update it to true
|
|
1784
|
-
// So first time after login will be always silent
|
|
1785
|
-
this.lastCalled = false;
|
|
1786
|
-
|
|
1787
|
-
this.refreshCountdownSeed = window.setTimeout(() => {
|
|
1788
|
-
if (this.lastCalled) {
|
|
1789
|
-
// Call refreshToken to update access token
|
|
1790
|
-
this.refreshToken();
|
|
1791
|
-
} else {
|
|
1792
|
-
// Popup countdown for user action
|
|
1793
|
-
this.freshCountdownUI();
|
|
1794
|
-
}
|
|
1795
|
-
}, 1000 * seconds);
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
protected refreshCountdownClear() {
|
|
1799
|
-
// Clear the current timeout seed
|
|
1800
|
-
if (this.refreshCountdownSeed > 0) {
|
|
1801
|
-
window.clearTimeout(this.refreshCountdownSeed);
|
|
1802
|
-
this.refreshCountdownSeed = 0;
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
1882
|
/**
|
|
1807
1883
|
* Fresh countdown UI
|
|
1808
1884
|
* @param callback Callback
|
|
@@ -1822,6 +1898,9 @@ export abstract class CoreApp<
|
|
|
1822
1898
|
* Setup callback
|
|
1823
1899
|
*/
|
|
1824
1900
|
setup() {
|
|
1901
|
+
// Done already
|
|
1902
|
+
if (this.isReady) return;
|
|
1903
|
+
|
|
1825
1904
|
// Ready
|
|
1826
1905
|
this.isReady = true;
|
|
1827
1906
|
|
|
@@ -1830,6 +1909,167 @@ export abstract class CoreApp<
|
|
|
1830
1909
|
|
|
1831
1910
|
// Pending actions
|
|
1832
1911
|
this.pendings.forEach((p) => p());
|
|
1912
|
+
|
|
1913
|
+
// Setup scheduled tasks
|
|
1914
|
+
this.setupTasks();
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/**
|
|
1918
|
+
* Exchange token data
|
|
1919
|
+
* @param api API
|
|
1920
|
+
* @param token Core system's refresh token to exchange
|
|
1921
|
+
* @returns Result
|
|
1922
|
+
*/
|
|
1923
|
+
async exchangeToken(api: IApi, token: string) {
|
|
1924
|
+
// Call the API quietly, no loading bar and no error popup
|
|
1925
|
+
const data = await api.put<ApiRefreshTokenDto>(
|
|
1926
|
+
'Auth/ExchangeToken',
|
|
1927
|
+
{
|
|
1928
|
+
token
|
|
1929
|
+
},
|
|
1930
|
+
{
|
|
1931
|
+
showLoading: false,
|
|
1932
|
+
onError: (error) => {
|
|
1933
|
+
console.error(
|
|
1934
|
+
`CoreApp.${api.name}.ExchangeToken error`,
|
|
1935
|
+
error
|
|
1936
|
+
);
|
|
1937
|
+
|
|
1938
|
+
// Prevent further processing
|
|
1939
|
+
return false;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
);
|
|
1943
|
+
|
|
1944
|
+
if (data) {
|
|
1945
|
+
// Update the access token
|
|
1946
|
+
api.authorize(data.tokenType, data.accessToken);
|
|
1947
|
+
|
|
1948
|
+
// Update the API
|
|
1949
|
+
this.updateApi(api.name, data.refreshToken, data.expiresIn);
|
|
1950
|
+
|
|
1951
|
+
// Update notice
|
|
1952
|
+
this.exchangeTokenUpdate(api, data);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/**
|
|
1957
|
+
* Exchange token update, override to get the new token
|
|
1958
|
+
* @param api API
|
|
1959
|
+
* @param data API refresh token data
|
|
1960
|
+
*/
|
|
1961
|
+
protected exchangeTokenUpdate(api: IApi, data: ApiRefreshTokenDto) {}
|
|
1962
|
+
|
|
1963
|
+
/**
|
|
1964
|
+
* Exchange intergration tokens for all APIs
|
|
1965
|
+
* @param token Core system's refresh token to exchange
|
|
1966
|
+
*/
|
|
1967
|
+
exchangeTokenAll(token: string) {
|
|
1968
|
+
for (const name in this.apis) {
|
|
1969
|
+
const api = this.apis[name];
|
|
1970
|
+
this.exchangeToken(api[0], token);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
/**
|
|
1975
|
+
* API refresh token
|
|
1976
|
+
* @param api Current API
|
|
1977
|
+
* @param token Refresh token
|
|
1978
|
+
* @returns Result
|
|
1979
|
+
*/
|
|
1980
|
+
protected async apiRefreshToken(
|
|
1981
|
+
api: IApi,
|
|
1982
|
+
token: string
|
|
1983
|
+
): Promise<[string, number] | undefined> {
|
|
1984
|
+
// Call the API quietly, no loading bar and no error popup
|
|
1985
|
+
const data = await api.put<ApiRefreshTokenDto>(
|
|
1986
|
+
'Auth/ApiRefreshToken',
|
|
1987
|
+
{
|
|
1988
|
+
token
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
showLoading: false,
|
|
1992
|
+
onError: (error) => {
|
|
1993
|
+
console.error(
|
|
1994
|
+
`CoreApp.${api.name}.apiRefreshToken error`,
|
|
1995
|
+
error
|
|
1996
|
+
);
|
|
1997
|
+
|
|
1998
|
+
// Prevent further processing
|
|
1999
|
+
return false;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
);
|
|
2003
|
+
|
|
2004
|
+
if (data == null) return undefined;
|
|
2005
|
+
|
|
2006
|
+
// Update the access token
|
|
2007
|
+
api.authorize(data.tokenType, data.accessToken);
|
|
2008
|
+
|
|
2009
|
+
// Return the new refresh token and access token expiration seconds
|
|
2010
|
+
return [data.refreshToken, data.expiresIn];
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/**
|
|
2014
|
+
* Setup tasks
|
|
2015
|
+
*/
|
|
2016
|
+
protected setupTasks() {
|
|
2017
|
+
ExtendUtils.intervalFor(() => {
|
|
2018
|
+
// Exit when not authorized
|
|
2019
|
+
if (!this.authorized) return;
|
|
2020
|
+
|
|
2021
|
+
// APIs
|
|
2022
|
+
for (const name in this.apis) {
|
|
2023
|
+
// Get the API
|
|
2024
|
+
const api = this.apis[name];
|
|
2025
|
+
|
|
2026
|
+
// Skip the negative value or when refresh token is not set
|
|
2027
|
+
if (!api[4] || api[2] < 0) continue;
|
|
2028
|
+
|
|
2029
|
+
// Minus one second
|
|
2030
|
+
api[2] -= 1;
|
|
2031
|
+
|
|
2032
|
+
// Ready to trigger
|
|
2033
|
+
if (api[2] === 0) {
|
|
2034
|
+
// Refresh token
|
|
2035
|
+
api[3](api[0], api[4]).then((data) => {
|
|
2036
|
+
if (data == null) {
|
|
2037
|
+
// Failed, try it again in 2 seconds
|
|
2038
|
+
api[2] = 2;
|
|
2039
|
+
} else {
|
|
2040
|
+
// Reset the API
|
|
2041
|
+
const [token, seconds] = data;
|
|
2042
|
+
this.updateApi(api, token, seconds);
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
for (let t = this.tasks.length - 1; t >= 0; t--) {
|
|
2049
|
+
// Get the task
|
|
2050
|
+
const task = this.tasks[t];
|
|
2051
|
+
|
|
2052
|
+
// Minus one second
|
|
2053
|
+
task[2] -= 1;
|
|
2054
|
+
|
|
2055
|
+
// Remove the tasks with negative value with splice
|
|
2056
|
+
if (task[2] < 0) {
|
|
2057
|
+
this.tasks.splice(t, 1);
|
|
2058
|
+
} else if (task[2] === 0) {
|
|
2059
|
+
// Ready to trigger
|
|
2060
|
+
// Reset the task
|
|
2061
|
+
task[2] = task[1];
|
|
2062
|
+
|
|
2063
|
+
// Trigger the task
|
|
2064
|
+
task[0]().then((result) => {
|
|
2065
|
+
if (result === false) {
|
|
2066
|
+
// Asynchronous task, unsafe to splice the index, flag as pending
|
|
2067
|
+
task[2] = -1;
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}, 1000);
|
|
1833
2073
|
}
|
|
1834
2074
|
|
|
1835
2075
|
/**
|
|
@@ -1872,10 +2112,9 @@ export abstract class CoreApp<
|
|
|
1872
2112
|
/**
|
|
1873
2113
|
* Try login, returning false means is loading
|
|
1874
2114
|
* UI get involved while refreshToken not intended
|
|
1875
|
-
* @param data Additional request data
|
|
1876
2115
|
* @param showLoading Show loading bar or not during call
|
|
1877
2116
|
*/
|
|
1878
|
-
async tryLogin
|
|
2117
|
+
async tryLogin(_showLoading?: boolean) {
|
|
1879
2118
|
if (this._isTryingLogin) return false;
|
|
1880
2119
|
this._isTryingLogin = true;
|
|
1881
2120
|
return true;
|
|
@@ -1,36 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* External
|
|
2
|
+
* External endpoint
|
|
3
3
|
*/
|
|
4
|
-
export
|
|
4
|
+
export type ExternalEndpoint = {
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* API endpoint
|
|
7
7
|
*/
|
|
8
8
|
readonly endpoint: string;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*/
|
|
13
|
-
readonly messageHub?: string;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Core system app root url
|
|
11
|
+
* Web url
|
|
17
12
|
*/
|
|
18
|
-
readonly
|
|
13
|
+
readonly webUrl: string;
|
|
14
|
+
};
|
|
19
15
|
|
|
16
|
+
/**
|
|
17
|
+
* External settings items
|
|
18
|
+
*/
|
|
19
|
+
export interface IExternalSettings extends ExternalEndpoint {
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Message hub endpoint
|
|
22
22
|
*/
|
|
23
|
-
readonly
|
|
23
|
+
readonly messageHub?: string;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* App root url
|
|
27
27
|
*/
|
|
28
|
-
readonly
|
|
28
|
+
readonly homepage: string;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* Endpoints to other services
|
|
32
32
|
*/
|
|
33
|
-
readonly
|
|
33
|
+
readonly endpoints?: Record<
|
|
34
|
+
'core' | 'accounting' | 'crm' | 'calandar' | 'task' | string,
|
|
35
|
+
ExternalEndpoint
|
|
36
|
+
>;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/**
|
|
@@ -69,6 +72,8 @@ export namespace ExternalSettings {
|
|
|
69
72
|
const value = settings[key];
|
|
70
73
|
if (typeof value === 'string') {
|
|
71
74
|
settings[key] = value.replace('{hostname}', hostname);
|
|
75
|
+
} else if (typeof value === 'object') {
|
|
76
|
+
format(value, hostname);
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
|