@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.
- package/__tests__/app/CoreApp.ts +12 -0
- package/lib/cjs/app/CoreApp.d.ts +15 -15
- package/lib/cjs/app/CoreApp.js +97 -109
- package/lib/cjs/app/ExternalSettings.d.ts +1 -1
- package/lib/cjs/app/ExternalSettings.js +8 -8
- package/lib/cjs/app/IApp.d.ts +12 -12
- package/lib/cjs/app/IApp.js +8 -8
- package/lib/cjs/bridges/BridgeUtils.d.ts +2 -2
- package/lib/cjs/bridges/BridgeUtils.js +2 -2
- package/lib/cjs/i18n/en.json +1 -0
- package/lib/cjs/i18n/zh-Hans.json +1 -0
- package/lib/cjs/i18n/zh-Hant.json +1 -0
- package/lib/mjs/app/CoreApp.d.ts +15 -15
- package/lib/mjs/app/CoreApp.js +108 -120
- package/lib/mjs/app/ExternalSettings.d.ts +1 -1
- package/lib/mjs/app/ExternalSettings.js +8 -8
- package/lib/mjs/app/IApp.d.ts +12 -12
- package/lib/mjs/app/IApp.js +8 -8
- package/lib/mjs/bridges/BridgeUtils.d.ts +2 -2
- package/lib/mjs/bridges/BridgeUtils.js +3 -3
- package/lib/mjs/i18n/en.json +1 -0
- package/lib/mjs/i18n/zh-Hans.json +1 -0
- package/lib/mjs/i18n/zh-Hant.json +1 -0
- package/package.json +4 -4
- package/src/app/CoreApp.ts +2246 -2303
- package/src/app/ExternalSettings.ts +61 -61
- package/src/app/IApp.ts +760 -766
- package/src/bridges/BridgeUtils.ts +12 -12
- package/src/i18n/en.json +1 -0
- package/src/i18n/zh-Hans.json +1 -0
- package/src/i18n/zh-Hant.json +1 -0
package/src/app/CoreApp.ts
CHANGED
|
@@ -1,2385 +1,2328 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from
|
|
10
|
-
import { ApiDataError, createClient, IApi, IPData } from
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from
|
|
25
|
-
import { AddressRegion } from
|
|
26
|
-
import { BridgeUtils } from
|
|
27
|
-
import { DataPrivacy } from
|
|
28
|
-
import { EntityStatus } from
|
|
29
|
-
import { InitCallDto } from
|
|
30
|
-
import { ActionResultError } from
|
|
31
|
-
import { InitCallResult, InitCallResultData } from
|
|
32
|
-
import { IUser } from
|
|
33
|
-
import { IAppSettings } from
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
} from
|
|
45
|
-
import { UserRole } from
|
|
46
|
-
import type CryptoJS from
|
|
47
|
-
import { Currency } from
|
|
48
|
-
import { ExternalEndpoint } from
|
|
49
|
-
import { ApiRefreshTokenDto } from
|
|
50
|
-
import { ApiRefreshTokenRQ } from
|
|
51
|
-
import { AuthApi } from
|
|
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(
|
|
56
|
+
const loadCrypto = () => import("crypto-js");
|
|
57
57
|
|
|
58
58
|
// API refresh token function interface
|
|
59
59
|
type ApiRefreshTokenFunction = (
|
|
60
|
-
|
|
61
|
-
|
|
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 =
|
|
68
|
+
const systemApi = "system";
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Core application interface
|
|
72
72
|
*/
|
|
73
73
|
export interface ICoreApp<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
U extends IUser,
|
|
75
|
+
S extends IAppSettings,
|
|
76
|
+
N,
|
|
77
|
+
C extends NotificationCallProps
|
|
78
78
|
> extends IApp {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
386
|
-
systemApi,
|
|
387
|
-
{
|
|
388
|
-
endpoint: settings.endpoint,
|
|
389
|
-
webUrl: settings.webUrl
|
|
390
|
-
},
|
|
391
|
-
refresh
|
|
392
|
-
);
|
|
715
|
+
this.tryLogin();
|
|
393
716
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
//
|
|
401
|
-
this.
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
521
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
537
|
-
}
|
|
907
|
+
if (callback) callback(false);
|
|
538
908
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
909
|
+
// Clear device id
|
|
910
|
+
this.clearDeviceId();
|
|
542
911
|
|
|
543
|
-
|
|
912
|
+
return;
|
|
544
913
|
}
|
|
545
914
|
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
931
|
+
// Update
|
|
932
|
+
this.passphrase = passphrase;
|
|
933
|
+
this.storage.setData(
|
|
934
|
+
this.fields.devicePassphrase,
|
|
935
|
+
this.encrypt(passphrase, this.name)
|
|
936
|
+
);
|
|
594
937
|
|
|
595
|
-
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
|
626
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
835
|
-
|
|
1280
|
+
(durationSeconds <= 12 && timespan.totalMonths > durationSeconds) ||
|
|
1281
|
+
(durationSeconds > 12 && timespan.totalSeconds > durationSeconds)
|
|
836
1282
|
)
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
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
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
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
|
-
|
|
1869
|
-
|
|
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
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
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
|
-
|
|
2140
|
+
// Reset the API
|
|
2141
|
+
const [token, seconds] = data;
|
|
2142
|
+
this.updateApi(api, token, seconds);
|
|
2113
2143
|
}
|
|
2144
|
+
});
|
|
2114
2145
|
}
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
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
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
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
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
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
|
}
|