@nymphjs/tilmeld 1.0.0-beta.17 → 1.0.0-beta.18

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/src/User.ts CHANGED
@@ -14,6 +14,7 @@ import { difference } from 'lodash';
14
14
  import { humanSecret, nanoid } from '@nymphjs/guid';
15
15
 
16
16
  import type Tilmeld from './Tilmeld';
17
+ import { enforceTilmeld } from './enforceTilmeld';
17
18
  import AbleObject from './AbleObject';
18
19
  import Group, { GroupData } from './Group';
19
20
  import {
@@ -211,23 +212,31 @@ export default class User extends AbleObject<UserData> {
211
212
  '$register',
212
213
  ];
213
214
  private static DEFAULT_PRIVATE_DATA = [
215
+ // The things that are commented are there to show what is _not_ private.
216
+ 'abilities',
217
+ // 'username',
218
+ // 'nameFirst',
219
+ // 'nameMiddle',
220
+ // 'nameLast',
221
+ // 'name',
214
222
  'email',
215
- 'originalEmail',
223
+ // 'avatar',
216
224
  'phone',
217
225
  'group',
218
226
  'groups',
219
- 'abilities',
220
227
  'inheritAbilities',
221
- 'recoverSecret',
222
- 'recoverSecretDate',
223
- 'password',
224
- 'salt',
228
+ 'enabled',
225
229
  'secret',
226
- 'newEmailAddress',
230
+ 'emailChangeDate',
227
231
  'newEmailSecret',
228
- 'cancelEmailAddress',
232
+ 'newEmailAddress',
229
233
  'cancelEmailSecret',
230
- 'emailChangeDate',
234
+ 'cancelEmailAddress',
235
+ 'recoverSecret',
236
+ 'recoverSecretDate',
237
+ 'salt',
238
+ 'password',
239
+ 'passwordTemp',
231
240
  ];
232
241
  private static DEFAULT_ALLOWLIST_DATA: string[] = [];
233
242
 
@@ -512,7 +521,7 @@ export default class User extends AbleObject<UserData> {
512
521
  password: string;
513
522
  additionalData?: { [k: string]: any };
514
523
  }) {
515
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
524
+ const tilmeld = enforceTilmeld(this);
516
525
  if (this.guid == null) {
517
526
  return { result: false, message: 'Incorrect login/password.' };
518
527
  }
@@ -556,12 +565,84 @@ export default class User extends AbleObject<UserData> {
556
565
  return { result: true, message: 'You are logged in.' };
557
566
  }
558
567
 
568
+ public async $switchUser(data?: { additionalData?: { [k: string]: any } }) {
569
+ const tilmeld = enforceTilmeld(this);
570
+ if (this.guid == null) {
571
+ return { result: false, message: 'Incorrect login/password.' };
572
+ }
573
+
574
+ let user = tilmeld.User.current();
575
+
576
+ if (!user || user.guid == null) {
577
+ return { result: false, message: "You're not logged in." };
578
+ }
579
+
580
+ const isCurrentUser = user != null && user.$is(this);
581
+
582
+ if (isCurrentUser) {
583
+ return { result: false, message: "You can't switch to yourself." };
584
+ }
585
+
586
+ const toAbilities = this.$data.abilities || [];
587
+ const fromAbilities = user.abilities || [];
588
+
589
+ if (
590
+ fromAbilities.indexOf('tilmeld/switch') === -1 &&
591
+ fromAbilities.indexOf('system/admin') === -1
592
+ ) {
593
+ return {
594
+ result: false,
595
+ message: "You don't have permission to switch users.",
596
+ };
597
+ }
598
+
599
+ if (
600
+ toAbilities.indexOf('system/admin') !== -1 ||
601
+ toAbilities.indexOf('tilmeld/admin') !== -1
602
+ ) {
603
+ return { result: false, message: "You can't switch to an admin." };
604
+ }
605
+
606
+ try {
607
+ for (let callback of (this.constructor as typeof User)
608
+ .beforeLoginCallbacks) {
609
+ if (callback) {
610
+ await callback(this, {
611
+ username: this.$data.username || '',
612
+ password: '',
613
+ ...(data || {}),
614
+ });
615
+ }
616
+ }
617
+ } catch (e: any) {
618
+ return {
619
+ result: false,
620
+ message: e.message,
621
+ };
622
+ }
623
+
624
+ // Authentication was successful, attempt to login.
625
+ if (!tilmeld.loginSwitch(this, true)) {
626
+ return { result: false, message: 'Incorrect login/password.' };
627
+ }
628
+
629
+ for (let callback of (this.constructor as typeof User)
630
+ .afterLoginCallbacks) {
631
+ if (callback) {
632
+ await callback(this);
633
+ }
634
+ }
635
+
636
+ // Login was successful.
637
+ return { result: true, message: 'You are logged in.' };
638
+ }
639
+
559
640
  /**
560
641
  * Log a user out of the system.
561
642
  * @returns An object with a boolean 'result' entry and a 'message' entry.
562
643
  */
563
644
  public async $logout() {
564
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
645
+ const tilmeld = enforceTilmeld(this);
565
646
 
566
647
  try {
567
648
  for (let callback of (this.constructor as typeof User)
@@ -640,18 +721,39 @@ export default class User extends AbleObject<UserData> {
640
721
  }
641
722
 
642
723
  public $jsonAcceptData(input: EntityJson, allowConflict = false) {
643
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
724
+ const tilmeld = enforceTilmeld(this);
644
725
  this.$referenceWake();
645
726
 
646
727
  if (
647
728
  'abilities' in input.data &&
648
- input.data.abilities.indexOf('system/admin') !== -1 &&
649
- this.$data.abilities?.indexOf('system/admin') === -1 &&
650
- tilmeld.gatekeeper('tilmeld/admin') &&
729
+ input.data.abilities?.includes('system/admin') !==
730
+ this.$data.abilities?.includes('system/admin') &&
651
731
  !tilmeld.gatekeeper('system/admin')
652
732
  ) {
653
733
  throw new BadDataError(
654
- "You don't have the authority to make this user a system admin."
734
+ "You don't have the authority to grant or revoke system/admin."
735
+ );
736
+ }
737
+
738
+ if (
739
+ 'abilities' in input.data &&
740
+ input.data.abilities?.includes('tilmeld/admin') !==
741
+ this.$data.abilities?.includes('tilmeld/admin') &&
742
+ !tilmeld.gatekeeper('system/admin')
743
+ ) {
744
+ throw new BadDataError(
745
+ "You don't have the authority to grant or revoke tilmeld/admin."
746
+ );
747
+ }
748
+
749
+ if (
750
+ 'abilities' in input.data &&
751
+ input.data.abilities?.includes('tilmeld/switch') !==
752
+ this.$data.abilities?.includes('tilmeld/switch') &&
753
+ !tilmeld.gatekeeper('system/admin')
754
+ ) {
755
+ throw new BadDataError(
756
+ "You don't have the authority to grant or revoke tilmeld/switch."
655
757
  );
656
758
  }
657
759
 
@@ -660,18 +762,39 @@ export default class User extends AbleObject<UserData> {
660
762
  }
661
763
 
662
764
  public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
663
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
765
+ const tilmeld = enforceTilmeld(this);
664
766
  this.$referenceWake();
665
767
 
666
768
  if (
667
769
  'abilities' in patch.set &&
668
- patch.set.abilities.indexOf('system/admin') !== -1 &&
669
- this.$data.abilities?.indexOf('system/admin') === -1 &&
670
- tilmeld.gatekeeper('tilmeld/admin') &&
770
+ patch.set.abilities?.includes('system/admin') !==
771
+ this.$data.abilities?.includes('system/admin') &&
671
772
  !tilmeld.gatekeeper('system/admin')
672
773
  ) {
673
774
  throw new BadDataError(
674
- "You don't have the authority to make this user a system admin."
775
+ "You don't have the authority to grant or revoke system/admin."
776
+ );
777
+ }
778
+
779
+ if (
780
+ 'abilities' in patch.set &&
781
+ patch.set.abilities?.includes('tilmeld/admin') !==
782
+ this.$data.abilities?.includes('tilmeld/admin') &&
783
+ !tilmeld.gatekeeper('system/admin')
784
+ ) {
785
+ throw new BadDataError(
786
+ "You don't have the authority to grant or revoke tilmeld/admin."
787
+ );
788
+ }
789
+
790
+ if (
791
+ 'abilities' in patch.set &&
792
+ patch.set.abilities?.includes('tilmeld/switch') !==
793
+ this.$data.abilities?.includes('tilmeld/switch') &&
794
+ !tilmeld.gatekeeper('system/admin')
795
+ ) {
796
+ throw new BadDataError(
797
+ "You don't have the authority to grant or revoke tilmeld/switch."
675
798
  );
676
799
  }
677
800
 
@@ -690,7 +813,7 @@ export default class User extends AbleObject<UserData> {
690
813
  * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
691
814
  */
692
815
  public $updateDataProtection(givenUser?: User & UserData) {
693
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
816
+ const tilmeld = enforceTilmeld(this);
694
817
  let user = givenUser ?? tilmeld.User.current();
695
818
 
696
819
  this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
@@ -745,17 +868,43 @@ export default class User extends AbleObject<UserData> {
745
868
  this.$allowlistData.push('phone');
746
869
  }
747
870
  this.$privateData = [
871
+ // The things that are commented are there to show what is _not_ private.
872
+ // 'abilities',
873
+ // 'username',
874
+ // 'nameFirst',
875
+ // 'nameMiddle',
876
+ // 'nameLast',
877
+ // 'name',
878
+ // 'email',
879
+ // 'avatar',
880
+ // 'phone',
881
+ // 'group',
882
+ // 'groups',
883
+ // 'inheritAbilities',
884
+ 'enabled',
748
885
  'secret',
886
+ 'emailChangeDate',
749
887
  'newEmailSecret',
750
- 'cancelEmailAddress',
888
+ // 'newEmailAddress',
751
889
  'cancelEmailSecret',
752
- 'emailChangeDate',
890
+ 'cancelEmailAddress',
753
891
  'recoverSecret',
754
892
  'recoverSecretDate',
755
- 'password',
756
893
  'salt',
894
+ 'password',
895
+ // 'passwordTemp',
757
896
  ];
758
897
  }
898
+
899
+ if (
900
+ user != null &&
901
+ !isCurrentUser &&
902
+ (abilities.indexOf('tilmeld/switch') !== -1 ||
903
+ abilities.indexOf('system/admin') !== -1)
904
+ ) {
905
+ // Users with this ability can switch to other users.
906
+ this.$clientEnabledMethods.push('$switchUser');
907
+ }
759
908
  }
760
909
 
761
910
  /**
@@ -773,7 +922,7 @@ export default class User extends AbleObject<UserData> {
773
922
  * @returns True or false.
774
923
  */
775
924
  public $gatekeeper(ability?: string): boolean {
776
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
925
+ const tilmeld = enforceTilmeld(this);
777
926
  if (ability == null) {
778
927
  return tilmeld.User.current()?.$is(this) ?? false;
779
928
  }
@@ -800,7 +949,7 @@ export default class User extends AbleObject<UserData> {
800
949
  }
801
950
  return (
802
951
  (ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
803
- !!this.$gatekeeperCache['system/admin']
952
+ !!this.$data.abilities?.includes('system/admin')
804
953
  );
805
954
  }
806
955
 
@@ -817,7 +966,7 @@ export default class User extends AbleObject<UserData> {
817
966
  * @returns True on success, false on failure.
818
967
  */
819
968
  public async $sendEmailVerification() {
820
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
969
+ const tilmeld = enforceTilmeld(this);
821
970
  if (this.guid == null) {
822
971
  return false;
823
972
  }
@@ -930,7 +1079,7 @@ export default class User extends AbleObject<UserData> {
930
1079
  * @returns True if the passwords match, otherwise false.
931
1080
  */
932
1081
  public $checkPassword(password: string) {
933
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1082
+ const tilmeld = enforceTilmeld(this);
934
1083
  switch (tilmeld.config.pwMethod) {
935
1084
  case 'plain':
936
1085
  return this.$data.password === password;
@@ -1041,7 +1190,7 @@ export default class User extends AbleObject<UserData> {
1041
1190
  * @returns The resulting password or hash which is stored in the entity.
1042
1191
  */
1043
1192
  public $password(password: string) {
1044
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1193
+ const tilmeld = enforceTilmeld(this);
1045
1194
  switch (tilmeld.config.pwMethod) {
1046
1195
  case 'plain':
1047
1196
  delete this.$data.salt;
@@ -1068,7 +1217,7 @@ export default class User extends AbleObject<UserData> {
1068
1217
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1069
1218
  */
1070
1219
  public async $checkUsername(): Promise<{ result: boolean; message: string }> {
1071
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1220
+ const tilmeld = enforceTilmeld(this);
1072
1221
  if (!tilmeld.config.emailUsernames) {
1073
1222
  if (this.$data.username == null || !this.$data.username.length) {
1074
1223
  return { result: false, message: 'Please specify a username.' };
@@ -1167,7 +1316,7 @@ export default class User extends AbleObject<UserData> {
1167
1316
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1168
1317
  */
1169
1318
  public async $checkEmail(): Promise<{ result: boolean; message: string }> {
1170
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1319
+ const tilmeld = enforceTilmeld(this);
1171
1320
  if (this.$data.email == null || !this.$data.email.length) {
1172
1321
  if (tilmeld.config.verifyEmail) {
1173
1322
  return { result: false, message: 'Please specify an email.' };
@@ -1212,7 +1361,7 @@ export default class User extends AbleObject<UserData> {
1212
1361
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1213
1362
  */
1214
1363
  public async $checkPhone(): Promise<{ result: boolean; message: string }> {
1215
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1364
+ const tilmeld = enforceTilmeld(this);
1216
1365
  if (this.$data.phone == null || !this.$data.phone.length) {
1217
1366
  return { result: false, message: 'Please specify a phone number.' };
1218
1367
  }
@@ -1251,7 +1400,7 @@ export default class User extends AbleObject<UserData> {
1251
1400
  password: string;
1252
1401
  additionalData?: { [k: string]: any };
1253
1402
  }): Promise<{ result: boolean; loggedin: boolean; message: string }> {
1254
- let tilmeld = this.$nymph.tilmeld as Tilmeld;
1403
+ let tilmeld = enforceTilmeld(this);
1255
1404
  if (!tilmeld.config.allowRegistration) {
1256
1405
  return {
1257
1406
  result: false,
@@ -1313,7 +1462,7 @@ export default class User extends AbleObject<UserData> {
1313
1462
  const nymph = this.$nymph;
1314
1463
  const tnymph = await this.$nymph.startTransaction(transaction);
1315
1464
  this.$nymph = tnymph;
1316
- tilmeld = this.$nymph.tilmeld as Tilmeld;
1465
+ tilmeld = enforceTilmeld(this);
1317
1466
 
1318
1467
  try {
1319
1468
  // Add primary group.
@@ -1518,7 +1667,7 @@ export default class User extends AbleObject<UserData> {
1518
1667
  }
1519
1668
 
1520
1669
  public async $save() {
1521
- let tilmeld = this.$nymph.tilmeld as Tilmeld;
1670
+ let tilmeld = enforceTilmeld(this);
1522
1671
  if (this.$data.username == null || !this.$data.username.trim().length) {
1523
1672
  return false;
1524
1673
  }
@@ -1526,7 +1675,7 @@ export default class User extends AbleObject<UserData> {
1526
1675
  if (
1527
1676
  tilmeld.gatekeeper('tilmeld/admin') &&
1528
1677
  !tilmeld.gatekeeper('system/admin') &&
1529
- this.$gatekeeper('system/admin')
1678
+ this.$data.abilities?.includes('system/admin')
1530
1679
  ) {
1531
1680
  throw new BadDataError(
1532
1681
  "You don't have the authority to modify system admins."
@@ -1716,7 +1865,7 @@ export default class User extends AbleObject<UserData> {
1716
1865
  const nymph = this.$nymph;
1717
1866
  const tnymph = await this.$nymph.startTransaction(transaction);
1718
1867
  this.$nymph = tnymph;
1719
- tilmeld = this.$nymph.tilmeld as Tilmeld;
1868
+ tilmeld = enforceTilmeld(this);
1720
1869
 
1721
1870
  if (
1722
1871
  this.$data.group != null &&
@@ -1767,9 +1916,9 @@ export default class User extends AbleObject<UserData> {
1767
1916
  this.$gatekeeperCache = undefined;
1768
1917
  await tnymph.commit(transaction);
1769
1918
 
1770
- if ((nymph.tilmeld as Tilmeld).User.current(true).$is(this)) {
1919
+ if (enforceTilmeld(nymph).User.current(true).$is(this)) {
1771
1920
  // Update the user in the session cache.
1772
- (nymph.tilmeld as Tilmeld).fillSession(this);
1921
+ enforceTilmeld(nymph).fillSession(this);
1773
1922
  }
1774
1923
  } else {
1775
1924
  await tnymph.rollback(transaction);
@@ -1795,13 +1944,13 @@ export default class User extends AbleObject<UserData> {
1795
1944
  }
1796
1945
 
1797
1946
  public async $delete() {
1798
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1947
+ const tilmeld = enforceTilmeld(this);
1799
1948
  if (!tilmeld.gatekeeper('tilmeld/admin')) {
1800
1949
  throw new BadDataError("You don't have the authority to delete users.");
1801
1950
  }
1802
1951
  if (
1803
1952
  !tilmeld.gatekeeper('system/admin') &&
1804
- this.$gatekeeper('system/admin')
1953
+ this.$data.abilities?.includes('system/admin')
1805
1954
  ) {
1806
1955
  throw new BadDataError(
1807
1956
  "You don't have the authority to delete system admins."
package/src/conf/d.ts CHANGED
@@ -217,6 +217,11 @@ export interface Config {
217
217
  * How long from current time, in seconds, the JWT token expires.
218
218
  */
219
219
  jwtExpire: number;
220
+ /**
221
+ * How long from current time, in seconds, the JWT token for a user switch
222
+ * expires.
223
+ */
224
+ jwtSwitchExpire: number;
220
225
  /**
221
226
  * How long, in seconds, before the JWT token expires to give the user a new
222
227
  * one.
@@ -225,7 +230,11 @@ export interface Config {
225
230
  /**
226
231
  * Function to build the JWT for user sessions.
227
232
  */
228
- jwtBuilder: (config: Config, user: User) => string;
233
+ jwtBuilder: (
234
+ config: Config,
235
+ user: User & UserData,
236
+ switchToken?: boolean
237
+ ) => string;
229
238
  /**
230
239
  * Function to verify that a JWT was signed with the secret key, vaildate its
231
240
  * data, validate the XSRF token, and extract the GUID.
@@ -52,8 +52,9 @@ export default {
52
52
  maxUsernameLength: 128,
53
53
  jwtSecret: '',
54
54
  jwtExpire: 60 * 60 * 24 * 7 * 8, // 8 weeks(ish)
55
+ jwtSwitchExpire: 60 * 60 * 6, // 6 hours
55
56
  jwtRenew: 60 * 60 * 24 * 7 * 2, // 2 weeks(ish)
56
- jwtBuilder: (config, user) => {
57
+ jwtBuilder: (config, user, switchToken) => {
57
58
  const secret = config.jwtSecret;
58
59
  if (secret === '') {
59
60
  throw new Error('JWT secret is not configured.');
@@ -67,7 +68,7 @@ export default {
67
68
  xsrfToken: 'TILMELDXSRF-' + nanoid(),
68
69
  },
69
70
  secret,
70
- { expiresIn: config.jwtExpire }
71
+ { expiresIn: switchToken ? config.jwtSwitchExpire : config.jwtExpire }
71
72
  );
72
73
  },
73
74
  jwtExtract: (config, token, xsrfToken?) => {
@@ -0,0 +1,20 @@
1
+ import { Entity, EntityConstructor, Nymph } from '@nymphjs/nymph';
2
+
3
+ import Tilmeld from './Tilmeld';
4
+
5
+ export function enforceTilmeld(object: Nymph | Entity | EntityConstructor) {
6
+ let tilmeld: Tilmeld | undefined;
7
+ if (object instanceof Entity) {
8
+ tilmeld = object.$nymph.tilmeld as Tilmeld | undefined;
9
+ } else if (object instanceof Nymph) {
10
+ tilmeld = object.tilmeld as Tilmeld | undefined;
11
+ } else {
12
+ tilmeld = object.nymph.tilmeld as Tilmeld | undefined;
13
+ }
14
+
15
+ if (tilmeld == null) {
16
+ throw new Error('Tilmeld was not configured on this Nymph instance!');
17
+ }
18
+
19
+ return tilmeld;
20
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './conf';
2
2
 
3
+ export * from './enforceTilmeld';
4
+
3
5
  export * from './errors';
4
6
 
5
7
  import AbleObject from './AbleObject';