@etsoo/appscript 1.1.67 → 1.1.71

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.
@@ -28,8 +28,10 @@ import {
28
28
  } from 'crypto-js';
29
29
  import { AddressRegion } from '../address/AddressRegion';
30
30
  import { AddressUtils } from '../address/AddressUtils';
31
+ import { InitCallDto } from '../dto/InitCallDto';
31
32
  import { ActionResultError } from '../result/ActionResultError';
32
33
  import { IActionResult } from '../result/IActionResult';
34
+ import { InitCallResult, InitCallResultData } from '../result/InitCallResult';
33
35
  import { IUserData } from '../state/User';
34
36
  import { IAppSettings } from './AppSettings';
35
37
  import { UserRole } from './UserRole';
@@ -140,11 +142,6 @@ export interface ICoreApp<
140
142
  */
141
143
  userData?: IUserData;
142
144
 
143
- /**
144
- * Passphrase for encryption
145
- */
146
- passphrase?: string;
147
-
148
145
  /**
149
146
  * Search input element
150
147
  */
@@ -182,7 +179,20 @@ export interface ICoreApp<
182
179
  * @param passphrase Secret passphrase
183
180
  * @returns Pure text
184
181
  */
185
- decrypt(messageEncrypted: string, passphrase: string): string;
182
+ decrypt(messageEncrypted: string, passphrase?: string): string | undefined;
183
+
184
+ /**
185
+ * Enhanced decrypt message
186
+ * @param messageEncrypted Encrypted message
187
+ * @param passphrase Secret passphrase
188
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
189
+ * @returns Pure text
190
+ */
191
+ decryptEnhanced(
192
+ messageEncrypted: string,
193
+ passphrase?: string,
194
+ durationSeconds?: number
195
+ ): string | undefined;
186
196
 
187
197
  /**
188
198
  * Detect IP data, call only one time
@@ -197,7 +207,20 @@ export interface ICoreApp<
197
207
  * @param iterations Iterations, 1000 times, 1 - 99
198
208
  * @returns Result
199
209
  */
200
- encrypt(message: string, passphrase: string, iterations?: number): string;
210
+ encrypt(message: string, passphrase?: string, iterations?: number): string;
211
+
212
+ /**
213
+ * Enhanced encrypt message
214
+ * @param message Message
215
+ * @param passphrase Secret passphrase
216
+ * @param iterations Iterations, 1000 times, 1 - 99
217
+ * @returns Result
218
+ */
219
+ encryptEnhanced(
220
+ message: string,
221
+ passphrase?: string,
222
+ iterations?: number
223
+ ): string;
201
224
 
202
225
  /**
203
226
  * Format date to string
@@ -309,6 +332,14 @@ export interface ICoreApp<
309
332
  */
310
333
  hash(message: string, passphrase?: string): string;
311
334
 
335
+ /**
336
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
337
+ * https://cryptojs.gitbook.io/docs/
338
+ * @param message Message
339
+ * @param passphrase Secret passphrase
340
+ */
341
+ hashHex(message: string, passphrase?: string): string;
342
+
312
343
  /**
313
344
  * Check use has the specific role permission or not
314
345
  * @param roles Roles to check
@@ -316,6 +347,13 @@ export interface ICoreApp<
316
347
  */
317
348
  hasPermission(roles: number | UserRole | number[] | UserRole[]): boolean;
318
349
 
350
+ /**
351
+ * Init call
352
+ * @param callback Callback
353
+ * @returns Result
354
+ */
355
+ initCall(callback?: (result: boolean) => void): Promise<void>;
356
+
319
357
  /**
320
358
  * Callback where exit a page
321
359
  */
@@ -457,11 +495,6 @@ export abstract class CoreApp<
457
495
  */
458
496
  userData?: IUserData;
459
497
 
460
- /**
461
- * Passphrase for encryption
462
- */
463
- passphrase?: string;
464
-
465
498
  /**
466
499
  * Response token header field name
467
500
  */
@@ -499,6 +532,21 @@ export abstract class CoreApp<
499
532
  */
500
533
  protected refreshCountdownSeed = 0;
501
534
 
535
+ /**
536
+ * Device id field name
537
+ */
538
+ protected deviceIdField: string = 'SmartERPDeviceId';
539
+
540
+ /**
541
+ * Device id
542
+ */
543
+ protected deviceId: string;
544
+
545
+ /**
546
+ * Passphrase for encryption
547
+ */
548
+ protected passphrase: string = '***';
549
+
502
550
  /**
503
551
  * Protected constructor
504
552
  * @param settings Settings
@@ -517,6 +565,11 @@ export abstract class CoreApp<
517
565
  this.notifier = notifier;
518
566
  this.name = name;
519
567
 
568
+ this.deviceId = StorageUtils.getLocalData<string>(
569
+ this.deviceIdField,
570
+ ''
571
+ );
572
+
520
573
  this.setApi(api);
521
574
 
522
575
  const { currentCulture, currentRegion } = settings;
@@ -561,6 +614,116 @@ export abstract class CoreApp<
561
614
  };
562
615
  }
563
616
 
617
+ /**
618
+ * Init call
619
+ * @param callback Callback
620
+ * @returns Result
621
+ */
622
+ async initCall(callback?: (result: boolean) => void) {
623
+ const data: InitCallDto = {
624
+ timestamp: new Date().getTime(),
625
+ deviceId: this.deviceId === '' ? undefined : this.deviceId
626
+ };
627
+ const result = await this.api.put<InitCallResult>(
628
+ 'Auth/WebInitCall',
629
+ data
630
+ );
631
+ if (result == null) {
632
+ if (callback) callback(false);
633
+ return;
634
+ }
635
+
636
+ if (result.data == null) {
637
+ this.notifier.alert(this.get<string>('noData')!);
638
+ if (callback) callback(false);
639
+ return;
640
+ }
641
+
642
+ if (!result.ok) {
643
+ const seconds = result.data.seconds;
644
+ const validSeconds = result.data.validSeconds;
645
+ if (
646
+ result.title === 'timeDifferenceInvalid' &&
647
+ seconds != null &&
648
+ validSeconds != null
649
+ ) {
650
+ const title = this.get('timeDifferenceInvalid')?.format(
651
+ seconds.toString(),
652
+ validSeconds.toString()
653
+ );
654
+ this.notifier.alert(title!);
655
+ } else {
656
+ this.alertResult(result);
657
+ }
658
+
659
+ if (callback) callback(false);
660
+
661
+ return;
662
+ }
663
+
664
+ this.initCallUpdate(result.data, data.timestamp);
665
+
666
+ if (callback) callback(true);
667
+ }
668
+
669
+ /**
670
+ * Init call update
671
+ * @param data Result data
672
+ * @param timestamp Timestamp
673
+ */
674
+ protected initCallUpdate(data: InitCallResultData, timestamp: number) {
675
+ if (data.deviceId == null || data.passphrase == null) return;
676
+
677
+ // Decrypt
678
+ // Should be done within 120 seconds after returning from the backend
679
+ const passphrase = this.decrypt(data.passphrase, timestamp.toString());
680
+ if (passphrase == null) return;
681
+
682
+ // Update device id and cache it
683
+ this.deviceId = data.deviceId;
684
+ StorageUtils.setLocalData(this.deviceIdField, this.deviceId);
685
+
686
+ // Current passphrase
687
+ this.passphrase = passphrase;
688
+
689
+ // Previous passphrase
690
+ if (data.previousPassphrase) {
691
+ const prev = this.decrypt(
692
+ data.previousPassphrase,
693
+ timestamp.toString()
694
+ );
695
+
696
+ // Update
697
+ const fields = this.initCallUpdateFields();
698
+ for (const field of fields) {
699
+ const currentValue = StorageUtils.getLocalData<string>(
700
+ field,
701
+ ''
702
+ );
703
+ if (currentValue === '' || currentValue.indexOf('+') === -1)
704
+ continue;
705
+
706
+ const newValueSource = this.decryptEnhanced(
707
+ currentValue,
708
+ prev,
709
+ 12
710
+ );
711
+ if (newValueSource == null) continue;
712
+
713
+ const newValue = this.encryptEnhanced(newValueSource);
714
+ StorageUtils.setLocalData(field, newValue);
715
+ }
716
+ }
717
+ }
718
+
719
+ /**
720
+ * Init call update fields in local storage
721
+ * @returns Fields
722
+ */
723
+ protected initCallUpdateFields(): string[] {
724
+ return [];
725
+ }
726
+
564
727
  /**
565
728
  * Alert action result
566
729
  * @param result Action result
@@ -678,28 +841,20 @@ export abstract class CoreApp<
678
841
  * @param passphrase Secret passphrase
679
842
  * @returns Pure text
680
843
  */
681
- decrypt(messageEncrypted: string, passphrase: string) {
682
- // Timestamp splitter
683
- const pos = messageEncrypted.indexOf('+');
684
- const timestamp = messageEncrypted.substring(0, pos);
685
- const message = messageEncrypted.substring(pos + 1);
686
-
844
+ decrypt(messageEncrypted: string, passphrase?: string) {
687
845
  // Iterations
688
- const iterations = parseInt(message.substring(0, 2), 10);
846
+ const iterations = parseInt(messageEncrypted.substring(0, 2), 10);
847
+ if (isNaN(iterations)) return undefined;
689
848
 
690
- const salt = enc.Hex.parse(message.substring(2, 34));
691
- const iv = enc.Hex.parse(message.substring(34, 66));
692
- const encrypted = message.substring(66);
849
+ const salt = enc.Hex.parse(messageEncrypted.substring(2, 34));
850
+ const iv = enc.Hex.parse(messageEncrypted.substring(34, 66));
851
+ const encrypted = messageEncrypted.substring(66);
693
852
 
694
- const key = PBKDF2(
695
- this.encryptionEnhance(passphrase, timestamp),
696
- salt,
697
- {
698
- keySize: 8, // 256 / 32
699
- hasher: algo.SHA256,
700
- iterations: 1000 * iterations
701
- }
702
- );
853
+ const key = PBKDF2(passphrase ?? this.passphrase, salt, {
854
+ keySize: 8, // 256 / 32
855
+ hasher: algo.SHA256,
856
+ iterations: 1000 * iterations
857
+ });
703
858
 
704
859
  return AES.decrypt(encrypted, key, {
705
860
  iv,
@@ -708,6 +863,46 @@ export abstract class CoreApp<
708
863
  }).toString(enc.Utf8);
709
864
  }
710
865
 
866
+ /**
867
+ * Enhanced decrypt message
868
+ * @param messageEncrypted Encrypted message
869
+ * @param passphrase Secret passphrase
870
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
871
+ * @returns Pure text
872
+ */
873
+ decryptEnhanced(
874
+ messageEncrypted: string,
875
+ passphrase?: string,
876
+ durationSeconds?: number
877
+ ) {
878
+ // Timestamp splitter
879
+ const pos = messageEncrypted.indexOf('+');
880
+ if (pos === -1 || messageEncrypted.length <= 66) return undefined;
881
+
882
+ const timestamp = messageEncrypted.substring(0, pos);
883
+
884
+ if (durationSeconds != null && durationSeconds > 0) {
885
+ const milseconds = Utils.charsToNumber(timestamp);
886
+ if (isNaN(milseconds) || milseconds < 1) return undefined;
887
+ const timespan = new Date().substract(new Date(milseconds));
888
+ if (
889
+ (durationSeconds <= 12 &&
890
+ timespan.totalMonths > durationSeconds) ||
891
+ (durationSeconds > 12 &&
892
+ timespan.totalSeconds > durationSeconds)
893
+ )
894
+ return undefined;
895
+ }
896
+
897
+ const message = messageEncrypted.substring(pos + 1);
898
+ passphrase = this.encryptionEnhance(
899
+ passphrase ?? this.passphrase,
900
+ timestamp
901
+ );
902
+
903
+ return this.decrypt(message, passphrase);
904
+ }
905
+
711
906
  /**
712
907
  * Detect IP data, call only one time
713
908
  * @param callback Callback will be called when the IP is ready
@@ -755,29 +950,20 @@ export abstract class CoreApp<
755
950
  * @param iterations Iterations, 1000 times, 1 - 99
756
951
  * @returns Result
757
952
  */
758
- encrypt(message: string, passphrase: string, iterations?: number) {
953
+ encrypt(message: string, passphrase?: string, iterations?: number) {
759
954
  // Default 1 * 1000
760
955
  iterations ??= 1;
761
956
 
762
- // Timestamp
763
- const timestamp = Utils.numberToChars(new Date().getTime());
764
-
765
957
  const bits = 16; // 128 / 8
766
958
  const salt = lib.WordArray.random(bits);
767
- const key = PBKDF2(
768
- this.encryptionEnhance(passphrase, timestamp),
769
- salt,
770
- {
771
- keySize: 8, // 256 / 32
772
- hasher: algo.SHA256,
773
- iterations: 1000 * iterations
774
- }
775
- );
959
+ const key = PBKDF2(passphrase ?? this.passphrase, salt, {
960
+ keySize: 8, // 256 / 32
961
+ hasher: algo.SHA256,
962
+ iterations: 1000 * iterations
963
+ });
776
964
  const iv = lib.WordArray.random(bits);
777
965
 
778
966
  return (
779
- timestamp +
780
- '+' +
781
967
  iterations.toString().padStart(2, '0') +
782
968
  salt.toString(enc.Hex) +
783
969
  iv.toString(enc.Hex) +
@@ -789,6 +975,25 @@ export abstract class CoreApp<
789
975
  );
790
976
  }
791
977
 
978
+ /**
979
+ * Enhanced encrypt message
980
+ * @param message Message
981
+ * @param passphrase Secret passphrase
982
+ * @param iterations Iterations, 1000 times, 1 - 99
983
+ * @returns Result
984
+ */
985
+ encryptEnhanced(message: string, passphrase?: string, iterations?: number) {
986
+ // Timestamp
987
+ const timestamp = Utils.numberToChars(new Date().getTime());
988
+
989
+ passphrase = this.encryptionEnhance(
990
+ passphrase ?? this.passphrase,
991
+ timestamp
992
+ );
993
+
994
+ return timestamp + '+' + this.encrypt(message, passphrase, iterations);
995
+ }
996
+
792
997
  /**
793
998
  * Enchance secret passphrase
794
999
  * @param passphrase Secret passphrase
@@ -798,7 +1003,7 @@ export abstract class CoreApp<
798
1003
  protected encryptionEnhance(passphrase: string, timestamp: string) {
799
1004
  passphrase += timestamp;
800
1005
  passphrase += passphrase.length.toString();
801
- return passphrase + (this.passphrase ?? '');
1006
+ return passphrase;
802
1007
  }
803
1008
 
804
1009
  /**
@@ -880,7 +1085,18 @@ export abstract class CoreApp<
880
1085
  * @param forceToLocal Force to local labels
881
1086
  */
882
1087
  formatResult(result: IActionResult, forceToLocal?: boolean) {
883
- if ((result.title == null || forceToLocal) && result.type != null) {
1088
+ const title = result.title;
1089
+ if (title && /^\w+$/.test(title)) {
1090
+ const key = title.formatInitial(false);
1091
+ const localTitle = this.get(key);
1092
+ if (localTitle) {
1093
+ result.title = localTitle;
1094
+
1095
+ // Hold the original title in type when type is null
1096
+ if (result.type == null) result.type = title;
1097
+ }
1098
+ } else if ((title == null || forceToLocal) && result.type != null) {
1099
+ // Get label from type
884
1100
  const key = result.type.formatInitial(false);
885
1101
  result.title = this.get(key);
886
1102
  }
@@ -918,7 +1134,10 @@ export abstract class CoreApp<
918
1134
  * @returns Cached token
919
1135
  */
920
1136
  getCacheToken(): string | null {
921
- let refreshToken = StorageUtils.getLocalData(this.headerTokenField, '');
1137
+ let refreshToken = StorageUtils.getLocalData<string>(
1138
+ this.headerTokenField,
1139
+ ''
1140
+ );
922
1141
  if (refreshToken === '')
923
1142
  refreshToken = StorageUtils.getSessionData(
924
1143
  this.headerTokenField,
@@ -971,6 +1190,18 @@ export abstract class CoreApp<
971
1190
  else return HmacSHA512(message, passphrase).toString(enc.Base64);
972
1191
  }
973
1192
 
1193
+ /**
1194
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
1195
+ * https://cryptojs.gitbook.io/docs/
1196
+ * @param message Message
1197
+ * @param passphrase Secret passphrase
1198
+ */
1199
+ hashHex(message: string, passphrase?: string) {
1200
+ if (passphrase == null)
1201
+ return SHA3(message, { outputLength: 512 }).toString(enc.Hex);
1202
+ else return HmacSHA512(message, passphrase).toString(enc.Hex);
1203
+ }
1204
+
974
1205
  /**
975
1206
  * Check use has the specific role permission or not
976
1207
  * @param roles Roles to check
@@ -1139,7 +1370,6 @@ export abstract class CoreApp<
1139
1370
  */
1140
1371
  userLogin(user: IUserData, refreshToken: string, keep: boolean = false) {
1141
1372
  this.userData = user;
1142
- this.passphrase = user.passphrase;
1143
1373
  this.authorize(user.token, refreshToken, keep);
1144
1374
  }
1145
1375
 
@@ -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
+ };
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
@@ -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
@@ -1,23 +1,33 @@
1
1
  import { IActionResult, IResultData } from './IActionResult';
2
2
 
3
3
  /**
4
- * Result data with id, follow this style to extend for specific model
4
+ * Init call result data
5
5
  */
6
6
  export interface InitCallResultData extends IResultData {
7
+ /**
8
+ * Device id
9
+ */
10
+ deviceId?: string;
11
+
7
12
  /**
8
13
  * Secret passphrase
9
14
  */
10
- passphrase: string;
15
+ passphrase?: string;
16
+
17
+ /**
18
+ * Previous secret passphrase
19
+ */
20
+ previousPassphrase?: string;
11
21
 
12
22
  /**
13
23
  * Actual seconds gap
14
24
  */
15
- seconds: number;
25
+ seconds?: number;
16
26
 
17
27
  /**
18
28
  * Valid seconds gap
19
29
  */
20
- validSeconds: number;
30
+ validSeconds?: number;
21
31
  }
22
32
 
23
33
  /**
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
  /**