@etsoo/appscript 1.5.67 → 1.5.69

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.
@@ -1,2385 +1,2328 @@
1
1
  import {
2
- INotification,
3
- INotifier,
4
- NotificationAlign,
5
- NotificationCallProps,
6
- NotificationContent,
7
- NotificationMessageType,
8
- NotificationReturn
9
- } from '@etsoo/notificationbase';
10
- import { ApiDataError, createClient, IApi, IPData } from '@etsoo/restclient';
2
+ INotification,
3
+ INotifier,
4
+ NotificationAlign,
5
+ NotificationCallProps,
6
+ NotificationContent,
7
+ NotificationMessageType,
8
+ NotificationReturn
9
+ } from "@etsoo/notificationbase";
10
+ import { ApiDataError, createClient, IApi, IPData } from "@etsoo/restclient";
11
11
  import {
12
- DataTypes,
13
- DateUtils,
14
- DomUtils,
15
- ErrorData,
16
- ErrorType,
17
- ExtendUtils,
18
- IActionResult,
19
- IStorage,
20
- ListType,
21
- ListType1,
22
- NumberUtils,
23
- Utils
24
- } from '@etsoo/shared';
25
- import { AddressRegion } from '../address/AddressRegion';
26
- import { BridgeUtils } from '../bridges/BridgeUtils';
27
- import { DataPrivacy } from '../business/DataPrivacy';
28
- import { EntityStatus } from '../business/EntityStatus';
29
- import { InitCallDto } from '../api/dto/InitCallDto';
30
- import { ActionResultError } from '../result/ActionResultError';
31
- import { InitCallResult, InitCallResultData } from '../result/InitCallResult';
32
- import { IUser } from '../state/User';
33
- import { IAppSettings } from './AppSettings';
12
+ DataTypes,
13
+ DateUtils,
14
+ DomUtils,
15
+ ErrorData,
16
+ ErrorType,
17
+ ExtendUtils,
18
+ IActionResult,
19
+ IStorage,
20
+ ListType,
21
+ ListType1,
22
+ NumberUtils,
23
+ Utils
24
+ } from "@etsoo/shared";
25
+ import { AddressRegion } from "../address/AddressRegion";
26
+ import { BridgeUtils } from "../bridges/BridgeUtils";
27
+ import { DataPrivacy } from "../business/DataPrivacy";
28
+ import { EntityStatus } from "../business/EntityStatus";
29
+ import { InitCallDto } from "../api/dto/InitCallDto";
30
+ import { ActionResultError } from "../result/ActionResultError";
31
+ import { InitCallResult, InitCallResultData } from "../result/InitCallResult";
32
+ import { IUser } from "../state/User";
33
+ import { IAppSettings } from "./AppSettings";
34
34
  import {
35
- appFields,
36
- AppLoginParams,
37
- AppTryLoginParams,
38
- FormatResultCustomCallback,
39
- IApp,
40
- IAppFields,
41
- IDetectIPCallback,
42
- NavigateOptions,
43
- RefreshTokenProps
44
- } from './IApp';
45
- import { UserRole } from './UserRole';
46
- import type CryptoJS from 'crypto-js';
47
- import { Currency } from '../business/Currency';
48
- import { ExternalEndpoint } from './ExternalSettings';
49
- import { ApiRefreshTokenDto } from '../api/dto/ApiRefreshTokenDto';
50
- import { ApiRefreshTokenRQ } from '../api/rq/ApiRefreshTokenRQ';
51
- import { AuthApi } from '../api/AuthApi';
35
+ appFields,
36
+ AppLoginParams,
37
+ AppTryLoginParams,
38
+ FormatResultCustomCallback,
39
+ IApp,
40
+ IAppFields,
41
+ IDetectIPCallback,
42
+ NavigateOptions,
43
+ RefreshTokenProps
44
+ } from "./IApp";
45
+ import { UserRole } from "./UserRole";
46
+ import type CryptoJS from "crypto-js";
47
+ import { Currency } from "../business/Currency";
48
+ import { ExternalEndpoint } from "./ExternalSettings";
49
+ import { ApiRefreshTokenDto } from "../api/dto/ApiRefreshTokenDto";
50
+ import { ApiRefreshTokenRQ } from "../api/rq/ApiRefreshTokenRQ";
51
+ import { AuthApi } from "../api/AuthApi";
52
52
 
53
53
  type CJType = typeof CryptoJS;
54
54
  let CJ: CJType;
55
55
 
56
- const loadCrypto = () => import('crypto-js');
56
+ const loadCrypto = () => import("crypto-js");
57
57
 
58
58
  // API refresh token function interface
59
59
  type ApiRefreshTokenFunction = (
60
- api: IApi,
61
- rq: ApiRefreshTokenRQ
60
+ api: IApi,
61
+ rq: ApiRefreshTokenRQ
62
62
  ) => Promise<[string, number] | undefined>;
63
63
 
64
64
  // API task data
65
65
  type ApiTaskData = [IApi, number, number, ApiRefreshTokenFunction, string?];
66
66
 
67
67
  // System API name
68
- const systemApi = 'system';
68
+ const systemApi = "system";
69
69
 
70
70
  /**
71
71
  * Core application interface
72
72
  */
73
73
  export interface ICoreApp<
74
- U extends IUser,
75
- S extends IAppSettings,
76
- N,
77
- C extends NotificationCallProps
74
+ U extends IUser,
75
+ S extends IAppSettings,
76
+ N,
77
+ C extends NotificationCallProps
78
78
  > extends IApp {
79
- /**
80
- * Settings
81
- */
82
- readonly settings: S;
83
-
84
- /**
85
- * Notifier
86
- */
87
- readonly notifier: INotifier<N, C>;
88
-
89
- /**
90
- * User data
91
- */
92
- userData?: U;
79
+ /**
80
+ * Settings
81
+ */
82
+ readonly settings: S;
83
+
84
+ /**
85
+ * Notifier
86
+ */
87
+ readonly notifier: INotifier<N, C>;
88
+
89
+ /**
90
+ * User data
91
+ */
92
+ userData?: U;
93
93
  }
94
94
 
95
95
  /**
96
96
  * Core application
97
97
  */
98
98
  export abstract class CoreApp<
99
- U extends IUser,
100
- S extends IAppSettings,
101
- N,
102
- C extends NotificationCallProps
99
+ U extends IUser,
100
+ S extends IAppSettings,
101
+ N,
102
+ C extends NotificationCallProps
103
103
  > implements ICoreApp<U, S, N, C>
104
104
  {
105
- /**
106
- * Settings
107
- */
108
- readonly settings: S;
109
-
110
- /**
111
- * Default region
112
- */
113
- readonly defaultRegion: AddressRegion;
114
-
115
- /**
116
- * Fields
117
- */
118
- readonly fields: IAppFields;
119
-
120
- /**
121
- * API, not recommend to use it directly in code, wrap to separate methods
122
- */
123
- readonly api: IApi;
124
-
125
- /**
126
- * Application name
127
- */
128
- readonly name: string;
129
-
130
- /**
131
- * Notifier
132
- */
133
- readonly notifier: INotifier<N, C>;
134
-
135
- /**
136
- * Storage
137
- */
138
- readonly storage: IStorage;
139
-
140
- /**
141
- * Pending actions
142
- */
143
- readonly pendings: (() => any)[] = [];
144
-
145
- /**
146
- * Debug mode
147
- */
148
- readonly debug: boolean;
149
-
150
- private _culture!: string;
151
- /**
152
- * Culture, like zh-CN
153
- */
154
- get culture() {
155
- return this._culture;
156
- }
157
-
158
- private _currency!: Currency;
159
- /**
160
- * Currency, like USD for US dollar
161
- */
162
- get currency() {
163
- return this._currency;
164
- }
165
-
166
- private _region!: string;
167
- /**
168
- * Country or region, like CN
169
- */
170
- get region() {
171
- return this._region;
172
- }
173
-
174
- private _deviceId: string;
175
- /**
176
- * Device id, randome string from ServiceBase.InitCallAsync
177
- */
178
- get deviceId() {
179
- return this._deviceId;
180
- }
181
- protected set deviceId(value: string) {
182
- this._deviceId = value;
183
- this.storage.setData(this.fields.deviceId, this._deviceId);
184
- }
185
-
186
- /**
187
- * Label delegate
188
- */
189
- get labelDelegate() {
190
- return this.get.bind(this);
191
- }
192
-
193
- private _ipData?: IPData;
194
- /**
195
- * IP data
196
- */
197
- get ipData() {
198
- return this._ipData;
199
- }
200
- protected set ipData(value: IPData | undefined) {
201
- this._ipData = value;
202
- }
203
-
204
- private _userData?: U;
205
- /**
206
- * User data
207
- */
208
- get userData() {
209
- return this._userData;
210
- }
211
- protected set userData(value: U | undefined) {
212
- this._userData = value;
213
- }
214
-
215
- // IP detect ready callbacks
216
- private ipDetectCallbacks?: IDetectIPCallback[];
217
-
218
- /**
219
- * Search input element
220
- */
221
- searchInput?: HTMLInputElement;
222
-
223
- private _authorized: boolean = false;
224
- /**
225
- * Is current authorized
226
- */
227
- get authorized() {
228
- return this._authorized;
229
- }
230
-
231
- private set authorized(value: boolean) {
232
- this._authorized = value;
233
- }
234
-
235
- private _isReady: boolean = false;
236
- /**
237
- * Is the app ready
238
- */
239
- get isReady() {
240
- return this._isReady;
241
- }
242
-
243
- private set isReady(value: boolean) {
244
- this._isReady = value;
245
- }
246
-
247
- /**
248
- * Current cached URL
249
- */
250
- get cachedUrl() {
251
- return this.storage.getData(this.fields.cachedUrl);
252
- }
253
- set cachedUrl(value: string | undefined | null) {
254
- this.storage.setData(this.fields.cachedUrl, value);
255
- }
256
-
257
- /**
258
- * Keep login or not
259
- */
260
- get keepLogin() {
261
- return this.storage.getData<boolean>(this.fields.keepLogin) ?? false;
262
- }
263
- set keepLogin(value: boolean) {
264
- const field = this.fields.headerToken;
265
- if (!value) {
266
- // Clear the token
267
- this.clearCacheToken();
268
-
269
- // Remove the token field
270
- this.persistedFields.remove(field);
271
- } else if (!this.persistedFields.includes(field)) {
272
- this.persistedFields.push(field);
105
+ /**
106
+ * Settings
107
+ */
108
+ readonly settings: S;
109
+
110
+ /**
111
+ * Default region
112
+ */
113
+ readonly defaultRegion: AddressRegion;
114
+
115
+ /**
116
+ * Fields
117
+ */
118
+ readonly fields: IAppFields;
119
+
120
+ /**
121
+ * API, not recommend to use it directly in code, wrap to separate methods
122
+ */
123
+ readonly api: IApi;
124
+
125
+ /**
126
+ * Application name
127
+ */
128
+ readonly name: string;
129
+
130
+ /**
131
+ * Notifier
132
+ */
133
+ readonly notifier: INotifier<N, C>;
134
+
135
+ /**
136
+ * Storage
137
+ */
138
+ readonly storage: IStorage;
139
+
140
+ /**
141
+ * Pending actions
142
+ */
143
+ readonly pendings: (() => any)[] = [];
144
+
145
+ /**
146
+ * Debug mode
147
+ */
148
+ readonly debug: boolean;
149
+
150
+ private _culture!: string;
151
+ /**
152
+ * Culture, like zh-CN
153
+ */
154
+ get culture() {
155
+ return this._culture;
156
+ }
157
+
158
+ private _currency!: Currency;
159
+ /**
160
+ * Currency, like USD for US dollar
161
+ */
162
+ get currency() {
163
+ return this._currency;
164
+ }
165
+
166
+ private _region!: string;
167
+ /**
168
+ * Country or region, like CN
169
+ */
170
+ get region() {
171
+ return this._region;
172
+ }
173
+
174
+ private _deviceId: string;
175
+ /**
176
+ * Device id, randome string from ServiceBase.InitCallAsync
177
+ */
178
+ get deviceId() {
179
+ return this._deviceId;
180
+ }
181
+ protected set deviceId(value: string) {
182
+ this._deviceId = value;
183
+ this.storage.setData(this.fields.deviceId, this._deviceId);
184
+ }
185
+
186
+ /**
187
+ * Label delegate
188
+ */
189
+ get labelDelegate() {
190
+ return this.get.bind(this);
191
+ }
192
+
193
+ private _ipData?: IPData;
194
+ /**
195
+ * IP data
196
+ */
197
+ get ipData() {
198
+ return this._ipData;
199
+ }
200
+ protected set ipData(value: IPData | undefined) {
201
+ this._ipData = value;
202
+ }
203
+
204
+ private _userData?: U;
205
+ /**
206
+ * User data
207
+ */
208
+ get userData() {
209
+ return this._userData;
210
+ }
211
+ protected set userData(value: U | undefined) {
212
+ this._userData = value;
213
+ }
214
+
215
+ // IP detect ready callbacks
216
+ private ipDetectCallbacks?: IDetectIPCallback[];
217
+
218
+ /**
219
+ * Search input element
220
+ */
221
+ searchInput?: HTMLInputElement;
222
+
223
+ private _authorized: boolean = false;
224
+ /**
225
+ * Is current authorized
226
+ */
227
+ get authorized() {
228
+ return this._authorized;
229
+ }
230
+
231
+ private set authorized(value: boolean) {
232
+ this._authorized = value;
233
+ }
234
+
235
+ private _isReady: boolean = false;
236
+ /**
237
+ * Is the app ready
238
+ */
239
+ get isReady() {
240
+ return this._isReady;
241
+ }
242
+
243
+ private set isReady(value: boolean) {
244
+ this._isReady = value;
245
+ }
246
+
247
+ /**
248
+ * Current cached URL
249
+ */
250
+ get cachedUrl() {
251
+ return this.storage.getData(this.fields.cachedUrl);
252
+ }
253
+ set cachedUrl(value: string | undefined | null) {
254
+ this.storage.setData(this.fields.cachedUrl, value);
255
+ }
256
+
257
+ /**
258
+ * Keep login or not
259
+ */
260
+ get keepLogin() {
261
+ return this.storage.getData<boolean>(this.fields.keepLogin) ?? false;
262
+ }
263
+ set keepLogin(value: boolean) {
264
+ const field = this.fields.headerToken;
265
+ if (!value) {
266
+ // Clear the token
267
+ this.clearCacheToken();
268
+
269
+ // Remove the token field
270
+ this.persistedFields.remove(field);
271
+ } else if (!this.persistedFields.includes(field)) {
272
+ this.persistedFields.push(field);
273
+ }
274
+ this.storage.setData(this.fields.keepLogin, value);
275
+ }
276
+
277
+ private _embedded: boolean;
278
+ /**
279
+ * Is embedded
280
+ */
281
+ get embedded() {
282
+ return this._embedded;
283
+ }
284
+
285
+ private _isTryingLogin = false;
286
+ /**
287
+ * Is trying login
288
+ */
289
+ get isTryingLogin() {
290
+ return this._isTryingLogin;
291
+ }
292
+ protected set isTryingLogin(value: boolean) {
293
+ this._isTryingLogin = value;
294
+ }
295
+
296
+ /**
297
+ * Last called with token refresh
298
+ */
299
+ protected lastCalled = false;
300
+
301
+ /**
302
+ * Init call Api URL
303
+ */
304
+ protected initCallApi: string = "Auth/WebInitCall";
305
+
306
+ /**
307
+ * Passphrase for encryption
308
+ */
309
+ protected passphrase: string = "";
310
+
311
+ private apis: Record<string, ApiTaskData> = {};
312
+
313
+ private tasks: [() => PromiseLike<void | false>, number, number][] = [];
314
+
315
+ private clearInterval?: () => void;
316
+
317
+ /**
318
+ * Get persisted fields
319
+ */
320
+ protected get persistedFields() {
321
+ return [
322
+ this.fields.deviceId,
323
+ this.fields.devicePassphrase,
324
+ this.fields.serversideDeviceId,
325
+ this.fields.keepLogin
326
+ ];
327
+ }
328
+
329
+ /**
330
+ * Protected constructor
331
+ * @param settings Settings
332
+ * @param api API
333
+ * @param notifier Notifier
334
+ * @param storage Storage
335
+ * @param name Application name
336
+ * @param debug Debug mode
337
+ */
338
+ protected constructor(
339
+ settings: S,
340
+ api: IApi | undefined | null,
341
+ notifier: INotifier<N, C>,
342
+ storage: IStorage,
343
+ name: string,
344
+ debug: boolean = false
345
+ ) {
346
+ if (settings?.regions?.length === 0) {
347
+ throw new Error("No regions defined");
348
+ }
349
+ this.settings = settings;
350
+
351
+ const region = AddressRegion.getById(settings.regions[0]);
352
+ if (region == null) {
353
+ throw new Error("No default region defined");
354
+ }
355
+ this.defaultRegion = region;
356
+
357
+ // Current system refresh token
358
+ const refresh: ApiRefreshTokenFunction = async (api, rq) => {
359
+ if (this.lastCalled) {
360
+ // Call refreshToken to update access token
361
+ await this.refreshToken(
362
+ { token: rq.token, showLoading: false },
363
+ (result) => {
364
+ if (result === true) return;
365
+ console.log(`CoreApp.${this.name}.RefreshToken`, result);
366
+ }
367
+ );
368
+ } else {
369
+ // Popup countdown for user action
370
+ this.freshCountdownUI();
371
+ }
372
+ return undefined;
373
+ };
374
+
375
+ if (api) {
376
+ // Base URL of the API
377
+ api.baseUrl = this.settings.endpoint;
378
+ api.name = systemApi;
379
+ this.setApi(api, refresh);
380
+ this.api = api;
381
+ } else {
382
+ this.api = this.createApi(
383
+ systemApi,
384
+ {
385
+ endpoint: settings.endpoint,
386
+ webUrl: settings.webUrl
387
+ },
388
+ refresh
389
+ );
390
+ }
391
+
392
+ this.notifier = notifier;
393
+ this.storage = storage;
394
+ this.name = name;
395
+ this.debug = debug;
396
+
397
+ // Fields, attach with the name identifier
398
+ this.fields = appFields.reduce(
399
+ (a, v) => ({ ...a, [v]: "smarterp-" + v + "-" + name }),
400
+ {} as any
401
+ );
402
+
403
+ // Device id
404
+ this._deviceId = storage.getData(this.fields.deviceId, "");
405
+
406
+ // Embedded
407
+ this._embedded =
408
+ this.storage.getData<boolean>(this.fields.embedded) ?? false;
409
+
410
+ const { currentCulture, currentRegion } = settings;
411
+
412
+ // Load resources
413
+ Promise.all([loadCrypto(), this.changeCulture(currentCulture)]).then(
414
+ ([cj, _resources]) => {
415
+ CJ = cj.default;
416
+
417
+ // Debug
418
+ if (this.debug) {
419
+ console.debug(
420
+ "CoreApp.constructor.ready",
421
+ this._deviceId,
422
+ this.fields,
423
+ cj,
424
+ currentCulture,
425
+ currentRegion
426
+ );
273
427
  }
274
- this.storage.setData(this.fields.keepLogin, value);
275
- }
276
-
277
- private _embedded: boolean;
278
- /**
279
- * Is embedded
280
- */
281
- get embedded() {
282
- return this._embedded;
283
- }
284
428
 
285
- private _isTryingLogin = false;
286
- /**
287
- * Is trying login
288
- */
289
- get isTryingLogin() {
290
- return this._isTryingLogin;
291
- }
292
- protected set isTryingLogin(value: boolean) {
293
- this._isTryingLogin = value;
294
- }
295
-
296
- /**
297
- * Last called with token refresh
298
- */
299
- protected lastCalled = false;
300
-
301
- /**
302
- * Init call Api URL
303
- */
304
- protected initCallApi: string = 'Auth/WebInitCall';
305
-
306
- /**
307
- * Passphrase for encryption
308
- */
309
- protected passphrase: string = '';
310
-
311
- private apis: Record<string, ApiTaskData> = {};
312
-
313
- private tasks: [() => PromiseLike<void | false>, number, number][] = [];
314
-
315
- private clearInterval?: () => void;
316
-
317
- /**
318
- * Get persisted fields
319
- */
320
- protected get persistedFields() {
321
- return [
322
- this.fields.deviceId,
323
- this.fields.devicePassphrase,
324
- this.fields.serversideDeviceId,
325
- this.fields.keepLogin
326
- ];
327
- }
429
+ this.changeRegion(currentRegion);
430
+ this.setup();
431
+ }
432
+ );
433
+ }
434
+
435
+ private getDeviceId() {
436
+ return this.deviceId.substring(0, 15);
437
+ }
438
+
439
+ private resetKeys() {
440
+ this.storage.clear(
441
+ [
442
+ this.fields.devicePassphrase,
443
+ this.fields.headerToken,
444
+ this.fields.serversideDeviceId
445
+ ],
446
+ false
447
+ );
448
+ this.passphrase = "";
449
+ }
450
+
451
+ /**
452
+ * Add app name as identifier
453
+ * @param field Field
454
+ * @returns Result
455
+ */
456
+ protected addIdentifier(field: string) {
457
+ return field + "-" + this.name;
458
+ }
459
+
460
+ /**
461
+ * Add root (homepage) to the URL
462
+ * @param url URL to add
463
+ * @returns Result
464
+ */
465
+ addRootUrl(url: string) {
466
+ const page = this.settings.homepage;
467
+ const endSlash = page.endsWith("/");
468
+ return (
469
+ page +
470
+ (endSlash
471
+ ? Utils.trimStart(url, "/")
472
+ : url.startsWith("/")
473
+ ? url
474
+ : "/" + url)
475
+ );
476
+ }
477
+
478
+ /**
479
+ * Create Auth API
480
+ * @param api Specify the API to use
481
+ * @returns Result
482
+ */
483
+ protected createAuthApi(api?: IApi) {
484
+ return new AuthApi(this, api);
485
+ }
486
+
487
+ /**
488
+ * Restore settings from persisted source
489
+ */
490
+ protected async restore() {
491
+ // Devices
492
+ const devices = this.storage.getPersistedData<string[]>(
493
+ this.fields.devices,
494
+ []
495
+ );
496
+
497
+ if (this._deviceId === "") {
498
+ // First vist, restore and keep the source
499
+ this.storage.copyFrom(this.persistedFields, false);
500
+
501
+ // Reset device id
502
+ this._deviceId = this.storage.getData(this.fields.deviceId, "");
503
+
504
+ // Totally new, no data restored
505
+ if (this._deviceId === "") return false;
506
+ }
507
+
508
+ // Device exists or not
509
+ const d = this.getDeviceId();
510
+ if (devices.includes(d)) {
511
+ // Duplicate tab, session data copied
512
+ // Remove the token, deviceId, and passphrase
513
+ this.resetKeys();
514
+ return false;
515
+ }
516
+
517
+ const passphraseEncrypted = this.storage.getData<string>(
518
+ this.fields.devicePassphrase
519
+ );
520
+ if (passphraseEncrypted) {
521
+ // this.name to identifier different app's secret
522
+ const passphraseDecrypted = this.decrypt(passphraseEncrypted, this.name);
523
+ if (passphraseDecrypted != null) {
524
+ // Add the device to the list
525
+ devices.push(d);
526
+ this.storage.setPersistedData(this.fields.devices, devices);
328
527
 
329
- /**
330
- * Protected constructor
331
- * @param settings Settings
332
- * @param api API
333
- * @param notifier Notifier
334
- * @param storage Storage
335
- * @param name Application name
336
- * @param debug Debug mode
337
- */
338
- protected constructor(
339
- settings: S,
340
- api: IApi | undefined | null,
341
- notifier: INotifier<N, C>,
342
- storage: IStorage,
343
- name: string,
344
- debug: boolean = false
345
- ) {
346
- if (settings?.regions?.length === 0) {
347
- throw new Error('No regions defined');
348
- }
349
- this.settings = settings;
528
+ this.passphrase = passphraseDecrypted;
350
529
 
351
- const region = AddressRegion.getById(settings.regions[0]);
352
- if (region == null) {
353
- throw new Error('No default region defined');
354
- }
355
- this.defaultRegion = region;
356
-
357
- // Current system refresh token
358
- const refresh: ApiRefreshTokenFunction = async (api, rq) => {
359
- if (this.lastCalled) {
360
- // Call refreshToken to update access token
361
- await this.refreshToken(
362
- { token: rq.token, showLoading: false },
363
- (result) => {
364
- if (result === true) return;
365
- console.log(
366
- `CoreApp.${this.name}.RefreshToken`,
367
- result
368
- );
369
- }
370
- );
371
- } else {
372
- // Popup countdown for user action
373
- this.freshCountdownUI();
374
- }
375
- return undefined;
376
- };
377
-
378
- if (api) {
379
- // Base URL of the API
380
- api.baseUrl = this.settings.endpoint;
381
- api.name = systemApi;
382
- this.setApi(api, refresh);
383
- this.api = api;
530
+ return true;
531
+ }
532
+
533
+ // Failed, reset keys
534
+ this.resetKeys();
535
+ }
536
+
537
+ return false;
538
+ }
539
+
540
+ /**
541
+ * Dispose the application
542
+ */
543
+ dispose() {
544
+ // Avoid duplicated call
545
+ if (!this._isReady) return;
546
+
547
+ // Persist storage defined fields
548
+ this.persist();
549
+
550
+ // Clear the interval
551
+ this.clearInterval?.();
552
+
553
+ // Reset the status to false
554
+ this.isReady = false;
555
+ }
556
+
557
+ /**
558
+ * Is valid password, override to implement custom check
559
+ * @param password Input password
560
+ */
561
+ isValidPassword(password: string) {
562
+ // Length check
563
+ if (password.length < 6) return false;
564
+
565
+ // One letter and number required
566
+ if (/\d+/gi.test(password) && /[a-z]+/gi.test(password)) {
567
+ return true;
568
+ }
569
+
570
+ return false;
571
+ }
572
+
573
+ /**
574
+ * Persist settings to source when application exit
575
+ */
576
+ persist() {
577
+ // Devices
578
+ const devices = this.storage.getPersistedData<string[]>(
579
+ this.fields.devices
580
+ );
581
+ if (devices != null) {
582
+ if (devices.remove(this.getDeviceId()).length > 0) {
583
+ this.storage.setPersistedData(this.fields.devices, devices);
584
+ }
585
+ }
586
+
587
+ if (!this.authorized) return;
588
+
589
+ this.storage.copyTo(this.persistedFields);
590
+ }
591
+
592
+ /**
593
+ * Add scheduled task
594
+ * @param task Task, return false to stop
595
+ * @param seconds Interval in seconds
596
+ */
597
+ addTask(task: () => PromiseLike<void | false>, seconds: number) {
598
+ this.tasks.push([task, seconds, seconds]);
599
+ }
600
+
601
+ /**
602
+ * Create API client, override to implement custom client creation by name
603
+ * @param name Client name
604
+ * @param item External endpoint item
605
+ * @returns Result
606
+ */
607
+ createApi(
608
+ name: string,
609
+ item: ExternalEndpoint,
610
+ refresh?: (
611
+ api: IApi,
612
+ rq: ApiRefreshTokenRQ
613
+ ) => Promise<[string, number] | undefined>
614
+ ) {
615
+ if (this.apis[name] != null) {
616
+ throw new Error(`API ${name} already exists`);
617
+ }
618
+
619
+ const api = createClient();
620
+ api.name = name;
621
+ api.baseUrl = item.endpoint;
622
+ this.setApi(api, refresh);
623
+ return api;
624
+ }
625
+
626
+ /**
627
+ * Reset all APIs
628
+ */
629
+ protected resetApis() {
630
+ for (const name in this.apis) {
631
+ const data = this.apis[name];
632
+ this.updateApi(data, undefined, -1);
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Update API token and expires
638
+ * @param name Api name
639
+ * @param token Refresh token
640
+ * @param seconds Access token expires in seconds
641
+ */
642
+ updateApi(name: string, token: string | undefined, seconds: number): void;
643
+ updateApi(
644
+ data: ApiTaskData,
645
+ token: string | undefined,
646
+ seconds: number
647
+ ): void;
648
+ updateApi(
649
+ nameOrData: string | ApiTaskData,
650
+ token: string | undefined,
651
+ seconds: number
652
+ ) {
653
+ const api =
654
+ typeof nameOrData === "string" ? this.apis[nameOrData] : nameOrData;
655
+ if (api == null) return;
656
+
657
+ // Consider the API call delay
658
+ if (seconds > 0) {
659
+ seconds -= 30;
660
+ if (seconds < 10) seconds = 10;
661
+ }
662
+
663
+ api[1] = seconds;
664
+ api[2] = seconds;
665
+ api[4] = token;
666
+ }
667
+
668
+ /**
669
+ * Setup Api
670
+ * @param api Api
671
+ */
672
+ protected setApi(api: IApi, refresh?: ApiRefreshTokenFunction) {
673
+ // onRequest, show loading or not, rewrite the property to override default action
674
+ this.setApiLoading(api);
675
+
676
+ // Global API error handler
677
+ this.setApiErrorHandler(api);
678
+
679
+ // Setup API countdown
680
+ refresh ??= this.apiRefreshToken.bind(this);
681
+ this.apis[api.name] = [api, -1, -1, refresh];
682
+ }
683
+
684
+ /**
685
+ * Setup Api error handler
686
+ * @param api Api
687
+ * @param handlerFor401 Handler for 401 error
688
+ */
689
+ setApiErrorHandler(
690
+ api: IApi,
691
+ handlerFor401?: boolean | (() => Promise<void>)
692
+ ) {
693
+ api.onError = (error: ApiDataError) => {
694
+ // Debug
695
+ if (this.debug) {
696
+ console.debug(
697
+ `CoreApp.${this.name}.setApiErrorHandler`,
698
+ api,
699
+ error,
700
+ handlerFor401
701
+ );
702
+ }
703
+
704
+ // Error code
705
+ const status = error.response
706
+ ? api.transformResponse(error.response).status
707
+ : undefined;
708
+
709
+ if (status === 401) {
710
+ // Unauthorized
711
+ if (handlerFor401 === false) return;
712
+ if (typeof handlerFor401 === "function") {
713
+ handlerFor401();
384
714
  } else {
385
- this.api = this.createApi(
386
- systemApi,
387
- {
388
- endpoint: settings.endpoint,
389
- webUrl: settings.webUrl
390
- },
391
- refresh
392
- );
715
+ this.tryLogin();
393
716
  }
394
-
395
- this.notifier = notifier;
396
- this.storage = storage;
397
- this.name = name;
398
- this.debug = debug;
399
-
400
- // Fields, attach with the name identifier
401
- this.fields = appFields.reduce(
402
- (a, v) => ({ ...a, [v]: 'smarterp-' + v + '-' + name }),
403
- {} as any
717
+ return;
718
+ } else if (
719
+ error.response == null &&
720
+ (error.message === "Network Error" ||
721
+ error.message === "Failed to fetch")
722
+ ) {
723
+ // Network error
724
+ this.notifier.alert(this.get("networkError") + ` [${this.name}]`);
725
+ return;
726
+ } else {
727
+ // Log
728
+ console.error(`${this.name} API error`, error);
729
+ }
730
+
731
+ // Report the error
732
+ this.notifier.alert(this.formatError(error));
733
+ };
734
+ }
735
+
736
+ /**
737
+ * Setup Api loading
738
+ * @param api Api
739
+ */
740
+ setApiLoading(api: IApi) {
741
+ // onRequest, show loading or not, rewrite the property to override default action
742
+ api.onRequest = (data) => {
743
+ // Debug
744
+ if (this.debug) {
745
+ console.debug(
746
+ `CoreApp.${this.name}.setApiLoading.onRequest`,
747
+ api,
748
+ data,
749
+ this.notifier.loadingCount
404
750
  );
405
-
406
- // Device id
407
- this._deviceId = storage.getData(this.fields.deviceId, '');
408
-
409
- // Embedded
410
- this._embedded =
411
- this.storage.getData<boolean>(this.fields.embedded) ?? false;
412
-
413
- const { currentCulture, currentRegion } = settings;
414
-
415
- // Load resources
416
- Promise.all([loadCrypto(), this.changeCulture(currentCulture)]).then(
417
- ([cj, _resources]) => {
418
- CJ = cj.default;
419
-
420
- // Debug
421
- if (this.debug) {
422
- console.debug(
423
- 'CoreApp.constructor.ready',
424
- this._deviceId,
425
- this.fields,
426
- cj,
427
- currentCulture,
428
- currentRegion
429
- );
430
- }
431
-
432
- this.changeRegion(currentRegion);
433
- this.setup();
434
- }
435
- );
436
- }
437
-
438
- private getDeviceId() {
439
- return this.deviceId.substring(0, 15);
440
- }
441
-
442
- private resetKeys() {
443
- this.storage.clear(
444
- [
445
- this.fields.devicePassphrase,
446
- this.fields.headerToken,
447
- this.fields.serversideDeviceId
448
- ],
449
- false
450
- );
451
- this.passphrase = '';
452
- }
453
-
454
- /**
455
- * Add app name as identifier
456
- * @param field Field
457
- * @returns Result
458
- */
459
- protected addIdentifier(field: string) {
460
- return field + '-' + this.name;
461
- }
462
-
463
- /**
464
- * Add root (homepage) to the URL
465
- * @param url URL to add
466
- * @returns Result
467
- */
468
- addRootUrl(url: string) {
469
- const page = this.settings.homepage;
470
- const endSlash = page.endsWith('/');
471
- return (
472
- page +
473
- (endSlash
474
- ? Utils.trimStart(url, '/')
475
- : url.startsWith('/')
476
- ? url
477
- : '/' + url)
751
+ }
752
+
753
+ if (data.showLoading == null || data.showLoading) {
754
+ this.notifier.showLoading();
755
+ }
756
+ };
757
+
758
+ // onComplete, hide loading, rewrite the property to override default action
759
+ api.onComplete = (data) => {
760
+ // Debug
761
+ if (this.debug) {
762
+ console.debug(
763
+ `CoreApp.${this.name}.setApiLoading.onComplete`,
764
+ api,
765
+ data,
766
+ this.notifier.loadingCount,
767
+ this.lastCalled
478
768
  );
479
- }
480
-
481
- /**
482
- * Create Auth API
483
- * @param api Specify the API to use
484
- * @returns Result
485
- */
486
- protected createAuthApi(api?: IApi) {
487
- return new AuthApi(this, api);
488
- }
489
-
490
- /**
491
- * Restore settings from persisted source
492
- */
493
- protected async restore() {
494
- // Devices
495
- const devices = this.storage.getPersistedData<string[]>(
496
- this.fields.devices,
497
- []
498
- );
499
-
500
- if (this._deviceId === '') {
501
- // First vist, restore and keep the source
502
- this.storage.copyFrom(this.persistedFields, false);
503
-
504
- // Reset device id
505
- this._deviceId = this.storage.getData(this.fields.deviceId, '');
506
-
507
- // Totally new, no data restored
508
- if (this._deviceId === '') return false;
769
+ }
770
+
771
+ if (data.showLoading == null || data.showLoading) {
772
+ this.notifier.hideLoading();
773
+
774
+ // Debug
775
+ if (this.debug) {
776
+ console.debug(
777
+ `CoreApp.${this.name}.setApiLoading.onComplete.showLoading`,
778
+ api,
779
+ this.notifier.loadingCount
780
+ );
509
781
  }
510
-
511
- // Device exists or not
512
- const d = this.getDeviceId();
513
- if (devices.includes(d)) {
514
- // Duplicate tab, session data copied
515
- // Remove the token, deviceId, and passphrase
516
- this.resetKeys();
517
- return false;
782
+ }
783
+ this.lastCalled = true;
784
+ };
785
+ }
786
+
787
+ /**
788
+ * Setup frontend logging
789
+ * @param action Custom action
790
+ * @param preventDefault Is prevent default action
791
+ */
792
+ setupLogging(
793
+ action?: (data: ErrorData) => void | Promise<void>,
794
+ preventDefault?: ((type: ErrorType) => boolean) | boolean
795
+ ) {
796
+ action ??= (data) => {
797
+ this.api.post("Auth/LogFrontendError", data, {
798
+ onError: (error) => {
799
+ // Use 'debug' to avoid infinite loop
800
+ console.debug("Log front-end error", data, error);
801
+
802
+ // Prevent global error handler
803
+ return false;
518
804
  }
519
-
520
- const passphraseEncrypted = this.storage.getData<string>(
521
- this.fields.devicePassphrase
805
+ });
806
+ };
807
+ DomUtils.setupLogging(action, preventDefault);
808
+ }
809
+
810
+ /**
811
+ * Api init call
812
+ * @param data Data
813
+ * @returns Result
814
+ */
815
+ protected async apiInitCall(data: InitCallDto) {
816
+ return await this.api.put<InitCallResult>(this.initCallApi, data);
817
+ }
818
+
819
+ /**
820
+ * Check the action result is about device invalid
821
+ * @param result Action result
822
+ * @returns true means device is invalid
823
+ */
824
+ checkDeviceResult(result: IActionResult): boolean {
825
+ if (
826
+ result.type === "DataProcessingFailed" ||
827
+ (result.type === "NoValidData" && result.field === "Device")
828
+ )
829
+ return true;
830
+ return false;
831
+ }
832
+
833
+ /**
834
+ * Clear device id
835
+ */
836
+ clearDeviceId() {
837
+ this._deviceId = "";
838
+ this.storage.clear([this.fields.deviceId], false);
839
+ this.storage.clear([this.fields.deviceId], true);
840
+ }
841
+
842
+ /**
843
+ * Init call
844
+ * @param callback Callback
845
+ * @param resetKeys Reset all keys first
846
+ * @returns Result
847
+ */
848
+ async initCall(callback?: (result: boolean) => void, resetKeys?: boolean) {
849
+ // Reset keys
850
+ if (resetKeys) {
851
+ this.clearDeviceId();
852
+ this.resetKeys();
853
+ }
854
+
855
+ // Passphrase exists?
856
+ if (this.passphrase) {
857
+ if (callback) callback(true);
858
+ return;
859
+ }
860
+
861
+ // Serverside encrypted device id
862
+ const identifier = this.storage.getData<string>(
863
+ this.fields.serversideDeviceId
864
+ );
865
+
866
+ // Timestamp
867
+ const timestamp = new Date().getTime();
868
+
869
+ // Request data
870
+ const data: InitCallDto = {
871
+ timestamp,
872
+ identifier,
873
+ deviceId: this.deviceId ? this.deviceId : undefined
874
+ };
875
+
876
+ const result = await this.apiInitCall(data);
877
+ if (result == null) {
878
+ // API error will popup
879
+ if (callback) callback(false);
880
+ return;
881
+ }
882
+
883
+ if (result.data == null) {
884
+ // Popup no data error
885
+ this.notifier.alert(this.get<string>("noData")!);
886
+ if (callback) callback(false);
887
+ return;
888
+ }
889
+
890
+ if (!result.ok) {
891
+ const seconds = result.data.seconds;
892
+ const validSeconds = result.data.validSeconds;
893
+ if (
894
+ result.title === "timeDifferenceInvalid" &&
895
+ seconds != null &&
896
+ validSeconds != null
897
+ ) {
898
+ const title = this.get("timeDifferenceInvalid")?.format(
899
+ seconds.toString(),
900
+ validSeconds.toString()
522
901
  );
523
- if (passphraseEncrypted) {
524
- // this.name to identifier different app's secret
525
- const passphraseDecrypted = this.decrypt(
526
- passphraseEncrypted,
527
- this.name
528
- );
529
- if (passphraseDecrypted != null) {
530
- // Add the device to the list
531
- devices.push(d);
532
- this.storage.setPersistedData(this.fields.devices, devices);
533
-
534
- this.passphrase = passphraseDecrypted;
902
+ this.notifier.alert(title!);
903
+ } else {
904
+ this.alertResult(result);
905
+ }
535
906
 
536
- return true;
537
- }
907
+ if (callback) callback(false);
538
908
 
539
- // Failed, reset keys
540
- this.resetKeys();
541
- }
909
+ // Clear device id
910
+ this.clearDeviceId();
542
911
 
543
- return false;
912
+ return;
544
913
  }
545
914
 
546
- /**
547
- * Dispose the application
548
- */
549
- dispose() {
550
- // Avoid duplicated call
551
- if (!this._isReady) return;
552
-
553
- // Persist storage defined fields
554
- this.persist();
555
-
556
- // Clear the interval
557
- this.clearInterval?.();
558
-
559
- // Reset the status to false
560
- this.isReady = false;
915
+ const updateResult = await this.initCallUpdate(result.data, data.timestamp);
916
+ if (!updateResult) {
917
+ this.notifier.alert(this.get<string>("noData")! + "(Update)");
561
918
  }
562
919
 
563
- /**
564
- * Is valid password, override to implement custom check
565
- * @param password Input password
566
- */
567
- isValidPassword(password: string) {
568
- // Length check
569
- if (password.length < 6) return false;
570
-
571
- // One letter and number required
572
- if (/\d+/gi.test(password) && /[a-z]+/gi.test(password)) {
573
- return true;
574
- }
575
-
576
- return false;
577
- }
920
+ if (callback) callback(updateResult);
921
+ }
578
922
 
579
- /**
580
- * Persist settings to source when application exit
581
- */
582
- persist() {
583
- // Devices
584
- const devices = this.storage.getPersistedData<string[]>(
585
- this.fields.devices
586
- );
587
- if (devices != null) {
588
- if (devices.remove(this.getDeviceId()).length > 0) {
589
- this.storage.setPersistedData(this.fields.devices, devices);
590
- }
591
- }
923
+ /**
924
+ * Update passphrase
925
+ * @param passphrase Secret passphrase
926
+ */
927
+ protected updatePassphrase(passphrase: string) {
928
+ // Previous passphrase
929
+ const prev = this.passphrase;
592
930
 
593
- if (!this.authorized) return;
931
+ // Update
932
+ this.passphrase = passphrase;
933
+ this.storage.setData(
934
+ this.fields.devicePassphrase,
935
+ this.encrypt(passphrase, this.name)
936
+ );
594
937
 
595
- this.storage.copyTo(this.persistedFields);
596
- }
938
+ if (prev) {
939
+ const fields = this.initCallEncryptedUpdateFields();
940
+ for (const field of fields) {
941
+ const currentValue = this.storage.getData<string>(field);
942
+ if (currentValue == null || currentValue === "") continue;
597
943
 
598
- /**
599
- * Add scheduled task
600
- * @param task Task, return false to stop
601
- * @param seconds Interval in seconds
602
- */
603
- addTask(task: () => PromiseLike<void | false>, seconds: number) {
604
- this.tasks.push([task, seconds, seconds]);
605
- }
606
-
607
- /**
608
- * Create API client, override to implement custom client creation by name
609
- * @param name Client name
610
- * @param item External endpoint item
611
- * @returns Result
612
- */
613
- createApi(
614
- name: string,
615
- item: ExternalEndpoint,
616
- refresh?: (
617
- api: IApi,
618
- rq: ApiRefreshTokenRQ
619
- ) => Promise<[string, number] | undefined>
620
- ) {
621
- if (this.apis[name] != null) {
622
- throw new Error(`API ${name} already exists`);
944
+ if (prev == null) {
945
+ // Reset the field
946
+ this.storage.setData(field, undefined);
947
+ continue;
623
948
  }
624
949
 
625
- const api = createClient();
626
- api.name = name;
627
- api.baseUrl = item.endpoint;
628
- this.setApi(api, refresh);
629
- return api;
630
- }
950
+ const enhanced = currentValue.indexOf("!") >= 8;
951
+ let newValueSource: string | undefined;
631
952
 
632
- /**
633
- * Reset all APIs
634
- */
635
- protected resetApis() {
636
- for (const name in this.apis) {
637
- const data = this.apis[name];
638
- this.updateApi(data, undefined, -1);
953
+ if (enhanced) {
954
+ newValueSource = this.decryptEnhanced(currentValue, prev, 12);
955
+ } else {
956
+ newValueSource = this.decrypt(currentValue, prev);
639
957
  }
640
- }
641
958
 
642
- /**
643
- * Update API token and expires
644
- * @param name Api name
645
- * @param token Refresh token
646
- * @param seconds Access token expires in seconds
647
- */
648
- updateApi(name: string, token: string | undefined, seconds: number): void;
649
- updateApi(
650
- data: ApiTaskData,
651
- token: string | undefined,
652
- seconds: number
653
- ): void;
654
- updateApi(
655
- nameOrData: string | ApiTaskData,
656
- token: string | undefined,
657
- seconds: number
658
- ) {
659
- const api =
660
- typeof nameOrData === 'string' ? this.apis[nameOrData] : nameOrData;
661
- if (api == null) return;
662
-
663
- // Consider the API call delay
664
- if (seconds > 0) {
665
- seconds -= 30;
666
- if (seconds < 10) seconds = 10;
959
+ if (newValueSource == null || newValueSource === "") {
960
+ // Reset the field
961
+ this.storage.setData(field, undefined);
962
+ continue;
667
963
  }
668
964
 
669
- api[1] = seconds;
670
- api[2] = seconds;
671
- api[4] = token;
672
- }
673
-
674
- /**
675
- * Setup Api
676
- * @param api Api
677
- */
678
- protected setApi(api: IApi, refresh?: ApiRefreshTokenFunction) {
679
- // onRequest, show loading or not, rewrite the property to override default action
680
- this.setApiLoading(api);
681
-
682
- // Global API error handler
683
- this.setApiErrorHandler(api);
684
-
685
- // Setup API countdown
686
- refresh ??= this.apiRefreshToken.bind(this);
687
- this.apis[api.name] = [api, -1, -1, refresh];
688
- }
689
-
690
- /**
691
- * Setup Api error handler
692
- * @param api Api
693
- * @param handlerFor401 Handler for 401 error
694
- */
695
- setApiErrorHandler(
696
- api: IApi,
697
- handlerFor401?: boolean | (() => Promise<void>)
698
- ) {
699
- api.onError = (error: ApiDataError) => {
700
- // Debug
701
- if (this.debug) {
702
- console.debug(
703
- `CoreApp.${this.name}.setApiErrorHandler`,
704
- api,
705
- error,
706
- handlerFor401
707
- );
708
- }
709
-
710
- // Error code
711
- const status = error.response
712
- ? api.transformResponse(error.response).status
713
- : undefined;
714
-
715
- if (status === 401) {
716
- // Unauthorized
717
- if (handlerFor401 === false) return;
718
- if (typeof handlerFor401 === 'function') {
719
- handlerFor401();
720
- } else {
721
- this.tryLogin();
722
- }
723
- return;
724
- } else if (
725
- error.response == null &&
726
- (error.message === 'Network Error' ||
727
- error.message === 'Failed to fetch')
728
- ) {
729
- // Network error
730
- this.notifier.alert(
731
- this.get('networkError') + ` [${this.name}]`
732
- );
733
- return;
734
- } else {
735
- // Log
736
- console.error(`${this.name} API error`, error);
737
- }
738
-
739
- // Report the error
740
- this.notifier.alert(this.formatError(error));
741
- };
742
- }
743
-
744
- /**
745
- * Setup Api loading
746
- * @param api Api
747
- */
748
- setApiLoading(api: IApi) {
749
- // onRequest, show loading or not, rewrite the property to override default action
750
- api.onRequest = (data) => {
751
- // Debug
752
- if (this.debug) {
753
- console.debug(
754
- `CoreApp.${this.name}.setApiLoading.onRequest`,
755
- api,
756
- data,
757
- this.notifier.loadingCount
758
- );
759
- }
760
-
761
- if (data.showLoading == null || data.showLoading) {
762
- this.notifier.showLoading();
763
- }
764
- };
765
-
766
- // onComplete, hide loading, rewrite the property to override default action
767
- api.onComplete = (data) => {
768
- // Debug
769
- if (this.debug) {
770
- console.debug(
771
- `CoreApp.${this.name}.setApiLoading.onComplete`,
772
- api,
773
- data,
774
- this.notifier.loadingCount,
775
- this.lastCalled
776
- );
777
- }
778
-
779
- if (data.showLoading == null || data.showLoading) {
780
- this.notifier.hideLoading();
781
-
782
- // Debug
783
- if (this.debug) {
784
- console.debug(
785
- `CoreApp.${this.name}.setApiLoading.onComplete.showLoading`,
786
- api,
787
- this.notifier.loadingCount
788
- );
789
- }
790
- }
791
- this.lastCalled = true;
792
- };
793
- }
794
-
795
- /**
796
- * Setup frontend logging
797
- * @param action Custom action
798
- * @param preventDefault Is prevent default action
799
- */
800
- setupLogging(
801
- action?: (data: ErrorData) => void | Promise<void>,
802
- preventDefault?: ((type: ErrorType) => boolean) | boolean
803
- ) {
804
- action ??= (data) => {
805
- this.api.post('Auth/LogFrontendError', data, {
806
- onError: (error) => {
807
- // Use 'debug' to avoid infinite loop
808
- console.debug('Log front-end error', data, error);
809
-
810
- // Prevent global error handler
811
- return false;
812
- }
813
- });
814
- };
815
- DomUtils.setupLogging(action, preventDefault);
816
- }
817
-
818
- /**
819
- * Api init call
820
- * @param data Data
821
- * @returns Result
822
- */
823
- protected async apiInitCall(data: InitCallDto) {
824
- return await this.api.put<InitCallResult>(this.initCallApi, data);
825
- }
826
-
827
- /**
828
- * Check the action result is about device invalid
829
- * @param result Action result
830
- * @returns true means device is invalid
831
- */
832
- checkDeviceResult(result: IActionResult): boolean {
965
+ const newValue = enhanced
966
+ ? this.encryptEnhanced(newValueSource)
967
+ : this.encrypt(newValueSource);
968
+
969
+ this.storage.setData(field, newValue);
970
+ }
971
+ }
972
+ }
973
+
974
+ /**
975
+ * Init call update
976
+ * @param data Result data
977
+ * @param timestamp Timestamp
978
+ */
979
+ protected async initCallUpdate(
980
+ data: InitCallResultData,
981
+ timestamp: number
982
+ ): Promise<boolean> {
983
+ // Data check
984
+ if (data.deviceId == null || data.passphrase == null) return false;
985
+
986
+ // Decrypt
987
+ // Should be done within 120 seconds after returning from the backend
988
+ const passphrase = this.decrypt(data.passphrase, timestamp.toString());
989
+ if (passphrase == null) return false;
990
+
991
+ // Update device id and cache it
992
+ this.deviceId = data.deviceId;
993
+
994
+ // Devices
995
+ const devices = this.storage.getPersistedData<string[]>(
996
+ this.fields.devices,
997
+ []
998
+ );
999
+ devices.push(this.getDeviceId());
1000
+ this.storage.setPersistedData(this.fields.devices, devices);
1001
+
1002
+ // Previous passphrase
1003
+ if (data.previousPassphrase) {
1004
+ const prev = this.decrypt(data.previousPassphrase, timestamp.toString());
1005
+ this.passphrase = prev ?? "";
1006
+ }
1007
+
1008
+ // Update passphrase
1009
+ this.updatePassphrase(passphrase);
1010
+
1011
+ return true;
1012
+ }
1013
+
1014
+ /**
1015
+ * Init call encrypted fields update
1016
+ * @returns Fields
1017
+ */
1018
+ protected initCallEncryptedUpdateFields(): string[] {
1019
+ return [this.fields.headerToken];
1020
+ }
1021
+
1022
+ /**
1023
+ * Alert result
1024
+ * @param result Result message
1025
+ * @param callback Callback
1026
+ */
1027
+ alertResult(result: string, callback?: NotificationReturn<void>): void;
1028
+
1029
+ /**
1030
+ * Alert action result
1031
+ * @param result Action result
1032
+ * @param callback Callback
1033
+ * @param forceToLocal Force to local labels
1034
+ */
1035
+ alertResult(
1036
+ result: IActionResult,
1037
+ callback?: NotificationReturn<void>,
1038
+ forceToLocal?: FormatResultCustomCallback
1039
+ ): void;
1040
+
1041
+ alertResult(
1042
+ result: IActionResult | string,
1043
+ callback?: NotificationReturn<void>,
1044
+ forceToLocal?: FormatResultCustomCallback
1045
+ ) {
1046
+ const message =
1047
+ typeof result === "string"
1048
+ ? result
1049
+ : this.formatResult(result, forceToLocal);
1050
+ this.notifier.alert(message, callback);
1051
+ }
1052
+
1053
+ /**
1054
+ * Authorize
1055
+ * @param token New access token
1056
+ * @param schema Access token schema
1057
+ * @param refreshToken Refresh token
1058
+ */
1059
+ authorize(token?: string, schema?: string, refreshToken?: string) {
1060
+ // State, when token is null, means logout
1061
+ const authorized = token != null;
1062
+
1063
+ // Token
1064
+ schema ??= "Bearer";
1065
+ this.api.authorize(schema, token);
1066
+
1067
+ // Overwrite the current value
1068
+ if (refreshToken !== "") {
1069
+ if (refreshToken != null) refreshToken = this.encrypt(refreshToken);
1070
+ this.storage.setData(this.fields.headerToken, refreshToken);
1071
+ }
1072
+
1073
+ // Reset tryLogin state
1074
+ this._isTryingLogin = false;
1075
+
1076
+ // Token countdown
1077
+ if (authorized) {
1078
+ this.lastCalled = false;
1079
+ if (refreshToken) {
1080
+ this.updateApi(this.api.name, refreshToken, this.userData!.seconds);
1081
+ }
1082
+ } else {
1083
+ this.updateApi(this.api.name, undefined, -1);
1084
+ }
1085
+
1086
+ // Host notice
1087
+ BridgeUtils.host?.userAuthorization(authorized);
1088
+
1089
+ // Callback
1090
+ this.onAuthorized(authorized);
1091
+
1092
+ // Everything is ready, update the state
1093
+ this.authorized = authorized;
1094
+
1095
+ // Persist
1096
+ this.persist();
1097
+ }
1098
+
1099
+ /**
1100
+ * On authorized or not callback
1101
+ * @param success Success or not
1102
+ */
1103
+ protected onAuthorized(success: boolean) {}
1104
+
1105
+ /**
1106
+ * Change country or region
1107
+ * @param regionId New country or region
1108
+ */
1109
+ changeRegion(region: string | AddressRegion) {
1110
+ // Get data
1111
+ let regionId: string;
1112
+ let regionItem: AddressRegion | undefined;
1113
+ if (typeof region === "string") {
1114
+ regionId = region;
1115
+ regionItem = AddressRegion.getById(region);
1116
+ } else {
1117
+ regionId = region.id;
1118
+ regionItem = region;
1119
+ }
1120
+
1121
+ // Same
1122
+ if (regionId === this._region) return;
1123
+
1124
+ // Not included
1125
+ if (regionItem == null || !this.settings.regions.includes(regionId)) return;
1126
+
1127
+ // Save the id to local storage
1128
+ this.storage.setPersistedData(DomUtils.CountryField, regionId);
1129
+
1130
+ // Set the currency and culture
1131
+ this._currency = regionItem.currency;
1132
+ this._region = regionId;
1133
+
1134
+ // Hold the current country or region
1135
+ this.settings.currentRegion = regionItem;
1136
+ this.updateRegionLabel();
1137
+ }
1138
+
1139
+ /**
1140
+ * Change culture
1141
+ * @param culture New culture definition
1142
+ * @param onReady On ready callback
1143
+ */
1144
+ async changeCulture(culture: DataTypes.CultureDefinition) {
1145
+ // Name
1146
+ const { name } = culture;
1147
+
1148
+ // Same?
1149
+ let resources = culture.resources;
1150
+ if (this._culture === name && typeof resources === "object")
1151
+ return resources;
1152
+
1153
+ // Save the cultrue to local storage
1154
+ this.storage.setPersistedData(DomUtils.CultureField, name);
1155
+
1156
+ // Change the API's Content-Language header
1157
+ // .net 5 API, UseRequestLocalization, RequestCultureProviders, ContentLanguageHeaderRequestCultureProvider
1158
+ this.api.setContentLanguage(name);
1159
+
1160
+ // Set the culture
1161
+ this._culture = name;
1162
+
1163
+ // Hold the current resources
1164
+ this.settings.currentCulture = culture;
1165
+
1166
+ if (typeof resources !== "object") {
1167
+ resources = await resources();
1168
+
1169
+ // Set static resources back
1170
+ culture.resources = resources;
1171
+ }
1172
+ this.updateRegionLabel();
1173
+
1174
+ return resources;
1175
+ }
1176
+
1177
+ /**
1178
+ * Update current region label
1179
+ */
1180
+ protected updateRegionLabel() {
1181
+ const region = this.settings.currentRegion;
1182
+ region.label = this.getRegionLabel(region.id);
1183
+ }
1184
+
1185
+ /**
1186
+ * Check language is supported or not, return a valid language when supported
1187
+ * @param language Language
1188
+ * @returns Result
1189
+ */
1190
+ checkLanguage(language?: string) {
1191
+ if (language) {
1192
+ const [cultrue, match] = DomUtils.getCulture(
1193
+ this.settings.cultures,
1194
+ language
1195
+ );
1196
+ if (cultrue != null && match != DomUtils.CultureMatch.Default)
1197
+ return cultrue.name;
1198
+ }
1199
+
1200
+ // Default language
1201
+ return this.culture;
1202
+ }
1203
+
1204
+ /**
1205
+ * Clear cache data
1206
+ */
1207
+ clearCacheData() {
1208
+ this.clearCacheToken();
1209
+ this.storage.setData(this.fields.devicePassphrase, undefined);
1210
+ }
1211
+
1212
+ /**
1213
+ * Clear cached token
1214
+ */
1215
+ clearCacheToken() {
1216
+ this.storage.setPersistedData(this.fields.headerToken, undefined);
1217
+ }
1218
+
1219
+ /**
1220
+ * Decrypt message
1221
+ * @param messageEncrypted Encrypted message
1222
+ * @param passphrase Secret passphrase
1223
+ * @returns Pure text
1224
+ */
1225
+ decrypt(messageEncrypted: string, passphrase?: string) {
1226
+ // Iterations
1227
+ const iterations = parseInt(messageEncrypted.substring(0, 2), 10);
1228
+ if (isNaN(iterations)) return undefined;
1229
+
1230
+ const { PBKDF2, algo, enc, AES, pad, mode } = CJ;
1231
+
1232
+ try {
1233
+ const salt = enc.Hex.parse(messageEncrypted.substring(2, 34));
1234
+ const iv = enc.Hex.parse(messageEncrypted.substring(34, 66));
1235
+ const encrypted = messageEncrypted.substring(66);
1236
+
1237
+ const key = PBKDF2(passphrase ?? this.passphrase, salt, {
1238
+ keySize: 8, // 256 / 32
1239
+ hasher: algo.SHA256,
1240
+ iterations: 1000 * iterations
1241
+ });
1242
+
1243
+ return AES.decrypt(encrypted, key, {
1244
+ iv,
1245
+ padding: pad.Pkcs7,
1246
+ mode: mode.CBC
1247
+ }).toString(enc.Utf8);
1248
+ } catch (e) {
1249
+ console.error(`CoreApp.decrypt ${messageEncrypted} error`, e);
1250
+ return undefined;
1251
+ }
1252
+ }
1253
+
1254
+ /**
1255
+ * Enhanced decrypt message
1256
+ * @param messageEncrypted Encrypted message
1257
+ * @param passphrase Secret passphrase
1258
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
1259
+ * @returns Pure text
1260
+ */
1261
+ decryptEnhanced(
1262
+ messageEncrypted: string,
1263
+ passphrase?: string,
1264
+ durationSeconds?: number
1265
+ ) {
1266
+ // Timestamp splitter
1267
+ const pos = messageEncrypted.indexOf("!");
1268
+
1269
+ // Miliseconds chars are longer than 8
1270
+ if (pos < 8 || messageEncrypted.length <= 66) return undefined;
1271
+
1272
+ const timestamp = messageEncrypted.substring(0, pos);
1273
+
1274
+ try {
1275
+ if (durationSeconds != null && durationSeconds > 0) {
1276
+ const milseconds = Utils.charsToNumber(timestamp);
1277
+ if (isNaN(milseconds) || milseconds < 1) return undefined;
1278
+ const timespan = new Date().substract(new Date(milseconds));
833
1279
  if (
834
- result.type === 'DataProcessingFailed' ||
835
- (result.type === 'NoValidData' && result.field === 'Device')
1280
+ (durationSeconds <= 12 && timespan.totalMonths > durationSeconds) ||
1281
+ (durationSeconds > 12 && timespan.totalSeconds > durationSeconds)
836
1282
  )
837
- return true;
838
- return false;
839
- }
840
-
841
- /**
842
- * Clear device id
843
- */
844
- clearDeviceId() {
845
- this._deviceId = '';
846
- this.storage.clear([this.fields.deviceId], false);
847
- this.storage.clear([this.fields.deviceId], true);
848
- }
849
-
850
- /**
851
- * Init call
852
- * @param callback Callback
853
- * @param resetKeys Reset all keys first
854
- * @returns Result
855
- */
856
- async initCall(callback?: (result: boolean) => void, resetKeys?: boolean) {
857
- // Reset keys
858
- if (resetKeys) {
859
- this.clearDeviceId();
860
- this.resetKeys();
861
- }
862
-
863
- // Passphrase exists?
864
- if (this.passphrase) {
865
- if (callback) callback(true);
866
- return;
867
- }
868
-
869
- // Serverside encrypted device id
870
- const identifier = this.storage.getData<string>(
871
- this.fields.serversideDeviceId
872
- );
873
-
874
- // Timestamp
875
- const timestamp = new Date().getTime();
876
-
877
- // Request data
878
- const data: InitCallDto = {
879
- timestamp,
880
- identifier,
881
- deviceId: this.deviceId ? this.deviceId : undefined
882
- };
883
-
884
- const result = await this.apiInitCall(data);
885
- if (result == null) {
886
- // API error will popup
887
- if (callback) callback(false);
888
- return;
889
- }
890
-
891
- if (result.data == null) {
892
- // Popup no data error
893
- this.notifier.alert(this.get<string>('noData')!);
894
- if (callback) callback(false);
895
- return;
896
- }
897
-
898
- if (!result.ok) {
899
- const seconds = result.data.seconds;
900
- const validSeconds = result.data.validSeconds;
901
- if (
902
- result.title === 'timeDifferenceInvalid' &&
903
- seconds != null &&
904
- validSeconds != null
905
- ) {
906
- const title = this.get('timeDifferenceInvalid')?.format(
907
- seconds.toString(),
908
- validSeconds.toString()
909
- );
910
- this.notifier.alert(title!);
911
- } else {
912
- this.alertResult(result);
913
- }
914
-
915
- if (callback) callback(false);
916
-
917
- // Clear device id
918
- this.clearDeviceId();
919
-
920
- return;
921
- }
922
-
923
- const updateResult = await this.initCallUpdate(
924
- result.data,
925
- data.timestamp
926
- );
927
- if (!updateResult) {
928
- this.notifier.alert(this.get<string>('noData')! + '(Update)');
929
- }
930
-
931
- if (callback) callback(updateResult);
932
- }
933
-
934
- /**
935
- * Update passphrase
936
- * @param passphrase Secret passphrase
937
- */
938
- protected updatePassphrase(passphrase: string) {
939
- // Previous passphrase
940
- const prev = this.passphrase;
941
-
942
- // Update
943
- this.passphrase = passphrase;
944
- this.storage.setData(
945
- this.fields.devicePassphrase,
946
- this.encrypt(passphrase, this.name)
947
- );
948
-
949
- if (prev) {
950
- const fields = this.initCallEncryptedUpdateFields();
951
- for (const field of fields) {
952
- const currentValue = this.storage.getData<string>(field);
953
- if (currentValue == null || currentValue === '') continue;
954
-
955
- if (prev == null) {
956
- // Reset the field
957
- this.storage.setData(field, undefined);
958
- continue;
959
- }
960
-
961
- const enhanced = currentValue.indexOf('!') >= 8;
962
- let newValueSource: string | undefined;
963
-
964
- if (enhanced) {
965
- newValueSource = this.decryptEnhanced(
966
- currentValue,
967
- prev,
968
- 12
969
- );
970
- } else {
971
- newValueSource = this.decrypt(currentValue, prev);
972
- }
973
-
974
- if (newValueSource == null || newValueSource === '') {
975
- // Reset the field
976
- this.storage.setData(field, undefined);
977
- continue;
978
- }
979
-
980
- const newValue = enhanced
981
- ? this.encryptEnhanced(newValueSource)
982
- : this.encrypt(newValueSource);
983
-
984
- this.storage.setData(field, newValue);
985
- }
986
- }
987
- }
988
-
989
- /**
990
- * Init call update
991
- * @param data Result data
992
- * @param timestamp Timestamp
993
- */
994
- protected async initCallUpdate(
995
- data: InitCallResultData,
996
- timestamp: number
997
- ): Promise<boolean> {
998
- // Data check
999
- if (data.deviceId == null || data.passphrase == null) return false;
1000
-
1001
- // Decrypt
1002
- // Should be done within 120 seconds after returning from the backend
1003
- const passphrase = this.decrypt(data.passphrase, timestamp.toString());
1004
- if (passphrase == null) return false;
1005
-
1006
- // Update device id and cache it
1007
- this.deviceId = data.deviceId;
1008
-
1009
- // Devices
1010
- const devices = this.storage.getPersistedData<string[]>(
1011
- this.fields.devices,
1012
- []
1283
+ return undefined;
1284
+ }
1285
+
1286
+ const message = messageEncrypted.substring(pos + 1);
1287
+ passphrase = this.encryptionEnhance(
1288
+ passphrase ?? this.passphrase,
1289
+ timestamp
1290
+ );
1291
+
1292
+ return this.decrypt(message, passphrase);
1293
+ } catch (e) {
1294
+ console.error(`CoreApp.decryptEnhanced ${messageEncrypted} error`, e);
1295
+ return undefined;
1296
+ }
1297
+ }
1298
+
1299
+ /**
1300
+ * Detect IP data, call only one time
1301
+ * @param callback Callback will be called when the IP is ready
1302
+ */
1303
+ detectIP(callback?: IDetectIPCallback) {
1304
+ if (this.ipData != null) {
1305
+ if (callback != null) callback();
1306
+ return;
1307
+ }
1308
+
1309
+ // First time
1310
+ if (this.ipDetectCallbacks == null) {
1311
+ // Init
1312
+ this.ipDetectCallbacks = [];
1313
+
1314
+ // Call the API
1315
+ this.api.detectIP().then(
1316
+ (data) => {
1317
+ if (data != null) {
1318
+ // Hold the data
1319
+ this.ipData = data;
1320
+ }
1321
+
1322
+ this.detectIPCallbacks();
1323
+ },
1324
+ (_reason) => this.detectIPCallbacks()
1325
+ );
1326
+ }
1327
+
1328
+ if (callback != null) {
1329
+ // Push the callback to the collection
1330
+ this.ipDetectCallbacks.push(callback);
1331
+ }
1332
+ }
1333
+
1334
+ // Detect IP callbacks
1335
+ private detectIPCallbacks() {
1336
+ this.ipDetectCallbacks?.forEach((f) => f());
1337
+ }
1338
+
1339
+ /**
1340
+ * Download file
1341
+ * @param stream File stream
1342
+ * @param filename File name
1343
+ * @param callback callback
1344
+ */
1345
+ async download(
1346
+ stream: ReadableStream,
1347
+ filename?: string,
1348
+ callback?: (success: boolean | undefined) => void
1349
+ ) {
1350
+ const downloadFile = async () => {
1351
+ let success = await DomUtils.downloadFile(
1352
+ stream,
1353
+ filename,
1354
+ BridgeUtils.host == null
1355
+ );
1356
+ if (success == null) {
1357
+ success = await DomUtils.downloadFile(stream, filename, false);
1358
+ }
1359
+
1360
+ if (callback) {
1361
+ callback(success);
1362
+ } else if (success)
1363
+ this.notifier.message(
1364
+ NotificationMessageType.Success,
1365
+ this.get("fileDownloaded")!
1013
1366
  );
1014
- devices.push(this.getDeviceId());
1015
- this.storage.setPersistedData(this.fields.devices, devices);
1016
-
1017
- // Previous passphrase
1018
- if (data.previousPassphrase) {
1019
- const prev = this.decrypt(
1020
- data.previousPassphrase,
1021
- timestamp.toString()
1022
- );
1023
- this.passphrase = prev ?? '';
1024
- }
1025
-
1026
- // Update passphrase
1027
- this.updatePassphrase(passphrase);
1028
-
1029
- return true;
1030
- }
1031
-
1032
- /**
1033
- * Init call encrypted fields update
1034
- * @returns Fields
1035
- */
1036
- protected initCallEncryptedUpdateFields(): string[] {
1037
- return [this.fields.headerToken];
1038
- }
1039
-
1040
- /**
1041
- * Alert result
1042
- * @param result Result message
1043
- * @param callback Callback
1044
- */
1045
- alertResult(result: string, callback?: NotificationReturn<void>): void;
1046
-
1047
- /**
1048
- * Alert action result
1049
- * @param result Action result
1050
- * @param callback Callback
1051
- * @param forceToLocal Force to local labels
1052
- */
1053
- alertResult(
1054
- result: IActionResult,
1055
- callback?: NotificationReturn<void>,
1056
- forceToLocal?: FormatResultCustomCallback
1057
- ): void;
1058
-
1059
- alertResult(
1060
- result: IActionResult | string,
1061
- callback?: NotificationReturn<void>,
1062
- forceToLocal?: FormatResultCustomCallback
1063
- ) {
1064
- const message =
1065
- typeof result === 'string'
1066
- ? result
1067
- : this.formatResult(result, forceToLocal);
1068
- this.notifier.alert(message, callback);
1069
- }
1070
-
1071
- /**
1072
- * Authorize
1073
- * @param token New access token
1074
- * @param schema Access token schema
1075
- * @param refreshToken Refresh token
1076
- */
1077
- authorize(token?: string, schema?: string, refreshToken?: string) {
1078
- // State, when token is null, means logout
1079
- const authorized = token != null;
1080
-
1081
- // Token
1082
- schema ??= 'Bearer';
1083
- this.api.authorize(schema, token);
1084
-
1085
- // Overwrite the current value
1086
- if (refreshToken !== '') {
1087
- if (refreshToken != null) refreshToken = this.encrypt(refreshToken);
1088
- this.storage.setData(this.fields.headerToken, refreshToken);
1089
- }
1090
-
1091
- // Reset tryLogin state
1092
- this._isTryingLogin = false;
1093
-
1094
- // Token countdown
1095
- if (authorized) {
1096
- this.lastCalled = false;
1097
- if (refreshToken) {
1098
- this.updateApi(
1099
- this.api.name,
1100
- refreshToken,
1101
- this.userData!.seconds
1102
- );
1103
- }
1104
- } else {
1105
- this.updateApi(this.api.name, undefined, -1);
1106
- }
1107
-
1108
- // Host notice
1109
- BridgeUtils.host?.userAuthorization(authorized);
1110
-
1111
- // Callback
1112
- this.onAuthorized(authorized);
1113
-
1114
- // Everything is ready, update the state
1115
- this.authorized = authorized;
1116
-
1117
- // Persist
1118
- this.persist();
1119
- }
1120
-
1121
- /**
1122
- * On authorized or not callback
1123
- * @param success Success or not
1124
- */
1125
- protected onAuthorized(success: boolean) {}
1126
-
1127
- /**
1128
- * Change country or region
1129
- * @param regionId New country or region
1130
- */
1131
- changeRegion(region: string | AddressRegion) {
1132
- // Get data
1133
- let regionId: string;
1134
- let regionItem: AddressRegion | undefined;
1135
- if (typeof region === 'string') {
1136
- regionId = region;
1137
- regionItem = AddressRegion.getById(region);
1138
- } else {
1139
- regionId = region.id;
1140
- regionItem = region;
1141
- }
1142
-
1143
- // Same
1144
- if (regionId === this._region) return;
1145
-
1146
- // Not included
1147
- if (regionItem == null || !this.settings.regions.includes(regionId))
1148
- return;
1149
-
1150
- // Save the id to local storage
1151
- this.storage.setPersistedData(DomUtils.CountryField, regionId);
1152
-
1153
- // Set the currency and culture
1154
- this._currency = regionItem.currency;
1155
- this._region = regionId;
1156
-
1157
- // Hold the current country or region
1158
- this.settings.currentRegion = regionItem;
1159
- this.updateRegionLabel();
1160
- }
1161
-
1162
- /**
1163
- * Change culture
1164
- * @param culture New culture definition
1165
- * @param onReady On ready callback
1166
- */
1167
- async changeCulture(culture: DataTypes.CultureDefinition) {
1168
- // Name
1169
- const { name } = culture;
1170
-
1171
- // Same?
1172
- let resources = culture.resources;
1173
- if (this._culture === name && typeof resources === 'object')
1174
- return resources;
1175
-
1176
- // Save the cultrue to local storage
1177
- this.storage.setPersistedData(DomUtils.CultureField, name);
1178
-
1179
- // Change the API's Content-Language header
1180
- // .net 5 API, UseRequestLocalization, RequestCultureProviders, ContentLanguageHeaderRequestCultureProvider
1181
- this.api.setContentLanguage(name);
1182
-
1183
- // Set the culture
1184
- this._culture = name;
1185
-
1186
- // Hold the current resources
1187
- this.settings.currentCulture = culture;
1188
-
1189
- if (typeof resources !== 'object') {
1190
- resources = await resources();
1191
-
1192
- // Set static resources back
1193
- culture.resources = resources;
1194
- }
1195
- this.updateRegionLabel();
1367
+ };
1196
1368
 
1197
- return resources;
1198
- }
1199
-
1200
- /**
1201
- * Update current region label
1202
- */
1203
- protected updateRegionLabel() {
1204
- const region = this.settings.currentRegion;
1205
- region.label = this.getRegionLabel(region.id);
1206
- }
1207
-
1208
- /**
1209
- * Check language is supported or not, return a valid language when supported
1210
- * @param language Language
1211
- * @returns Result
1212
- */
1213
- checkLanguage(language?: string) {
1214
- if (language) {
1215
- const [cultrue, match] = DomUtils.getCulture(
1216
- this.settings.cultures,
1217
- language
1218
- );
1219
- if (cultrue != null && match != DomUtils.CultureMatch.Default)
1220
- return cultrue.name;
1221
- }
1222
-
1223
- // Default language
1224
- return this.culture;
1225
- }
1226
-
1227
- /**
1228
- * Clear cache data
1229
- */
1230
- clearCacheData() {
1231
- this.clearCacheToken();
1232
- this.storage.setData(this.fields.devicePassphrase, undefined);
1233
- }
1234
-
1235
- /**
1236
- * Clear cached token
1237
- */
1238
- clearCacheToken() {
1239
- this.storage.setPersistedData(this.fields.headerToken, undefined);
1240
- }
1241
-
1242
- /**
1243
- * Decrypt message
1244
- * @param messageEncrypted Encrypted message
1245
- * @param passphrase Secret passphrase
1246
- * @returns Pure text
1247
- */
1248
- decrypt(messageEncrypted: string, passphrase?: string) {
1249
- // Iterations
1250
- const iterations = parseInt(messageEncrypted.substring(0, 2), 10);
1251
- if (isNaN(iterations)) return undefined;
1252
-
1253
- const { PBKDF2, algo, enc, AES, pad, mode } = CJ;
1254
-
1255
- try {
1256
- const salt = enc.Hex.parse(messageEncrypted.substring(2, 34));
1257
- const iv = enc.Hex.parse(messageEncrypted.substring(34, 66));
1258
- const encrypted = messageEncrypted.substring(66);
1259
-
1260
- const key = PBKDF2(passphrase ?? this.passphrase, salt, {
1261
- keySize: 8, // 256 / 32
1262
- hasher: algo.SHA256,
1263
- iterations: 1000 * iterations
1264
- });
1265
-
1266
- return AES.decrypt(encrypted, key, {
1267
- iv,
1268
- padding: pad.Pkcs7,
1269
- mode: mode.CBC
1270
- }).toString(enc.Utf8);
1271
- } catch (e) {
1272
- console.error(`CoreApp.decrypt ${messageEncrypted} error`, e);
1273
- return undefined;
1274
- }
1275
- }
1276
-
1277
- /**
1278
- * Enhanced decrypt message
1279
- * @param messageEncrypted Encrypted message
1280
- * @param passphrase Secret passphrase
1281
- * @param durationSeconds Duration seconds, <= 12 will be considered as month
1282
- * @returns Pure text
1283
- */
1284
- decryptEnhanced(
1285
- messageEncrypted: string,
1286
- passphrase?: string,
1287
- durationSeconds?: number
1369
+ // https://developer.mozilla.org/en-US/docs/Web/API/UserActivation/isActive
1370
+ if (
1371
+ "userActivation" in navigator &&
1372
+ !(navigator.userActivation as any).isActive
1288
1373
  ) {
1289
- // Timestamp splitter
1290
- const pos = messageEncrypted.indexOf('!');
1291
-
1292
- // Miliseconds chars are longer than 8
1293
- if (pos < 8 || messageEncrypted.length <= 66) return undefined;
1294
-
1295
- const timestamp = messageEncrypted.substring(0, pos);
1296
-
1297
- try {
1298
- if (durationSeconds != null && durationSeconds > 0) {
1299
- const milseconds = Utils.charsToNumber(timestamp);
1300
- if (isNaN(milseconds) || milseconds < 1) return undefined;
1301
- const timespan = new Date().substract(new Date(milseconds));
1302
- if (
1303
- (durationSeconds <= 12 &&
1304
- timespan.totalMonths > durationSeconds) ||
1305
- (durationSeconds > 12 &&
1306
- timespan.totalSeconds > durationSeconds)
1307
- )
1308
- return undefined;
1309
- }
1310
-
1311
- const message = messageEncrypted.substring(pos + 1);
1312
- passphrase = this.encryptionEnhance(
1313
- passphrase ?? this.passphrase,
1314
- timestamp
1315
- );
1316
-
1317
- return this.decrypt(message, passphrase);
1318
- } catch (e) {
1319
- console.error(
1320
- `CoreApp.decryptEnhanced ${messageEncrypted} error`,
1321
- e
1322
- );
1323
- return undefined;
1324
- }
1325
- }
1326
-
1327
- /**
1328
- * Detect IP data, call only one time
1329
- * @param callback Callback will be called when the IP is ready
1330
- */
1331
- detectIP(callback?: IDetectIPCallback) {
1332
- if (this.ipData != null) {
1333
- if (callback != null) callback();
1334
- return;
1335
- }
1336
-
1337
- // First time
1338
- if (this.ipDetectCallbacks == null) {
1339
- // Init
1340
- this.ipDetectCallbacks = [];
1341
-
1342
- // Call the API
1343
- this.api.detectIP().then(
1344
- (data) => {
1345
- if (data != null) {
1346
- // Hold the data
1347
- this.ipData = data;
1348
- }
1349
-
1350
- this.detectIPCallbacks();
1351
- },
1352
- (_reason) => this.detectIPCallbacks()
1353
- );
1374
+ this.notifier.alert(this.get("reactivateTip")!, async () => {
1375
+ await downloadFile();
1376
+ });
1377
+ } else {
1378
+ await downloadFile();
1379
+ }
1380
+ }
1381
+
1382
+ /**
1383
+ * Encrypt message
1384
+ * @param message Message
1385
+ * @param passphrase Secret passphrase
1386
+ * @param iterations Iterations, 1000 times, 1 - 99
1387
+ * @returns Result
1388
+ */
1389
+ encrypt(message: string, passphrase?: string, iterations?: number) {
1390
+ // Default 1 * 1000
1391
+ iterations ??= 1;
1392
+
1393
+ const { lib, PBKDF2, algo, enc, AES, pad, mode } = CJ;
1394
+
1395
+ const bits = 16; // 128 / 8
1396
+ const salt = lib.WordArray.random(bits);
1397
+ const key = PBKDF2(passphrase ?? this.passphrase, salt, {
1398
+ keySize: 8, // 256 / 32
1399
+ hasher: algo.SHA256,
1400
+ iterations: 1000 * iterations
1401
+ });
1402
+ const iv = lib.WordArray.random(bits);
1403
+
1404
+ return (
1405
+ iterations.toString().padStart(2, "0") +
1406
+ salt.toString(enc.Hex) +
1407
+ iv.toString(enc.Hex) +
1408
+ AES.encrypt(message, key, {
1409
+ iv,
1410
+ padding: pad.Pkcs7,
1411
+ mode: mode.CBC
1412
+ }).toString() // enc.Base64
1413
+ );
1414
+ }
1415
+
1416
+ /**
1417
+ * Enhanced encrypt message
1418
+ * @param message Message
1419
+ * @param passphrase Secret passphrase
1420
+ * @param iterations Iterations, 1000 times, 1 - 99
1421
+ * @returns Result
1422
+ */
1423
+ encryptEnhanced(message: string, passphrase?: string, iterations?: number) {
1424
+ // Timestamp
1425
+ const timestamp = Utils.numberToChars(new Date().getTime());
1426
+
1427
+ passphrase = this.encryptionEnhance(
1428
+ passphrase ?? this.passphrase,
1429
+ timestamp
1430
+ );
1431
+
1432
+ const result = this.encrypt(message, passphrase, iterations);
1433
+
1434
+ return timestamp + "!" + result;
1435
+ }
1436
+
1437
+ /**
1438
+ * Enchance secret passphrase
1439
+ * @param passphrase Secret passphrase
1440
+ * @param timestamp Timestamp
1441
+ * @returns Enhanced passphrase
1442
+ */
1443
+ protected encryptionEnhance(passphrase: string, timestamp: string) {
1444
+ passphrase += timestamp;
1445
+ passphrase += passphrase.length.toString();
1446
+ return passphrase;
1447
+ }
1448
+
1449
+ /**
1450
+ * Format action
1451
+ * @param action Action
1452
+ * @param target Target name or title
1453
+ * @param items More items
1454
+ * @returns Result
1455
+ */
1456
+ formatAction(action: string, target: string, ...items: string[]) {
1457
+ let more = items.join(", ");
1458
+ return `[${this.get("appName")}] ${action} - ${target}${
1459
+ more ? `, ${more}` : more
1460
+ }`;
1461
+ }
1462
+
1463
+ /**
1464
+ * Format date to string
1465
+ * @param input Input date
1466
+ * @param options Options
1467
+ * @param timeZone Time zone
1468
+ * @returns string
1469
+ */
1470
+ formatDate(
1471
+ input?: Date | string,
1472
+ options?: DateUtils.FormatOptions,
1473
+ timeZone?: string
1474
+ ) {
1475
+ const { currentCulture, timeZone: defaultTimeZone } = this.settings;
1476
+ timeZone ??= defaultTimeZone;
1477
+ return DateUtils.format(input, currentCulture.name, options, timeZone);
1478
+ }
1479
+
1480
+ /**
1481
+ * Format money number
1482
+ * @param input Input money number
1483
+ * @param isInteger Is integer
1484
+ * @param options Options
1485
+ * @returns Result
1486
+ */
1487
+ formatMoney(
1488
+ input: number | bigint,
1489
+ isInteger: boolean = false,
1490
+ options?: Intl.NumberFormatOptions
1491
+ ) {
1492
+ return NumberUtils.formatMoney(
1493
+ input,
1494
+ this.currency,
1495
+ this.culture,
1496
+ isInteger,
1497
+ options
1498
+ );
1499
+ }
1500
+
1501
+ /**
1502
+ * Format number
1503
+ * @param input Input number
1504
+ * @param options Options
1505
+ * @returns Result
1506
+ */
1507
+ formatNumber(input: number | bigint, options?: Intl.NumberFormatOptions) {
1508
+ return NumberUtils.format(input, this.culture, options);
1509
+ }
1510
+
1511
+ /**
1512
+ * Format error
1513
+ * @param error Error
1514
+ * @returns Error message
1515
+ */
1516
+ formatError(error: ApiDataError) {
1517
+ return `${error.message} (${error.name})`;
1518
+ }
1519
+
1520
+ /**
1521
+ * Format as full name
1522
+ * @param familyName Family name
1523
+ * @param givenName Given name
1524
+ */
1525
+ formatFullName(
1526
+ familyName: string | undefined | null,
1527
+ givenName: string | undefined | null
1528
+ ) {
1529
+ if (!familyName) return givenName ?? "";
1530
+ if (!givenName) return familyName ?? "";
1531
+ const wf = givenName + " " + familyName;
1532
+ if (wf.containChinese() || wf.containJapanese() || wf.containKorean()) {
1533
+ return familyName + givenName;
1534
+ }
1535
+ return wf;
1536
+ }
1537
+
1538
+ private getFieldLabel(field: string) {
1539
+ return this.get(field.formatInitial(false)) ?? field;
1540
+ }
1541
+
1542
+ /**
1543
+ * Format result text
1544
+ * @param result Action result
1545
+ * @param forceToLocal Force to local labels
1546
+ */
1547
+ formatResult(
1548
+ result: IActionResult,
1549
+ forceToLocal?: FormatResultCustomCallback
1550
+ ) {
1551
+ // Destruct the result
1552
+ const { title, type, field } = result;
1553
+ const data = { title, type, field };
1554
+
1555
+ if (type === "ItemExists" && field) {
1556
+ // Special case
1557
+ const fieldLabel =
1558
+ (typeof forceToLocal === "function" ? forceToLocal(data) : undefined) ??
1559
+ this.getFieldLabel(field);
1560
+ result.title = this.get("itemExists")?.format(fieldLabel);
1561
+ } else if (title?.includes("{0}")) {
1562
+ // When title contains {0}, replace with the field label
1563
+ const fieldLabel =
1564
+ (typeof forceToLocal === "function" ? forceToLocal(data) : undefined) ??
1565
+ (field ? this.getFieldLabel(field) : "");
1566
+
1567
+ result.title = title.format(fieldLabel);
1568
+ } else if (title && /^\w+$/.test(title)) {
1569
+ // When title is a single word
1570
+ // Hold the original title in type when type is null
1571
+ if (type == null) result.type = title;
1572
+ const localTitle =
1573
+ (typeof forceToLocal === "function" ? forceToLocal(data) : undefined) ??
1574
+ this.getFieldLabel(title);
1575
+ result.title = localTitle;
1576
+ } else if ((title == null || forceToLocal) && type != null) {
1577
+ const localTitle =
1578
+ (typeof forceToLocal === "function" ? forceToLocal(data) : undefined) ??
1579
+ this.getFieldLabel(type);
1580
+ result.title = localTitle;
1581
+ }
1582
+
1583
+ return ActionResultError.format(result);
1584
+ }
1585
+
1586
+ /**
1587
+ * Get culture resource
1588
+ * @param key key
1589
+ * @returns Resource
1590
+ */
1591
+ get<T = string>(key: string): T | undefined {
1592
+ // Make sure the resource files are loaded first
1593
+ const resources = this.settings.currentCulture.resources;
1594
+ const value = typeof resources === "object" ? resources[key] : undefined;
1595
+ if (value == null) return undefined;
1596
+
1597
+ // No strict type convertion here
1598
+ // Make sure the type is strictly match
1599
+ // Otherwise even request number, may still return the source string type
1600
+ return value as T;
1601
+ }
1602
+
1603
+ /**
1604
+ * Get multiple culture labels
1605
+ * @param keys Keys
1606
+ */
1607
+ getLabels<T extends string>(...keys: T[]): { [K in T]: string } {
1608
+ const init: any = {};
1609
+ return keys.reduce(
1610
+ (a, v) => ({ ...a, [v]: this.get<string>(v) ?? "" }),
1611
+ init
1612
+ );
1613
+ }
1614
+
1615
+ /**
1616
+ * Get bool items
1617
+ * @returns Bool items
1618
+ */
1619
+ getBools(): ListType1[] {
1620
+ const { no = "No", yes = "Yes" } = this.getLabels("no", "yes");
1621
+ return [
1622
+ { id: "false", label: no },
1623
+ { id: "true", label: yes }
1624
+ ];
1625
+ }
1626
+
1627
+ /**
1628
+ * Get cached token
1629
+ * @returns Cached token
1630
+ */
1631
+ getCacheToken(): string | undefined {
1632
+ return this.storage.getData<string>(this.fields.headerToken);
1633
+ }
1634
+
1635
+ /**
1636
+ * Get data privacies
1637
+ * @returns Result
1638
+ */
1639
+ getDataPrivacies() {
1640
+ return this.getEnumList(DataPrivacy, "dataPrivacy");
1641
+ }
1642
+
1643
+ /**
1644
+ * Get enum item number id list
1645
+ * @param em Enum
1646
+ * @param prefix Label prefix or callback
1647
+ * @param filter Filter
1648
+ * @returns List
1649
+ */
1650
+ getEnumList<E extends DataTypes.EnumBase = DataTypes.EnumBase>(
1651
+ em: E,
1652
+ prefix: string | ((key: string) => string),
1653
+ filter?:
1654
+ | ((id: E[keyof E], key: keyof E & string) => E[keyof E] | undefined)
1655
+ | E[keyof E][]
1656
+ ): ListType[] {
1657
+ const list: ListType[] = [];
1658
+
1659
+ const getKey =
1660
+ typeof prefix === "function" ? prefix : (key: string) => prefix + key;
1661
+
1662
+ if (Array.isArray(filter)) {
1663
+ filter.forEach((id) => {
1664
+ if (typeof id !== "number") return;
1665
+ const key = DataTypes.getEnumKey(em, id);
1666
+ const label = this.get<string>(getKey(key)) ?? key;
1667
+ list.push({ id, label });
1668
+ });
1669
+ } else {
1670
+ const keys = DataTypes.getEnumKeys(em);
1671
+ for (const key of keys) {
1672
+ let id = em[key as keyof E];
1673
+ if (filter) {
1674
+ const fid = filter(id, key);
1675
+ if (fid == null) continue;
1676
+ id = fid;
1354
1677
  }
1355
-
1356
- if (callback != null) {
1357
- // Push the callback to the collection
1358
- this.ipDetectCallbacks.push(callback);
1359
- }
1360
- }
1361
-
1362
- // Detect IP callbacks
1363
- private detectIPCallbacks() {
1364
- this.ipDetectCallbacks?.forEach((f) => f());
1365
- }
1366
-
1367
- /**
1368
- * Download file
1369
- * @param stream File stream
1370
- * @param filename File name
1371
- * @param callback callback
1372
- */
1373
- async download(
1374
- stream: ReadableStream,
1375
- filename?: string,
1376
- callback?: (success: boolean | undefined) => void
1377
- ) {
1378
- const downloadFile = async () => {
1379
- let success = await DomUtils.downloadFile(
1380
- stream,
1381
- filename,
1382
- BridgeUtils.host == null
1383
- );
1384
- if (success == null) {
1385
- success = await DomUtils.downloadFile(stream, filename, false);
1386
- }
1387
-
1388
- if (callback) {
1389
- callback(success);
1390
- } else if (success)
1391
- this.notifier.message(
1392
- NotificationMessageType.Success,
1393
- this.get('fileDownloaded')!
1394
- );
1395
- };
1396
-
1397
- // https://developer.mozilla.org/en-US/docs/Web/API/UserActivation/isActive
1398
- if (
1399
- 'userActivation' in navigator &&
1400
- !(navigator.userActivation as any).isActive
1401
- ) {
1402
- this.notifier.alert(this.get('reactivateTip')!, async () => {
1403
- await downloadFile();
1404
- });
1678
+ if (typeof id !== "number") continue;
1679
+ const label = this.get<string>(getKey(key)) ?? key;
1680
+ list.push({ id, label });
1681
+ }
1682
+ }
1683
+ return list;
1684
+ }
1685
+
1686
+ /**
1687
+ * Get enum item string id list
1688
+ * @param em Enum
1689
+ * @param prefix Label prefix or callback
1690
+ * @param filter Filter
1691
+ * @returns List
1692
+ */
1693
+ getEnumStrList<E extends DataTypes.EnumBase = DataTypes.EnumBase>(
1694
+ em: E,
1695
+ prefix: string | ((key: string) => string),
1696
+ filter?: (id: E[keyof E], key: keyof E & string) => E[keyof E] | undefined
1697
+ ): ListType1[] {
1698
+ const list: ListType1[] = [];
1699
+
1700
+ const getKey =
1701
+ typeof prefix === "function" ? prefix : (key: string) => prefix + key;
1702
+
1703
+ const keys = DataTypes.getEnumKeys(em);
1704
+ for (const key of keys) {
1705
+ let id = em[key as keyof E];
1706
+ if (filter) {
1707
+ const fid = filter(id, key);
1708
+ if (fid == null) continue;
1709
+ id = fid;
1710
+ }
1711
+ var label = this.get<string>(getKey(key)) ?? key;
1712
+ list.push({ id: id.toString(), label });
1713
+ }
1714
+ return list;
1715
+ }
1716
+
1717
+ /**
1718
+ * Get region label
1719
+ * @param id Region id
1720
+ * @returns Label
1721
+ */
1722
+ getRegionLabel(id: string) {
1723
+ return this.get("region" + id) ?? id;
1724
+ }
1725
+
1726
+ /**
1727
+ * Get all regions
1728
+ * @returns Regions
1729
+ */
1730
+ getRegions() {
1731
+ return this.settings.regions.map((id) => {
1732
+ return AddressRegion.getById(id)!;
1733
+ });
1734
+ }
1735
+
1736
+ /**
1737
+ * Get roles
1738
+ * @param role Combination role value
1739
+ */
1740
+ getRoles(role: number) {
1741
+ return this.getEnumList(UserRole, "role", (id, _key) => {
1742
+ if ((id & role) > 0) return id;
1743
+ });
1744
+ }
1745
+
1746
+ /**
1747
+ * Get status list
1748
+ * @param ids Limited ids
1749
+ * @returns list
1750
+ */
1751
+ getStatusList(ids?: EntityStatus[]) {
1752
+ return this.getEnumList(EntityStatus, "status", ids);
1753
+ }
1754
+
1755
+ /**
1756
+ * Get status label
1757
+ * @param status Status value
1758
+ */
1759
+ getStatusLabel(status: number | null | undefined) {
1760
+ if (status == null) return "";
1761
+ const key = EntityStatus[status];
1762
+ return this.get<string>("status" + key) ?? key;
1763
+ }
1764
+
1765
+ /**
1766
+ * Get refresh token from response headers
1767
+ * @param rawResponse Raw response from API call
1768
+ * @param tokenKey Refresh token key
1769
+ * @returns response refresh token
1770
+ */
1771
+ getResponseToken(rawResponse: any, tokenKey: string): string | null {
1772
+ const response = this.api.transformResponse(rawResponse);
1773
+ if (!response.ok) return null;
1774
+ return this.api.getHeaderValue(response.headers, tokenKey);
1775
+ }
1776
+
1777
+ /**
1778
+ * Get time zone
1779
+ * @returns Time zone
1780
+ */
1781
+ getTimeZone(): string | undefined {
1782
+ // settings.timeZone = Utils.getTimeZone()
1783
+ return this.settings.timeZone ?? this.ipData?.timezone;
1784
+ }
1785
+
1786
+ /**
1787
+ * Hash message, SHA3 or HmacSHA512, 512 as Base64
1788
+ * https://cryptojs.gitbook.io/docs/
1789
+ * @param message Message
1790
+ * @param passphrase Secret passphrase
1791
+ */
1792
+ hash(message: string, passphrase?: string) {
1793
+ const { SHA3, enc, HmacSHA512 } = CJ;
1794
+ if (passphrase == null)
1795
+ return SHA3(message, { outputLength: 512 }).toString(enc.Base64);
1796
+ else return HmacSHA512(message, passphrase).toString(enc.Base64);
1797
+ }
1798
+
1799
+ /**
1800
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
1801
+ * https://cryptojs.gitbook.io/docs/
1802
+ * @param message Message
1803
+ * @param passphrase Secret passphrase
1804
+ */
1805
+ hashHex(message: string, passphrase?: string) {
1806
+ const { SHA3, enc, HmacSHA512 } = CJ;
1807
+ if (passphrase == null)
1808
+ return SHA3(message, { outputLength: 512 }).toString(enc.Hex);
1809
+ else return HmacSHA512(message, passphrase).toString(enc.Hex);
1810
+ }
1811
+
1812
+ /**
1813
+ * Check use has the specific role permission or not
1814
+ * @param roles Roles to check
1815
+ * @returns Result
1816
+ */
1817
+ hasPermission(roles: number | UserRole | number[] | UserRole[]): boolean {
1818
+ const userRole = this.userData?.role;
1819
+ if (userRole == null) return false;
1820
+
1821
+ if (Array.isArray(roles)) {
1822
+ return roles.some((role) => (userRole & role) === role);
1823
+ }
1824
+
1825
+ // One role check
1826
+ if ((userRole & roles) === roles) return true;
1827
+
1828
+ return false;
1829
+ }
1830
+
1831
+ /**
1832
+ * Is admin user
1833
+ * @returns Result
1834
+ */
1835
+ isAdminUser() {
1836
+ return this.hasPermission([UserRole.Admin, UserRole.Founder]);
1837
+ }
1838
+
1839
+ /**
1840
+ * Is Finance user
1841
+ * @returns Result
1842
+ */
1843
+ isFinanceUser() {
1844
+ return this.hasPermission(UserRole.Finance) || this.isAdminUser();
1845
+ }
1846
+
1847
+ /**
1848
+ * Is HR user
1849
+ * @returns Result
1850
+ */
1851
+ isHRUser() {
1852
+ return this.hasPermission(UserRole.HRManager) || this.isAdminUser();
1853
+ }
1854
+
1855
+ /**
1856
+ * Navigate to Url or delta
1857
+ * @param url Url or delta
1858
+ * @param options Options
1859
+ */
1860
+ navigate<T extends number | string | URL>(
1861
+ to: T,
1862
+ options?: T extends number ? never : NavigateOptions
1863
+ ) {
1864
+ if (typeof to === "number") {
1865
+ globalThis.history.go(to);
1866
+ } else {
1867
+ const { state, replace = false } = options ?? {};
1868
+
1869
+ if (replace) {
1870
+ if (state) globalThis.history.replaceState(state, "", to);
1871
+ else globalThis.location.replace(to);
1872
+ } else {
1873
+ if (state) globalThis.history.pushState(state, "", to);
1874
+ else globalThis.location.assign(to);
1875
+ }
1876
+ }
1877
+ }
1878
+
1879
+ /**
1880
+ * Notify user with success message
1881
+ * @param callback Popup close callback
1882
+ * @param message Success message
1883
+ */
1884
+ ok(callback?: NotificationReturn<void>, message?: NotificationContent<N>) {
1885
+ message ??= this.get("operationSucceeded")!;
1886
+ this.notifier.succeed(message, undefined, callback);
1887
+ }
1888
+
1889
+ /**
1890
+ * Callback where exit a page
1891
+ */
1892
+ pageExit() {
1893
+ this.lastWarning?.dismiss();
1894
+ this.notifier.hideLoading(true);
1895
+ }
1896
+
1897
+ /**
1898
+ * Fresh countdown UI
1899
+ * @param callback Callback
1900
+ */
1901
+ abstract freshCountdownUI(callback?: () => PromiseLike<unknown>): void;
1902
+
1903
+ /**
1904
+ * Refresh token with result
1905
+ * @param props Props
1906
+ * @param callback Callback
1907
+ */
1908
+ async refreshToken(
1909
+ props?: RefreshTokenProps,
1910
+ callback?: (result?: boolean | IActionResult) => boolean | void
1911
+ ) {
1912
+ // Check props
1913
+ props ??= {};
1914
+ props.token ??= this.getCacheToken();
1915
+
1916
+ // Call refresh token API
1917
+ let data = await this.createAuthApi().refreshToken<IActionResult<U>>(props);
1918
+
1919
+ let r: IActionResult;
1920
+ if (Array.isArray(data)) {
1921
+ const [token, result] = data;
1922
+ if (result.ok) {
1923
+ if (!token) {
1924
+ data = {
1925
+ ok: false,
1926
+ type: "noData",
1927
+ field: "token",
1928
+ title: this.get("noData")
1929
+ };
1930
+ } else if (result.data == null) {
1931
+ data = {
1932
+ ok: false,
1933
+ type: "noData",
1934
+ field: "user",
1935
+ title: this.get("noData")
1936
+ };
1405
1937
  } else {
1406
- await downloadFile();
1407
- }
1408
- }
1409
-
1410
- /**
1411
- * Encrypt message
1412
- * @param message Message
1413
- * @param passphrase Secret passphrase
1414
- * @param iterations Iterations, 1000 times, 1 - 99
1415
- * @returns Result
1416
- */
1417
- encrypt(message: string, passphrase?: string, iterations?: number) {
1418
- // Default 1 * 1000
1419
- iterations ??= 1;
1420
-
1421
- const { lib, PBKDF2, algo, enc, AES, pad, mode } = CJ;
1422
-
1423
- const bits = 16; // 128 / 8
1424
- const salt = lib.WordArray.random(bits);
1425
- const key = PBKDF2(passphrase ?? this.passphrase, salt, {
1426
- keySize: 8, // 256 / 32
1427
- hasher: algo.SHA256,
1428
- iterations: 1000 * iterations
1429
- });
1430
- const iv = lib.WordArray.random(bits);
1431
-
1432
- return (
1433
- iterations.toString().padStart(2, '0') +
1434
- salt.toString(enc.Hex) +
1435
- iv.toString(enc.Hex) +
1436
- AES.encrypt(message, key, {
1437
- iv,
1438
- padding: pad.Pkcs7,
1439
- mode: mode.CBC
1440
- }).toString() // enc.Base64
1441
- );
1442
- }
1443
-
1444
- /**
1445
- * Enhanced encrypt message
1446
- * @param message Message
1447
- * @param passphrase Secret passphrase
1448
- * @param iterations Iterations, 1000 times, 1 - 99
1449
- * @returns Result
1450
- */
1451
- encryptEnhanced(message: string, passphrase?: string, iterations?: number) {
1452
- // Timestamp
1453
- const timestamp = Utils.numberToChars(new Date().getTime());
1454
-
1455
- passphrase = this.encryptionEnhance(
1456
- passphrase ?? this.passphrase,
1457
- timestamp
1458
- );
1459
-
1460
- const result = this.encrypt(message, passphrase, iterations);
1461
-
1462
- return timestamp + '!' + result;
1463
- }
1464
-
1465
- /**
1466
- * Enchance secret passphrase
1467
- * @param passphrase Secret passphrase
1468
- * @param timestamp Timestamp
1469
- * @returns Enhanced passphrase
1470
- */
1471
- protected encryptionEnhance(passphrase: string, timestamp: string) {
1472
- passphrase += timestamp;
1473
- passphrase += passphrase.length.toString();
1474
- return passphrase;
1475
- }
1476
-
1477
- /**
1478
- * Format action
1479
- * @param action Action
1480
- * @param target Target name or title
1481
- * @param items More items
1482
- * @returns Result
1483
- */
1484
- formatAction(action: string, target: string, ...items: string[]) {
1485
- let more = items.join(', ');
1486
- return `[${this.get('appName')}] ${action} - ${target}${
1487
- more ? `, ${more}` : more
1488
- }`;
1489
- }
1490
-
1491
- /**
1492
- * Format date to string
1493
- * @param input Input date
1494
- * @param options Options
1495
- * @param timeZone Time zone
1496
- * @returns string
1497
- */
1498
- formatDate(
1499
- input?: Date | string,
1500
- options?: DateUtils.FormatOptions,
1501
- timeZone?: string
1502
- ) {
1503
- const { currentCulture, timeZone: defaultTimeZone } = this.settings;
1504
- timeZone ??= defaultTimeZone;
1505
- return DateUtils.format(input, currentCulture.name, options, timeZone);
1506
- }
1507
-
1508
- /**
1509
- * Format money number
1510
- * @param input Input money number
1511
- * @param isInteger Is integer
1512
- * @param options Options
1513
- * @returns Result
1514
- */
1515
- formatMoney(
1516
- input: number | bigint,
1517
- isInteger: boolean = false,
1518
- options?: Intl.NumberFormatOptions
1519
- ) {
1520
- return NumberUtils.formatMoney(
1521
- input,
1522
- this.currency,
1523
- this.culture,
1524
- isInteger,
1525
- options
1526
- );
1527
- }
1528
-
1529
- /**
1530
- * Format number
1531
- * @param input Input number
1532
- * @param options Options
1533
- * @returns Result
1534
- */
1535
- formatNumber(input: number | bigint, options?: Intl.NumberFormatOptions) {
1536
- return NumberUtils.format(input, this.culture, options);
1537
- }
1938
+ // User login
1939
+ this.userLogin(result.data, token);
1538
1940
 
1539
- /**
1540
- * Format error
1541
- * @param error Error
1542
- * @returns Error message
1543
- */
1544
- formatError(error: ApiDataError) {
1545
- return `${error.message} (${error.name})`;
1546
- }
1941
+ if (callback) callback(true);
1547
1942
 
1548
- /**
1549
- * Format as full name
1550
- * @param familyName Family name
1551
- * @param givenName Given name
1552
- */
1553
- formatFullName(
1554
- familyName: string | undefined | null,
1555
- givenName: string | undefined | null
1556
- ) {
1557
- if (!familyName) return givenName ?? '';
1558
- if (!givenName) return familyName ?? '';
1559
- const wf = givenName + ' ' + familyName;
1560
- if (wf.containChinese() || wf.containJapanese() || wf.containKorean()) {
1561
- return familyName + givenName;
1943
+ // Exit
1944
+ return;
1562
1945
  }
1563
- return wf;
1564
- }
1565
-
1566
- private getFieldLabel(field: string) {
1567
- return this.get(field.formatInitial(false)) ?? field;
1568
- }
1569
-
1570
- /**
1571
- * Format result text
1572
- * @param result Action result
1573
- * @param forceToLocal Force to local labels
1574
- */
1575
- formatResult(
1576
- result: IActionResult,
1577
- forceToLocal?: FormatResultCustomCallback
1578
- ) {
1579
- // Destruct the result
1580
- const { title, type, field } = result;
1581
- const data = { title, type, field };
1582
-
1583
- if (type === 'ItemExists' && field) {
1584
- // Special case
1585
- const fieldLabel =
1586
- (typeof forceToLocal === 'function'
1587
- ? forceToLocal(data)
1588
- : undefined) ?? this.getFieldLabel(field);
1589
- result.title = this.get('itemExists')?.format(fieldLabel);
1590
- } else if (title?.includes('{0}')) {
1591
- // When title contains {0}, replace with the field label
1592
- const fieldLabel =
1593
- (typeof forceToLocal === 'function'
1594
- ? forceToLocal(data)
1595
- : undefined) ?? (field ? this.getFieldLabel(field) : '');
1596
-
1597
- result.title = title.format(fieldLabel);
1598
- } else if (title && /^\w+$/.test(title)) {
1599
- // When title is a single word
1600
- // Hold the original title in type when type is null
1601
- if (type == null) result.type = title;
1602
- const localTitle =
1603
- (typeof forceToLocal === 'function'
1604
- ? forceToLocal(data)
1605
- : undefined) ?? this.getFieldLabel(title);
1606
- result.title = localTitle;
1607
- } else if ((title == null || forceToLocal) && type != null) {
1608
- const localTitle =
1609
- (typeof forceToLocal === 'function'
1610
- ? forceToLocal(data)
1611
- : undefined) ?? this.getFieldLabel(type);
1612
- result.title = localTitle;
1613
- }
1614
-
1615
- return ActionResultError.format(result);
1616
- }
1617
-
1618
- /**
1619
- * Get culture resource
1620
- * @param key key
1621
- * @returns Resource
1622
- */
1623
- get<T = string>(key: string): T | undefined {
1624
- // Make sure the resource files are loaded first
1625
- const resources = this.settings.currentCulture.resources;
1626
- const value =
1627
- typeof resources === 'object' ? resources[key] : undefined;
1628
- if (value == null) return undefined;
1629
-
1630
- // No strict type convertion here
1631
- // Make sure the type is strictly match
1632
- // Otherwise even request number, may still return the source string type
1633
- return value as T;
1634
- }
1635
-
1636
- /**
1637
- * Get multiple culture labels
1638
- * @param keys Keys
1639
- */
1640
- getLabels<T extends string>(...keys: T[]): { [K in T]: string } {
1641
- const init: any = {};
1642
- return keys.reduce(
1643
- (a, v) => ({ ...a, [v]: this.get<string>(v) ?? '' }),
1644
- init
1645
- );
1646
- }
1647
-
1648
- /**
1649
- * Get bool items
1650
- * @returns Bool items
1651
- */
1652
- getBools(): ListType1[] {
1653
- const { no = 'No', yes = 'Yes' } = this.getLabels('no', 'yes');
1654
- return [
1655
- { id: 'false', label: no },
1656
- { id: 'true', label: yes }
1657
- ];
1658
- }
1659
-
1660
- /**
1661
- * Get cached token
1662
- * @returns Cached token
1663
- */
1664
- getCacheToken(): string | undefined {
1665
- return this.storage.getData<string>(this.fields.headerToken);
1666
- }
1667
-
1668
- /**
1669
- * Get data privacies
1670
- * @returns Result
1671
- */
1672
- getDataPrivacies() {
1673
- return this.getEnumList(DataPrivacy, 'dataPrivacy');
1674
- }
1675
-
1676
- /**
1677
- * Get enum item number id list
1678
- * @param em Enum
1679
- * @param prefix Label prefix or callback
1680
- * @param filter Filter
1681
- * @returns List
1682
- */
1683
- getEnumList<E extends DataTypes.EnumBase = DataTypes.EnumBase>(
1684
- em: E,
1685
- prefix: string | ((key: string) => string),
1686
- filter?:
1687
- | ((
1688
- id: E[keyof E],
1689
- key: keyof E & string
1690
- ) => E[keyof E] | undefined)
1691
- | E[keyof E][]
1692
- ): ListType[] {
1693
- const list: ListType[] = [];
1694
-
1695
- const getKey =
1696
- typeof prefix === 'function'
1697
- ? prefix
1698
- : (key: string) => prefix + key;
1699
-
1700
- if (Array.isArray(filter)) {
1701
- filter.forEach((id) => {
1702
- if (typeof id !== 'number') return;
1703
- const key = DataTypes.getEnumKey(em, id);
1704
- const label = this.get<string>(getKey(key)) ?? key;
1705
- list.push({ id, label });
1706
- });
1707
- } else {
1708
- const keys = DataTypes.getEnumKeys(em);
1709
- for (const key of keys) {
1710
- let id = em[key as keyof E];
1711
- if (filter) {
1712
- const fid = filter(id, key);
1713
- if (fid == null) continue;
1714
- id = fid;
1946
+ } else if (this.checkDeviceResult(result)) {
1947
+ if (callback == null || callback(result) !== true) {
1948
+ this.initCall((ir) => {
1949
+ if (!ir) return;
1950
+ this.notifier.alert(
1951
+ this.get("environmentChanged") ?? "Environment changed",
1952
+ () => {
1953
+ // Callback, return true to prevent the default reload action
1954
+ if (callback == null || callback() !== true) {
1955
+ // Reload the page
1956
+ history.go(0);
1715
1957
  }
1716
- if (typeof id !== 'number') continue;
1717
- const label = this.get<string>(getKey(key)) ?? key;
1718
- list.push({ id, label });
1719
- }
1720
- }
1721
- return list;
1722
- }
1723
-
1724
- /**
1725
- * Get enum item string id list
1726
- * @param em Enum
1727
- * @param prefix Label prefix or callback
1728
- * @param filter Filter
1729
- * @returns List
1730
- */
1731
- getEnumStrList<E extends DataTypes.EnumBase = DataTypes.EnumBase>(
1732
- em: E,
1733
- prefix: string | ((key: string) => string),
1734
- filter?: (
1735
- id: E[keyof E],
1736
- key: keyof E & string
1737
- ) => E[keyof E] | undefined
1738
- ): ListType1[] {
1739
- const list: ListType1[] = [];
1740
-
1741
- const getKey =
1742
- typeof prefix === 'function'
1743
- ? prefix
1744
- : (key: string) => prefix + key;
1745
-
1746
- const keys = DataTypes.getEnumKeys(em);
1747
- for (const key of keys) {
1748
- let id = em[key as keyof E];
1749
- if (filter) {
1750
- const fid = filter(id, key);
1751
- if (fid == null) continue;
1752
- id = fid;
1753
- }
1754
- var label = this.get<string>(getKey(key)) ?? key;
1755
- list.push({ id: id.toString(), label });
1958
+ }
1959
+ );
1960
+ }, true);
1961
+ return;
1756
1962
  }
1757
- return list;
1758
- }
1759
-
1760
- /**
1761
- * Get region label
1762
- * @param id Region id
1763
- * @returns Label
1764
- */
1765
- getRegionLabel(id: string) {
1766
- return this.get('region' + id) ?? id;
1767
- }
1768
-
1769
- /**
1770
- * Get all regions
1771
- * @returns Regions
1772
- */
1773
- getRegions() {
1774
- return this.settings.regions.map((id) => {
1775
- return AddressRegion.getById(id)!;
1776
- });
1777
- }
1778
-
1779
- /**
1780
- * Get roles
1781
- * @param role Combination role value
1782
- */
1783
- getRoles(role: number) {
1784
- return this.getEnumList(UserRole, 'role', (id, _key) => {
1785
- if ((id & role) > 0) return id;
1786
- });
1787
- }
1788
-
1789
- /**
1790
- * Get status list
1791
- * @param ids Limited ids
1792
- * @returns list
1793
- */
1794
- getStatusList(ids?: EntityStatus[]) {
1795
- return this.getEnumList(EntityStatus, 'status', ids);
1796
- }
1797
-
1798
- /**
1799
- * Get status label
1800
- * @param status Status value
1801
- */
1802
- getStatusLabel(status: number | null | undefined) {
1803
- if (status == null) return '';
1804
- const key = EntityStatus[status];
1805
- return this.get<string>('status' + key) ?? key;
1806
- }
1807
-
1808
- /**
1809
- * Get refresh token from response headers
1810
- * @param rawResponse Raw response from API call
1811
- * @param tokenKey Refresh token key
1812
- * @returns response refresh token
1813
- */
1814
- getResponseToken(rawResponse: any, tokenKey: string): string | null {
1815
- const response = this.api.transformResponse(rawResponse);
1816
- if (!response.ok) return null;
1817
- return this.api.getHeaderValue(response.headers, tokenKey);
1818
- }
1819
-
1820
- /**
1821
- * Get time zone
1822
- * @returns Time zone
1823
- */
1824
- getTimeZone(): string | undefined {
1825
- // settings.timeZone = Utils.getTimeZone()
1826
- return this.settings.timeZone ?? this.ipData?.timezone;
1827
- }
1828
-
1829
- /**
1830
- * Hash message, SHA3 or HmacSHA512, 512 as Base64
1831
- * https://cryptojs.gitbook.io/docs/
1832
- * @param message Message
1833
- * @param passphrase Secret passphrase
1834
- */
1835
- hash(message: string, passphrase?: string) {
1836
- const { SHA3, enc, HmacSHA512 } = CJ;
1837
- if (passphrase == null)
1838
- return SHA3(message, { outputLength: 512 }).toString(enc.Base64);
1839
- else return HmacSHA512(message, passphrase).toString(enc.Base64);
1840
- }
1841
-
1842
- /**
1843
- * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
1844
- * https://cryptojs.gitbook.io/docs/
1845
- * @param message Message
1846
- * @param passphrase Secret passphrase
1847
- */
1848
- hashHex(message: string, passphrase?: string) {
1849
- const { SHA3, enc, HmacSHA512 } = CJ;
1850
- if (passphrase == null)
1851
- return SHA3(message, { outputLength: 512 }).toString(enc.Hex);
1852
- else return HmacSHA512(message, passphrase).toString(enc.Hex);
1853
- }
1854
-
1855
- /**
1856
- * Check use has the specific role permission or not
1857
- * @param roles Roles to check
1858
- * @returns Result
1859
- */
1860
- hasPermission(roles: number | UserRole | number[] | UserRole[]): boolean {
1861
- const userRole = this.userData?.role;
1862
- if (userRole == null) return false;
1863
-
1864
- if (Array.isArray(roles)) {
1865
- return roles.some((role) => (userRole & role) === role);
1963
+ }
1964
+
1965
+ r = result;
1966
+ } else {
1967
+ r = data;
1968
+ }
1969
+
1970
+ if (callback == null || callback(r) !== true) {
1971
+ this.alertResult(r, () => {
1972
+ if (callback) callback(false);
1973
+ });
1974
+ }
1975
+ }
1976
+
1977
+ /**
1978
+ * Setup callback
1979
+ */
1980
+ setup() {
1981
+ // Done already
1982
+ if (this.isReady) return;
1983
+
1984
+ // Ready
1985
+ this.isReady = true;
1986
+
1987
+ // Restore
1988
+ this.restore();
1989
+
1990
+ // Pending actions
1991
+ this.pendings.forEach((p) => p());
1992
+
1993
+ // Setup scheduled tasks
1994
+ this.setupTasks();
1995
+ }
1996
+
1997
+ /**
1998
+ * Exchange token data
1999
+ * @param api API
2000
+ * @param token Core system's refresh token to exchange
2001
+ * @returns Result
2002
+ */
2003
+ async exchangeToken(api: IApi, token: string) {
2004
+ // Avoid to call the system API
2005
+ if (api.name === systemApi) {
2006
+ throw new Error("System API is not allowed to exchange token");
2007
+ }
2008
+
2009
+ // Call the API quietly, no loading bar and no error popup
2010
+ const data = await this.createAuthApi().exchangeToken(
2011
+ { token },
2012
+ {
2013
+ showLoading: false,
2014
+ onError: (error) => {
2015
+ console.error(`CoreApp.${api.name}.ExchangeToken error`, error);
2016
+
2017
+ // Prevent further processing
2018
+ return false;
1866
2019
  }
2020
+ }
2021
+ );
2022
+
2023
+ if (data) {
2024
+ // Update the access token
2025
+ api.authorize(data.tokenType, data.accessToken);
2026
+
2027
+ // Update the API
2028
+ this.updateApi(api.name, data.refreshToken, data.expiresIn);
2029
+
2030
+ // Update notice
2031
+ this.exchangeTokenUpdate(api, data);
2032
+ }
2033
+ }
2034
+
2035
+ /**
2036
+ * Exchange token update, override to get the new token
2037
+ * @param api API
2038
+ * @param data API refresh token data
2039
+ */
2040
+ protected exchangeTokenUpdate(api: IApi, data: ApiRefreshTokenDto) {}
2041
+
2042
+ /**
2043
+ * Exchange intergration tokens for all APIs
2044
+ * @param coreData Core system's token data to exchange
2045
+ * @param coreName Core system's name, default is 'core'
2046
+ */
2047
+ exchangeTokenAll(coreData: ApiRefreshTokenDto, coreName?: string) {
2048
+ coreName ??= "core";
2049
+
2050
+ for (const name in this.apis) {
2051
+ // Ignore the system API as it has its own logic with refreshToken
2052
+ if (name === systemApi) continue;
2053
+
2054
+ const data = this.apis[name];
2055
+ const api = data[0];
2056
+
2057
+ // The core API
2058
+ if (name === coreName) {
2059
+ api.authorize(coreData.tokenType, coreData.accessToken);
2060
+ this.updateApi(data, coreData.refreshToken, coreData.expiresIn);
2061
+ } else {
2062
+ this.exchangeToken(api, coreData.refreshToken);
2063
+ }
2064
+ }
2065
+ }
2066
+
2067
+ /**
2068
+ * API refresh token data
2069
+ * @param api Current API
2070
+ * @param rq Request data
2071
+ * @returns Result
2072
+ */
2073
+ protected async apiRefreshTokenData(
2074
+ api: IApi,
2075
+ rq: ApiRefreshTokenRQ
2076
+ ): Promise<ApiRefreshTokenDto | undefined> {
2077
+ // Default appId
2078
+ rq.appId ??= this.settings.appId;
1867
2079
 
1868
- // One role check
1869
- if ((userRole & roles) === roles) return true;
2080
+ // Call the API quietly, no loading bar and no error popup
2081
+ return this.createAuthApi(api).apiRefreshToken(rq, {
2082
+ showLoading: false,
2083
+ onError: (error) => {
2084
+ console.error(`CoreApp.${api.name}.apiRefreshToken error`, error);
1870
2085
 
2086
+ // Prevent further processing
1871
2087
  return false;
1872
- }
1873
-
1874
- /**
1875
- * Is admin user
1876
- * @returns Result
1877
- */
1878
- isAdminUser() {
1879
- return this.hasPermission([UserRole.Admin, UserRole.Founder]);
1880
- }
1881
-
1882
- /**
1883
- * Is Finance user
1884
- * @returns Result
1885
- */
1886
- isFinanceUser() {
1887
- return this.hasPermission(UserRole.Finance) || this.isAdminUser();
1888
- }
1889
-
1890
- /**
1891
- * Is HR user
1892
- * @returns Result
1893
- */
1894
- isHRUser() {
1895
- return this.hasPermission(UserRole.HRManager) || this.isAdminUser();
1896
- }
1897
-
1898
- /**
1899
- * Navigate to Url or delta
1900
- * @param url Url or delta
1901
- * @param options Options
1902
- */
1903
- navigate<T extends number | string | URL>(
1904
- to: T,
1905
- options?: T extends number ? never : NavigateOptions
1906
- ) {
1907
- if (typeof to === 'number') {
1908
- globalThis.history.go(to);
1909
- } else {
1910
- const { state, replace = false } = options ?? {};
1911
-
1912
- if (replace) {
1913
- if (state) globalThis.history.replaceState(state, '', to);
1914
- else globalThis.location.replace(to);
1915
- } else {
1916
- if (state) globalThis.history.pushState(state, '', to);
1917
- else globalThis.location.assign(to);
1918
- }
1919
- }
1920
- }
1921
-
1922
- /**
1923
- * Notify user with success message
1924
- * @param callback Popup close callback
1925
- * @param message Success message
1926
- */
1927
- ok(callback?: NotificationReturn<void>, message?: NotificationContent<N>) {
1928
- message ??= this.get('operationSucceeded')!;
1929
- this.notifier.succeed(message, undefined, callback);
1930
- }
1931
-
1932
- /**
1933
- * Callback where exit a page
1934
- */
1935
- pageExit() {
1936
- this.lastWarning?.dismiss();
1937
- this.notifier.hideLoading(true);
1938
- }
1939
-
1940
- /**
1941
- * Fresh countdown UI
1942
- * @param callback Callback
1943
- */
1944
- abstract freshCountdownUI(callback?: () => PromiseLike<unknown>): void;
1945
-
1946
- /**
1947
- * Refresh token with result
1948
- * @param props Props
1949
- * @param callback Callback
1950
- */
1951
- async refreshToken(
1952
- props?: RefreshTokenProps,
1953
- callback?: (result?: boolean | IActionResult) => boolean | void
1954
- ) {
1955
- // Check props
1956
- props ??= {};
1957
- props.token ??= this.getCacheToken();
1958
-
1959
- // Call refresh token API
1960
- let data = await this.createAuthApi().refreshToken<IActionResult<U>>(
1961
- props
1962
- );
1963
-
1964
- let r: IActionResult;
1965
- if (Array.isArray(data)) {
1966
- const [token, result] = data;
1967
- if (result.ok) {
1968
- if (!token) {
1969
- data = {
1970
- ok: false,
1971
- type: 'noData',
1972
- field: 'token',
1973
- title: this.get('noData')
1974
- };
1975
- } else if (result.data == null) {
1976
- data = {
1977
- ok: false,
1978
- type: 'noData',
1979
- field: 'user',
1980
- title: this.get('noData')
1981
- };
1982
- } else {
1983
- // User login
1984
- this.userLogin(result.data, token);
1985
-
1986
- if (callback) callback(true);
1987
-
1988
- // Exit
1989
- return;
1990
- }
1991
- } else if (this.checkDeviceResult(result)) {
1992
- if (callback == null || callback(result) !== true) {
1993
- this.initCall((ir) => {
1994
- if (!ir) return;
1995
- this.notifier.alert(
1996
- this.get('environmentChanged') ??
1997
- 'Environment changed',
1998
- () => {
1999
- // Callback, return true to prevent the default reload action
2000
- if (callback == null || callback() !== true) {
2001
- // Reload the page
2002
- history.go(0);
2003
- }
2004
- }
2005
- );
2006
- }, true);
2007
- return;
2008
- }
2009
- }
2010
-
2011
- r = result;
2012
- } else {
2013
- r = data;
2014
- }
2015
-
2016
- if (callback == null || callback(r) !== true) {
2017
- const message = `${r.title} (${r.field})`;
2018
- this.notifier.alert(message, () => {
2019
- if (callback) callback(false);
2020
- });
2021
- }
2022
- }
2023
-
2024
- /**
2025
- * Setup callback
2026
- */
2027
- setup() {
2028
- // Done already
2029
- if (this.isReady) return;
2030
-
2031
- // Ready
2032
- this.isReady = true;
2033
-
2034
- // Restore
2035
- this.restore();
2036
-
2037
- // Pending actions
2038
- this.pendings.forEach((p) => p());
2039
-
2040
- // Setup scheduled tasks
2041
- this.setupTasks();
2042
- }
2043
-
2044
- /**
2045
- * Exchange token data
2046
- * @param api API
2047
- * @param token Core system's refresh token to exchange
2048
- * @returns Result
2049
- */
2050
- async exchangeToken(api: IApi, token: string) {
2051
- // Avoid to call the system API
2052
- if (api.name === systemApi) {
2053
- throw new Error('System API is not allowed to exchange token');
2054
- }
2055
-
2056
- // Call the API quietly, no loading bar and no error popup
2057
- const data = await this.createAuthApi().exchangeToken(
2058
- { token },
2059
- {
2060
- showLoading: false,
2061
- onError: (error) => {
2062
- console.error(
2063
- `CoreApp.${api.name}.ExchangeToken error`,
2064
- error
2065
- );
2066
-
2067
- // Prevent further processing
2068
- return false;
2069
- }
2070
- }
2071
- );
2072
-
2073
- if (data) {
2074
- // Update the access token
2075
- api.authorize(data.tokenType, data.accessToken);
2076
-
2077
- // Update the API
2078
- this.updateApi(api.name, data.refreshToken, data.expiresIn);
2079
-
2080
- // Update notice
2081
- this.exchangeTokenUpdate(api, data);
2082
- }
2083
- }
2084
-
2085
- /**
2086
- * Exchange token update, override to get the new token
2087
- * @param api API
2088
- * @param data API refresh token data
2089
- */
2090
- protected exchangeTokenUpdate(api: IApi, data: ApiRefreshTokenDto) {}
2091
-
2092
- /**
2093
- * Exchange intergration tokens for all APIs
2094
- * @param coreData Core system's token data to exchange
2095
- * @param coreName Core system's name, default is 'core'
2096
- */
2097
- exchangeTokenAll(coreData: ApiRefreshTokenDto, coreName?: string) {
2098
- coreName ??= 'core';
2099
-
2100
- for (const name in this.apis) {
2101
- // Ignore the system API as it has its own logic with refreshToken
2102
- if (name === systemApi) continue;
2103
-
2104
- const data = this.apis[name];
2105
- const api = data[0];
2106
-
2107
- // The core API
2108
- if (name === coreName) {
2109
- api.authorize(coreData.tokenType, coreData.accessToken);
2110
- this.updateApi(data, coreData.refreshToken, coreData.expiresIn);
2088
+ }
2089
+ });
2090
+ }
2091
+
2092
+ /**
2093
+ * API refresh token
2094
+ * @param api Current API
2095
+ * @param rq Request data
2096
+ * @returns Result
2097
+ */
2098
+ protected async apiRefreshToken(
2099
+ api: IApi,
2100
+ rq: ApiRefreshTokenRQ
2101
+ ): Promise<[string, number] | undefined> {
2102
+ // Call the API quietly, no loading bar and no error popup
2103
+ const data = await this.apiRefreshTokenData(api, rq);
2104
+ if (data == null) return undefined;
2105
+
2106
+ // Update the access token
2107
+ api.authorize(data.tokenType, data.accessToken);
2108
+
2109
+ // Return the new refresh token and access token expiration seconds
2110
+ return [data.refreshToken, data.expiresIn];
2111
+ }
2112
+
2113
+ /**
2114
+ * Setup tasks
2115
+ */
2116
+ protected setupTasks() {
2117
+ this.clearInterval = ExtendUtils.intervalFor(() => {
2118
+ // Exit when not authorized
2119
+ if (!this.authorized) return;
2120
+
2121
+ // APIs
2122
+ for (const name in this.apis) {
2123
+ // Get the API
2124
+ const api = this.apis[name];
2125
+
2126
+ // Skip the negative value or when refresh token is not set
2127
+ if (!api[4] || api[2] < 0) continue;
2128
+
2129
+ // Minus one second
2130
+ api[2] -= 1;
2131
+
2132
+ // Ready to trigger
2133
+ if (api[2] === 0) {
2134
+ // Refresh token
2135
+ api[3](api[0], { token: api[4] }).then((data) => {
2136
+ if (data == null) {
2137
+ // Failed, try it again in 2 seconds
2138
+ api[2] = 2;
2111
2139
  } else {
2112
- this.exchangeToken(api, coreData.refreshToken);
2140
+ // Reset the API
2141
+ const [token, seconds] = data;
2142
+ this.updateApi(api, token, seconds);
2113
2143
  }
2144
+ });
2114
2145
  }
2115
- }
2116
-
2117
- /**
2118
- * API refresh token data
2119
- * @param api Current API
2120
- * @param rq Request data
2121
- * @returns Result
2122
- */
2123
- protected async apiRefreshTokenData(
2124
- api: IApi,
2125
- rq: ApiRefreshTokenRQ
2126
- ): Promise<ApiRefreshTokenDto | undefined> {
2127
- // Default appId
2128
- rq.appId ??= this.settings.appId;
2129
-
2130
- // Call the API quietly, no loading bar and no error popup
2131
- return this.createAuthApi(api).apiRefreshToken(rq, {
2132
- showLoading: false,
2133
- onError: (error) => {
2134
- console.error(
2135
- `CoreApp.${api.name}.apiRefreshToken error`,
2136
- error
2137
- );
2138
-
2139
- // Prevent further processing
2140
- return false;
2141
- }
2142
- });
2143
- }
2144
-
2145
- /**
2146
- * API refresh token
2147
- * @param api Current API
2148
- * @param rq Request data
2149
- * @returns Result
2150
- */
2151
- protected async apiRefreshToken(
2152
- api: IApi,
2153
- rq: ApiRefreshTokenRQ
2154
- ): Promise<[string, number] | undefined> {
2155
- // Call the API quietly, no loading bar and no error popup
2156
- const data = await this.apiRefreshTokenData(api, rq);
2157
- if (data == null) return undefined;
2158
-
2159
- // Update the access token
2160
- api.authorize(data.tokenType, data.accessToken);
2161
-
2162
- // Return the new refresh token and access token expiration seconds
2163
- return [data.refreshToken, data.expiresIn];
2164
- }
2165
-
2166
- /**
2167
- * Setup tasks
2168
- */
2169
- protected setupTasks() {
2170
- this.clearInterval = ExtendUtils.intervalFor(() => {
2171
- // Exit when not authorized
2172
- if (!this.authorized) return;
2173
-
2174
- // APIs
2175
- for (const name in this.apis) {
2176
- // Get the API
2177
- const api = this.apis[name];
2178
-
2179
- // Skip the negative value or when refresh token is not set
2180
- if (!api[4] || api[2] < 0) continue;
2181
-
2182
- // Minus one second
2183
- api[2] -= 1;
2184
-
2185
- // Ready to trigger
2186
- if (api[2] === 0) {
2187
- // Refresh token
2188
- api[3](api[0], { token: api[4] }).then((data) => {
2189
- if (data == null) {
2190
- // Failed, try it again in 2 seconds
2191
- api[2] = 2;
2192
- } else {
2193
- // Reset the API
2194
- const [token, seconds] = data;
2195
- this.updateApi(api, token, seconds);
2196
- }
2197
- });
2198
- }
2146
+ }
2147
+
2148
+ for (let t = this.tasks.length - 1; t >= 0; t--) {
2149
+ // Get the task
2150
+ const task = this.tasks[t];
2151
+
2152
+ // Minus one second
2153
+ task[2] -= 1;
2154
+
2155
+ // Remove the tasks with negative value with splice
2156
+ if (task[2] < 0) {
2157
+ this.tasks.splice(t, 1);
2158
+ } else if (task[2] === 0) {
2159
+ // Ready to trigger
2160
+ // Reset the task
2161
+ task[2] = task[1];
2162
+
2163
+ // Trigger the task
2164
+ task[0]().then((result) => {
2165
+ if (result === false) {
2166
+ // Asynchronous task, unsafe to splice the index, flag as pending
2167
+ task[2] = -1;
2199
2168
  }
2200
-
2201
- for (let t = this.tasks.length - 1; t >= 0; t--) {
2202
- // Get the task
2203
- const task = this.tasks[t];
2204
-
2205
- // Minus one second
2206
- task[2] -= 1;
2207
-
2208
- // Remove the tasks with negative value with splice
2209
- if (task[2] < 0) {
2210
- this.tasks.splice(t, 1);
2211
- } else if (task[2] === 0) {
2212
- // Ready to trigger
2213
- // Reset the task
2214
- task[2] = task[1];
2215
-
2216
- // Trigger the task
2217
- task[0]().then((result) => {
2218
- if (result === false) {
2219
- // Asynchronous task, unsafe to splice the index, flag as pending
2220
- task[2] = -1;
2221
- }
2222
- });
2223
- }
2224
- }
2225
- }, 1000);
2226
- }
2227
-
2228
- /**
2229
- * Signout, with userLogout and toLoginPage
2230
- * @param action Callback
2231
- */
2232
- async signout(action?: () => void | boolean) {
2233
- // Clear the keep login status
2234
- this.keepLogin = false;
2235
-
2236
- // Reset all APIs
2237
- this.resetApis();
2238
-
2239
- const token = this.getCacheToken();
2240
- if (token) {
2241
- const result = await this.createAuthApi().signout(
2242
- {
2243
- deviceId: this.deviceId,
2244
- token
2245
- },
2246
- {
2247
- onError: (error) => {
2248
- console.error(
2249
- `CoreApp.${this.name}.signout error`,
2250
- error
2251
- );
2252
- // Prevent further processing
2253
- return false;
2254
- }
2255
- }
2256
- );
2257
-
2258
- if (result && !result.ok) {
2259
- console.error(`CoreApp.${this.name}.signout failed`, result);
2260
- }
2261
- }
2262
-
2263
- // Clear, noTrigger = true, avoid state update
2264
- this.userLogout(true, true);
2265
-
2266
- if (action == null || action() !== false) {
2267
- // Go to login page
2268
- this.toLoginPage({ params: { tryLogin: false }, removeUrl: true });
2269
- }
2270
- }
2271
-
2272
- /**
2273
- * Go to the login page
2274
- * @param data Login parameters
2275
- */
2276
- toLoginPage(data?: AppLoginParams) {
2277
- // Destruct
2278
- const { params = {}, removeUrl } = data ?? {};
2279
-
2280
- // Save the current URL
2281
- this.cachedUrl = removeUrl ? undefined : globalThis.location.href;
2282
-
2283
- // URL with parameters
2284
- const url = '/'.addUrlParams(params);
2285
-
2286
- this.navigate(url);
2287
- }
2288
-
2289
- /**
2290
- * Try login, returning false means is loading
2291
- * UI get involved while refreshToken not intended
2292
- * @param data Login parameters
2293
- */
2294
- async tryLogin(data?: AppTryLoginParams) {
2295
- // Check status
2296
- if (this._isTryingLogin) return false;
2297
- this._isTryingLogin = true;
2298
-
2299
- return true;
2300
- }
2301
-
2302
- /**
2303
- * Update embedded status
2304
- * @param embedded New embedded status
2305
- * @param isWeb Is web or not
2306
- */
2307
- updateEmbedded(embedded: boolean | undefined | null, isWeb?: boolean) {
2308
- // Check current session when it's undefined
2309
- if (embedded == null) {
2310
- embedded = this.storage.getData<boolean>(this.fields.embedded);
2311
- if (embedded == null) return;
2312
- }
2313
-
2314
- // Is web way?
2315
- // Pass the true embedded status from parent to child (Both conditions are true)
2316
- if (isWeb && embedded && globalThis.self === globalThis.parent) {
2317
- embedded = false;
2169
+ });
2318
2170
  }
2319
-
2320
- // Ignore the same value
2321
- if (embedded === this._embedded) return;
2322
-
2323
- // Save the embedded status
2324
- this.storage.setData(this.fields.embedded, embedded);
2325
-
2326
- // Update the embedded status
2327
- this._embedded = embedded;
2328
- }
2329
-
2330
- /**
2331
- * User login
2332
- * @param user User data
2333
- * @param refreshToken Refresh token
2334
- */
2335
- userLogin(user: U, refreshToken: string) {
2336
- // Hold the user data
2337
- this.userData = user;
2338
-
2339
- // Cache the encrypted serverside device id
2340
- if (user.deviceId) {
2341
- this.storage.setData(this.fields.serversideDeviceId, user.deviceId);
2171
+ }
2172
+ }, 1000);
2173
+ }
2174
+
2175
+ /**
2176
+ * Signout, with userLogout and toLoginPage
2177
+ * @param action Callback
2178
+ */
2179
+ async signout(action?: () => void | boolean) {
2180
+ // Clear the keep login status
2181
+ this.keepLogin = false;
2182
+
2183
+ // Reset all APIs
2184
+ this.resetApis();
2185
+
2186
+ const token = this.getCacheToken();
2187
+ if (token) {
2188
+ const result = await this.createAuthApi().signout(
2189
+ {
2190
+ deviceId: this.deviceId,
2191
+ token
2192
+ },
2193
+ {
2194
+ onError: (error) => {
2195
+ console.error(`CoreApp.${this.name}.signout error`, error);
2196
+ // Prevent further processing
2197
+ return false;
2198
+ }
2342
2199
  }
2343
-
2344
- // Authorize
2345
- this.authorize(user.token, user.tokenScheme, refreshToken);
2346
- }
2347
-
2348
- /**
2349
- * User logout
2350
- * @param clearToken Clear refresh token or not
2351
- * @param noTrigger No trigger for state change
2352
- */
2353
- userLogout(clearToken: boolean = true, noTrigger: boolean = false) {
2354
- this.authorize(undefined, undefined, clearToken ? undefined : '');
2355
- }
2356
-
2357
- /**
2358
- * User unauthorized
2359
- */
2360
- userUnauthorized() {
2361
- this.authorize(undefined, undefined);
2362
- }
2363
-
2364
- private lastWarning?: INotification<N, C>;
2365
-
2366
- /**
2367
- * Show warning message
2368
- * @param message Message
2369
- * @param align Align, default as TopRight
2370
- */
2371
- warning(message: NotificationContent<N>, align?: NotificationAlign) {
2372
- // Same message is open
2373
- if (this.lastWarning?.open && this.lastWarning?.content === message)
2374
- return;
2375
-
2376
- this.lastWarning = this.notifier.message(
2377
- NotificationMessageType.Warning,
2378
- message,
2379
- undefined,
2380
- {
2381
- align: align ?? NotificationAlign.TopRight
2382
- }
2383
- );
2384
- }
2200
+ );
2201
+
2202
+ if (result && !result.ok) {
2203
+ console.error(`CoreApp.${this.name}.signout failed`, result);
2204
+ }
2205
+ }
2206
+
2207
+ // Clear, noTrigger = true, avoid state update
2208
+ this.userLogout(true, true);
2209
+
2210
+ if (action == null || action() !== false) {
2211
+ // Go to login page
2212
+ this.toLoginPage({ params: { tryLogin: false }, removeUrl: true });
2213
+ }
2214
+ }
2215
+
2216
+ /**
2217
+ * Go to the login page
2218
+ * @param data Login parameters
2219
+ */
2220
+ toLoginPage(data?: AppLoginParams) {
2221
+ // Destruct
2222
+ const { params = {}, removeUrl } = data ?? {};
2223
+
2224
+ // Save the current URL
2225
+ this.cachedUrl = removeUrl ? undefined : globalThis.location.href;
2226
+
2227
+ // URL with parameters
2228
+ const url = "/".addUrlParams(params);
2229
+
2230
+ this.navigate(url);
2231
+ }
2232
+
2233
+ /**
2234
+ * Try login, returning false means is loading
2235
+ * UI get involved while refreshToken not intended
2236
+ * @param data Login parameters
2237
+ */
2238
+ async tryLogin(data?: AppTryLoginParams) {
2239
+ // Check status
2240
+ if (this._isTryingLogin) return false;
2241
+ this._isTryingLogin = true;
2242
+
2243
+ return true;
2244
+ }
2245
+
2246
+ /**
2247
+ * Update embedded status
2248
+ * @param embedded New embedded status
2249
+ * @param isWeb Is web or not
2250
+ */
2251
+ updateEmbedded(embedded: boolean | undefined | null, isWeb?: boolean) {
2252
+ // Check current session when it's undefined
2253
+ if (embedded == null) {
2254
+ embedded = this.storage.getData<boolean>(this.fields.embedded);
2255
+ if (embedded == null) return;
2256
+ }
2257
+
2258
+ // Is web way?
2259
+ // Pass the true embedded status from parent to child (Both conditions are true)
2260
+ if (isWeb && embedded && globalThis.self === globalThis.parent) {
2261
+ embedded = false;
2262
+ }
2263
+
2264
+ // Ignore the same value
2265
+ if (embedded === this._embedded) return;
2266
+
2267
+ // Save the embedded status
2268
+ this.storage.setData(this.fields.embedded, embedded);
2269
+
2270
+ // Update the embedded status
2271
+ this._embedded = embedded;
2272
+ }
2273
+
2274
+ /**
2275
+ * User login
2276
+ * @param user User data
2277
+ * @param refreshToken Refresh token
2278
+ */
2279
+ userLogin(user: U, refreshToken: string) {
2280
+ // Hold the user data
2281
+ this.userData = user;
2282
+
2283
+ // Cache the encrypted serverside device id
2284
+ if (user.deviceId) {
2285
+ this.storage.setData(this.fields.serversideDeviceId, user.deviceId);
2286
+ }
2287
+
2288
+ // Authorize
2289
+ this.authorize(user.token, user.tokenScheme, refreshToken);
2290
+ }
2291
+
2292
+ /**
2293
+ * User logout
2294
+ * @param clearToken Clear refresh token or not
2295
+ * @param noTrigger No trigger for state change
2296
+ */
2297
+ userLogout(clearToken: boolean = true, noTrigger: boolean = false) {
2298
+ this.authorize(undefined, undefined, clearToken ? undefined : "");
2299
+ }
2300
+
2301
+ /**
2302
+ * User unauthorized
2303
+ */
2304
+ userUnauthorized() {
2305
+ this.authorize(undefined, undefined);
2306
+ }
2307
+
2308
+ private lastWarning?: INotification<N, C>;
2309
+
2310
+ /**
2311
+ * Show warning message
2312
+ * @param message Message
2313
+ * @param align Align, default as TopRight
2314
+ */
2315
+ warning(message: NotificationContent<N>, align?: NotificationAlign) {
2316
+ // Same message is open
2317
+ if (this.lastWarning?.open && this.lastWarning?.content === message) return;
2318
+
2319
+ this.lastWarning = this.notifier.message(
2320
+ NotificationMessageType.Warning,
2321
+ message,
2322
+ undefined,
2323
+ {
2324
+ align: align ?? NotificationAlign.TopRight
2325
+ }
2326
+ );
2327
+ }
2385
2328
  }