@etsoo/appscript 1.1.65 → 1.1.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.
@@ -15,11 +15,23 @@ import {
15
15
  StorageUtils,
16
16
  Utils
17
17
  } from '@etsoo/shared';
18
- import { AES } from 'crypto-js';
18
+ import {
19
+ AES,
20
+ algo,
21
+ enc,
22
+ HmacSHA512,
23
+ lib,
24
+ mode,
25
+ pad,
26
+ PBKDF2,
27
+ SHA3
28
+ } from 'crypto-js';
19
29
  import { AddressRegion } from '../address/AddressRegion';
20
30
  import { AddressUtils } from '../address/AddressUtils';
31
+ import { InitCallDto } from '../dto/InitCallDto';
21
32
  import { ActionResultError } from '../result/ActionResultError';
22
33
  import { IActionResult } from '../result/IActionResult';
34
+ import { InitCallResult, InitCallResultData } from '../result/InitCallResult';
23
35
  import { IUserData } from '../state/User';
24
36
  import { IAppSettings } from './AppSettings';
25
37
  import { UserRole } from './UserRole';
@@ -130,11 +142,6 @@ export interface ICoreApp<
130
142
  */
131
143
  userData?: IUserData;
132
144
 
133
- /**
134
- * Passphrase for encryption
135
- */
136
- passphrase?: string;
137
-
138
145
  /**
139
146
  * Search input element
140
147
  */
@@ -170,9 +177,14 @@ export interface ICoreApp<
170
177
  * Decrypt message
171
178
  * @param messageEncrypted Encrypted message
172
179
  * @param passphrase Secret passphrase
180
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
173
181
  * @returns Pure text
174
182
  */
175
- decrypt(messageEncrypted: string, passphrase: string): string;
183
+ decrypt(
184
+ messageEncrypted: string,
185
+ passphrase?: string,
186
+ durationSeconds?: number
187
+ ): string | undefined;
176
188
 
177
189
  /**
178
190
  * Detect IP data, call only one time
@@ -184,9 +196,10 @@ export interface ICoreApp<
184
196
  * Encrypt message
185
197
  * @param message Message
186
198
  * @param passphrase Secret passphrase
199
+ * @param iterations Iterations, 1000 times, 1 - 99
187
200
  * @returns Result
188
201
  */
189
- encrypt(message: string, passphrase: string): string;
202
+ encrypt(message: string, passphrase?: string, iterations?: number): string;
190
203
 
191
204
  /**
192
205
  * Format date to string
@@ -290,6 +303,22 @@ export interface ICoreApp<
290
303
  */
291
304
  getTimeZone(): string | undefined;
292
305
 
306
+ /**
307
+ * Hash message, SHA3 or HmacSHA512, 512 as Base64
308
+ * https://cryptojs.gitbook.io/docs/
309
+ * @param message Message
310
+ * @param passphrase Secret passphrase
311
+ */
312
+ hash(message: string, passphrase?: string): string;
313
+
314
+ /**
315
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
316
+ * https://cryptojs.gitbook.io/docs/
317
+ * @param message Message
318
+ * @param passphrase Secret passphrase
319
+ */
320
+ hashHex(message: string, passphrase?: string): string;
321
+
293
322
  /**
294
323
  * Check use has the specific role permission or not
295
324
  * @param roles Roles to check
@@ -297,6 +326,13 @@ export interface ICoreApp<
297
326
  */
298
327
  hasPermission(roles: number | UserRole | number[] | UserRole[]): boolean;
299
328
 
329
+ /**
330
+ * Init call
331
+ * @param callback Callback
332
+ * @returns Result
333
+ */
334
+ initCall(callback?: (result: boolean) => void): Promise<void>;
335
+
300
336
  /**
301
337
  * Callback where exit a page
302
338
  */
@@ -438,11 +474,6 @@ export abstract class CoreApp<
438
474
  */
439
475
  userData?: IUserData;
440
476
 
441
- /**
442
- * Passphrase for encryption
443
- */
444
- passphrase?: string;
445
-
446
477
  /**
447
478
  * Response token header field name
448
479
  */
@@ -480,6 +511,21 @@ export abstract class CoreApp<
480
511
  */
481
512
  protected refreshCountdownSeed = 0;
482
513
 
514
+ /**
515
+ * Device id field name
516
+ */
517
+ protected deviceIdField: string = 'SmartERPDeviceId';
518
+
519
+ /**
520
+ * Device id
521
+ */
522
+ protected deviceId: string;
523
+
524
+ /**
525
+ * Passphrase for encryption
526
+ */
527
+ protected passphrase: string = '***';
528
+
483
529
  /**
484
530
  * Protected constructor
485
531
  * @param settings Settings
@@ -498,6 +544,11 @@ export abstract class CoreApp<
498
544
  this.notifier = notifier;
499
545
  this.name = name;
500
546
 
547
+ this.deviceId = StorageUtils.getLocalData<string>(
548
+ this.deviceIdField,
549
+ ''
550
+ );
551
+
501
552
  this.setApi(api);
502
553
 
503
554
  const { currentCulture, currentRegion } = settings;
@@ -542,6 +593,117 @@ export abstract class CoreApp<
542
593
  };
543
594
  }
544
595
 
596
+ /**
597
+ * Init call
598
+ * @param callback Callback
599
+ * @returns Result
600
+ */
601
+ async initCall(callback?: (result: boolean) => void) {
602
+ const data: InitCallDto = {
603
+ timestamp: new Date().getTime(),
604
+ deviceId: this.deviceId === '' ? undefined : this.deviceId
605
+ };
606
+ const result = await this.api.put<InitCallResult>(
607
+ 'Auth/WebInitCall',
608
+ data
609
+ );
610
+ if (result == null) {
611
+ if (callback) callback(false);
612
+ return;
613
+ }
614
+
615
+ if (result.data == null) {
616
+ this.notifier.alert(this.get<string>('noData')!);
617
+ if (callback) callback(false);
618
+ return;
619
+ }
620
+
621
+ if (!result.ok) {
622
+ const seconds = result.data.seconds;
623
+ const validSeconds = result.data.validSeconds;
624
+ if (
625
+ result.title === 'timeDifferenceInvalid' &&
626
+ seconds != null &&
627
+ validSeconds != null
628
+ ) {
629
+ const title = this.get('timeDifferenceInvalid')?.format(
630
+ seconds.toString(),
631
+ validSeconds.toString()
632
+ );
633
+ this.notifier.alert(title!);
634
+ } else {
635
+ this.alertResult(result);
636
+ }
637
+
638
+ if (callback) callback(false);
639
+
640
+ return;
641
+ }
642
+
643
+ this.initCallUpdate(result.data, data.timestamp);
644
+
645
+ if (callback) callback(true);
646
+ }
647
+
648
+ /**
649
+ * Init call update
650
+ * @param data Result data
651
+ * @param timestamp Timestamp
652
+ */
653
+ protected initCallUpdate(data: InitCallResultData, timestamp: number) {
654
+ if (data.deviceId == null || data.passphrase == null) return;
655
+
656
+ // Decrypt
657
+ // Should be done within 120 seconds after returning from the backend
658
+ const passphrase = this.decrypt(
659
+ data.passphrase,
660
+ timestamp.toString(),
661
+ 120
662
+ );
663
+ if (passphrase == null) return;
664
+
665
+ // Update device id and cache it
666
+ this.deviceId = data.deviceId;
667
+ StorageUtils.setLocalData(this.deviceIdField, this.deviceId);
668
+
669
+ // Current passphrase
670
+ this.passphrase = passphrase;
671
+
672
+ // Previous passphrase
673
+ if (data.previousPassphrase) {
674
+ const prev = this.decrypt(
675
+ data.previousPassphrase,
676
+ timestamp.toString(),
677
+ 120
678
+ );
679
+
680
+ // Update
681
+ const fields = this.initCallUpdateFields();
682
+ for (const field of fields) {
683
+ const currentValue = StorageUtils.getLocalData<string>(
684
+ field,
685
+ ''
686
+ );
687
+ if (currentValue === '' || currentValue.indexOf('+') === -1)
688
+ continue;
689
+
690
+ const newValueSource = this.decrypt(currentValue, prev, 12);
691
+ if (newValueSource == null) continue;
692
+
693
+ const newValue = this.encrypt(newValueSource);
694
+ StorageUtils.setLocalData(field, newValue);
695
+ }
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Init call update fields in local storage
701
+ * @returns Fields
702
+ */
703
+ protected initCallUpdateFields(): string[] {
704
+ return [];
705
+ }
706
+
545
707
  /**
546
708
  * Alert action result
547
709
  * @param result Action result
@@ -657,16 +819,57 @@ export abstract class CoreApp<
657
819
  * Decrypt message
658
820
  * @param messageEncrypted Encrypted message
659
821
  * @param passphrase Secret passphrase
822
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
660
823
  * @returns Pure text
661
824
  */
662
- decrypt(messageEncrypted: string, passphrase: string) {
825
+ decrypt(
826
+ messageEncrypted: string,
827
+ passphrase?: string,
828
+ durationSeconds?: number
829
+ ) {
830
+ // Timestamp splitter
663
831
  const pos = messageEncrypted.indexOf('+');
832
+ if (pos === -1 || messageEncrypted.length <= 66) return undefined;
833
+
664
834
  const timestamp = messageEncrypted.substring(0, pos);
665
835
  const message = messageEncrypted.substring(pos + 1);
666
- return AES.decrypt(
667
- message,
668
- this.encryptionEnhance(passphrase, timestamp)
669
- ).toString();
836
+
837
+ if (durationSeconds != null && durationSeconds > 0) {
838
+ const milseconds = Utils.charsToNumber(timestamp);
839
+ if (isNaN(milseconds) || milseconds < 1) return undefined;
840
+ const timespan = new Date().substract(new Date(milseconds));
841
+ if (
842
+ (durationSeconds <= 12 &&
843
+ timespan.totalMonths > durationSeconds) ||
844
+ (durationSeconds > 12 &&
845
+ timespan.totalSeconds > durationSeconds)
846
+ )
847
+ return undefined;
848
+ }
849
+
850
+ // Iterations
851
+ const iterations = parseInt(message.substring(0, 2), 10);
852
+ if (isNaN(iterations)) return undefined;
853
+
854
+ const salt = enc.Hex.parse(message.substring(2, 34));
855
+ const iv = enc.Hex.parse(message.substring(34, 66));
856
+ const encrypted = message.substring(66);
857
+
858
+ const key = PBKDF2(
859
+ this.encryptionEnhance(passphrase ?? this.passphrase, timestamp),
860
+ salt,
861
+ {
862
+ keySize: 8, // 256 / 32
863
+ hasher: algo.SHA256,
864
+ iterations: 1000 * iterations
865
+ }
866
+ );
867
+
868
+ return AES.decrypt(encrypted, key, {
869
+ iv,
870
+ padding: pad.Pkcs7,
871
+ mode: mode.CBC
872
+ }).toString(enc.Utf8);
670
873
  }
671
874
 
672
875
  /**
@@ -713,17 +916,40 @@ export abstract class CoreApp<
713
916
  * Encrypt message
714
917
  * @param message Message
715
918
  * @param passphrase Secret passphrase
919
+ * @param iterations Iterations, 1000 times, 1 - 99
716
920
  * @returns Result
717
921
  */
718
- encrypt(message: string, passphrase: string) {
922
+ encrypt(message: string, passphrase?: string, iterations?: number) {
923
+ // Default 1 * 1000
924
+ iterations ??= 1;
925
+
926
+ // Timestamp
719
927
  const timestamp = Utils.numberToChars(new Date().getTime());
928
+
929
+ const bits = 16; // 128 / 8
930
+ const salt = lib.WordArray.random(bits);
931
+ const key = PBKDF2(
932
+ this.encryptionEnhance(passphrase ?? this.passphrase, timestamp),
933
+ salt,
934
+ {
935
+ keySize: 8, // 256 / 32
936
+ hasher: algo.SHA256,
937
+ iterations: 1000 * iterations
938
+ }
939
+ );
940
+ const iv = lib.WordArray.random(bits);
941
+
720
942
  return (
721
943
  timestamp +
722
944
  '+' +
723
- AES.encrypt(
724
- message,
725
- this.encryptionEnhance(passphrase, timestamp)
726
- ).toString()
945
+ iterations.toString().padStart(2, '0') +
946
+ salt.toString(enc.Hex) +
947
+ iv.toString(enc.Hex) +
948
+ AES.encrypt(message, key, {
949
+ iv,
950
+ padding: pad.Pkcs7,
951
+ mode: mode.CBC
952
+ }).toString() // enc.Base64
727
953
  );
728
954
  }
729
955
 
@@ -736,7 +962,7 @@ export abstract class CoreApp<
736
962
  protected encryptionEnhance(passphrase: string, timestamp: string) {
737
963
  passphrase += timestamp;
738
964
  passphrase += passphrase.length.toString();
739
- return passphrase + (this.passphrase ?? '');
965
+ return passphrase;
740
966
  }
741
967
 
742
968
  /**
@@ -818,7 +1044,18 @@ export abstract class CoreApp<
818
1044
  * @param forceToLocal Force to local labels
819
1045
  */
820
1046
  formatResult(result: IActionResult, forceToLocal?: boolean) {
821
- if ((result.title == null || forceToLocal) && result.type != null) {
1047
+ const title = result.title;
1048
+ if (title && /^\w+$/.test(title)) {
1049
+ const key = title.formatInitial(false);
1050
+ const localTitle = this.get(key);
1051
+ if (localTitle) {
1052
+ result.title = localTitle;
1053
+
1054
+ // Hold the original title in type when type is null
1055
+ if (result.type == null) result.type = title;
1056
+ }
1057
+ } else if ((title == null || forceToLocal) && result.type != null) {
1058
+ // Get label from type
822
1059
  const key = result.type.formatInitial(false);
823
1060
  result.title = this.get(key);
824
1061
  }
@@ -856,7 +1093,10 @@ export abstract class CoreApp<
856
1093
  * @returns Cached token
857
1094
  */
858
1095
  getCacheToken(): string | null {
859
- let refreshToken = StorageUtils.getLocalData(this.headerTokenField, '');
1096
+ let refreshToken = StorageUtils.getLocalData<string>(
1097
+ this.headerTokenField,
1098
+ ''
1099
+ );
860
1100
  if (refreshToken === '')
861
1101
  refreshToken = StorageUtils.getSessionData(
862
1102
  this.headerTokenField,
@@ -897,6 +1137,30 @@ export abstract class CoreApp<
897
1137
  return this.settings.timeZone ?? this.ipData?.timezone;
898
1138
  }
899
1139
 
1140
+ /**
1141
+ * Hash message, SHA3 or HmacSHA512, 512 as Base64
1142
+ * https://cryptojs.gitbook.io/docs/
1143
+ * @param message Message
1144
+ * @param passphrase Secret passphrase
1145
+ */
1146
+ hash(message: string, passphrase?: string) {
1147
+ if (passphrase == null)
1148
+ return SHA3(message, { outputLength: 512 }).toString(enc.Base64);
1149
+ else return HmacSHA512(message, passphrase).toString(enc.Base64);
1150
+ }
1151
+
1152
+ /**
1153
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
1154
+ * https://cryptojs.gitbook.io/docs/
1155
+ * @param message Message
1156
+ * @param passphrase Secret passphrase
1157
+ */
1158
+ hashHex(message: string, passphrase?: string) {
1159
+ if (passphrase == null)
1160
+ return SHA3(message, { outputLength: 512 }).toString(enc.Hex);
1161
+ else return HmacSHA512(message, passphrase).toString(enc.Hex);
1162
+ }
1163
+
900
1164
  /**
901
1165
  * Check use has the specific role permission or not
902
1166
  * @param roles Roles to check
@@ -1065,7 +1329,6 @@ export abstract class CoreApp<
1065
1329
  */
1066
1330
  userLogin(user: IUserData, refreshToken: string, keep: boolean = false) {
1067
1331
  this.userData = user;
1068
- this.passphrase = user.passphrase;
1069
1332
  this.authorize(user.token, refreshToken, keep);
1070
1333
  }
1071
1334
 
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Init call dto
3
+ */
4
+ export type InitCallDto = {
5
+ /**
6
+ * Device id
7
+ */
8
+ deviceId?: string;
9
+
10
+ /**
11
+ * Timestamp
12
+ */
13
+ timestamp: number;
14
+ };
@@ -59,6 +59,7 @@
59
59
  "status": "Status",
60
60
  "submit": "Submit",
61
61
  "success": "Success",
62
+ "timeDifferenceInvalid": "The time difference between the device and the server is {0}, which exceeds the limit of {1} seconds. Please adjust the device time. If it is abnormal, please inform the administrator",
62
63
  "tokenExpiry": "Your session is about to expire. Click the Cancel button to continue",
63
64
  "yes": "Yes",
64
65
  "unknownError": "Unknown Error",
@@ -59,6 +59,7 @@
59
59
  "status": "状态",
60
60
  "submit": "提交",
61
61
  "success": "成功",
62
+ "timeDifferenceInvalid": "设备时间和服务器时间差为{0},超过{1}秒的限制,请调整设备时间,如果异常请告知管理员",
62
63
  "tokenExpiry": "您的会话即将过期。点击 取消 按钮继续使用",
63
64
  "yes": "是",
64
65
  "unknownError": "未知错误",
@@ -59,6 +59,7 @@
59
59
  "status": "狀態",
60
60
  "submit": "提交",
61
61
  "success": "成功",
62
+ "timeDifferenceInvalid": "設備時間和服務器時間差為{0},超過{1}秒的限制,請調整設備時間,如果異常請告知管理員",
62
63
  "tokenExpiry": "您的會話即將過期。點擊 取消 按鈕繼續使用",
63
64
  "yes": "是",
64
65
  "unknownError": "未知錯誤",
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export * from './def/ListItem';
27
27
  export * from './dto/IdDto';
28
28
  export * from './dto/IdLabelDto';
29
29
  export * from './dto/IdLabelPrimaryDto';
30
+ export * from './dto/InitCallDto';
30
31
  export * from './dto/UpdateDto';
31
32
 
32
33
  // i18n
@@ -42,6 +43,7 @@ export type { IApi, IApiPayload } from '@etsoo/restclient';
42
43
  export * from './result/ActionResult';
43
44
  export * from './result/ActionResultError';
44
45
  export * from './result/IActionResult';
46
+ export * from './result/InitCallResult';
45
47
 
46
48
  // state
47
49
  export * from './state/Culture';
@@ -56,12 +56,12 @@ export interface IActionResult<D extends IResultData = IResultData> {
56
56
  /**
57
57
  * Trace id
58
58
  */
59
- readonly traceId?: string;
59
+ traceId?: string;
60
60
 
61
61
  /**
62
62
  * Type
63
63
  */
64
- readonly type: string;
64
+ type: string;
65
65
 
66
66
  /**
67
67
  * Success or not
@@ -0,0 +1,36 @@
1
+ import { IActionResult, IResultData } from './IActionResult';
2
+
3
+ /**
4
+ * Init call result data
5
+ */
6
+ export interface InitCallResultData extends IResultData {
7
+ /**
8
+ * Device id
9
+ */
10
+ deviceId?: string;
11
+
12
+ /**
13
+ * Secret passphrase
14
+ */
15
+ passphrase?: string;
16
+
17
+ /**
18
+ * Previous secret passphrase
19
+ */
20
+ previousPassphrase?: string;
21
+
22
+ /**
23
+ * Actual seconds gap
24
+ */
25
+ seconds?: number;
26
+
27
+ /**
28
+ * Valid seconds gap
29
+ */
30
+ validSeconds?: number;
31
+ }
32
+
33
+ /**
34
+ * Init call result
35
+ */
36
+ export type InitCallResult = IActionResult<InitCallResultData>;
package/src/state/User.ts CHANGED
@@ -33,11 +33,6 @@ export interface IUserData {
33
33
  * Access token
34
34
  */
35
35
  readonly token: string;
36
-
37
- /**
38
- * Secret passphrase
39
- */
40
- readonly passphrase: string;
41
36
  }
42
37
 
43
38
  /**