@etsoo/appscript 1.1.66 → 1.1.70

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.
@@ -3,6 +3,7 @@ import { ApiDataError, IApi, IPData } from '@etsoo/restclient';
3
3
  import { DataTypes, DateUtils } from '@etsoo/shared';
4
4
  import { AddressRegion } from '../address/AddressRegion';
5
5
  import { IActionResult } from '../result/IActionResult';
6
+ import { InitCallResultData } from '../result/InitCallResult';
6
7
  import { IUserData } from '../state/User';
7
8
  import { IAppSettings } from './AppSettings';
8
9
  import { UserRole } from './UserRole';
@@ -87,10 +88,6 @@ export interface ICoreApp<S extends IAppSettings, N, C extends NotificationCallP
87
88
  * User data
88
89
  */
89
90
  userData?: IUserData;
90
- /**
91
- * Passphrase for encryption
92
- */
93
- passphrase?: string;
94
91
  /**
95
92
  * Search input element
96
93
  */
@@ -121,9 +118,10 @@ export interface ICoreApp<S extends IAppSettings, N, C extends NotificationCallP
121
118
  * Decrypt message
122
119
  * @param messageEncrypted Encrypted message
123
120
  * @param passphrase Secret passphrase
121
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
124
122
  * @returns Pure text
125
123
  */
126
- decrypt(messageEncrypted: string, passphrase: string): string;
124
+ decrypt(messageEncrypted: string, passphrase?: string, durationSeconds?: number): string | undefined;
127
125
  /**
128
126
  * Detect IP data, call only one time
129
127
  * @param callback Callback will be called when the IP is ready
@@ -133,9 +131,10 @@ export interface ICoreApp<S extends IAppSettings, N, C extends NotificationCallP
133
131
  * Encrypt message
134
132
  * @param message Message
135
133
  * @param passphrase Secret passphrase
134
+ * @param iterations Iterations, 1000 times, 1 - 99
136
135
  * @returns Result
137
136
  */
138
- encrypt(message: string, passphrase: string): string;
137
+ encrypt(message: string, passphrase?: string, iterations?: number): string;
139
138
  /**
140
139
  * Format date to string
141
140
  * @param input Input date
@@ -216,12 +215,32 @@ export interface ICoreApp<S extends IAppSettings, N, C extends NotificationCallP
216
215
  * @returns Time zone
217
216
  */
218
217
  getTimeZone(): string | undefined;
218
+ /**
219
+ * Hash message, SHA3 or HmacSHA512, 512 as Base64
220
+ * https://cryptojs.gitbook.io/docs/
221
+ * @param message Message
222
+ * @param passphrase Secret passphrase
223
+ */
224
+ hash(message: string, passphrase?: string): string;
225
+ /**
226
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
227
+ * https://cryptojs.gitbook.io/docs/
228
+ * @param message Message
229
+ * @param passphrase Secret passphrase
230
+ */
231
+ hashHex(message: string, passphrase?: string): string;
219
232
  /**
220
233
  * Check use has the specific role permission or not
221
234
  * @param roles Roles to check
222
235
  * @returns Result
223
236
  */
224
237
  hasPermission(roles: number | UserRole | number[] | UserRole[]): boolean;
238
+ /**
239
+ * Init call
240
+ * @param callback Callback
241
+ * @returns Result
242
+ */
243
+ initCall(callback?: (result: boolean) => void): Promise<void>;
225
244
  /**
226
245
  * Callback where exit a page
227
246
  */
@@ -327,10 +346,6 @@ export declare abstract class CoreApp<S extends IAppSettings, N, C extends Notif
327
346
  * User data
328
347
  */
329
348
  userData?: IUserData;
330
- /**
331
- * Passphrase for encryption
332
- */
333
- passphrase?: string;
334
349
  /**
335
350
  * Response token header field name
336
351
  */
@@ -355,6 +370,18 @@ export declare abstract class CoreApp<S extends IAppSettings, N, C extends Notif
355
370
  * Token refresh count down seed
356
371
  */
357
372
  protected refreshCountdownSeed: number;
373
+ /**
374
+ * Device id field name
375
+ */
376
+ protected deviceIdField: string;
377
+ /**
378
+ * Device id
379
+ */
380
+ protected deviceId: string;
381
+ /**
382
+ * Passphrase for encryption
383
+ */
384
+ protected passphrase: string;
358
385
  /**
359
386
  * Protected constructor
360
387
  * @param settings Settings
@@ -364,6 +391,23 @@ export declare abstract class CoreApp<S extends IAppSettings, N, C extends Notif
364
391
  */
365
392
  protected constructor(settings: S, api: IApi, notifier: INotifier<N, C>, name: string);
366
393
  protected setApi(api: IApi): void;
394
+ /**
395
+ * Init call
396
+ * @param callback Callback
397
+ * @returns Result
398
+ */
399
+ initCall(callback?: (result: boolean) => void): Promise<void>;
400
+ /**
401
+ * Init call update
402
+ * @param data Result data
403
+ * @param timestamp Timestamp
404
+ */
405
+ protected initCallUpdate(data: InitCallResultData, timestamp: number): void;
406
+ /**
407
+ * Init call update fields in local storage
408
+ * @returns Fields
409
+ */
410
+ protected initCallUpdateFields(): string[];
367
411
  /**
368
412
  * Alert action result
369
413
  * @param result Action result
@@ -390,9 +434,10 @@ export declare abstract class CoreApp<S extends IAppSettings, N, C extends Notif
390
434
  * Decrypt message
391
435
  * @param messageEncrypted Encrypted message
392
436
  * @param passphrase Secret passphrase
437
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
393
438
  * @returns Pure text
394
439
  */
395
- decrypt(messageEncrypted: string, passphrase: string): string;
440
+ decrypt(messageEncrypted: string, passphrase?: string, durationSeconds?: number): string | undefined;
396
441
  /**
397
442
  * Detect IP data, call only one time
398
443
  * @param callback Callback will be called when the IP is ready
@@ -403,9 +448,10 @@ export declare abstract class CoreApp<S extends IAppSettings, N, C extends Notif
403
448
  * Encrypt message
404
449
  * @param message Message
405
450
  * @param passphrase Secret passphrase
451
+ * @param iterations Iterations, 1000 times, 1 - 99
406
452
  * @returns Result
407
453
  */
408
- encrypt(message: string, passphrase: string): string;
454
+ encrypt(message: string, passphrase?: string, iterations?: number): string;
409
455
  /**
410
456
  * Enchance secret passphrase
411
457
  * @param passphrase Secret passphrase
@@ -488,6 +534,20 @@ export declare abstract class CoreApp<S extends IAppSettings, N, C extends Notif
488
534
  * @returns Time zone
489
535
  */
490
536
  getTimeZone(): string | undefined;
537
+ /**
538
+ * Hash message, SHA3 or HmacSHA512, 512 as Base64
539
+ * https://cryptojs.gitbook.io/docs/
540
+ * @param message Message
541
+ * @param passphrase Secret passphrase
542
+ */
543
+ hash(message: string, passphrase?: string): string;
544
+ /**
545
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
546
+ * https://cryptojs.gitbook.io/docs/
547
+ * @param message Message
548
+ * @param passphrase Secret passphrase
549
+ */
550
+ hashHex(message: string, passphrase?: string): string;
491
551
  /**
492
552
  * Check use has the specific role permission or not
493
553
  * @param roles Roles to check
@@ -1,7 +1,7 @@
1
1
  import { NotificationAlign, NotificationMessageType } from '@etsoo/notificationbase';
2
2
  import { ApiDataError } from '@etsoo/restclient';
3
3
  import { DateUtils, DomUtils, NumberUtils, StorageUtils, Utils } from '@etsoo/shared';
4
- import { AES } from 'crypto-js';
4
+ import { AES, algo, enc, HmacSHA512, lib, mode, pad, PBKDF2, SHA3 } from 'crypto-js';
5
5
  import { AddressRegion } from '../address/AddressRegion';
6
6
  import { AddressUtils } from '../address/AddressUtils';
7
7
  import { ActionResultError } from '../result/ActionResultError';
@@ -31,10 +31,19 @@ export class CoreApp {
31
31
  * Token refresh count down seed
32
32
  */
33
33
  this.refreshCountdownSeed = 0;
34
+ /**
35
+ * Device id field name
36
+ */
37
+ this.deviceIdField = 'SmartERPDeviceId';
38
+ /**
39
+ * Passphrase for encryption
40
+ */
41
+ this.passphrase = '***';
34
42
  this.settings = settings;
35
43
  this.api = api;
36
44
  this.notifier = notifier;
37
45
  this.name = name;
46
+ this.deviceId = StorageUtils.getLocalData(this.deviceIdField, '');
38
47
  this.setApi(api);
39
48
  const { currentCulture, currentRegion } = settings;
40
49
  this.changeCulture(currentCulture);
@@ -105,6 +114,91 @@ export class CoreApp {
105
114
  }
106
115
  };
107
116
  }
117
+ /**
118
+ * Init call
119
+ * @param callback Callback
120
+ * @returns Result
121
+ */
122
+ async initCall(callback) {
123
+ var _a;
124
+ const data = {
125
+ timestamp: new Date().getTime(),
126
+ deviceId: this.deviceId === '' ? undefined : this.deviceId
127
+ };
128
+ const result = await this.api.put('Auth/WebInitCall', data);
129
+ if (result == null) {
130
+ if (callback)
131
+ callback(false);
132
+ return;
133
+ }
134
+ if (result.data == null) {
135
+ this.notifier.alert(this.get('noData'));
136
+ if (callback)
137
+ callback(false);
138
+ return;
139
+ }
140
+ if (!result.ok) {
141
+ const seconds = result.data.seconds;
142
+ const validSeconds = result.data.validSeconds;
143
+ if (result.title === 'timeDifferenceInvalid' &&
144
+ seconds != null &&
145
+ validSeconds != null) {
146
+ const title = (_a = this.get('timeDifferenceInvalid')) === null || _a === void 0 ? void 0 : _a.format(seconds.toString(), validSeconds.toString());
147
+ this.notifier.alert(title);
148
+ }
149
+ else {
150
+ this.alertResult(result);
151
+ }
152
+ if (callback)
153
+ callback(false);
154
+ return;
155
+ }
156
+ this.initCallUpdate(result.data, data.timestamp);
157
+ if (callback)
158
+ callback(true);
159
+ }
160
+ /**
161
+ * Init call update
162
+ * @param data Result data
163
+ * @param timestamp Timestamp
164
+ */
165
+ initCallUpdate(data, timestamp) {
166
+ if (data.deviceId == null || data.passphrase == null)
167
+ return;
168
+ // Decrypt
169
+ // Should be done within 120 seconds after returning from the backend
170
+ const passphrase = this.decrypt(data.passphrase, timestamp.toString(), 120);
171
+ if (passphrase == null)
172
+ return;
173
+ // Update device id and cache it
174
+ this.deviceId = data.deviceId;
175
+ StorageUtils.setLocalData(this.deviceIdField, this.deviceId);
176
+ // Current passphrase
177
+ this.passphrase = passphrase;
178
+ // Previous passphrase
179
+ if (data.previousPassphrase) {
180
+ const prev = this.decrypt(data.previousPassphrase, timestamp.toString(), 120);
181
+ // Update
182
+ const fields = this.initCallUpdateFields();
183
+ for (const field of fields) {
184
+ const currentValue = StorageUtils.getLocalData(field, '');
185
+ if (currentValue === '' || currentValue.indexOf('+') === -1)
186
+ continue;
187
+ const newValueSource = this.decrypt(currentValue, prev, 12);
188
+ if (newValueSource == null)
189
+ continue;
190
+ const newValue = this.encrypt(newValueSource);
191
+ StorageUtils.setLocalData(field, newValue);
192
+ }
193
+ }
194
+ }
195
+ /**
196
+ * Init call update fields in local storage
197
+ * @returns Fields
198
+ */
199
+ initCallUpdateFields() {
200
+ return [];
201
+ }
108
202
  /**
109
203
  * Alert action result
110
204
  * @param result Action result
@@ -197,13 +291,44 @@ export class CoreApp {
197
291
  * Decrypt message
198
292
  * @param messageEncrypted Encrypted message
199
293
  * @param passphrase Secret passphrase
294
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
200
295
  * @returns Pure text
201
296
  */
202
- decrypt(messageEncrypted, passphrase) {
297
+ decrypt(messageEncrypted, passphrase, durationSeconds) {
298
+ // Timestamp splitter
203
299
  const pos = messageEncrypted.indexOf('+');
300
+ if (pos === -1 || messageEncrypted.length <= 66)
301
+ return undefined;
204
302
  const timestamp = messageEncrypted.substring(0, pos);
205
303
  const message = messageEncrypted.substring(pos + 1);
206
- return AES.decrypt(message, this.encryptionEnhance(passphrase, timestamp)).toString();
304
+ if (durationSeconds != null && durationSeconds > 0) {
305
+ const milseconds = Utils.charsToNumber(timestamp);
306
+ if (isNaN(milseconds) || milseconds < 1)
307
+ return undefined;
308
+ const timespan = new Date().substract(new Date(milseconds));
309
+ if ((durationSeconds <= 12 &&
310
+ timespan.totalMonths > durationSeconds) ||
311
+ (durationSeconds > 12 &&
312
+ timespan.totalSeconds > durationSeconds))
313
+ return undefined;
314
+ }
315
+ // Iterations
316
+ const iterations = parseInt(message.substring(0, 2), 10);
317
+ if (isNaN(iterations))
318
+ return undefined;
319
+ const salt = enc.Hex.parse(message.substring(2, 34));
320
+ const iv = enc.Hex.parse(message.substring(34, 66));
321
+ const encrypted = message.substring(66);
322
+ const key = PBKDF2(this.encryptionEnhance(passphrase !== null && passphrase !== void 0 ? passphrase : this.passphrase, timestamp), salt, {
323
+ keySize: 8,
324
+ hasher: algo.SHA256,
325
+ iterations: 1000 * iterations
326
+ });
327
+ return AES.decrypt(encrypted, key, {
328
+ iv,
329
+ padding: pad.Pkcs7,
330
+ mode: mode.CBC
331
+ }).toString(enc.Utf8);
207
332
  }
208
333
  /**
209
334
  * Detect IP data, call only one time
@@ -242,13 +367,33 @@ export class CoreApp {
242
367
  * Encrypt message
243
368
  * @param message Message
244
369
  * @param passphrase Secret passphrase
370
+ * @param iterations Iterations, 1000 times, 1 - 99
245
371
  * @returns Result
246
372
  */
247
- encrypt(message, passphrase) {
373
+ encrypt(message, passphrase, iterations) {
374
+ // Default 1 * 1000
375
+ iterations !== null && iterations !== void 0 ? iterations : (iterations = 1);
376
+ // Timestamp
248
377
  const timestamp = Utils.numberToChars(new Date().getTime());
378
+ const bits = 16; // 128 / 8
379
+ const salt = lib.WordArray.random(bits);
380
+ const key = PBKDF2(this.encryptionEnhance(passphrase !== null && passphrase !== void 0 ? passphrase : this.passphrase, timestamp), salt, {
381
+ keySize: 8,
382
+ hasher: algo.SHA256,
383
+ iterations: 1000 * iterations
384
+ });
385
+ const iv = lib.WordArray.random(bits);
249
386
  return (timestamp +
250
387
  '+' +
251
- AES.encrypt(message, this.encryptionEnhance(passphrase, timestamp)).toString());
388
+ iterations.toString().padStart(2, '0') +
389
+ salt.toString(enc.Hex) +
390
+ iv.toString(enc.Hex) +
391
+ AES.encrypt(message, key, {
392
+ iv,
393
+ padding: pad.Pkcs7,
394
+ mode: mode.CBC
395
+ }).toString() // enc.Base64
396
+ );
252
397
  }
253
398
  /**
254
399
  * Enchance secret passphrase
@@ -257,10 +402,9 @@ export class CoreApp {
257
402
  * @returns Enhanced passphrase
258
403
  */
259
404
  encryptionEnhance(passphrase, timestamp) {
260
- var _a;
261
405
  passphrase += timestamp;
262
406
  passphrase += passphrase.length.toString();
263
- return passphrase + ((_a = this.passphrase) !== null && _a !== void 0 ? _a : '');
407
+ return passphrase;
264
408
  }
265
409
  /**
266
410
  * Format date to string
@@ -322,7 +466,19 @@ export class CoreApp {
322
466
  * @param forceToLocal Force to local labels
323
467
  */
324
468
  formatResult(result, forceToLocal) {
325
- if ((result.title == null || forceToLocal) && result.type != null) {
469
+ const title = result.title;
470
+ if (title && /^\w+$/.test(title)) {
471
+ const key = title.formatInitial(false);
472
+ const localTitle = this.get(key);
473
+ if (localTitle) {
474
+ result.title = localTitle;
475
+ // Hold the original title in type when type is null
476
+ if (result.type == null)
477
+ result.type = title;
478
+ }
479
+ }
480
+ else if ((title == null || forceToLocal) && result.type != null) {
481
+ // Get label from type
326
482
  const key = result.type.formatInitial(false);
327
483
  result.title = this.get(key);
328
484
  }
@@ -388,6 +544,30 @@ export class CoreApp {
388
544
  // settings.timeZone = Utils.getTimeZone()
389
545
  return (_a = this.settings.timeZone) !== null && _a !== void 0 ? _a : (_b = this.ipData) === null || _b === void 0 ? void 0 : _b.timezone;
390
546
  }
547
+ /**
548
+ * Hash message, SHA3 or HmacSHA512, 512 as Base64
549
+ * https://cryptojs.gitbook.io/docs/
550
+ * @param message Message
551
+ * @param passphrase Secret passphrase
552
+ */
553
+ hash(message, passphrase) {
554
+ if (passphrase == null)
555
+ return SHA3(message, { outputLength: 512 }).toString(enc.Base64);
556
+ else
557
+ return HmacSHA512(message, passphrase).toString(enc.Base64);
558
+ }
559
+ /**
560
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
561
+ * https://cryptojs.gitbook.io/docs/
562
+ * @param message Message
563
+ * @param passphrase Secret passphrase
564
+ */
565
+ hashHex(message, passphrase) {
566
+ if (passphrase == null)
567
+ return SHA3(message, { outputLength: 512 }).toString(enc.Hex);
568
+ else
569
+ return HmacSHA512(message, passphrase).toString(enc.Hex);
570
+ }
391
571
  /**
392
572
  * Check use has the specific role permission or not
393
573
  * @param roles Roles to check
@@ -537,7 +717,6 @@ export class CoreApp {
537
717
  */
538
718
  userLogin(user, refreshToken, keep = false) {
539
719
  this.userData = user;
540
- this.passphrase = user.passphrase;
541
720
  this.authorize(user.token, refreshToken, keep);
542
721
  }
543
722
  /**
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Init call dto
3
+ */
4
+ export declare type InitCallDto = {
5
+ /**
6
+ * Device id
7
+ */
8
+ deviceId?: string;
9
+ /**
10
+ * Timestamp
11
+ */
12
+ timestamp: number;
13
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -16,6 +16,7 @@ export * from './def/ListItem';
16
16
  export * from './dto/IdDto';
17
17
  export * from './dto/IdLabelDto';
18
18
  export * from './dto/IdLabelPrimaryDto';
19
+ export * from './dto/InitCallDto';
19
20
  export * from './dto/UpdateDto';
20
21
  export * from './i18n/enUS';
21
22
  export * from './i18n/zhCN';
package/lib/mjs/index.js CHANGED
@@ -22,6 +22,7 @@ export * from './def/ListItem';
22
22
  export * from './dto/IdDto';
23
23
  export * from './dto/IdLabelDto';
24
24
  export * from './dto/IdLabelPrimaryDto';
25
+ export * from './dto/InitCallDto';
25
26
  export * from './dto/UpdateDto';
26
27
  // i18n
27
28
  export * from './i18n/enUS';
@@ -48,11 +48,11 @@ export interface IActionResult<D extends IResultData = IResultData> {
48
48
  /**
49
49
  * Trace id
50
50
  */
51
- readonly traceId?: string;
51
+ traceId?: string;
52
52
  /**
53
53
  * Type
54
54
  */
55
- readonly type: string;
55
+ type: string;
56
56
  /**
57
57
  * Success or not
58
58
  */
@@ -1,20 +1,28 @@
1
1
  import { IActionResult, IResultData } from './IActionResult';
2
2
  /**
3
- * Result data with id, follow this style to extend for specific model
3
+ * Init call result data
4
4
  */
5
5
  export interface InitCallResultData extends IResultData {
6
+ /**
7
+ * Device id
8
+ */
9
+ deviceId?: string;
6
10
  /**
7
11
  * Secret passphrase
8
12
  */
9
- passphrase: string;
13
+ passphrase?: string;
14
+ /**
15
+ * Previous secret passphrase
16
+ */
17
+ previousPassphrase?: string;
10
18
  /**
11
19
  * Actual seconds gap
12
20
  */
13
- seconds: number;
21
+ seconds?: number;
14
22
  /**
15
23
  * Valid seconds gap
16
24
  */
17
- validSeconds: number;
25
+ validSeconds?: number;
18
26
  }
19
27
  /**
20
28
  * Init call result
@@ -27,10 +27,6 @@ export interface IUserData {
27
27
  * Access token
28
28
  */
29
29
  readonly token: string;
30
- /**
31
- * Secret passphrase
32
- */
33
- readonly passphrase: string;
34
30
  }
35
31
  /**
36
32
  * User interface
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/appscript",
3
- "version": "1.1.66",
3
+ "version": "1.1.70",
4
4
  "description": "Applications shared TypeScript framework",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/mjs/index.js",
@@ -23,7 +23,7 @@
23
23
  "testMatch": [
24
24
  "<rootDir>/__tests__/**/*.ts"
25
25
  ],
26
- "testEnvironment": "node",
26
+ "testEnvironment": "jsdom",
27
27
  "transform": {
28
28
  ".+\\.jsx?$": "babel-jest",
29
29
  ".+\\.tsx?$": "ts-jest"
@@ -65,13 +65,13 @@
65
65
  "@babel/preset-env": "^7.16.4",
66
66
  "@babel/runtime-corejs3": "^7.16.3",
67
67
  "@types/jest": "^27.0.3",
68
- "@typescript-eslint/eslint-plugin": "^5.5.0",
69
- "@typescript-eslint/parser": "^5.5.0",
70
- "eslint": "^8.4.0",
68
+ "@typescript-eslint/eslint-plugin": "^5.6.0",
69
+ "@typescript-eslint/parser": "^5.6.0",
70
+ "eslint": "^8.4.1",
71
71
  "eslint-config-airbnb-base": "^15.0.0",
72
72
  "eslint-plugin-import": "^2.25.3",
73
- "jest": "^27.4.3",
74
- "ts-jest": "^27.1.0",
75
- "typescript": "^4.5.2"
73
+ "jest": "^27.4.4",
74
+ "ts-jest": "^27.1.1",
75
+ "typescript": "^4.5.3"
76
76
  }
77
77
  }