@nymphjs/tilmeld 1.0.0-beta.55 → 1.0.0-beta.57

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/AbleObject.d.ts +15 -0
  3. package/dist/AbleObject.js +15 -0
  4. package/dist/AbleObject.js.map +1 -1
  5. package/dist/Group.d.ts +116 -0
  6. package/dist/Group.js +98 -0
  7. package/dist/Group.js.map +1 -1
  8. package/dist/Group.test.js +1 -0
  9. package/dist/Group.test.js.map +1 -1
  10. package/dist/Tilmeld.d.ts +226 -1
  11. package/dist/Tilmeld.js +306 -13
  12. package/dist/Tilmeld.js.map +1 -1
  13. package/dist/User.d.ts +265 -0
  14. package/dist/User.js +246 -0
  15. package/dist/User.js.map +1 -1
  16. package/dist/User.test.js +1 -0
  17. package/dist/User.test.js.map +1 -1
  18. package/dist/conf/d.d.ts +244 -0
  19. package/dist/conf/defaults.js +7 -3
  20. package/dist/conf/defaults.js.map +1 -1
  21. package/dist/errors/AccessControlError.d.ts +2 -1
  22. package/dist/errors/AccessControlError.js +3 -2
  23. package/dist/errors/AccessControlError.js.map +1 -1
  24. package/dist/errors/BadDataError.d.ts +2 -1
  25. package/dist/errors/BadDataError.js +3 -2
  26. package/dist/errors/BadDataError.js.map +1 -1
  27. package/dist/errors/BadEmailError.d.ts +2 -1
  28. package/dist/errors/BadEmailError.js +3 -2
  29. package/dist/errors/BadEmailError.js.map +1 -1
  30. package/dist/errors/BadUsernameError.d.ts +2 -1
  31. package/dist/errors/BadUsernameError.js +3 -2
  32. package/dist/errors/BadUsernameError.js.map +1 -1
  33. package/package.json +8 -7
  34. package/src/Tilmeld.ts +9 -3
  35. package/src/User.ts +67 -0
  36. package/src/conf/defaults.ts +1 -0
  37. package/src/errors/AccessControlError.ts +4 -2
  38. package/src/errors/BadDataError.ts +4 -2
  39. package/src/errors/BadEmailError.ts +4 -2
  40. package/src/errors/BadUsernameError.ts +4 -2
  41. package/tsconfig.json +1 -1
package/dist/User.js CHANGED
@@ -14,6 +14,15 @@ const qrcode_1 = require("qrcode");
14
14
  const enforceTilmeld_1 = require("./enforceTilmeld");
15
15
  const AbleObject_1 = __importDefault(require("./AbleObject"));
16
16
  const errors_1 = require("./errors");
17
+ /**
18
+ * A user data model.
19
+ *
20
+ * Written by Hunter Perrin for SciActive.
21
+ *
22
+ * @author Hunter Perrin <hperrin@gmail.com>
23
+ * @copyright SciActive Inc
24
+ * @see http://nymph.io/
25
+ */
17
26
  class User extends AbleObject_1.default {
18
27
  static async factory(guid) {
19
28
  return (await super.factory(guid));
@@ -42,7 +51,11 @@ class User extends AbleObject_1.default {
42
51
  this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
43
52
  this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
44
53
  this.$allowlistTags = [];
54
+ /**
55
+ * This is explicitly used only during the registration proccess.
56
+ */
45
57
  this.$skipAcWhenSaving = false;
58
+ // Defaults.
46
59
  this.$data.enabled = true;
47
60
  this.$data.abilities = [];
48
61
  this.$data.groups = [];
@@ -56,6 +69,12 @@ class User extends AbleObject_1.default {
56
69
  }
57
70
  return tilmeld.currentUser;
58
71
  }
72
+ /**
73
+ * Send an account recovery link.
74
+ *
75
+ * @param data The input data from the client.
76
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
77
+ */
59
78
  static async sendRecovery(data) {
60
79
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
61
80
  if (!tilmeld.config.pwRecovery) {
@@ -67,6 +86,7 @@ class User extends AbleObject_1.default {
67
86
  let user;
68
87
  const options = {};
69
88
  if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
89
+ // Create a username recovery email.
70
90
  const getUser = await this.nymph.getEntity({ class: tilmeld.User, skipAc: true }, {
71
91
  type: '&',
72
92
  ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
@@ -77,6 +97,7 @@ class User extends AbleObject_1.default {
77
97
  message: 'Requested account is not accessible.',
78
98
  };
79
99
  }
100
+ // Send the recovery email.
80
101
  user = getUser;
81
102
  options.template = 'RecoverUsername';
82
103
  options.message = {
@@ -88,6 +109,7 @@ class User extends AbleObject_1.default {
88
109
  options.locals = {};
89
110
  }
90
111
  else if (data.recoveryType === 'password') {
112
+ // Create a password recovery email.
91
113
  const getUser = await tilmeld.User.factoryUsername(data.account);
92
114
  if (getUser.guid == null) {
93
115
  return {
@@ -95,11 +117,13 @@ class User extends AbleObject_1.default {
95
117
  message: 'Requested account is not accessible.',
96
118
  };
97
119
  }
120
+ // Create a unique secret.
98
121
  getUser.recoverSecret = (0, guid_1.humanSecret)();
99
122
  getUser.recoverSecretDate = Date.now();
100
123
  if (!(await getUser.$saveSkipAC())) {
101
124
  return { result: false, message: "Couldn't save recovery secret." };
102
125
  }
126
+ // Send the recovery email.
103
127
  user = getUser;
104
128
  options.template = 'RecoverPassword';
105
129
  options.message = {
@@ -116,6 +140,8 @@ class User extends AbleObject_1.default {
116
140
  else {
117
141
  return { result: false, message: 'Invalid recovery type.' };
118
142
  }
143
+ this.nymph.config.debugLog('tilmeld', `User account recovery (${options.template}) request for "${user.username}".`);
144
+ // Send the email.
119
145
  if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
120
146
  return {
121
147
  result: true,
@@ -127,6 +153,12 @@ class User extends AbleObject_1.default {
127
153
  return { result: false, message: "Couldn't send recovery email." };
128
154
  }
129
155
  }
156
+ /**
157
+ * Recover account details.
158
+ *
159
+ * @param data The input data from the client.
160
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
161
+ */
130
162
  static async recover(data) {
131
163
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
132
164
  if (!tilmeld.config.pwRecovery) {
@@ -150,6 +182,7 @@ class User extends AbleObject_1.default {
150
182
  if (!('password' in data) || !data.password.length) {
151
183
  return { result: false, message: 'Password cannot be empty.' };
152
184
  }
185
+ this.nymph.config.debugLog('tilmeld', `User account recovery for "${user.username}".`);
153
186
  user.$password(data.password);
154
187
  delete user.recoverSecret;
155
188
  delete user.recoverSecretDate;
@@ -231,6 +264,8 @@ class User extends AbleObject_1.default {
231
264
  message: e.message,
232
265
  };
233
266
  }
267
+ this.$nymph.config.debugLog('tilmeld', `User login for "${this.$data.username}".`);
268
+ // Authentication was successful, attempt to login.
234
269
  if (!(await tilmeld.login(this, true))) {
235
270
  return { result: false, message: 'Incorrect login/password.' };
236
271
  }
@@ -240,6 +275,7 @@ class User extends AbleObject_1.default {
240
275
  await callback(this);
241
276
  }
242
277
  }
278
+ // Login was successful.
243
279
  return { result: true, message: 'You are logged in.' };
244
280
  }
245
281
  async $switchUser(data) {
@@ -286,6 +322,8 @@ class User extends AbleObject_1.default {
286
322
  message: e.message,
287
323
  };
288
324
  }
325
+ this.$nymph.config.debugLog('tilmeld', `User login switch for "${user.username}" to "${this.$data.username}".`);
326
+ // Authentication was successful, attempt to login.
289
327
  if (!(await tilmeld.loginSwitch(this, true))) {
290
328
  return { result: false, message: 'Incorrect login/password.' };
291
329
  }
@@ -295,8 +333,13 @@ class User extends AbleObject_1.default {
295
333
  await callback(this);
296
334
  }
297
335
  }
336
+ // Login was successful.
298
337
  return { result: true, message: 'You are logged in.' };
299
338
  }
339
+ /**
340
+ * Log a user out of the system.
341
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
342
+ */
300
343
  async $logout() {
301
344
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
302
345
  try {
@@ -313,6 +356,7 @@ class User extends AbleObject_1.default {
313
356
  message: e.message,
314
357
  };
315
358
  }
359
+ this.$nymph.config.debugLog('tilmeld', `User logout for "${this.$data.username}".`);
316
360
  await tilmeld.logout();
317
361
  for (let callback of this.constructor
318
362
  .afterLogoutCallbacks) {
@@ -347,6 +391,9 @@ class User extends AbleObject_1.default {
347
391
  }
348
392
  return null;
349
393
  }
394
+ /**
395
+ * Get the user's group descendants.
396
+ */
350
397
  async $getDescendantGroups() {
351
398
  if (this.$descendantGroups == null) {
352
399
  this.$descendantGroups = [];
@@ -419,6 +466,11 @@ class User extends AbleObject_1.default {
419
466
  super.$putData(data, sdata);
420
467
  this.$updateDataProtection();
421
468
  }
469
+ /**
470
+ * Update the data protection arrays for a user.
471
+ *
472
+ * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
473
+ */
422
474
  $updateDataProtection(givenUser) {
423
475
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
424
476
  let user = givenUser ?? tilmeld.User.current();
@@ -429,11 +481,14 @@ class User extends AbleObject_1.default {
429
481
  this.$privateData.push('username');
430
482
  }
431
483
  const isCurrentUser = user != null && user.$is(this);
484
+ // While loading, accessing abilities on the current user will infinitely
485
+ // loop, but if this is the current user, we can use its abilities.
432
486
  const abilities = (isCurrentUser || user == null
433
487
  ? this.$data.abilities
434
488
  : user?.abilities) ?? [];
435
489
  const isNewUser = this.guid == null;
436
490
  if (isCurrentUser) {
491
+ // Users can check to see what abilities they have.
437
492
  this.$clientEnabledMethods.push('$gatekeeper');
438
493
  this.$clientEnabledMethods.push('$changePassword');
439
494
  this.$clientEnabledMethods.push('$revokeCurrentTokens');
@@ -447,12 +502,14 @@ class User extends AbleObject_1.default {
447
502
  if (user != null &&
448
503
  (abilities.includes('tilmeld/admin') ||
449
504
  abilities.includes('system/admin'))) {
505
+ // Users who can edit other users can see most of their data.
450
506
  this.$privateData = ['password', 'salt', 'totpSecret'];
451
507
  this.$clientEnabledMethods.push('$hasTOTPSecret');
452
508
  this.$clientEnabledMethods.push('$removeTOTPSecret');
453
509
  this.$allowlistData = undefined;
454
510
  }
455
511
  else if (isCurrentUser || isNewUser) {
512
+ // Users can see their own data, and edit some of it.
456
513
  if (tilmeld.config.allowUsernameChange || isNewUser) {
457
514
  this.$allowlistData.push('username');
458
515
  }
@@ -470,10 +527,24 @@ class User extends AbleObject_1.default {
470
527
  this.$allowlistData.push('phone');
471
528
  }
472
529
  this.$privateData = [
530
+ // The things that are commented are there to show what is _not_ private.
531
+ // 'abilities',
532
+ // 'username',
533
+ // 'nameFirst',
534
+ // 'nameMiddle',
535
+ // 'nameLast',
536
+ // 'name',
537
+ // 'email',
538
+ // 'avatar',
539
+ // 'phone',
540
+ // 'group',
541
+ // 'groups',
542
+ // 'inheritAbilities',
473
543
  'enabled',
474
544
  'secret',
475
545
  'emailChangeDate',
476
546
  'newEmailSecret',
547
+ // 'newEmailAddress',
477
548
  'cancelEmailSecret',
478
549
  'cancelEmailAddress',
479
550
  'recoverSecret',
@@ -481,6 +552,7 @@ class User extends AbleObject_1.default {
481
552
  'salt',
482
553
  'password',
483
554
  'passwordTemp',
555
+ // 'revokeTokenDate',
484
556
  'totpSecret',
485
557
  ];
486
558
  }
@@ -488,6 +560,7 @@ class User extends AbleObject_1.default {
488
560
  !isCurrentUser &&
489
561
  (abilities.includes('tilmeld/switch') ||
490
562
  abilities.includes('system/admin'))) {
563
+ // Users with this ability can switch to other users.
491
564
  this.$clientEnabledMethods.push('$switchUser');
492
565
  }
493
566
  }
@@ -501,18 +574,36 @@ class User extends AbleObject_1.default {
501
574
  }
502
575
  }
503
576
  }
577
+ /**
578
+ * Check to see if a user has an ability.
579
+ *
580
+ * This function will check both user and group abilities, if the user is
581
+ * marked to inherit the abilities of its group.
582
+ *
583
+ * If `ability` is undefined, it will check to see if the user is currently
584
+ * logged in.
585
+ *
586
+ * If the user has the "system/admin" ability, this function will return true.
587
+ *
588
+ * @param ability The ability.
589
+ * @returns True or false.
590
+ */
504
591
  async $gatekeeper(ability) {
505
592
  await this.$wake();
506
593
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
507
594
  if (ability == null) {
508
595
  return tilmeld.User.current()?.$is(this) ?? false;
509
596
  }
597
+ // Check the cache to see if we've already checked this user.
510
598
  if (this.$gatekeeperCache == null) {
511
599
  this.$gatekeeperCache = await this.$getGatekeeperCache();
512
600
  }
513
601
  return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
514
602
  !!this.$data.abilities?.includes('system/admin'));
515
603
  }
604
+ /**
605
+ * Build a gatekeeper cache object.
606
+ */
516
607
  async $getGatekeeperCache() {
517
608
  await this.$wake();
518
609
  let abilities = this.$data.abilities ?? [];
@@ -537,6 +628,11 @@ class User extends AbleObject_1.default {
537
628
  this.$gatekeeperCache = undefined;
538
629
  return ret;
539
630
  }
631
+ /**
632
+ * Send the user email verification/change/cancellation links.
633
+ *
634
+ * @returns True on success, false on failure.
635
+ */
540
636
  async $sendEmailVerification() {
541
637
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
542
638
  if (this.guid == null) {
@@ -601,6 +697,12 @@ class User extends AbleObject_1.default {
601
697
  }
602
698
  return success;
603
699
  }
700
+ /**
701
+ * Add the user to a (secondary) group.
702
+ *
703
+ * @param group The group.
704
+ * @returns True if the user is already in the group. The resulting array of groups if the user was not.
705
+ */
604
706
  async $addGroup(group) {
605
707
  await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
606
708
  let groups = this.$data.groups;
@@ -614,6 +716,12 @@ class User extends AbleObject_1.default {
614
716
  }
615
717
  return true;
616
718
  }
719
+ /**
720
+ * Check the given password against the user's.
721
+ *
722
+ * @param password The password in question.
723
+ * @returns True if the passwords match, otherwise false.
724
+ */
617
725
  $checkPassword(password) {
618
726
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
619
727
  switch (tilmeld.config.pwMethod) {
@@ -627,6 +735,12 @@ class User extends AbleObject_1.default {
627
735
  (0, sha256_1.default)(password + this.$data.salt).toString(enc_base64_1.default));
628
736
  }
629
737
  }
738
+ /**
739
+ * Remove the user from a (secondary) group.
740
+ *
741
+ * @param group The group.
742
+ * @returns True if the user wasn't in the group. The resulting array of groups if the user was.
743
+ */
630
744
  async $delGroup(group) {
631
745
  await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
632
746
  let groups = this.$data.groups || [];
@@ -642,6 +756,12 @@ class User extends AbleObject_1.default {
642
756
  }
643
757
  return true;
644
758
  }
759
+ /**
760
+ * Check whether the user is in a (primary or secondary) group.
761
+ *
762
+ * @param group The group, or the group's GUID.
763
+ * @returns True or false.
764
+ */
645
765
  async $inGroup(group) {
646
766
  if (typeof group === 'string') {
647
767
  if (this.$data.group?.guid === group) {
@@ -656,7 +776,14 @@ class User extends AbleObject_1.default {
656
776
  }
657
777
  return (group.$is(this.$data.group) || group.$inArray(this.$data.groups ?? []));
658
778
  }
779
+ /**
780
+ * Check whether the user is a descendant of a group.
781
+ *
782
+ * @param group The group, or the group's GUID.
783
+ * @returns True or false.
784
+ */
659
785
  async $isDescendant(group) {
786
+ // Check to see if the user is in a descendant group of the given group.
660
787
  await this.$data.group?.$wake();
661
788
  if (this.$data.group != null &&
662
789
  this.$data.group?.cdate != null &&
@@ -671,11 +798,19 @@ class User extends AbleObject_1.default {
671
798
  }
672
799
  return false;
673
800
  }
801
+ /**
802
+ * A frontend accessible method to change the user's password.
803
+ *
804
+ * @param data The input data from the client.
805
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
806
+ */
674
807
  async $changePassword(data) {
808
+ this.$nymph.config.debugLog('tilmeld', `Password change request for "${this.$data.username}".`);
675
809
  if (!('newPassword' in data) || !data.newPassword.length) {
676
810
  return { result: false, message: 'Please specify a password.' };
677
811
  }
678
812
  if (!this.$checkPassword(data.currentPassword ?? '')) {
813
+ this.$nymph.config.debugLog('tilmeld', `Incorrect password for change request for "${this.$data.username}".`);
679
814
  return { result: false, message: 'Incorrect password.' };
680
815
  }
681
816
  this.$data.passwordTemp = data.newPassword;
@@ -693,6 +828,12 @@ class User extends AbleObject_1.default {
693
828
  return { result: false, message: "Couldn't save new password." };
694
829
  }
695
830
  }
831
+ /**
832
+ * Change the user's password.
833
+ *
834
+ * @param password The new password.
835
+ * @returns The resulting password or hash which is stored in the entity.
836
+ */
696
837
  $password(password) {
697
838
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
698
839
  switch (tilmeld.config.pwMethod) {
@@ -712,6 +853,12 @@ class User extends AbleObject_1.default {
712
853
  }
713
854
  return this.$data.password;
714
855
  }
856
+ /**
857
+ * A frontend accessible method to revoke all currently issued tokens.
858
+ *
859
+ * @param data The input data from the client.
860
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
861
+ */
715
862
  async $revokeCurrentTokens(data) {
716
863
  if (!this.$checkPassword(data.password ?? '')) {
717
864
  return { result: false, message: 'Incorrect password.' };
@@ -732,6 +879,12 @@ class User extends AbleObject_1.default {
732
879
  async $hasTOTPSecret() {
733
880
  return this.$data.totpSecret != null;
734
881
  }
882
+ /**
883
+ * Check the given code against the user's TOTP secret.
884
+ *
885
+ * @param code The code in question.
886
+ * @returns True if the code is valid, otherwise false.
887
+ */
735
888
  $checkTOTPCode(code) {
736
889
  if (this.$data.totpSecret == null) {
737
890
  return false;
@@ -747,6 +900,11 @@ class User extends AbleObject_1.default {
747
900
  });
748
901
  return totp.validate({ token: code, window: 2 }) != null;
749
902
  }
903
+ /**
904
+ * A frontend accessible method to generate a new TOTP secret.
905
+ *
906
+ * @returns An object with 'uri', 'qrcode', and 'secret'.
907
+ */
750
908
  async $getNewTOTPSecret() {
751
909
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
752
910
  if (this.$data.totpSecret != null) {
@@ -769,6 +927,12 @@ class User extends AbleObject_1.default {
769
927
  secret: secret.base32,
770
928
  };
771
929
  }
930
+ /**
931
+ * A frontend accessible method to save a TOTP secret into the user's account.
932
+ *
933
+ * @param data The input data from the client.
934
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
935
+ */
772
936
  async $saveTOTPSecret(data) {
773
937
  if (!this.$checkPassword(data.password ?? '')) {
774
938
  return { result: false, message: 'Incorrect password.' };
@@ -806,6 +970,13 @@ class User extends AbleObject_1.default {
806
970
  return { result: false, message: "Couldn't save two factor secret." };
807
971
  }
808
972
  }
973
+ /**
974
+ * A frontend accessible method to remove the TOTP secret from the user's
975
+ * account.
976
+ *
977
+ * @param data The input data from the client.
978
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
979
+ */
809
980
  async $removeTOTPSecret(data) {
810
981
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
811
982
  if (data) {
@@ -849,8 +1020,14 @@ class User extends AbleObject_1.default {
849
1020
  return { result: false, message: "Couldn't remove two factor secret." };
850
1021
  }
851
1022
  }
1023
+ /**
1024
+ * Check that a username is valid.
1025
+ *
1026
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1027
+ */
852
1028
  async $checkUsername() {
853
1029
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
1030
+ this.$nymph.config.debugLog('tilmeld', `Username check "${this.$data.username}".`);
854
1031
  if (!tilmeld.config.emailUsernames) {
855
1032
  if (this.$data.username == null || !this.$data.username.length) {
856
1033
  return { result: false, message: 'Please specify a username.' };
@@ -937,8 +1114,14 @@ class User extends AbleObject_1.default {
937
1114
  return await this.$checkEmail();
938
1115
  }
939
1116
  }
1117
+ /**
1118
+ * Check that an email is unique.
1119
+ *
1120
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1121
+ */
940
1122
  async $checkEmail() {
941
1123
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
1124
+ this.$nymph.config.debugLog('tilmeld', `Email check "${this.$data.email}".`);
942
1125
  if (this.$data.email == null || !this.$data.email.length) {
943
1126
  if (tilmeld.config.verifyEmail) {
944
1127
  return { result: false, message: 'Please specify an email.' };
@@ -972,8 +1155,14 @@ class User extends AbleObject_1.default {
972
1155
  message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
973
1156
  };
974
1157
  }
1158
+ /**
1159
+ * Check that a phone number is unique.
1160
+ *
1161
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1162
+ */
975
1163
  async $checkPhone() {
976
1164
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
1165
+ this.$nymph.config.debugLog('tilmeld', `Phone check "${this.$data.phone}".`);
977
1166
  if (this.$data.phone == null || !this.$data.phone.length) {
978
1167
  return { result: false, message: 'Please specify a phone number.' };
979
1168
  }
@@ -1040,6 +1229,7 @@ class User extends AbleObject_1.default {
1040
1229
  if (tilmeld.config.emailUsernames) {
1041
1230
  this.$data.email = this.$data.username;
1042
1231
  }
1232
+ // Start transaction.
1043
1233
  const transaction = 'tilmeld-register-' + (0, guid_1.nanoid)();
1044
1234
  const nymph = this.$nymph;
1045
1235
  const tnymph = await this.$nymph.startTransaction(transaction);
@@ -1065,8 +1255,10 @@ class User extends AbleObject_1.default {
1065
1255
  message: e.message,
1066
1256
  };
1067
1257
  }
1258
+ // Add primary group.
1068
1259
  let generatedPrimaryGroup = null;
1069
1260
  if (tilmeld.config.generatePrimary) {
1261
+ // Generate a new primary group for the user.
1070
1262
  generatedPrimaryGroup = await tilmeld.Group.factory();
1071
1263
  generatedPrimaryGroup.groupname = this.$data.username;
1072
1264
  generatedPrimaryGroup.avatar = this.$data.avatar;
@@ -1095,6 +1287,7 @@ class User extends AbleObject_1.default {
1095
1287
  this.$data.group = generatedPrimaryGroup;
1096
1288
  }
1097
1289
  else {
1290
+ // Add the default primary.
1098
1291
  const group = await tnymph.getEntity({ class: tilmeld.Group }, {
1099
1292
  type: '&',
1100
1293
  equal: ['defaultPrimary', true],
@@ -1103,15 +1296,18 @@ class User extends AbleObject_1.default {
1103
1296
  this.$data.group = group;
1104
1297
  }
1105
1298
  }
1299
+ // Add secondary groups.
1106
1300
  if (tilmeld.config.userFields.includes('email') &&
1107
1301
  tilmeld.config.verifyEmail &&
1108
1302
  tilmeld.config.unverifiedAccess) {
1303
+ // Add the default secondaries for unverified users.
1109
1304
  this.$data.groups = await tnymph.getEntities({ class: tilmeld.Group }, {
1110
1305
  type: '&',
1111
1306
  equal: ['unverifiedSecondary', true],
1112
1307
  });
1113
1308
  }
1114
1309
  else {
1310
+ // Add the default secondaries.
1115
1311
  this.$data.groups = await tnymph.getEntities({ class: tilmeld.Group }, {
1116
1312
  type: '&',
1117
1313
  equal: ['defaultSecondary', true],
@@ -1119,6 +1315,7 @@ class User extends AbleObject_1.default {
1119
1315
  }
1120
1316
  if (tilmeld.config.userFields.includes('email') &&
1121
1317
  tilmeld.config.verifyEmail) {
1318
+ // The user will be enabled after verifying their e-mail address.
1122
1319
  if (!tilmeld.config.unverifiedAccess) {
1123
1320
  this.$data.enabled = false;
1124
1321
  }
@@ -1126,6 +1323,8 @@ class User extends AbleObject_1.default {
1126
1323
  else {
1127
1324
  this.$data.enabled = true;
1128
1325
  }
1326
+ // If create_admin is true and there are no other users, grant
1327
+ // "system/admin".
1129
1328
  let madeAdmin = false;
1130
1329
  if (tilmeld.config.createAdmin) {
1131
1330
  const otherUsers = await tnymph.getEntities({
@@ -1134,13 +1333,17 @@ class User extends AbleObject_1.default {
1134
1333
  limit: 1,
1135
1334
  return: 'guid',
1136
1335
  });
1336
+ // Make sure it's not just null, cause that means an error.
1137
1337
  if (!otherUsers.length) {
1138
1338
  this.$grant('system/admin');
1139
1339
  this.$data.enabled = true;
1140
1340
  madeAdmin = true;
1141
1341
  }
1142
1342
  }
1343
+ this.$nymph.config.debugLog('tilmeld', `Registering new user "${this.$data.username}".`);
1143
1344
  if (await this.$saveSkipAC()) {
1345
+ this.$nymph.config.debugLog('tilmeld', `New user registered "${this.$data.username}".`);
1346
+ // Send the new user registered email.
1144
1347
  if (tilmeld.config.userRegisteredRecipient != null) {
1145
1348
  await tilmeld.config.sendEmail(tilmeld, {
1146
1349
  template: 'UserRegistered',
@@ -1157,6 +1360,7 @@ class User extends AbleObject_1.default {
1157
1360
  },
1158
1361
  }, this);
1159
1362
  }
1363
+ // Save the primary group.
1160
1364
  if (generatedPrimaryGroup != null) {
1161
1365
  generatedPrimaryGroup.user = this;
1162
1366
  if (!(await generatedPrimaryGroup.$saveSkipAC())) {
@@ -1169,6 +1373,7 @@ class User extends AbleObject_1.default {
1169
1373
  };
1170
1374
  }
1171
1375
  }
1376
+ // Finish up.
1172
1377
  if (tilmeld.config.verifyEmail &&
1173
1378
  !tilmeld.config.unverifiedAccess &&
1174
1379
  !madeAdmin &&
@@ -1210,6 +1415,7 @@ class User extends AbleObject_1.default {
1210
1415
  else {
1211
1416
  await tnymph.rollback(transaction);
1212
1417
  this.$nymph = nymph;
1418
+ this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}".`);
1213
1419
  return {
1214
1420
  result: false,
1215
1421
  loggedin: false,
@@ -1220,6 +1426,7 @@ class User extends AbleObject_1.default {
1220
1426
  catch (e) {
1221
1427
  await tnymph.rollback(transaction);
1222
1428
  this.$nymph = nymph;
1429
+ this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}": ${e}`);
1223
1430
  throw e;
1224
1431
  }
1225
1432
  try {
@@ -1247,6 +1454,7 @@ class User extends AbleObject_1.default {
1247
1454
  throw new errors_1.BadDataError("You don't have the authority to modify system admins.");
1248
1455
  }
1249
1456
  let sendVerification = false;
1457
+ // Formatting.
1250
1458
  this.$data.username = this.$data.username.trim();
1251
1459
  if (tilmeld.config.emailUsernames) {
1252
1460
  this.$data.email = this.$data.username;
@@ -1260,6 +1468,7 @@ class User extends AbleObject_1.default {
1260
1468
  (this.$data.nameMiddle ? ' ' + this.$data.nameMiddle : '') +
1261
1469
  (this.$data.nameLast ? ' ' + this.$data.nameLast : '');
1262
1470
  this.$data.enabled = !!this.$data.enabled;
1471
+ // Clear empty values.
1263
1472
  if (this.$data.nameFirst === '' ||
1264
1473
  !tilmeld.config.userFields.includes('name')) {
1265
1474
  delete this.$data.nameFirst;
@@ -1327,6 +1536,7 @@ class User extends AbleObject_1.default {
1327
1536
  if (this.$data.inheritAbilities === false) {
1328
1537
  delete this.$data.inheritAbilities;
1329
1538
  }
1539
+ // Verification.
1330
1540
  const unCheck = await this.$checkUsername();
1331
1541
  if (!unCheck.result) {
1332
1542
  throw new errors_1.BadUsernameError(unCheck.message);
@@ -1338,10 +1548,16 @@ class User extends AbleObject_1.default {
1338
1548
  throw new errors_1.BadEmailError(emCheck.message);
1339
1549
  }
1340
1550
  }
1551
+ // Email changes.
1341
1552
  if (!tilmeld.gatekeeper('tilmeld/admin') &&
1342
1553
  tilmeld.config.userFields.includes('email')) {
1554
+ // The user isn't an admin, so email address changes should contain some
1555
+ // security measures.
1343
1556
  if (tilmeld.config.verifyEmail) {
1557
+ // The user needs to verify this new email address.
1344
1558
  if (this.guid == null) {
1559
+ // If this is the first user, they'll be made an admin, so they don't
1560
+ // need to verify.
1345
1561
  if (!this.$data.abilities?.includes('system/admin')) {
1346
1562
  this.$data.secret = (0, guid_1.nanoid)();
1347
1563
  sendVerification = true;
@@ -1349,6 +1565,7 @@ class User extends AbleObject_1.default {
1349
1565
  }
1350
1566
  else if (this.$originalEmail != null &&
1351
1567
  this.$originalEmail !== this.$data.email) {
1568
+ // The user already has an old email address.
1352
1569
  if (tilmeld.config.emailRateLimit !== '' &&
1353
1570
  this.$data.emailChangeDate != null &&
1354
1571
  this.$data.emailChangeDate >=
@@ -1360,13 +1577,18 @@ class User extends AbleObject_1.default {
1360
1577
  else {
1361
1578
  if (this.$data.newEmailSecret == null &&
1362
1579
  tilmeld.config.emailRateLimit !== '' &&
1580
+ // Make sure the user has at least the rate limit time to cancel
1581
+ // an email change.
1363
1582
  (this.$data.emailChangeDate == null ||
1364
1583
  this.$data.emailChangeDate <
1365
1584
  (0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
1585
+ // Save the old email in case the cancel change link is clicked.
1366
1586
  this.$data.cancelEmailAddress = this.$originalEmail;
1367
1587
  this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
1368
1588
  this.$data.emailChangeDate = Date.now();
1369
1589
  }
1590
+ // Save the new address and reset to the old one (until they verify
1591
+ // it).
1370
1592
  this.$data.newEmailAddress = this.$data.email;
1371
1593
  this.$data.email = this.$originalEmail;
1372
1594
  if (tilmeld.config.emailUsernames) {
@@ -1380,9 +1602,13 @@ class User extends AbleObject_1.default {
1380
1602
  else if (this.guid != null &&
1381
1603
  this.$originalEmail != null &&
1382
1604
  this.$originalEmail !== this.$data.email &&
1605
+ // Make sure the user has at least the rate limit time to cancel an
1606
+ // email change.
1383
1607
  (this.$data.emailChangeDate == null ||
1384
1608
  this.$data.emailChangeDate <
1385
1609
  (0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
1610
+ // The user doesn't need to verify their new email address, but should
1611
+ // be able to cancel the email change from their old address.
1386
1612
  this.$data.cancelEmailAddress = this.$originalEmail;
1387
1613
  this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
1388
1614
  sendVerification = true;
@@ -1401,6 +1627,7 @@ class User extends AbleObject_1.default {
1401
1627
  catch (e) {
1402
1628
  throw new errors_1.BadDataError(e?.message);
1403
1629
  }
1630
+ // Start transaction.
1404
1631
  const transaction = 'tilmeld-save-' + (0, guid_1.nanoid)();
1405
1632
  const nymph = this.$nymph;
1406
1633
  const tnymph = await this.$nymph.startTransaction(transaction);
@@ -1410,6 +1637,7 @@ class User extends AbleObject_1.default {
1410
1637
  await group?.$wake();
1411
1638
  if (group != null && group.user != null && this.$is(group.user)) {
1412
1639
  try {
1640
+ // Update the user's generated primary group.
1413
1641
  group.groupname = this.$data.username;
1414
1642
  group.avatar = this.$data.avatar;
1415
1643
  if (tilmeld.config.userFields.includes('email')) {
@@ -1444,6 +1672,7 @@ class User extends AbleObject_1.default {
1444
1672
  }
1445
1673
  if (ret) {
1446
1674
  if (sendVerification) {
1675
+ // The email has changed, so send a new verification email.
1447
1676
  if (!(await this.$sendEmailVerification())) {
1448
1677
  await tnymph.rollback(transaction);
1449
1678
  this.guid = preGuid;
@@ -1458,6 +1687,7 @@ class User extends AbleObject_1.default {
1458
1687
  await tnymph.commit(transaction);
1459
1688
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
1460
1689
  if (tilmeld.User.current(true).$is(this)) {
1690
+ // Update the user in the session cache.
1461
1691
  await tilmeld.fillSession(this);
1462
1692
  }
1463
1693
  }
@@ -1467,6 +1697,9 @@ class User extends AbleObject_1.default {
1467
1697
  this.$nymph = nymph;
1468
1698
  return ret;
1469
1699
  }
1700
+ /**
1701
+ * This should *never* be accessible on the client.
1702
+ */
1470
1703
  async $saveSkipAC() {
1471
1704
  this.$skipAcWhenSaving = true;
1472
1705
  return await this.$save();
@@ -1497,6 +1730,7 @@ class User extends AbleObject_1.default {
1497
1730
  if (!(prop in this)) {
1498
1731
  throw new Error('Invalid event type.');
1499
1732
  }
1733
+ // @ts-ignore: The callback should always be the right type here.
1500
1734
  this[prop].push(callback);
1501
1735
  return () => this.off(event, callback);
1502
1736
  }
@@ -1505,13 +1739,18 @@ class User extends AbleObject_1.default {
1505
1739
  if (!(prop in this)) {
1506
1740
  return false;
1507
1741
  }
1742
+ // @ts-ignore: The callback should always be the right type here.
1508
1743
  const i = this[prop].indexOf(callback);
1509
1744
  if (i > -1) {
1745
+ // @ts-ignore: The callback should always be the right type here.
1510
1746
  this[prop].splice(i, 1);
1511
1747
  }
1512
1748
  return true;
1513
1749
  }
1514
1750
  }
1751
+ /**
1752
+ * The instance of Tilmeld to use for queries.
1753
+ */
1515
1754
  User.ETYPE = 'tilmeld_user';
1516
1755
  User.class = 'User';
1517
1756
  User.checkUsernameCallbacks = [];
@@ -1529,8 +1768,15 @@ User.DEFAULT_CLIENT_ENABLED_METHODS = [
1529
1768
  '$register',
1530
1769
  ];
1531
1770
  User.DEFAULT_PRIVATE_DATA = [
1771
+ // The things that are commented are there to show what is _not_ private.
1532
1772
  'abilities',
1773
+ // 'username',
1774
+ // 'nameFirst',
1775
+ // 'nameMiddle',
1776
+ // 'nameLast',
1777
+ // 'name',
1533
1778
  'email',
1779
+ // 'avatar',
1534
1780
  'phone',
1535
1781
  'group',
1536
1782
  'groups',