@nymphjs/tilmeld 1.0.0-beta.4 → 1.0.0-beta.41

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
@@ -5,15 +5,17 @@ import {
5
5
  Selector,
6
6
  SerializedEntityData,
7
7
  } from '@nymphjs/nymph';
8
+ import { humanSecret, nanoid } from '@nymphjs/guid';
8
9
  import { EmailOptions } from 'email-templates';
9
10
  import strtotime from 'locutus/php/datetime/strtotime';
10
11
  import Base64 from 'crypto-js/enc-base64';
11
12
  import sha256 from 'crypto-js/sha256';
12
13
  import md5 from 'crypto-js/md5';
13
14
  import { difference } from 'lodash';
14
- import { humanSecret, nanoid } from '@nymphjs/guid';
15
+ import { TOTP, Secret } from 'otpauth';
16
+ import { toDataURL } from 'qrcode';
15
17
 
16
- import type Tilmeld from './Tilmeld';
18
+ import { enforceTilmeld } from './enforceTilmeld';
17
19
  import AbleObject from './AbleObject';
18
20
  import Group, { GroupData } from './Group';
19
21
  import {
@@ -24,12 +26,22 @@ import {
24
26
  } from './errors';
25
27
 
26
28
  export type EventType =
29
+ | 'checkUsername'
27
30
  | 'beforeRegister'
28
31
  | 'afterRegister'
29
32
  | 'beforeLogin'
30
33
  | 'afterLogin'
31
34
  | 'beforeLogout'
32
35
  | 'afterLogout';
36
+ /**
37
+ * This is run when the user has entered an otherwise valid username into the
38
+ * signup form. It should return a result, which when false will stop the
39
+ * process and return the included message, disallowing the username.
40
+ */
41
+ export type TilmeldCheckUsernameCallback = (
42
+ user: User & UserData,
43
+ data: { username: string }
44
+ ) => Promise<{ result: boolean; message?: string }>;
33
45
  /**
34
46
  * Theses are run before the user data checks, so the only checks before are
35
47
  * whether registration is allowed and whether the user is already registered.
@@ -135,7 +147,8 @@ export type UserData = {
135
147
  */
136
148
  newEmailSecret?: string;
137
149
  /**
138
- * The new email address.
150
+ * If the user has changed their email address, this is the new one, awaiting
151
+ * verification.
139
152
  */
140
153
  newEmailAddress?: string;
141
154
  /**
@@ -167,6 +180,15 @@ export type UserData = {
167
180
  * database.
168
181
  */
169
182
  passwordTemp?: string;
183
+ /**
184
+ * If set, this timestamp is the cutoff point for JWT issue dates. Any token
185
+ * issued before this date will not authenticate the user.
186
+ */
187
+ revokeTokenDate?: number;
188
+ /**
189
+ * Two factor auth secret.
190
+ */
191
+ totpSecret?: string;
170
192
  };
171
193
 
172
194
  /**
@@ -182,9 +204,9 @@ export default class User extends AbleObject<UserData> {
182
204
  /**
183
205
  * The instance of Tilmeld to use for queries.
184
206
  */
185
- static tilmeld: Tilmeld;
186
207
  static ETYPE = 'tilmeld_user';
187
208
  static class = 'User';
209
+ private static checkUsernameCallbacks: TilmeldCheckUsernameCallback[] = [];
188
210
  private static beforeRegisterCallbacks: TilmeldBeforeRegisterCallback[] = [];
189
211
  private static afterRegisterCallbacks: TilmeldAfterRegisterCallback[] = [];
190
212
  private static beforeLoginCallbacks: TilmeldBeforeLoginCallback[] = [];
@@ -200,23 +222,33 @@ export default class User extends AbleObject<UserData> {
200
222
  '$register',
201
223
  ];
202
224
  private static DEFAULT_PRIVATE_DATA = [
225
+ // The things that are commented are there to show what is _not_ private.
226
+ 'abilities',
227
+ // 'username',
228
+ // 'nameFirst',
229
+ // 'nameMiddle',
230
+ // 'nameLast',
231
+ // 'name',
203
232
  'email',
204
- 'originalEmail',
233
+ // 'avatar',
205
234
  'phone',
206
235
  'group',
207
236
  'groups',
208
- 'abilities',
209
237
  'inheritAbilities',
210
- 'recoverSecret',
211
- 'recoverSecretDate',
212
- 'password',
213
- 'salt',
238
+ 'enabled',
214
239
  'secret',
215
- 'newEmailAddress',
240
+ 'emailChangeDate',
216
241
  'newEmailSecret',
217
- 'cancelEmailAddress',
242
+ 'newEmailAddress',
218
243
  'cancelEmailSecret',
219
- 'emailChangeDate',
244
+ 'cancelEmailAddress',
245
+ 'recoverSecret',
246
+ 'recoverSecretDate',
247
+ 'salt',
248
+ 'password',
249
+ 'passwordTemp',
250
+ 'revokeTokenDate',
251
+ 'totpSecret',
220
252
  ];
221
253
  private static DEFAULT_ALLOWLIST_DATA: string[] = [];
222
254
 
@@ -280,21 +312,19 @@ export default class User extends AbleObject<UserData> {
280
312
  return entity;
281
313
  }
282
314
 
283
- static factorySync(guid?: string): User & UserData {
284
- return super.factorySync(guid) as User & UserData;
315
+ static factorySync(): User & UserData {
316
+ return super.factorySync() as User & UserData;
285
317
  }
286
318
 
287
- constructor(guid?: string) {
288
- super(guid);
319
+ constructor() {
320
+ super();
289
321
 
290
- if (this.guid == null) {
291
- // Defaults.
292
- this.$data.enabled = true;
293
- this.$data.abilities = [];
294
- this.$data.groups = [];
295
- this.$data.inheritAbilities = true;
296
- this.$updateDataProtection();
297
- }
322
+ // Defaults.
323
+ this.$data.enabled = true;
324
+ this.$data.abilities = [];
325
+ this.$data.groups = [];
326
+ this.$data.inheritAbilities = true;
327
+ this.$updateDataProtection();
298
328
  }
299
329
 
300
330
  public static current(returnObjectIfNotExist: true): User & UserData;
@@ -304,10 +334,11 @@ export default class User extends AbleObject<UserData> {
304
334
  public static current(
305
335
  returnObjectIfNotExist?: boolean
306
336
  ): (User & UserData) | null {
307
- if (this.tilmeld.currentUser == null) {
337
+ const tilmeld = enforceTilmeld(this);
338
+ if (tilmeld.currentUser == null) {
308
339
  return returnObjectIfNotExist ? this.factorySync() : null;
309
340
  }
310
- return this.tilmeld.currentUser;
341
+ return tilmeld.currentUser;
311
342
  }
312
343
 
313
344
  /**
@@ -320,7 +351,8 @@ export default class User extends AbleObject<UserData> {
320
351
  recoveryType: 'username' | 'password';
321
352
  account: string;
322
353
  }): Promise<{ result: boolean; message: string }> {
323
- if (!this.tilmeld.config.pwRecovery) {
354
+ const tilmeld = enforceTilmeld(this);
355
+ if (!tilmeld.config.pwRecovery) {
324
356
  return {
325
357
  result: false,
326
358
  message: 'Account recovery is not allowed.',
@@ -330,14 +362,11 @@ export default class User extends AbleObject<UserData> {
330
362
  let user: User & UserData;
331
363
  const options: EmailOptions = {};
332
364
 
333
- if (
334
- !this.tilmeld.config.emailUsernames &&
335
- data.recoveryType === 'username'
336
- ) {
365
+ if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
337
366
  // Create a username recovery email.
338
367
 
339
368
  const getUser = await this.nymph.getEntity(
340
- { class: this.tilmeld.User, skipAc: true },
369
+ { class: tilmeld.User, skipAc: true },
341
370
  {
342
371
  type: '&',
343
372
  ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
@@ -364,7 +393,7 @@ export default class User extends AbleObject<UserData> {
364
393
  } else if (data.recoveryType === 'password') {
365
394
  // Create a password recovery email.
366
395
 
367
- const getUser = await this.tilmeld.User.factoryUsername(data.account);
396
+ const getUser = await tilmeld.User.factoryUsername(data.account);
368
397
 
369
398
  if (getUser.guid == null) {
370
399
  return {
@@ -391,14 +420,14 @@ export default class User extends AbleObject<UserData> {
391
420
  };
392
421
  options.locals = {
393
422
  recoverCode: getUser.recoverSecret,
394
- timeLimit: this.tilmeld.config.pwRecoveryTimeLimit,
423
+ timeLimit: tilmeld.config.pwRecoveryTimeLimit,
395
424
  };
396
425
  } else {
397
426
  return { result: false, message: 'Invalid recovery type.' };
398
427
  }
399
428
 
400
429
  // Send the email.
401
- if (await this.tilmeld.config.sendEmail(this.tilmeld, options, user)) {
430
+ if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
402
431
  return {
403
432
  result: true,
404
433
  message:
@@ -421,21 +450,22 @@ export default class User extends AbleObject<UserData> {
421
450
  secret: string;
422
451
  password: string;
423
452
  }): Promise<{ result: boolean; message: string }> {
424
- if (!this.tilmeld.config.pwRecovery) {
453
+ const tilmeld = enforceTilmeld(this);
454
+ if (!tilmeld.config.pwRecovery) {
425
455
  return {
426
456
  result: false,
427
457
  message: 'Account recovery is not allowed.',
428
458
  };
429
459
  }
430
460
 
431
- const user = await this.tilmeld.User.factoryUsername(data.username);
461
+ const user = await tilmeld.User.factoryUsername(data.username);
432
462
 
433
463
  if (
434
464
  user.guid == null ||
435
465
  user.recoverSecret == null ||
436
466
  data.secret !== user.recoverSecret ||
437
467
  strtotime(
438
- '+' + this.tilmeld.config.pwRecoveryTimeLimit,
468
+ '+' + tilmeld.config.pwRecoveryTimeLimit,
439
469
  Math.floor((user.recoverSecretDate ?? 0) / 1000)
440
470
  ) *
441
471
  1000 <
@@ -466,27 +496,30 @@ export default class User extends AbleObject<UserData> {
466
496
  }
467
497
 
468
498
  public static getClientConfig() {
499
+ const tilmeld = enforceTilmeld(this);
469
500
  return {
470
- regFields: this.tilmeld.config.regFields,
471
- userFields: this.tilmeld.config.userFields,
472
- emailUsernames: this.tilmeld.config.emailUsernames,
473
- allowRegistration: this.tilmeld.config.allowRegistration,
474
- allowUsernameChange: this.tilmeld.config.allowUsernameChange,
475
- pwRecovery: this.tilmeld.config.pwRecovery,
476
- verifyEmail: this.tilmeld.config.verifyEmail,
477
- unverifiedAccess: this.tilmeld.config.unverifiedAccess,
501
+ regFields: tilmeld.config.regFields,
502
+ userFields: tilmeld.config.userFields,
503
+ emailUsernames: tilmeld.config.emailUsernames,
504
+ allowRegistration: tilmeld.config.allowRegistration,
505
+ allowUsernameChange: tilmeld.config.allowUsernameChange,
506
+ pwRecovery: tilmeld.config.pwRecovery,
507
+ verifyEmail: tilmeld.config.verifyEmail,
508
+ unverifiedAccess: tilmeld.config.unverifiedAccess,
478
509
  };
479
510
  }
480
511
 
481
512
  public static async loginUser(data: {
482
513
  username: string;
483
514
  password: string;
515
+ code?: string;
484
516
  additionalData?: { [k: string]: any };
485
517
  }) {
518
+ const tilmeld = enforceTilmeld(this);
486
519
  if (!('username' in data) || !data.username.length) {
487
520
  return { result: false, message: 'Incorrect login/password.' };
488
521
  }
489
- const user = await this.tilmeld.User.factoryUsername(data.username);
522
+ const user = await tilmeld.User.factoryUsername(data.username);
490
523
  const result: { result: boolean; message: string; user?: User & UserData } =
491
524
  await user.$login(data);
492
525
  if (result.result) {
@@ -499,22 +532,41 @@ export default class User extends AbleObject<UserData> {
499
532
  public async $login(data: {
500
533
  username: string;
501
534
  password: string;
535
+ code?: string;
502
536
  additionalData?: { [k: string]: any };
503
537
  }) {
504
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
538
+ const tilmeld = enforceTilmeld(this);
505
539
  if (this.guid == null) {
506
540
  return { result: false, message: 'Incorrect login/password.' };
507
541
  }
508
542
  if (!this.$data.enabled) {
509
543
  return { result: false, message: 'This user is disabled.' };
510
544
  }
511
- if (this.$gatekeeper()) {
545
+ if (await this.$gatekeeper()) {
512
546
  return { result: true, message: 'You are already logged in.' };
513
547
  }
514
548
  if (!this.$checkPassword(data.password)) {
515
549
  return { result: false, message: 'Incorrect login/password.' };
516
550
  }
517
551
 
552
+ if (this.$data.totpSecret != null) {
553
+ if (
554
+ data.code == null ||
555
+ typeof data.code !== 'string' ||
556
+ data.code.length !== 6
557
+ ) {
558
+ return {
559
+ result: false,
560
+ needTOTP: true,
561
+ message: 'You need to provide a 2FA code.',
562
+ };
563
+ }
564
+
565
+ if (!this.$checkTOTPCode(data.code)) {
566
+ return { result: false, message: 'Incorrect 2FA code.' };
567
+ }
568
+ }
569
+
518
570
  try {
519
571
  for (let callback of (this.constructor as typeof User)
520
572
  .beforeLoginCallbacks) {
@@ -545,12 +597,84 @@ export default class User extends AbleObject<UserData> {
545
597
  return { result: true, message: 'You are logged in.' };
546
598
  }
547
599
 
600
+ public async $switchUser(data?: { additionalData?: { [k: string]: any } }) {
601
+ const tilmeld = enforceTilmeld(this);
602
+ if (this.guid == null) {
603
+ return { result: false, message: 'Incorrect login/password.' };
604
+ }
605
+
606
+ let user = tilmeld.User.current();
607
+
608
+ if (!user || user.guid == null) {
609
+ return { result: false, message: "You're not logged in." };
610
+ }
611
+
612
+ const isCurrentUser = user != null && user.$is(this);
613
+
614
+ if (isCurrentUser) {
615
+ return { result: false, message: "You can't switch to yourself." };
616
+ }
617
+
618
+ const toAbilities = this.$data.abilities || [];
619
+ const fromAbilities = user.abilities || [];
620
+
621
+ if (
622
+ fromAbilities.indexOf('tilmeld/switch') === -1 &&
623
+ fromAbilities.indexOf('system/admin') === -1
624
+ ) {
625
+ return {
626
+ result: false,
627
+ message: "You don't have permission to switch users.",
628
+ };
629
+ }
630
+
631
+ if (
632
+ toAbilities.indexOf('system/admin') !== -1 ||
633
+ toAbilities.indexOf('tilmeld/admin') !== -1
634
+ ) {
635
+ return { result: false, message: "You can't switch to an admin." };
636
+ }
637
+
638
+ try {
639
+ for (let callback of (this.constructor as typeof User)
640
+ .beforeLoginCallbacks) {
641
+ if (callback) {
642
+ await callback(this, {
643
+ username: this.$data.username || '',
644
+ password: '',
645
+ ...(data || {}),
646
+ });
647
+ }
648
+ }
649
+ } catch (e: any) {
650
+ return {
651
+ result: false,
652
+ message: e.message,
653
+ };
654
+ }
655
+
656
+ // Authentication was successful, attempt to login.
657
+ if (!tilmeld.loginSwitch(this, true)) {
658
+ return { result: false, message: 'Incorrect login/password.' };
659
+ }
660
+
661
+ for (let callback of (this.constructor as typeof User)
662
+ .afterLoginCallbacks) {
663
+ if (callback) {
664
+ await callback(this);
665
+ }
666
+ }
667
+
668
+ // Login was successful.
669
+ return { result: true, message: 'You are logged in.' };
670
+ }
671
+
548
672
  /**
549
673
  * Log a user out of the system.
550
674
  * @returns An object with a boolean 'result' entry and a 'message' entry.
551
675
  */
552
676
  public async $logout() {
553
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
677
+ const tilmeld = enforceTilmeld(this);
554
678
 
555
679
  try {
556
680
  for (let callback of (this.constructor as typeof User)
@@ -566,7 +690,7 @@ export default class User extends AbleObject<UserData> {
566
690
  };
567
691
  }
568
692
 
569
- tilmeld.logout();
693
+ await tilmeld.logout();
570
694
 
571
695
  for (let callback of (this.constructor as typeof User)
572
696
  .afterLogoutCallbacks) {
@@ -592,6 +716,22 @@ export default class User extends AbleObject<UserData> {
592
716
  );
593
717
  }
594
718
 
719
+ public $getGid() {
720
+ if (this.$data.group) {
721
+ return this.$data.group.guid;
722
+ }
723
+ return null;
724
+ }
725
+
726
+ public $getGids() {
727
+ if (this.$data.groups) {
728
+ return this.$data.groups
729
+ .map((group) => group.guid)
730
+ .filter((guid) => guid != null) as string[];
731
+ }
732
+ return null;
733
+ }
734
+
595
735
  /**
596
736
  * Get the user's group descendants.
597
737
  */
@@ -599,8 +739,10 @@ export default class User extends AbleObject<UserData> {
599
739
  if (this.$descendantGroups == null) {
600
740
  this.$descendantGroups = [];
601
741
  if (this.$data.group != null) {
742
+ await this.$data.group.$wake();
602
743
  this.$descendantGroups = await this.$data.group.$getDescendants();
603
744
  }
745
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
604
746
  for (let curGroup of this.$data.groups ?? []) {
605
747
  this.$descendantGroups = this.$descendantGroups?.concat(
606
748
  await curGroup.$getDescendants()
@@ -610,36 +752,43 @@ export default class User extends AbleObject<UserData> {
610
752
  return this.$descendantGroups;
611
753
  }
612
754
 
613
- /**
614
- * Get the user's group descendants.
615
- */
616
- public $getDescendantGroupsSync(): (Group & GroupData)[] {
617
- if (this.$descendantGroups == null) {
618
- this.$descendantGroups = [];
619
- if (this.$data.group != null) {
620
- this.$descendantGroups = this.$data.group.$getDescendantsSync();
621
- }
622
- for (let curGroup of this.$data.groups ?? []) {
623
- this.$descendantGroups = this.$descendantGroups?.concat(
624
- curGroup.$getDescendantsSync()
625
- );
626
- }
755
+ public $jsonAcceptData(input: EntityJson, allowConflict = false) {
756
+ const tilmeld = enforceTilmeld(this);
757
+ this.$check();
758
+
759
+ if (
760
+ 'abilities' in input.data &&
761
+ input.data.abilities?.includes('system/admin') !==
762
+ this.$data.abilities?.includes('system/admin') &&
763
+ (!tilmeld.currentUser ||
764
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
765
+ ) {
766
+ throw new BadDataError(
767
+ "You don't have the authority to grant or revoke system/admin."
768
+ );
627
769
  }
628
- return this.$descendantGroups;
629
- }
630
770
 
631
- public $jsonAcceptData(input: EntityJson, allowConflict = false) {
632
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
633
- this.$referenceWake();
771
+ if (
772
+ 'abilities' in input.data &&
773
+ input.data.abilities?.includes('tilmeld/admin') !==
774
+ this.$data.abilities?.includes('tilmeld/admin') &&
775
+ (!tilmeld.currentUser ||
776
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
777
+ ) {
778
+ throw new BadDataError(
779
+ "You don't have the authority to grant or revoke tilmeld/admin."
780
+ );
781
+ }
634
782
 
635
783
  if (
636
- input.data.abilities?.indexOf('system/admin') !== -1 &&
637
- this.$data.abilities?.indexOf('system/admin') === -1 &&
638
- tilmeld.gatekeeper('tilmeld/admin') &&
639
- !tilmeld.gatekeeper('system/admin')
784
+ 'abilities' in input.data &&
785
+ input.data.abilities?.includes('tilmeld/switch') !==
786
+ this.$data.abilities?.includes('tilmeld/switch') &&
787
+ (!tilmeld.currentUser ||
788
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
640
789
  ) {
641
790
  throw new BadDataError(
642
- "You don't have the authority to make this user a system admin."
791
+ "You don't have the authority to grant or revoke tilmeld/switch."
643
792
  );
644
793
  }
645
794
 
@@ -648,17 +797,42 @@ export default class User extends AbleObject<UserData> {
648
797
  }
649
798
 
650
799
  public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
651
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
652
- this.$referenceWake();
800
+ const tilmeld = enforceTilmeld(this);
801
+ this.$check();
653
802
 
654
803
  if (
655
- patch.set.abilities?.indexOf('system/admin') !== -1 &&
656
- this.$data.abilities?.indexOf('system/admin') === -1 &&
657
- tilmeld.gatekeeper('tilmeld/admin') &&
658
- !tilmeld.gatekeeper('system/admin')
804
+ 'abilities' in patch.set &&
805
+ patch.set.abilities?.includes('system/admin') !==
806
+ this.$data.abilities?.includes('system/admin') &&
807
+ (!tilmeld.currentUser ||
808
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
659
809
  ) {
660
810
  throw new BadDataError(
661
- "You don't have the authority to make this user a system admin."
811
+ "You don't have the authority to grant or revoke system/admin."
812
+ );
813
+ }
814
+
815
+ if (
816
+ 'abilities' in patch.set &&
817
+ patch.set.abilities?.includes('tilmeld/admin') !==
818
+ this.$data.abilities?.includes('tilmeld/admin') &&
819
+ (!tilmeld.currentUser ||
820
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
821
+ ) {
822
+ throw new BadDataError(
823
+ "You don't have the authority to grant or revoke tilmeld/admin."
824
+ );
825
+ }
826
+
827
+ if (
828
+ 'abilities' in patch.set &&
829
+ patch.set.abilities?.includes('tilmeld/switch') !==
830
+ this.$data.abilities?.includes('tilmeld/switch') &&
831
+ (!tilmeld.currentUser ||
832
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
833
+ ) {
834
+ throw new BadDataError(
835
+ "You don't have the authority to grant or revoke tilmeld/switch."
662
836
  );
663
837
  }
664
838
 
@@ -677,7 +851,7 @@ export default class User extends AbleObject<UserData> {
677
851
  * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
678
852
  */
679
853
  public $updateDataProtection(givenUser?: User & UserData) {
680
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
854
+ const tilmeld = enforceTilmeld(this);
681
855
  let user = givenUser ?? tilmeld.User.current();
682
856
 
683
857
  this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
@@ -701,8 +875,13 @@ export default class User extends AbleObject<UserData> {
701
875
  // Users can check to see what abilities they have.
702
876
  this.$clientEnabledMethods.push('$gatekeeper');
703
877
  this.$clientEnabledMethods.push('$changePassword');
878
+ this.$clientEnabledMethods.push('$revokeCurrentTokens');
704
879
  this.$clientEnabledMethods.push('$logout');
705
880
  this.$clientEnabledMethods.push('$sendEmailVerification');
881
+ this.$clientEnabledMethods.push('$hasTOTPSecret');
882
+ this.$clientEnabledMethods.push('$getNewTOTPSecret');
883
+ this.$clientEnabledMethods.push('$saveTOTPSecret');
884
+ this.$clientEnabledMethods.push('$removeTOTPSecret');
706
885
  }
707
886
 
708
887
  if (
@@ -711,7 +890,7 @@ export default class User extends AbleObject<UserData> {
711
890
  abilities.indexOf('system/admin') !== -1)
712
891
  ) {
713
892
  // Users who can edit other users can see most of their data.
714
- this.$privateData = ['password', 'salt'];
893
+ this.$privateData = ['password', 'salt', 'totpSecret'];
715
894
  this.$allowlistData = undefined;
716
895
  } else if (isCurrentUser || isNewUser) {
717
896
  // Users can see their own data, and edit some of it.
@@ -732,17 +911,56 @@ export default class User extends AbleObject<UserData> {
732
911
  this.$allowlistData.push('phone');
733
912
  }
734
913
  this.$privateData = [
914
+ // The things that are commented are there to show what is _not_ private.
915
+ // 'abilities',
916
+ // 'username',
917
+ // 'nameFirst',
918
+ // 'nameMiddle',
919
+ // 'nameLast',
920
+ // 'name',
921
+ // 'email',
922
+ // 'avatar',
923
+ // 'phone',
924
+ // 'group',
925
+ // 'groups',
926
+ // 'inheritAbilities',
927
+ 'enabled',
735
928
  'secret',
929
+ 'emailChangeDate',
736
930
  'newEmailSecret',
737
- 'cancelEmailAddress',
931
+ // 'newEmailAddress',
738
932
  'cancelEmailSecret',
739
- 'emailChangeDate',
933
+ 'cancelEmailAddress',
740
934
  'recoverSecret',
741
935
  'recoverSecretDate',
742
- 'password',
743
936
  'salt',
937
+ 'password',
938
+ 'passwordTemp',
939
+ // 'revokeTokenDate',
940
+ 'totpSecret',
744
941
  ];
745
942
  }
943
+
944
+ if (
945
+ user != null &&
946
+ !isCurrentUser &&
947
+ (abilities.indexOf('tilmeld/switch') !== -1 ||
948
+ abilities.indexOf('system/admin') !== -1)
949
+ ) {
950
+ // Users with this ability can switch to other users.
951
+ this.$clientEnabledMethods.push('$switchUser');
952
+ }
953
+ }
954
+
955
+ public $updateGroupDataProtection() {
956
+ if (this.$data.group != null && !this.$data.group.$asleep()) {
957
+ this.$data.group.$updateDataProtection();
958
+ }
959
+ for (let group of this.$data.groups ?? []) {
960
+ if (!group.$asleep()) {
961
+ group.$updateDataProtection();
962
+ }
963
+ }
746
964
  }
747
965
 
748
966
  /**
@@ -759,8 +977,8 @@ export default class User extends AbleObject<UserData> {
759
977
  * @param ability The ability.
760
978
  * @returns True or false.
761
979
  */
762
- public $gatekeeper(ability?: string): boolean {
763
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
980
+ public async $gatekeeper(ability?: string): Promise<boolean> {
981
+ const tilmeld = enforceTilmeld(this);
764
982
  if (ability == null) {
765
983
  return tilmeld.User.current()?.$is(this) ?? false;
766
984
  }
@@ -768,17 +986,16 @@ export default class User extends AbleObject<UserData> {
768
986
  if (this.$gatekeeperCache == null) {
769
987
  let abilities = this.$data.abilities ?? [];
770
988
  if (this.$data.inheritAbilities) {
989
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
771
990
  for (let curGroup of this.$data.groups ?? []) {
772
991
  if (curGroup.enabled) {
773
992
  abilities = abilities.concat(curGroup.abilities ?? []);
774
993
  }
775
994
  }
776
- if (
777
- this.$data.group != null &&
778
- this.$data.group.cdate != null &&
779
- this.$data.group.enabled
780
- ) {
781
- abilities = abilities.concat(this.$data.group.abilities ?? []);
995
+ await this.$data.group?.$wake();
996
+ const group = this.$data.group;
997
+ if (group != null && group.cdate != null && group.enabled) {
998
+ abilities = abilities.concat(group.abilities ?? []);
782
999
  }
783
1000
  }
784
1001
  this.$gatekeeperCache = Object.fromEntries(
@@ -787,7 +1004,7 @@ export default class User extends AbleObject<UserData> {
787
1004
  }
788
1005
  return (
789
1006
  (ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
790
- !!this.$gatekeeperCache['system/admin']
1007
+ !!this.$data.abilities?.includes('system/admin')
791
1008
  );
792
1009
  }
793
1010
 
@@ -804,7 +1021,7 @@ export default class User extends AbleObject<UserData> {
804
1021
  * @returns True on success, false on failure.
805
1022
  */
806
1023
  public async $sendEmailVerification() {
807
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1024
+ const tilmeld = enforceTilmeld(this);
808
1025
  if (this.guid == null) {
809
1026
  return false;
810
1027
  }
@@ -899,13 +1116,16 @@ export default class User extends AbleObject<UserData> {
899
1116
  * @param group The group.
900
1117
  * @returns True if the user is already in the group. The resulting array of groups if the user was not.
901
1118
  */
902
- public $addGroup(group: Group & GroupData) {
903
- if (this.$data.groups == null) {
904
- this.$data.groups = [];
1119
+ public async $addGroup(group: Group & GroupData) {
1120
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
1121
+ let groups = this.$data.groups;
1122
+ if (groups == null) {
1123
+ groups = [];
905
1124
  }
906
- if (!group.$inArray(this.$data.groups)) {
907
- this.$data.groups.push(group);
908
- return this.$data.groups;
1125
+ if (!group.$inArray(groups)) {
1126
+ groups.push(group);
1127
+ this.$data.groups = groups;
1128
+ return groups;
909
1129
  }
910
1130
  return true;
911
1131
  }
@@ -917,7 +1137,7 @@ export default class User extends AbleObject<UserData> {
917
1137
  * @returns True if the passwords match, otherwise false.
918
1138
  */
919
1139
  public $checkPassword(password: string) {
920
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1140
+ const tilmeld = enforceTilmeld(this);
921
1141
  switch (tilmeld.config.pwMethod) {
922
1142
  case 'plain':
923
1143
  return this.$data.password === password;
@@ -938,10 +1158,12 @@ export default class User extends AbleObject<UserData> {
938
1158
  * @param group The group.
939
1159
  * @returns True if the user wasn't in the group. The resulting array of groups if the user was.
940
1160
  */
941
- public $delGroup(group: Group & GroupData) {
942
- if (this.$data.groups != null && group.$inArray(this.$data.groups)) {
1161
+ public async $delGroup(group: Group & GroupData) {
1162
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
1163
+ let groups = this.$data.groups || [];
1164
+ if (this.$data.groups != null && group.$inArray(groups)) {
943
1165
  const newGroups: (Group & GroupData)[] = [];
944
- for (let curGroup of this.$data.groups) {
1166
+ for (let curGroup of groups) {
945
1167
  if (!group.$is(curGroup)) {
946
1168
  newGroups.push(curGroup);
947
1169
  }
@@ -958,7 +1180,7 @@ export default class User extends AbleObject<UserData> {
958
1180
  * @param group The group, or the group's GUID.
959
1181
  * @returns True or false.
960
1182
  */
961
- public $inGroup(group: (Group & GroupData) | string) {
1183
+ public async $inGroup(group: (Group & GroupData) | string) {
962
1184
  if (typeof group === 'string') {
963
1185
  if (this.$data.group?.guid === group) {
964
1186
  return true;
@@ -981,16 +1203,19 @@ export default class User extends AbleObject<UserData> {
981
1203
  * @param group The group, or the group's GUID.
982
1204
  * @returns True or false.
983
1205
  */
984
- public $isDescendant(group: (Group & GroupData) | string) {
1206
+ public async $isDescendant(group: (Group & GroupData) | string) {
985
1207
  // Check to see if the user is in a descendant group of the given group.
1208
+ await this.$data.group?.$wake();
986
1209
  if (
1210
+ this.$data.group != null &&
987
1211
  this.$data.group?.cdate != null &&
988
- this.$data.group.$isDescendant(group)
1212
+ (await this.$data.group?.$isDescendant(group))
989
1213
  ) {
990
1214
  return true;
991
1215
  }
1216
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
992
1217
  for (let curGroup of this.$data.groups ?? []) {
993
- if (curGroup.$isDescendant(group)) {
1218
+ if (await curGroup.$isDescendant(group)) {
994
1219
  return true;
995
1220
  }
996
1221
  }
@@ -1006,6 +1231,7 @@ export default class User extends AbleObject<UserData> {
1006
1231
  public async $changePassword(data: {
1007
1232
  newPassword: string;
1008
1233
  currentPassword: string;
1234
+ revokeCurrentTokens?: boolean;
1009
1235
  }): Promise<{ result: boolean; message: string }> {
1010
1236
  if (!('newPassword' in data) || !data.newPassword.length) {
1011
1237
  return { result: false, message: 'Please specify a password.' };
@@ -1014,7 +1240,14 @@ export default class User extends AbleObject<UserData> {
1014
1240
  return { result: false, message: 'Incorrect password.' };
1015
1241
  }
1016
1242
  this.$data.passwordTemp = data.newPassword;
1243
+ if (data.revokeCurrentTokens) {
1244
+ this.$data.revokeTokenDate = Date.now();
1245
+ }
1017
1246
  if (await this.$save()) {
1247
+ if (data.revokeCurrentTokens) {
1248
+ const tilmeld = enforceTilmeld(this);
1249
+ tilmeld.login(this, true);
1250
+ }
1018
1251
  return { result: true, message: 'Your password has been changed.' };
1019
1252
  } else {
1020
1253
  return { result: false, message: "Couldn't save new password." };
@@ -1028,7 +1261,7 @@ export default class User extends AbleObject<UserData> {
1028
1261
  * @returns The resulting password or hash which is stored in the entity.
1029
1262
  */
1030
1263
  public $password(password: string) {
1031
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1264
+ const tilmeld = enforceTilmeld(this);
1032
1265
  switch (tilmeld.config.pwMethod) {
1033
1266
  case 'plain':
1034
1267
  delete this.$data.salt;
@@ -1049,13 +1282,216 @@ export default class User extends AbleObject<UserData> {
1049
1282
  return this.$data.password;
1050
1283
  }
1051
1284
 
1285
+ /**
1286
+ * A frontend accessible method to revoke all currently issued tokens.
1287
+ *
1288
+ * @param data The input data from the client.
1289
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1290
+ */
1291
+ public async $revokeCurrentTokens(data: {
1292
+ password: string;
1293
+ }): Promise<{ result: boolean; message: string }> {
1294
+ if (!this.$checkPassword(data.password ?? '')) {
1295
+ return { result: false, message: 'Incorrect password.' };
1296
+ }
1297
+
1298
+ this.$data.revokeTokenDate = Date.now();
1299
+
1300
+ if (await this.$save()) {
1301
+ const tilmeld = enforceTilmeld(this);
1302
+ tilmeld.login(this, true);
1303
+ return {
1304
+ result: true,
1305
+ message: 'You have logged out of all other sessions.',
1306
+ };
1307
+ } else {
1308
+ return { result: false, message: "Couldn't save revocation date." };
1309
+ }
1310
+ }
1311
+
1312
+ public async $hasTOTPSecret() {
1313
+ return this.$data.totpSecret != null;
1314
+ }
1315
+
1316
+ /**
1317
+ * Check the given code against the user's TOTP secret.
1318
+ *
1319
+ * @param code The code in question.
1320
+ * @returns True if the code is valid, otherwise false.
1321
+ */
1322
+ public $checkTOTPCode(code: string) {
1323
+ if (this.$data.totpSecret == null) {
1324
+ return false;
1325
+ }
1326
+
1327
+ const tilmeld = enforceTilmeld(this);
1328
+
1329
+ const totp = new TOTP({
1330
+ issuer: tilmeld.config.appName,
1331
+ label: this.$data.username,
1332
+ algorithm: 'SHA1',
1333
+ digits: 6,
1334
+ period: 30,
1335
+ secret: this.$data.totpSecret,
1336
+ });
1337
+
1338
+ return totp.validate({ token: code, window: 2 }) != null;
1339
+ }
1340
+
1341
+ /**
1342
+ * A frontend accessible method to generate a new TOTP secret.
1343
+ *
1344
+ * @returns An object with 'uri', 'qrcode', and 'secret'.
1345
+ */
1346
+ public async $getNewTOTPSecret() {
1347
+ const tilmeld = enforceTilmeld(this);
1348
+
1349
+ if (this.$data.totpSecret != null) {
1350
+ throw new BadDataError('You already have a 2FA secret.');
1351
+ }
1352
+
1353
+ const secret = new Secret();
1354
+
1355
+ const totp = new TOTP({
1356
+ issuer: tilmeld.config.appName,
1357
+ label: this.$data.username,
1358
+ algorithm: 'SHA1',
1359
+ digits: 6,
1360
+ period: 30,
1361
+ secret,
1362
+ });
1363
+
1364
+ const uri = totp.toString();
1365
+
1366
+ const qrcode = await toDataURL(uri);
1367
+
1368
+ return {
1369
+ uri,
1370
+ qrcode,
1371
+ secret: secret.base32,
1372
+ };
1373
+ }
1374
+
1375
+ /**
1376
+ * A frontend accessible method to save a TOTP secret into the user's account.
1377
+ *
1378
+ * @param data The input data from the client.
1379
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1380
+ */
1381
+ public async $saveTOTPSecret(data: {
1382
+ password: string;
1383
+ secret: string;
1384
+ code: string;
1385
+ }): Promise<{ result: boolean; message: string }> {
1386
+ if (!this.$checkPassword(data.password ?? '')) {
1387
+ return { result: false, message: 'Incorrect password.' };
1388
+ }
1389
+
1390
+ if (this.$data.totpSecret != null) {
1391
+ return { result: false, message: 'You already have a 2FA secret.' };
1392
+ }
1393
+
1394
+ if (
1395
+ data.secret == null ||
1396
+ typeof data.secret !== 'string' ||
1397
+ data.secret === ''
1398
+ ) {
1399
+ return { result: false, message: '2FA secret is invalid.' };
1400
+ }
1401
+
1402
+ if (
1403
+ data.code == null ||
1404
+ typeof data.code !== 'string' ||
1405
+ data.code.length !== 6
1406
+ ) {
1407
+ return { result: false, message: '2FA code is invalid.' };
1408
+ }
1409
+
1410
+ const tilmeld = enforceTilmeld(this);
1411
+
1412
+ const totp = new TOTP({
1413
+ issuer: tilmeld.config.appName,
1414
+ label: this.$data.username,
1415
+ algorithm: 'SHA1',
1416
+ digits: 6,
1417
+ period: 30,
1418
+ secret: data.secret,
1419
+ });
1420
+
1421
+ if (totp.validate({ token: data.code, window: 2 }) == null) {
1422
+ return { result: false, message: '2FA code is incorrect.' };
1423
+ }
1424
+
1425
+ this.$data.totpSecret = data.secret;
1426
+
1427
+ if (await this.$save()) {
1428
+ return { result: true, message: 'Your two factor secret is now set.' };
1429
+ } else {
1430
+ return { result: false, message: "Couldn't save two factor secret." };
1431
+ }
1432
+ }
1433
+
1434
+ /**
1435
+ * A frontend accessible method to remove the TOTP secret from the user's
1436
+ * account.
1437
+ *
1438
+ * @param data The input data from the client.
1439
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1440
+ */
1441
+ public async $removeTOTPSecret(data: {
1442
+ password: string;
1443
+ code: string;
1444
+ }): Promise<{ result: boolean; message: string }> {
1445
+ if (!this.$checkPassword(data.password ?? '')) {
1446
+ return { result: false, message: 'Incorrect password.' };
1447
+ }
1448
+
1449
+ if (this.$data.totpSecret == null) {
1450
+ return { result: false, message: "You don't have a 2FA secret." };
1451
+ }
1452
+
1453
+ if (
1454
+ data.code == null ||
1455
+ typeof data.code !== 'string' ||
1456
+ data.code.length !== 6
1457
+ ) {
1458
+ return { result: false, message: '2FA code is invalid.' };
1459
+ }
1460
+
1461
+ const tilmeld = enforceTilmeld(this);
1462
+
1463
+ const totp = new TOTP({
1464
+ issuer: tilmeld.config.appName,
1465
+ label: this.$data.username,
1466
+ algorithm: 'SHA1',
1467
+ digits: 6,
1468
+ period: 30,
1469
+ secret: this.$data.totpSecret,
1470
+ });
1471
+
1472
+ if (totp.validate({ token: data.code, window: 2 }) == null) {
1473
+ return { result: false, message: '2FA code is incorrect.' };
1474
+ }
1475
+
1476
+ delete this.$data.totpSecret;
1477
+
1478
+ if (await this.$save()) {
1479
+ return {
1480
+ result: true,
1481
+ message: 'Your two factor secret has been removed.',
1482
+ };
1483
+ } else {
1484
+ return { result: false, message: "Couldn't remove two factor secret." };
1485
+ }
1486
+ }
1487
+
1052
1488
  /**
1053
1489
  * Check that a username is valid.
1054
1490
  *
1055
1491
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1056
1492
  */
1057
1493
  public async $checkUsername(): Promise<{ result: boolean; message: string }> {
1058
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1494
+ const tilmeld = enforceTilmeld(this);
1059
1495
  if (!tilmeld.config.emailUsernames) {
1060
1496
  if (this.$data.username == null || !this.$data.username.length) {
1061
1497
  return { result: false, message: 'Please specify a username.' };
@@ -1109,6 +1545,18 @@ export default class User extends AbleObject<UserData> {
1109
1545
  return { result: false, message: 'That username is taken.' };
1110
1546
  }
1111
1547
 
1548
+ for (let callback of (this.constructor as typeof User)
1549
+ .checkUsernameCallbacks) {
1550
+ if (callback) {
1551
+ const result = await callback(this, {
1552
+ username: this.$data.username,
1553
+ });
1554
+ if (!result.result) {
1555
+ return { message: 'Username is not available.', ...result };
1556
+ }
1557
+ }
1558
+ }
1559
+
1112
1560
  return {
1113
1561
  result: true,
1114
1562
  message:
@@ -1142,7 +1590,7 @@ export default class User extends AbleObject<UserData> {
1142
1590
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1143
1591
  */
1144
1592
  public async $checkEmail(): Promise<{ result: boolean; message: string }> {
1145
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1593
+ const tilmeld = enforceTilmeld(this);
1146
1594
  if (this.$data.email == null || !this.$data.email.length) {
1147
1595
  if (tilmeld.config.verifyEmail) {
1148
1596
  return { result: false, message: 'Please specify an email.' };
@@ -1187,7 +1635,7 @@ export default class User extends AbleObject<UserData> {
1187
1635
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1188
1636
  */
1189
1637
  public async $checkPhone(): Promise<{ result: boolean; message: string }> {
1190
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1638
+ const tilmeld = enforceTilmeld(this);
1191
1639
  if (this.$data.phone == null || !this.$data.phone.length) {
1192
1640
  return { result: false, message: 'Please specify a phone number.' };
1193
1641
  }
@@ -1226,7 +1674,7 @@ export default class User extends AbleObject<UserData> {
1226
1674
  password: string;
1227
1675
  additionalData?: { [k: string]: any };
1228
1676
  }): Promise<{ result: boolean; loggedin: boolean; message: string }> {
1229
- let tilmeld = this.$nymph.tilmeld as Tilmeld;
1677
+ let tilmeld = enforceTilmeld(this);
1230
1678
  if (!tilmeld.config.allowRegistration) {
1231
1679
  return {
1232
1680
  result: false,
@@ -1288,7 +1736,7 @@ export default class User extends AbleObject<UserData> {
1288
1736
  const nymph = this.$nymph;
1289
1737
  const tnymph = await this.$nymph.startTransaction(transaction);
1290
1738
  this.$nymph = tnymph;
1291
- tilmeld = this.$nymph.tilmeld as Tilmeld;
1739
+ tilmeld = enforceTilmeld(this);
1292
1740
 
1293
1741
  try {
1294
1742
  // Add primary group.
@@ -1493,15 +1941,15 @@ export default class User extends AbleObject<UserData> {
1493
1941
  }
1494
1942
 
1495
1943
  public async $save() {
1496
- let tilmeld = this.$nymph.tilmeld as Tilmeld;
1944
+ let tilmeld = enforceTilmeld(this);
1497
1945
  if (this.$data.username == null || !this.$data.username.trim().length) {
1498
1946
  return false;
1499
1947
  }
1500
1948
 
1501
1949
  if (
1502
- tilmeld.gatekeeper('tilmeld/admin') &&
1503
- !tilmeld.gatekeeper('system/admin') &&
1504
- this.$gatekeeper('system/admin')
1950
+ (await tilmeld.gatekeeper('tilmeld/admin')) &&
1951
+ !(await tilmeld.gatekeeper('system/admin')) &&
1952
+ this.$data.abilities?.includes('system/admin')
1505
1953
  ) {
1506
1954
  throw new BadDataError(
1507
1955
  "You don't have the authority to modify system admins."
@@ -1594,7 +2042,7 @@ export default class User extends AbleObject<UserData> {
1594
2042
  }
1595
2043
 
1596
2044
  // Email changes.
1597
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
2045
+ if (!(await tilmeld.gatekeeper('tilmeld/admin'))) {
1598
2046
  // The user isn't an admin, so email address changes should contain some
1599
2047
  // security measures.
1600
2048
  if (tilmeld.config.verifyEmail) {
@@ -1691,21 +2139,20 @@ export default class User extends AbleObject<UserData> {
1691
2139
  const nymph = this.$nymph;
1692
2140
  const tnymph = await this.$nymph.startTransaction(transaction);
1693
2141
  this.$nymph = tnymph;
1694
- tilmeld = this.$nymph.tilmeld as Tilmeld;
2142
+ tilmeld = enforceTilmeld(this);
1695
2143
 
1696
- if (
1697
- this.$data.group != null &&
1698
- this.$data.group.user != null &&
1699
- this.$is(this.$data.group.user)
1700
- ) {
2144
+ const group = this.$data.group;
2145
+ await group?.$wake();
2146
+ if (group != null && group.user != null && this.$is(group.user)) {
1701
2147
  try {
1702
2148
  // Update the user's generated primary group.
1703
- this.$data.group.groupname = this.$data.username;
1704
- this.$data.group.avatar = this.$data.avatar;
1705
- this.$data.group.email = this.$data.email;
1706
- this.$data.group.name = this.$data.name;
1707
- this.$data.group.phone = this.$data.phone;
1708
- await this.$data.group.$saveSkipAC();
2149
+ group.groupname = this.$data.username;
2150
+ group.avatar = this.$data.avatar;
2151
+ group.email = this.$data.email;
2152
+ group.name = this.$data.name;
2153
+ group.phone = this.$data.phone;
2154
+ await group.$saveSkipAC();
2155
+ this.$data.group = group;
1709
2156
  } catch (e: any) {
1710
2157
  await tnymph.rollback(transaction);
1711
2158
  this.$nymph = nymph;
@@ -1742,9 +2189,9 @@ export default class User extends AbleObject<UserData> {
1742
2189
  this.$gatekeeperCache = undefined;
1743
2190
  await tnymph.commit(transaction);
1744
2191
 
1745
- if ((nymph.tilmeld as Tilmeld).User.current(true).$is(this)) {
2192
+ if (enforceTilmeld(nymph).User.current(true).$is(this)) {
1746
2193
  // Update the user in the session cache.
1747
- (nymph.tilmeld as Tilmeld).fillSession(this);
2194
+ enforceTilmeld(nymph).fillSession(this);
1748
2195
  }
1749
2196
  } else {
1750
2197
  await tnymph.rollback(transaction);
@@ -1770,13 +2217,13 @@ export default class User extends AbleObject<UserData> {
1770
2217
  }
1771
2218
 
1772
2219
  public async $delete() {
1773
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1774
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
2220
+ const tilmeld = enforceTilmeld(this);
2221
+ if (!(await tilmeld.gatekeeper('tilmeld/admin'))) {
1775
2222
  throw new BadDataError("You don't have the authority to delete users.");
1776
2223
  }
1777
2224
  if (
1778
- !tilmeld.gatekeeper('system/admin') &&
1779
- this.$gatekeeper('system/admin')
2225
+ !(await tilmeld.gatekeeper('system/admin')) &&
2226
+ this.$data.abilities?.includes('system/admin')
1780
2227
  ) {
1781
2228
  throw new BadDataError(
1782
2229
  "You don't have the authority to delete system admins."
@@ -1790,7 +2237,9 @@ export default class User extends AbleObject<UserData> {
1790
2237
 
1791
2238
  public static on<T extends EventType>(
1792
2239
  event: T,
1793
- callback: T extends 'beforeRegister'
2240
+ callback: T extends 'checkUsername'
2241
+ ? TilmeldCheckUsernameCallback
2242
+ : T extends 'beforeRegister'
1794
2243
  ? TilmeldBeforeRegisterCallback
1795
2244
  : T extends 'afterRegister'
1796
2245
  ? TilmeldAfterRegisterCallback
@@ -1804,7 +2253,9 @@ export default class User extends AbleObject<UserData> {
1804
2253
  ? TilmeldAfterLogoutCallback
1805
2254
  : never
1806
2255
  ) {
1807
- const prop = (event + 'Callbacks') as T extends 'beforeRegister'
2256
+ const prop = (event + 'Callbacks') as T extends 'checkUsername'
2257
+ ? 'checkUsernameCallbacks'
2258
+ : T extends 'beforeRegister'
1808
2259
  ? 'beforeRegisterCallbacks'
1809
2260
  : T extends 'afterRegister'
1810
2261
  ? 'afterRegisterCallbacks'
@@ -1827,7 +2278,9 @@ export default class User extends AbleObject<UserData> {
1827
2278
 
1828
2279
  public static off<T extends EventType>(
1829
2280
  event: T,
1830
- callback: T extends 'beforeRegister'
2281
+ callback: T extends 'checkUsername'
2282
+ ? TilmeldCheckUsernameCallback
2283
+ : T extends 'beforeRegister'
1831
2284
  ? TilmeldBeforeRegisterCallback
1832
2285
  : T extends 'afterRegister'
1833
2286
  ? TilmeldAfterRegisterCallback
@@ -1841,7 +2294,9 @@ export default class User extends AbleObject<UserData> {
1841
2294
  ? TilmeldAfterLogoutCallback
1842
2295
  : never
1843
2296
  ) {
1844
- const prop = (event + 'Callbacks') as T extends 'beforeRegister'
2297
+ const prop = (event + 'Callbacks') as T extends 'checkUsername'
2298
+ ? 'checkUsernameCallbacks'
2299
+ : T extends 'beforeRegister'
1845
2300
  ? 'beforeRegisterCallbacks'
1846
2301
  : T extends 'afterRegister'
1847
2302
  ? 'afterRegisterCallbacks'