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

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