@nymphjs/tilmeld 1.0.0-beta.6 → 1.0.0-beta.61

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 (57) hide show
  1. package/CHANGELOG.md +284 -0
  2. package/README.md +14 -1
  3. package/dist/AbleObject.d.ts +15 -0
  4. package/dist/AbleObject.js +16 -1
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +119 -8
  7. package/dist/Group.js +230 -97
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.test.js +3 -2
  10. package/dist/Group.test.js.map +1 -1
  11. package/dist/Tilmeld.d.ts +237 -9
  12. package/dist/Tilmeld.js +559 -195
  13. package/dist/Tilmeld.js.map +1 -1
  14. package/dist/User.d.ts +333 -14
  15. package/dist/User.js +797 -204
  16. package/dist/User.js.map +1 -1
  17. package/dist/User.test.js +3 -2
  18. package/dist/User.test.js.map +1 -1
  19. package/dist/conf/d.d.ts +250 -3
  20. package/dist/conf/defaults.js +146 -98
  21. package/dist/conf/defaults.js.map +1 -1
  22. package/dist/enforceTilmeld.d.ts +3 -0
  23. package/dist/enforceTilmeld.js +22 -0
  24. package/dist/enforceTilmeld.js.map +1 -0
  25. package/dist/errors/AccessControlError.d.ts +2 -1
  26. package/dist/errors/AccessControlError.js +3 -2
  27. package/dist/errors/AccessControlError.js.map +1 -1
  28. package/dist/errors/BadDataError.d.ts +2 -1
  29. package/dist/errors/BadDataError.js +3 -2
  30. package/dist/errors/BadDataError.js.map +1 -1
  31. package/dist/errors/BadEmailError.d.ts +2 -1
  32. package/dist/errors/BadEmailError.js +3 -2
  33. package/dist/errors/BadEmailError.js.map +1 -1
  34. package/dist/errors/BadUsernameError.d.ts +2 -1
  35. package/dist/errors/BadUsernameError.js +3 -2
  36. package/dist/errors/BadUsernameError.js.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +1 -0
  39. package/dist/index.js.map +1 -1
  40. package/package.json +30 -26
  41. package/src/AbleObject.ts +2 -2
  42. package/src/Group.test.ts +3 -3
  43. package/src/Group.ts +194 -143
  44. package/src/Tilmeld.ts +384 -249
  45. package/src/User.test.ts +3 -3
  46. package/src/User.ts +876 -266
  47. package/src/conf/d.ts +26 -13
  48. package/src/conf/defaults.ts +151 -104
  49. package/src/enforceTilmeld.ts +20 -0
  50. package/src/errors/AccessControlError.ts +4 -2
  51. package/src/errors/BadDataError.ts +4 -2
  52. package/src/errors/BadEmailError.ts +4 -2
  53. package/src/errors/BadUsernameError.ts +4 -2
  54. package/src/index.ts +2 -0
  55. package/src/tilmeldJoiProps.ts +9 -9
  56. package/tsconfig.json +1 -1
  57. package/typedoc.json +4 -0
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,23 +26,33 @@ 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.
36
48
  */
37
49
  export type TilmeldBeforeRegisterCallback = (
38
50
  user: User & UserData,
39
- data: { password: string; additionalData?: { [k: string]: any } }
51
+ data: { password: string; additionalData?: { [k: string]: any } },
40
52
  ) => Promise<void>;
41
53
  export type TilmeldAfterRegisterCallback = (
42
54
  user: User & UserData,
43
- result: { loggedin: boolean; message: string }
55
+ result: { loggedin: boolean; message: string },
44
56
  ) => Promise<void>;
45
57
  /**
46
58
  * These are run after the authentication checks, but before the login action.
@@ -51,20 +63,20 @@ export type TilmeldBeforeLoginCallback = (
51
63
  username: string;
52
64
  password: string;
53
65
  additionalData?: { [k: string]: any };
54
- }
66
+ },
55
67
  ) => Promise<void>;
56
68
  /**
57
69
  * This is run before the transaction is committed, and you can perform
58
70
  * additional functions on the transaction, which is available in `user.$nymph`.
59
71
  */
60
72
  export type TilmeldAfterLoginCallback = (
61
- user: User & UserData
73
+ user: User & UserData,
62
74
  ) => Promise<void>;
63
75
  export type TilmeldBeforeLogoutCallback = (
64
- user: User & UserData
76
+ user: User & UserData,
65
77
  ) => Promise<void>;
66
78
  export type TilmeldAfterLogoutCallback = (
67
- user: User & UserData
79
+ user: User & UserData,
68
80
  ) => Promise<void>;
69
81
 
70
82
  export type UserData = {
@@ -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
 
@@ -257,10 +289,6 @@ export default class User extends AbleObject<UserData> {
257
289
  */
258
290
  public $originalEmail?: string;
259
291
 
260
- static async factory(guid?: string): Promise<User & UserData> {
261
- return (await super.factory(guid)) as User & UserData;
262
- }
263
-
264
292
  static async factoryUsername(username?: string): Promise<User & UserData> {
265
293
  const entity = new this();
266
294
  if (username != null) {
@@ -271,7 +299,7 @@ export default class User extends AbleObject<UserData> {
271
299
  {
272
300
  type: '&',
273
301
  ilike: ['username', username.replace(/([\\%_])/g, (s) => `\\${s}`)],
274
- }
302
+ },
275
303
  );
276
304
  if (entity != null) {
277
305
  return entity;
@@ -280,34 +308,29 @@ export default class User extends AbleObject<UserData> {
280
308
  return entity;
281
309
  }
282
310
 
283
- static factorySync(guid?: string): User & UserData {
284
- return super.factorySync(guid) as User & UserData;
285
- }
286
-
287
- constructor(guid?: string) {
288
- super(guid);
311
+ constructor() {
312
+ super();
289
313
 
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
- }
314
+ // Defaults.
315
+ this.$data.enabled = true;
316
+ this.$data.abilities = [];
317
+ this.$data.groups = [];
318
+ this.$data.inheritAbilities = true;
319
+ this.$updateDataProtection();
298
320
  }
299
321
 
300
322
  public static current(returnObjectIfNotExist: true): User & UserData;
301
323
  public static current(
302
- returnObjectIfNotExist?: false
324
+ returnObjectIfNotExist?: false,
303
325
  ): (User & UserData) | null;
304
326
  public static current(
305
- returnObjectIfNotExist?: boolean
327
+ returnObjectIfNotExist?: boolean,
306
328
  ): (User & UserData) | null {
307
- if (this.tilmeld.currentUser == null) {
329
+ const tilmeld = enforceTilmeld(this);
330
+ if (tilmeld.currentUser == null) {
308
331
  return returnObjectIfNotExist ? this.factorySync() : null;
309
332
  }
310
- return this.tilmeld.currentUser;
333
+ return tilmeld.currentUser;
311
334
  }
312
335
 
313
336
  /**
@@ -320,7 +343,8 @@ export default class User extends AbleObject<UserData> {
320
343
  recoveryType: 'username' | 'password';
321
344
  account: string;
322
345
  }): Promise<{ result: boolean; message: string }> {
323
- if (!this.tilmeld.config.pwRecovery) {
346
+ const tilmeld = enforceTilmeld(this);
347
+ if (!tilmeld.config.pwRecovery) {
324
348
  return {
325
349
  result: false,
326
350
  message: 'Account recovery is not allowed.',
@@ -330,18 +354,15 @@ export default class User extends AbleObject<UserData> {
330
354
  let user: User & UserData;
331
355
  const options: EmailOptions = {};
332
356
 
333
- if (
334
- !this.tilmeld.config.emailUsernames &&
335
- data.recoveryType === 'username'
336
- ) {
357
+ if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
337
358
  // Create a username recovery email.
338
359
 
339
360
  const getUser = await this.nymph.getEntity(
340
- { class: this.tilmeld.User, skipAc: true },
361
+ { class: tilmeld.User, skipAc: true },
341
362
  {
342
363
  type: '&',
343
364
  ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
344
- }
365
+ },
345
366
  );
346
367
 
347
368
  if (getUser == null) {
@@ -364,7 +385,7 @@ export default class User extends AbleObject<UserData> {
364
385
  } else if (data.recoveryType === 'password') {
365
386
  // Create a password recovery email.
366
387
 
367
- const getUser = await this.tilmeld.User.factoryUsername(data.account);
388
+ const getUser = await tilmeld.User.factoryUsername(data.account);
368
389
 
369
390
  if (getUser.guid == null) {
370
391
  return {
@@ -391,14 +412,19 @@ export default class User extends AbleObject<UserData> {
391
412
  };
392
413
  options.locals = {
393
414
  recoverCode: getUser.recoverSecret,
394
- timeLimit: this.tilmeld.config.pwRecoveryTimeLimit,
415
+ timeLimit: tilmeld.config.pwRecoveryTimeLimit,
395
416
  };
396
417
  } else {
397
418
  return { result: false, message: 'Invalid recovery type.' };
398
419
  }
399
420
 
421
+ this.nymph.config.debugLog(
422
+ 'tilmeld',
423
+ `User account recovery (${options.template}) request for "${user.username}".`,
424
+ );
425
+
400
426
  // Send the email.
401
- if (await this.tilmeld.config.sendEmail(this.tilmeld, options, user)) {
427
+ if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
402
428
  return {
403
429
  result: true,
404
430
  message:
@@ -421,22 +447,23 @@ export default class User extends AbleObject<UserData> {
421
447
  secret: string;
422
448
  password: string;
423
449
  }): Promise<{ result: boolean; message: string }> {
424
- if (!this.tilmeld.config.pwRecovery) {
450
+ const tilmeld = enforceTilmeld(this);
451
+ if (!tilmeld.config.pwRecovery) {
425
452
  return {
426
453
  result: false,
427
454
  message: 'Account recovery is not allowed.',
428
455
  };
429
456
  }
430
457
 
431
- const user = await this.tilmeld.User.factoryUsername(data.username);
458
+ const user = await tilmeld.User.factoryUsername(data.username);
432
459
 
433
460
  if (
434
461
  user.guid == null ||
435
462
  user.recoverSecret == null ||
436
463
  data.secret !== user.recoverSecret ||
437
464
  strtotime(
438
- '+' + this.tilmeld.config.pwRecoveryTimeLimit,
439
- Math.floor((user.recoverSecretDate ?? 0) / 1000)
465
+ '+' + tilmeld.config.pwRecoveryTimeLimit,
466
+ Math.floor((user.recoverSecretDate ?? 0) / 1000),
440
467
  ) *
441
468
  1000 <
442
469
  Date.now()
@@ -451,6 +478,11 @@ export default class User extends AbleObject<UserData> {
451
478
  return { result: false, message: 'Password cannot be empty.' };
452
479
  }
453
480
 
481
+ this.nymph.config.debugLog(
482
+ 'tilmeld',
483
+ `User account recovery for "${user.username}".`,
484
+ );
485
+
454
486
  user.$password(data.password);
455
487
  delete user.recoverSecret;
456
488
  delete user.recoverSecretDate;
@@ -466,27 +498,30 @@ export default class User extends AbleObject<UserData> {
466
498
  }
467
499
 
468
500
  public static getClientConfig() {
501
+ const tilmeld = enforceTilmeld(this);
469
502
  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,
503
+ regFields: tilmeld.config.regFields,
504
+ userFields: tilmeld.config.userFields,
505
+ emailUsernames: tilmeld.config.emailUsernames,
506
+ allowRegistration: tilmeld.config.allowRegistration,
507
+ allowUsernameChange: tilmeld.config.allowUsernameChange,
508
+ pwRecovery: tilmeld.config.pwRecovery,
509
+ verifyEmail: tilmeld.config.verifyEmail,
510
+ unverifiedAccess: tilmeld.config.unverifiedAccess,
478
511
  };
479
512
  }
480
513
 
481
514
  public static async loginUser(data: {
482
515
  username: string;
483
516
  password: string;
517
+ code?: string;
484
518
  additionalData?: { [k: string]: any };
485
519
  }) {
520
+ const tilmeld = enforceTilmeld(this);
486
521
  if (!('username' in data) || !data.username.length) {
487
522
  return { result: false, message: 'Incorrect login/password.' };
488
523
  }
489
- const user = await this.tilmeld.User.factoryUsername(data.username);
524
+ const user = await tilmeld.User.factoryUsername(data.username);
490
525
  const result: { result: boolean; message: string; user?: User & UserData } =
491
526
  await user.$login(data);
492
527
  if (result.result) {
@@ -499,22 +534,41 @@ export default class User extends AbleObject<UserData> {
499
534
  public async $login(data: {
500
535
  username: string;
501
536
  password: string;
537
+ code?: string;
502
538
  additionalData?: { [k: string]: any };
503
539
  }) {
504
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
540
+ const tilmeld = enforceTilmeld(this);
505
541
  if (this.guid == null) {
506
542
  return { result: false, message: 'Incorrect login/password.' };
507
543
  }
508
544
  if (!this.$data.enabled) {
509
545
  return { result: false, message: 'This user is disabled.' };
510
546
  }
511
- if (this.$gatekeeper()) {
547
+ if (await this.$gatekeeper()) {
512
548
  return { result: true, message: 'You are already logged in.' };
513
549
  }
514
550
  if (!this.$checkPassword(data.password)) {
515
551
  return { result: false, message: 'Incorrect login/password.' };
516
552
  }
517
553
 
554
+ if (this.$data.totpSecret != null) {
555
+ if (
556
+ data.code == null ||
557
+ typeof data.code !== 'string' ||
558
+ data.code.length !== 6
559
+ ) {
560
+ return {
561
+ result: false,
562
+ needTOTP: true,
563
+ message: 'You need to provide a 2FA code.',
564
+ };
565
+ }
566
+
567
+ if (!this.$checkTOTPCode(data.code)) {
568
+ return { result: false, message: 'Incorrect 2FA code.' };
569
+ }
570
+ }
571
+
518
572
  try {
519
573
  for (let callback of (this.constructor as typeof User)
520
574
  .beforeLoginCallbacks) {
@@ -529,8 +583,90 @@ export default class User extends AbleObject<UserData> {
529
583
  };
530
584
  }
531
585
 
586
+ this.$nymph.config.debugLog(
587
+ 'tilmeld',
588
+ `User login for "${this.$data.username}".`,
589
+ );
590
+
532
591
  // Authentication was successful, attempt to login.
533
- if (!tilmeld.login(this, true)) {
592
+ if (!(await tilmeld.login(this, true))) {
593
+ return { result: false, message: 'Incorrect login/password.' };
594
+ }
595
+
596
+ for (let callback of (this.constructor as typeof User)
597
+ .afterLoginCallbacks) {
598
+ if (callback) {
599
+ await callback(this);
600
+ }
601
+ }
602
+
603
+ // Login was successful.
604
+ return { result: true, message: 'You are logged in.' };
605
+ }
606
+
607
+ public async $switchUser(data?: { additionalData?: { [k: string]: any } }) {
608
+ const tilmeld = enforceTilmeld(this);
609
+ if (this.guid == null) {
610
+ return { result: false, message: 'Incorrect login/password.' };
611
+ }
612
+
613
+ let user = tilmeld.User.current();
614
+
615
+ if (!user || user.guid == null) {
616
+ return { result: false, message: "You're not logged in." };
617
+ }
618
+
619
+ const isCurrentUser = user != null && user.$is(this);
620
+
621
+ if (isCurrentUser) {
622
+ return { result: false, message: "You can't switch to yourself." };
623
+ }
624
+
625
+ const toAbilities = this.$data.abilities || [];
626
+ const fromAbilities = user.abilities || [];
627
+
628
+ if (
629
+ !fromAbilities.includes('tilmeld/switch') &&
630
+ !fromAbilities.includes('system/admin')
631
+ ) {
632
+ return {
633
+ result: false,
634
+ message: "You don't have permission to switch users.",
635
+ };
636
+ }
637
+
638
+ if (
639
+ toAbilities.includes('system/admin') ||
640
+ toAbilities.includes('tilmeld/admin')
641
+ ) {
642
+ return { result: false, message: "You can't switch to an admin." };
643
+ }
644
+
645
+ try {
646
+ for (let callback of (this.constructor as typeof User)
647
+ .beforeLoginCallbacks) {
648
+ if (callback) {
649
+ await callback(this, {
650
+ username: this.$data.username || '',
651
+ password: '',
652
+ ...(data || {}),
653
+ });
654
+ }
655
+ }
656
+ } catch (e: any) {
657
+ return {
658
+ result: false,
659
+ message: e.message,
660
+ };
661
+ }
662
+
663
+ this.$nymph.config.debugLog(
664
+ 'tilmeld',
665
+ `User login switch for "${user.username}" to "${this.$data.username}".`,
666
+ );
667
+
668
+ // Authentication was successful, attempt to login.
669
+ if (!(await tilmeld.loginSwitch(this, true))) {
534
670
  return { result: false, message: 'Incorrect login/password.' };
535
671
  }
536
672
 
@@ -550,7 +686,7 @@ export default class User extends AbleObject<UserData> {
550
686
  * @returns An object with a boolean 'result' entry and a 'message' entry.
551
687
  */
552
688
  public async $logout() {
553
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
689
+ const tilmeld = enforceTilmeld(this);
554
690
 
555
691
  try {
556
692
  for (let callback of (this.constructor as typeof User)
@@ -566,7 +702,12 @@ export default class User extends AbleObject<UserData> {
566
702
  };
567
703
  }
568
704
 
569
- tilmeld.logout();
705
+ this.$nymph.config.debugLog(
706
+ 'tilmeld',
707
+ `User logout for "${this.$data.username}".`,
708
+ );
709
+
710
+ await tilmeld.logout();
570
711
 
571
712
  for (let callback of (this.constructor as typeof User)
572
713
  .afterLogoutCallbacks) {
@@ -592,36 +733,36 @@ export default class User extends AbleObject<UserData> {
592
733
  );
593
734
  }
594
735
 
595
- /**
596
- * Get the user's group descendants.
597
- */
598
- public async $getDescendantGroups(): Promise<(Group & GroupData)[]> {
599
- if (this.$descendantGroups == null) {
600
- this.$descendantGroups = [];
601
- if (this.$data.group != null) {
602
- this.$descendantGroups = await this.$data.group.$getDescendants();
603
- }
604
- for (let curGroup of this.$data.groups ?? []) {
605
- this.$descendantGroups = this.$descendantGroups?.concat(
606
- await curGroup.$getDescendants()
607
- );
608
- }
736
+ public $getGid() {
737
+ if (this.$data.group) {
738
+ return this.$data.group.guid;
609
739
  }
610
- return this.$descendantGroups;
740
+ return null;
741
+ }
742
+
743
+ public $getGids() {
744
+ if (this.$data.groups) {
745
+ return this.$data.groups
746
+ .map((group) => group.guid)
747
+ .filter((guid) => guid != null) as string[];
748
+ }
749
+ return null;
611
750
  }
612
751
 
613
752
  /**
614
753
  * Get the user's group descendants.
615
754
  */
616
- public $getDescendantGroupsSync(): (Group & GroupData)[] {
755
+ public async $getDescendantGroups(): Promise<(Group & GroupData)[]> {
617
756
  if (this.$descendantGroups == null) {
618
757
  this.$descendantGroups = [];
619
758
  if (this.$data.group != null) {
620
- this.$descendantGroups = this.$data.group.$getDescendantsSync();
759
+ await this.$data.group.$wake();
760
+ this.$descendantGroups = await this.$data.group.$getDescendants();
621
761
  }
762
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
622
763
  for (let curGroup of this.$data.groups ?? []) {
623
764
  this.$descendantGroups = this.$descendantGroups?.concat(
624
- curGroup.$getDescendantsSync()
765
+ await curGroup.$getDescendants(),
625
766
  );
626
767
  }
627
768
  }
@@ -629,18 +770,42 @@ export default class User extends AbleObject<UserData> {
629
770
  }
630
771
 
631
772
  public $jsonAcceptData(input: EntityJson, allowConflict = false) {
632
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
633
- this.$referenceWake();
773
+ const tilmeld = enforceTilmeld(this);
774
+ this.$check();
634
775
 
635
776
  if (
636
777
  'abilities' in input.data &&
637
- input.data.abilities.indexOf('system/admin') !== -1 &&
638
- this.$data.abilities?.indexOf('system/admin') === -1 &&
639
- tilmeld.gatekeeper('tilmeld/admin') &&
640
- !tilmeld.gatekeeper('system/admin')
778
+ input.data.abilities?.includes('system/admin') !==
779
+ this.$data.abilities?.includes('system/admin') &&
780
+ (!tilmeld.currentUser ||
781
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
782
+ ) {
783
+ throw new BadDataError(
784
+ "You don't have the authority to grant or revoke system/admin.",
785
+ );
786
+ }
787
+
788
+ if (
789
+ 'abilities' in input.data &&
790
+ input.data.abilities?.includes('tilmeld/admin') !==
791
+ this.$data.abilities?.includes('tilmeld/admin') &&
792
+ (!tilmeld.currentUser ||
793
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
794
+ ) {
795
+ throw new BadDataError(
796
+ "You don't have the authority to grant or revoke tilmeld/admin.",
797
+ );
798
+ }
799
+
800
+ if (
801
+ 'abilities' in input.data &&
802
+ input.data.abilities?.includes('tilmeld/switch') !==
803
+ this.$data.abilities?.includes('tilmeld/switch') &&
804
+ (!tilmeld.currentUser ||
805
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
641
806
  ) {
642
807
  throw new BadDataError(
643
- "You don't have the authority to make this user a system admin."
808
+ "You don't have the authority to grant or revoke tilmeld/switch.",
644
809
  );
645
810
  }
646
811
 
@@ -649,18 +814,42 @@ export default class User extends AbleObject<UserData> {
649
814
  }
650
815
 
651
816
  public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
652
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
653
- this.$referenceWake();
817
+ const tilmeld = enforceTilmeld(this);
818
+ this.$check();
654
819
 
655
820
  if (
656
821
  'abilities' in patch.set &&
657
- patch.set.abilities.indexOf('system/admin') !== -1 &&
658
- this.$data.abilities?.indexOf('system/admin') === -1 &&
659
- tilmeld.gatekeeper('tilmeld/admin') &&
660
- !tilmeld.gatekeeper('system/admin')
822
+ patch.set.abilities?.includes('system/admin') !==
823
+ this.$data.abilities?.includes('system/admin') &&
824
+ (!tilmeld.currentUser ||
825
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
826
+ ) {
827
+ throw new BadDataError(
828
+ "You don't have the authority to grant or revoke system/admin.",
829
+ );
830
+ }
831
+
832
+ if (
833
+ 'abilities' in patch.set &&
834
+ patch.set.abilities?.includes('tilmeld/admin') !==
835
+ this.$data.abilities?.includes('tilmeld/admin') &&
836
+ (!tilmeld.currentUser ||
837
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
838
+ ) {
839
+ throw new BadDataError(
840
+ "You don't have the authority to grant or revoke tilmeld/admin.",
841
+ );
842
+ }
843
+
844
+ if (
845
+ 'abilities' in patch.set &&
846
+ patch.set.abilities?.includes('tilmeld/switch') !==
847
+ this.$data.abilities?.includes('tilmeld/switch') &&
848
+ (!tilmeld.currentUser ||
849
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
661
850
  ) {
662
851
  throw new BadDataError(
663
- "You don't have the authority to make this user a system admin."
852
+ "You don't have the authority to grant or revoke tilmeld/switch.",
664
853
  );
665
854
  }
666
855
 
@@ -679,7 +868,7 @@ export default class User extends AbleObject<UserData> {
679
868
  * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
680
869
  */
681
870
  public $updateDataProtection(givenUser?: User & UserData) {
682
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
871
+ const tilmeld = enforceTilmeld(this);
683
872
  let user = givenUser ?? tilmeld.User.current();
684
873
 
685
874
  this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
@@ -703,17 +892,24 @@ export default class User extends AbleObject<UserData> {
703
892
  // Users can check to see what abilities they have.
704
893
  this.$clientEnabledMethods.push('$gatekeeper');
705
894
  this.$clientEnabledMethods.push('$changePassword');
895
+ this.$clientEnabledMethods.push('$revokeCurrentTokens');
706
896
  this.$clientEnabledMethods.push('$logout');
707
897
  this.$clientEnabledMethods.push('$sendEmailVerification');
898
+ this.$clientEnabledMethods.push('$hasTOTPSecret');
899
+ this.$clientEnabledMethods.push('$getNewTOTPSecret');
900
+ this.$clientEnabledMethods.push('$saveTOTPSecret');
901
+ this.$clientEnabledMethods.push('$removeTOTPSecret');
708
902
  }
709
903
 
710
904
  if (
711
905
  user != null &&
712
- (abilities.indexOf('tilmeld/admin') !== -1 ||
713
- abilities.indexOf('system/admin') !== -1)
906
+ (abilities.includes('tilmeld/admin') ||
907
+ abilities.includes('system/admin'))
714
908
  ) {
715
909
  // Users who can edit other users can see most of their data.
716
- this.$privateData = ['password', 'salt'];
910
+ this.$privateData = ['password', 'salt', 'totpSecret'];
911
+ this.$clientEnabledMethods.push('$hasTOTPSecret');
912
+ this.$clientEnabledMethods.push('$removeTOTPSecret');
717
913
  this.$allowlistData = undefined;
718
914
  } else if (isCurrentUser || isNewUser) {
719
915
  // Users can see their own data, and edit some of it.
@@ -721,30 +917,69 @@ export default class User extends AbleObject<UserData> {
721
917
  this.$allowlistData.push('username');
722
918
  }
723
919
  this.$allowlistData.push('avatar');
724
- if (tilmeld.config.userFields.indexOf('name') !== -1) {
920
+ if (tilmeld.config.userFields.includes('name')) {
725
921
  this.$allowlistData.push('nameFirst');
726
922
  this.$allowlistData.push('nameMiddle');
727
923
  this.$allowlistData.push('nameLast');
728
924
  this.$allowlistData.push('name');
729
925
  }
730
- if (tilmeld.config.userFields.indexOf('email') !== -1) {
926
+ if (tilmeld.config.userFields.includes('email')) {
731
927
  this.$allowlistData.push('email');
732
928
  }
733
- if (tilmeld.config.userFields.indexOf('phone') !== -1) {
929
+ if (tilmeld.config.userFields.includes('phone')) {
734
930
  this.$allowlistData.push('phone');
735
931
  }
736
932
  this.$privateData = [
933
+ // The things that are commented are there to show what is _not_ private.
934
+ // 'abilities',
935
+ // 'username',
936
+ // 'nameFirst',
937
+ // 'nameMiddle',
938
+ // 'nameLast',
939
+ // 'name',
940
+ // 'email',
941
+ // 'avatar',
942
+ // 'phone',
943
+ // 'group',
944
+ // 'groups',
945
+ // 'inheritAbilities',
946
+ 'enabled',
737
947
  'secret',
948
+ 'emailChangeDate',
738
949
  'newEmailSecret',
739
- 'cancelEmailAddress',
950
+ // 'newEmailAddress',
740
951
  'cancelEmailSecret',
741
- 'emailChangeDate',
952
+ 'cancelEmailAddress',
742
953
  'recoverSecret',
743
954
  'recoverSecretDate',
744
- 'password',
745
955
  'salt',
956
+ 'password',
957
+ 'passwordTemp',
958
+ // 'revokeTokenDate',
959
+ 'totpSecret',
746
960
  ];
747
961
  }
962
+
963
+ if (
964
+ user != null &&
965
+ !isCurrentUser &&
966
+ (abilities.includes('tilmeld/switch') ||
967
+ abilities.includes('system/admin'))
968
+ ) {
969
+ // Users with this ability can switch to other users.
970
+ this.$clientEnabledMethods.push('$switchUser');
971
+ }
972
+ }
973
+
974
+ public $updateGroupDataProtection() {
975
+ if (this.$data.group != null && !this.$data.group.$asleep()) {
976
+ this.$data.group.$updateDataProtection();
977
+ }
978
+ for (let group of this.$data.groups ?? []) {
979
+ if (!group.$asleep()) {
980
+ group.$updateDataProtection();
981
+ }
982
+ }
748
983
  }
749
984
 
750
985
  /**
@@ -761,35 +996,43 @@ export default class User extends AbleObject<UserData> {
761
996
  * @param ability The ability.
762
997
  * @returns True or false.
763
998
  */
764
- public $gatekeeper(ability?: string): boolean {
765
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
999
+ public async $gatekeeper(ability?: string): Promise<boolean> {
1000
+ await this.$wake();
1001
+ const tilmeld = enforceTilmeld(this);
766
1002
  if (ability == null) {
767
1003
  return tilmeld.User.current()?.$is(this) ?? false;
768
1004
  }
769
1005
  // Check the cache to see if we've already checked this user.
770
1006
  if (this.$gatekeeperCache == null) {
771
- let abilities = this.$data.abilities ?? [];
772
- if (this.$data.inheritAbilities) {
773
- for (let curGroup of this.$data.groups ?? []) {
774
- if (curGroup.enabled) {
775
- abilities = abilities.concat(curGroup.abilities ?? []);
776
- }
777
- }
778
- if (
779
- this.$data.group != null &&
780
- this.$data.group.cdate != null &&
781
- this.$data.group.enabled
782
- ) {
783
- abilities = abilities.concat(this.$data.group.abilities ?? []);
784
- }
785
- }
786
- this.$gatekeeperCache = Object.fromEntries(
787
- abilities.map((ability) => [ability, true])
788
- );
1007
+ this.$gatekeeperCache = await this.$getGatekeeperCache();
789
1008
  }
790
1009
  return (
791
1010
  (ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
792
- !!this.$gatekeeperCache['system/admin']
1011
+ !!this.$data.abilities?.includes('system/admin')
1012
+ );
1013
+ }
1014
+
1015
+ /**
1016
+ * Build a gatekeeper cache object.
1017
+ */
1018
+ public async $getGatekeeperCache() {
1019
+ await this.$wake();
1020
+ let abilities = this.$data.abilities ?? [];
1021
+ if (this.$data.inheritAbilities) {
1022
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
1023
+ for (let curGroup of this.$data.groups ?? []) {
1024
+ if (curGroup.enabled) {
1025
+ abilities = abilities.concat(curGroup.abilities ?? []);
1026
+ }
1027
+ }
1028
+ await this.$data.group?.$wake();
1029
+ const group = this.$data.group;
1030
+ if (group != null && group.cdate != null && group.enabled) {
1031
+ abilities = abilities.concat(group.abilities ?? []);
1032
+ }
1033
+ }
1034
+ return Object.fromEntries(
1035
+ abilities.map((ability) => [ability, true as true]),
793
1036
  );
794
1037
  }
795
1038
 
@@ -806,7 +1049,7 @@ export default class User extends AbleObject<UserData> {
806
1049
  * @returns True on success, false on failure.
807
1050
  */
808
1051
  public async $sendEmailVerification() {
809
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1052
+ const tilmeld = enforceTilmeld(this);
810
1053
  if (this.guid == null) {
811
1054
  return false;
812
1055
  }
@@ -818,7 +1061,7 @@ export default class User extends AbleObject<UserData> {
818
1061
 
819
1062
  if (this.$data.secret != null) {
820
1063
  const link = `${verifyUrl}?action=verify&id=${encodeURIComponent(
821
- this.guid
1064
+ this.guid,
822
1065
  )}&secret=${encodeURIComponent(this.$data.secret)}`;
823
1066
  success =
824
1067
  success &&
@@ -836,13 +1079,13 @@ export default class User extends AbleObject<UserData> {
836
1079
  verifyLink: link,
837
1080
  },
838
1081
  },
839
- this
1082
+ this,
840
1083
  ));
841
1084
  }
842
1085
 
843
1086
  if (this.$data.newEmailSecret != null) {
844
1087
  const link = `${verifyUrl}?action=verifychange&id=${encodeURIComponent(
845
- this.guid
1088
+ this.guid,
846
1089
  )}&secret=${encodeURIComponent(this.$data.newEmailSecret)}`;
847
1090
  success =
848
1091
  success &&
@@ -862,13 +1105,13 @@ export default class User extends AbleObject<UserData> {
862
1105
  newEmail: this.$data.newEmailAddress,
863
1106
  },
864
1107
  },
865
- this
1108
+ this,
866
1109
  ));
867
1110
  }
868
1111
 
869
1112
  if (this.$data.cancelEmailSecret != null) {
870
1113
  const link = `${verifyUrl}?action=cancelchange&id=${encodeURIComponent(
871
- this.guid
1114
+ this.guid,
872
1115
  )}&secret=${encodeURIComponent(this.$data.cancelEmailSecret)}`;
873
1116
  success =
874
1117
  success &&
@@ -888,7 +1131,7 @@ export default class User extends AbleObject<UserData> {
888
1131
  newEmail: this.$data.newEmailAddress ?? this.$data.email,
889
1132
  },
890
1133
  },
891
- this
1134
+ this,
892
1135
  ));
893
1136
  }
894
1137
 
@@ -901,13 +1144,16 @@ export default class User extends AbleObject<UserData> {
901
1144
  * @param group The group.
902
1145
  * @returns True if the user is already in the group. The resulting array of groups if the user was not.
903
1146
  */
904
- public $addGroup(group: Group & GroupData) {
905
- if (this.$data.groups == null) {
906
- this.$data.groups = [];
1147
+ public async $addGroup(group: Group & GroupData) {
1148
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
1149
+ let groups = this.$data.groups;
1150
+ if (groups == null) {
1151
+ groups = [];
907
1152
  }
908
- if (!group.$inArray(this.$data.groups)) {
909
- this.$data.groups.push(group);
910
- return this.$data.groups;
1153
+ if (!group.$inArray(groups)) {
1154
+ groups.push(group);
1155
+ this.$data.groups = groups;
1156
+ return groups;
911
1157
  }
912
1158
  return true;
913
1159
  }
@@ -919,7 +1165,7 @@ export default class User extends AbleObject<UserData> {
919
1165
  * @returns True if the passwords match, otherwise false.
920
1166
  */
921
1167
  public $checkPassword(password: string) {
922
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1168
+ const tilmeld = enforceTilmeld(this);
923
1169
  switch (tilmeld.config.pwMethod) {
924
1170
  case 'plain':
925
1171
  return this.$data.password === password;
@@ -940,10 +1186,12 @@ export default class User extends AbleObject<UserData> {
940
1186
  * @param group The group.
941
1187
  * @returns True if the user wasn't in the group. The resulting array of groups if the user was.
942
1188
  */
943
- public $delGroup(group: Group & GroupData) {
944
- if (this.$data.groups != null && group.$inArray(this.$data.groups)) {
1189
+ public async $delGroup(group: Group & GroupData) {
1190
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
1191
+ let groups = this.$data.groups || [];
1192
+ if (this.$data.groups != null && group.$inArray(groups)) {
945
1193
  const newGroups: (Group & GroupData)[] = [];
946
- for (let curGroup of this.$data.groups) {
1194
+ for (let curGroup of groups) {
947
1195
  if (!group.$is(curGroup)) {
948
1196
  newGroups.push(curGroup);
949
1197
  }
@@ -960,7 +1208,7 @@ export default class User extends AbleObject<UserData> {
960
1208
  * @param group The group, or the group's GUID.
961
1209
  * @returns True or false.
962
1210
  */
963
- public $inGroup(group: (Group & GroupData) | string) {
1211
+ public async $inGroup(group: (Group & GroupData) | string) {
964
1212
  if (typeof group === 'string') {
965
1213
  if (this.$data.group?.guid === group) {
966
1214
  return true;
@@ -983,16 +1231,19 @@ export default class User extends AbleObject<UserData> {
983
1231
  * @param group The group, or the group's GUID.
984
1232
  * @returns True or false.
985
1233
  */
986
- public $isDescendant(group: (Group & GroupData) | string) {
1234
+ public async $isDescendant(group: (Group & GroupData) | string) {
987
1235
  // Check to see if the user is in a descendant group of the given group.
1236
+ await this.$data.group?.$wake();
988
1237
  if (
1238
+ this.$data.group != null &&
989
1239
  this.$data.group?.cdate != null &&
990
- this.$data.group.$isDescendant(group)
1240
+ (await this.$data.group?.$isDescendant(group))
991
1241
  ) {
992
1242
  return true;
993
1243
  }
1244
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
994
1245
  for (let curGroup of this.$data.groups ?? []) {
995
- if (curGroup.$isDescendant(group)) {
1246
+ if (await curGroup.$isDescendant(group)) {
996
1247
  return true;
997
1248
  }
998
1249
  }
@@ -1008,15 +1259,31 @@ export default class User extends AbleObject<UserData> {
1008
1259
  public async $changePassword(data: {
1009
1260
  newPassword: string;
1010
1261
  currentPassword: string;
1262
+ revokeCurrentTokens?: boolean;
1011
1263
  }): Promise<{ result: boolean; message: string }> {
1264
+ this.$nymph.config.debugLog(
1265
+ 'tilmeld',
1266
+ `Password change request for "${this.$data.username}".`,
1267
+ );
1012
1268
  if (!('newPassword' in data) || !data.newPassword.length) {
1013
1269
  return { result: false, message: 'Please specify a password.' };
1014
1270
  }
1015
1271
  if (!this.$checkPassword(data.currentPassword ?? '')) {
1272
+ this.$nymph.config.debugLog(
1273
+ 'tilmeld',
1274
+ `Incorrect password for change request for "${this.$data.username}".`,
1275
+ );
1016
1276
  return { result: false, message: 'Incorrect password.' };
1017
1277
  }
1018
1278
  this.$data.passwordTemp = data.newPassword;
1279
+ if (data.revokeCurrentTokens) {
1280
+ this.$data.revokeTokenDate = Date.now();
1281
+ }
1019
1282
  if (await this.$save()) {
1283
+ if (data.revokeCurrentTokens) {
1284
+ const tilmeld = enforceTilmeld(this);
1285
+ await tilmeld.login(this, true);
1286
+ }
1020
1287
  return { result: true, message: 'Your password has been changed.' };
1021
1288
  } else {
1022
1289
  return { result: false, message: "Couldn't save new password." };
@@ -1030,7 +1297,7 @@ export default class User extends AbleObject<UserData> {
1030
1297
  * @returns The resulting password or hash which is stored in the entity.
1031
1298
  */
1032
1299
  public $password(password: string) {
1033
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1300
+ const tilmeld = enforceTilmeld(this);
1034
1301
  switch (tilmeld.config.pwMethod) {
1035
1302
  case 'plain':
1036
1303
  delete this.$data.salt;
@@ -1044,28 +1311,250 @@ export default class User extends AbleObject<UserData> {
1044
1311
  default:
1045
1312
  this.$data.salt = nanoid();
1046
1313
  this.$data.password = sha256(password + this.$data.salt).toString(
1047
- Base64
1314
+ Base64,
1048
1315
  );
1049
1316
  break;
1050
1317
  }
1051
1318
  return this.$data.password;
1052
1319
  }
1053
1320
 
1321
+ /**
1322
+ * A frontend accessible method to revoke all currently issued tokens.
1323
+ *
1324
+ * @param data The input data from the client.
1325
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1326
+ */
1327
+ public async $revokeCurrentTokens(data: {
1328
+ password: string;
1329
+ }): Promise<{ result: boolean; message: string }> {
1330
+ if (!this.$checkPassword(data.password ?? '')) {
1331
+ return { result: false, message: 'Incorrect password.' };
1332
+ }
1333
+
1334
+ this.$data.revokeTokenDate = Date.now();
1335
+
1336
+ if (await this.$save()) {
1337
+ const tilmeld = enforceTilmeld(this);
1338
+ await tilmeld.login(this, true);
1339
+ return {
1340
+ result: true,
1341
+ message: 'You have logged out of all other sessions.',
1342
+ };
1343
+ } else {
1344
+ return { result: false, message: "Couldn't save revocation date." };
1345
+ }
1346
+ }
1347
+
1348
+ public async $hasTOTPSecret() {
1349
+ return this.$data.totpSecret != null;
1350
+ }
1351
+
1352
+ /**
1353
+ * Check the given code against the user's TOTP secret.
1354
+ *
1355
+ * @param code The code in question.
1356
+ * @returns True if the code is valid, otherwise false.
1357
+ */
1358
+ public $checkTOTPCode(code: string) {
1359
+ if (this.$data.totpSecret == null) {
1360
+ return false;
1361
+ }
1362
+
1363
+ const tilmeld = enforceTilmeld(this);
1364
+
1365
+ const totp = new TOTP({
1366
+ issuer: tilmeld.config.appName,
1367
+ label: this.$data.username,
1368
+ algorithm: 'SHA1',
1369
+ digits: 6,
1370
+ period: 30,
1371
+ secret: this.$data.totpSecret,
1372
+ });
1373
+
1374
+ return totp.validate({ token: code, window: 2 }) != null;
1375
+ }
1376
+
1377
+ /**
1378
+ * A frontend accessible method to generate a new TOTP secret.
1379
+ *
1380
+ * @returns An object with 'uri', 'qrcode', and 'secret'.
1381
+ */
1382
+ public async $getNewTOTPSecret() {
1383
+ const tilmeld = enforceTilmeld(this);
1384
+
1385
+ if (this.$data.totpSecret != null) {
1386
+ throw new BadDataError('You already have a 2FA secret.');
1387
+ }
1388
+
1389
+ const secret = new Secret();
1390
+
1391
+ const totp = new TOTP({
1392
+ issuer: tilmeld.config.appName,
1393
+ label: this.$data.username,
1394
+ algorithm: 'SHA1',
1395
+ digits: 6,
1396
+ period: 30,
1397
+ secret,
1398
+ });
1399
+
1400
+ const uri = totp.toString();
1401
+
1402
+ const qrcode = await toDataURL(uri);
1403
+
1404
+ return {
1405
+ uri,
1406
+ qrcode,
1407
+ secret: secret.base32,
1408
+ };
1409
+ }
1410
+
1411
+ /**
1412
+ * A frontend accessible method to save a TOTP secret into the user's account.
1413
+ *
1414
+ * @param data The input data from the client.
1415
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1416
+ */
1417
+ public async $saveTOTPSecret(data: {
1418
+ password: string;
1419
+ secret: string;
1420
+ code: string;
1421
+ }): Promise<{ result: boolean; message: string }> {
1422
+ if (!this.$checkPassword(data.password ?? '')) {
1423
+ return { result: false, message: 'Incorrect password.' };
1424
+ }
1425
+
1426
+ if (this.$data.totpSecret != null) {
1427
+ return { result: false, message: 'You already have a 2FA secret.' };
1428
+ }
1429
+
1430
+ if (
1431
+ data.secret == null ||
1432
+ typeof data.secret !== 'string' ||
1433
+ data.secret === ''
1434
+ ) {
1435
+ return { result: false, message: '2FA secret is invalid.' };
1436
+ }
1437
+
1438
+ if (
1439
+ data.code == null ||
1440
+ typeof data.code !== 'string' ||
1441
+ data.code.length !== 6
1442
+ ) {
1443
+ return { result: false, message: '2FA code is invalid.' };
1444
+ }
1445
+
1446
+ const tilmeld = enforceTilmeld(this);
1447
+
1448
+ const totp = new TOTP({
1449
+ issuer: tilmeld.config.appName,
1450
+ label: this.$data.username,
1451
+ algorithm: 'SHA1',
1452
+ digits: 6,
1453
+ period: 30,
1454
+ secret: data.secret,
1455
+ });
1456
+
1457
+ if (totp.validate({ token: data.code, window: 2 }) == null) {
1458
+ return { result: false, message: '2FA code is incorrect.' };
1459
+ }
1460
+
1461
+ this.$data.totpSecret = data.secret;
1462
+
1463
+ if (await this.$save()) {
1464
+ return { result: true, message: 'Your two factor secret is now set.' };
1465
+ } else {
1466
+ return { result: false, message: "Couldn't save two factor secret." };
1467
+ }
1468
+ }
1469
+
1470
+ /**
1471
+ * A frontend accessible method to remove the TOTP secret from the user's
1472
+ * account.
1473
+ *
1474
+ * @param data The input data from the client.
1475
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1476
+ */
1477
+ public async $removeTOTPSecret(data?: {
1478
+ password: string;
1479
+ code: string;
1480
+ }): Promise<{ result: boolean; message: string }> {
1481
+ const tilmeld = enforceTilmeld(this);
1482
+
1483
+ if (data) {
1484
+ if (!this.$checkPassword(data.password ?? '')) {
1485
+ return { result: false, message: 'Incorrect password.' };
1486
+ }
1487
+
1488
+ if (
1489
+ data.code == null ||
1490
+ typeof data.code !== 'string' ||
1491
+ data.code.length !== 6
1492
+ ) {
1493
+ return { result: false, message: '2FA code is invalid.' };
1494
+ }
1495
+
1496
+ if (this.$data.totpSecret == null) {
1497
+ return { result: false, message: "You don't have a 2FA secret." };
1498
+ }
1499
+
1500
+ const totp = new TOTP({
1501
+ issuer: tilmeld.config.appName,
1502
+ label: this.$data.username,
1503
+ algorithm: 'SHA1',
1504
+ digits: 6,
1505
+ period: 30,
1506
+ secret: this.$data.totpSecret,
1507
+ });
1508
+
1509
+ if (totp.validate({ token: data.code, window: 2 }) == null) {
1510
+ return { result: false, message: '2FA code is incorrect.' };
1511
+ }
1512
+ } else if (!tilmeld.gatekeeper('tilmeld/admin')) {
1513
+ return {
1514
+ result: false,
1515
+ message: 'You must provide your password and 2FA code.',
1516
+ };
1517
+ }
1518
+
1519
+ delete this.$data.totpSecret;
1520
+
1521
+ if (await this.$save()) {
1522
+ return {
1523
+ result: true,
1524
+ message: 'Two factor secret has been removed.',
1525
+ };
1526
+ } else {
1527
+ return { result: false, message: "Couldn't remove two factor secret." };
1528
+ }
1529
+ }
1530
+
1054
1531
  /**
1055
1532
  * Check that a username is valid.
1056
1533
  *
1057
1534
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1058
1535
  */
1059
1536
  public async $checkUsername(): Promise<{ result: boolean; message: string }> {
1060
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1537
+ const tilmeld = enforceTilmeld(this);
1538
+
1539
+ this.$nymph.config.debugLog(
1540
+ 'tilmeld',
1541
+ `Username check "${this.$data.username}".`,
1542
+ );
1543
+
1061
1544
  if (!tilmeld.config.emailUsernames) {
1062
1545
  if (this.$data.username == null || !this.$data.username.length) {
1063
1546
  return { result: false, message: 'Please specify a username.' };
1064
1547
  }
1065
- if (
1066
- tilmeld.config.maxUsernameLength > 0 &&
1067
- this.$data.username.length > tilmeld.config.maxUsernameLength
1068
- ) {
1548
+ if (this.$data.username.length < tilmeld.config.minUsernameLength) {
1549
+ return {
1550
+ result: false,
1551
+ message:
1552
+ 'Usernames must be at least ' +
1553
+ tilmeld.config.minUsernameLength +
1554
+ ' characters.',
1555
+ };
1556
+ }
1557
+ if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
1069
1558
  return {
1070
1559
  result: false,
1071
1560
  message:
@@ -1078,7 +1567,7 @@ export default class User extends AbleObject<UserData> {
1078
1567
  if (
1079
1568
  difference(
1080
1569
  this.$data.username.split(''),
1081
- tilmeld.config.validChars.split('')
1570
+ tilmeld.config.validChars.split(''),
1082
1571
  ).length
1083
1572
  ) {
1084
1573
  return {
@@ -1105,12 +1594,24 @@ export default class User extends AbleObject<UserData> {
1105
1594
  }
1106
1595
  const test = await this.$nymph.getEntity(
1107
1596
  { class: tilmeld.User, skipAc: true },
1108
- selector
1597
+ selector,
1109
1598
  );
1110
1599
  if (test != null) {
1111
1600
  return { result: false, message: 'That username is taken.' };
1112
1601
  }
1113
1602
 
1603
+ for (let callback of (this.constructor as typeof User)
1604
+ .checkUsernameCallbacks) {
1605
+ if (callback) {
1606
+ const result = await callback(this, {
1607
+ username: this.$data.username,
1608
+ });
1609
+ if (!result.result) {
1610
+ return { message: 'Username is not available.', ...result };
1611
+ }
1612
+ }
1613
+ }
1614
+
1114
1615
  return {
1115
1616
  result: true,
1116
1617
  message:
@@ -1121,10 +1622,16 @@ export default class User extends AbleObject<UserData> {
1121
1622
  if (this.$data.username == null || !this.$data.username.length) {
1122
1623
  return { result: false, message: 'Please specify an email.' };
1123
1624
  }
1124
- if (
1125
- tilmeld.config.maxUsernameLength > 0 &&
1126
- this.$data.username.length > tilmeld.config.maxUsernameLength
1127
- ) {
1625
+ if (this.$data.username.length < tilmeld.config.minUsernameLength) {
1626
+ return {
1627
+ result: false,
1628
+ message:
1629
+ 'Emails must be at least ' +
1630
+ tilmeld.config.minUsernameLength +
1631
+ ' characters.',
1632
+ };
1633
+ }
1634
+ if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
1128
1635
  return {
1129
1636
  result: false,
1130
1637
  message:
@@ -1144,7 +1651,13 @@ export default class User extends AbleObject<UserData> {
1144
1651
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1145
1652
  */
1146
1653
  public async $checkEmail(): Promise<{ result: boolean; message: string }> {
1147
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1654
+ const tilmeld = enforceTilmeld(this);
1655
+
1656
+ this.$nymph.config.debugLog(
1657
+ 'tilmeld',
1658
+ `Email check "${this.$data.email}".`,
1659
+ );
1660
+
1148
1661
  if (this.$data.email == null || !this.$data.email.length) {
1149
1662
  if (tilmeld.config.verifyEmail) {
1150
1663
  return { result: false, message: 'Please specify an email.' };
@@ -1167,7 +1680,7 @@ export default class User extends AbleObject<UserData> {
1167
1680
  }
1168
1681
  const test = await this.$nymph.getEntity(
1169
1682
  { class: tilmeld.User, skipAc: true },
1170
- selector
1683
+ selector,
1171
1684
  );
1172
1685
  if (test != null) {
1173
1686
  return {
@@ -1189,7 +1702,13 @@ export default class User extends AbleObject<UserData> {
1189
1702
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1190
1703
  */
1191
1704
  public async $checkPhone(): Promise<{ result: boolean; message: string }> {
1192
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1705
+ const tilmeld = enforceTilmeld(this);
1706
+
1707
+ this.$nymph.config.debugLog(
1708
+ 'tilmeld',
1709
+ `Phone check "${this.$data.phone}".`,
1710
+ );
1711
+
1193
1712
  if (this.$data.phone == null || !this.$data.phone.length) {
1194
1713
  return { result: false, message: 'Please specify a phone number.' };
1195
1714
  }
@@ -1211,7 +1730,7 @@ export default class User extends AbleObject<UserData> {
1211
1730
  }
1212
1731
  const test = await this.$nymph.getEntity(
1213
1732
  { class: tilmeld.User, skipAc: true },
1214
- selector
1733
+ selector,
1215
1734
  );
1216
1735
  if (test != null) {
1217
1736
  return { result: false, message: 'Phone number is in use.' };
@@ -1228,7 +1747,7 @@ export default class User extends AbleObject<UserData> {
1228
1747
  password: string;
1229
1748
  additionalData?: { [k: string]: any };
1230
1749
  }): Promise<{ result: boolean; loggedin: boolean; message: string }> {
1231
- let tilmeld = this.$nymph.tilmeld as Tilmeld;
1750
+ let tilmeld = enforceTilmeld(this);
1232
1751
  if (!tilmeld.config.allowRegistration) {
1233
1752
  return {
1234
1753
  result: false,
@@ -1244,21 +1763,6 @@ export default class User extends AbleObject<UserData> {
1244
1763
  };
1245
1764
  }
1246
1765
 
1247
- try {
1248
- for (let callback of (this.constructor as typeof User)
1249
- .beforeRegisterCallbacks) {
1250
- if (callback) {
1251
- await callback(this, data);
1252
- }
1253
- }
1254
- } catch (e: any) {
1255
- return {
1256
- result: false,
1257
- loggedin: false,
1258
- message: e.message,
1259
- };
1260
- }
1261
-
1262
1766
  if (!('password' in data) || !data.password.length) {
1263
1767
  return {
1264
1768
  result: false,
@@ -1272,7 +1776,7 @@ export default class User extends AbleObject<UserData> {
1272
1776
  }
1273
1777
 
1274
1778
  this.$password(data.password);
1275
- if (tilmeld.config.regFields.indexOf('name') !== -1) {
1779
+ if (tilmeld.config.regFields.includes('name')) {
1276
1780
  this.$data.name =
1277
1781
  this.$data.nameFirst +
1278
1782
  (this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
@@ -1290,9 +1794,29 @@ export default class User extends AbleObject<UserData> {
1290
1794
  const nymph = this.$nymph;
1291
1795
  const tnymph = await this.$nymph.startTransaction(transaction);
1292
1796
  this.$nymph = tnymph;
1293
- tilmeld = this.$nymph.tilmeld as Tilmeld;
1797
+ tilmeld = enforceTilmeld(this);
1798
+
1799
+ let message = '';
1800
+ let loggedin = false;
1294
1801
 
1295
1802
  try {
1803
+ try {
1804
+ for (let callback of (this.constructor as typeof User)
1805
+ .beforeRegisterCallbacks) {
1806
+ if (callback) {
1807
+ await callback(this, data);
1808
+ }
1809
+ }
1810
+ } catch (e: any) {
1811
+ await tnymph.rollback(transaction);
1812
+ this.$nymph = nymph;
1813
+ return {
1814
+ result: false,
1815
+ loggedin: false,
1816
+ message: e.message,
1817
+ };
1818
+ }
1819
+
1296
1820
  // Add primary group.
1297
1821
  let generatedPrimaryGroup: (Group & GroupData) | null = null;
1298
1822
  if (tilmeld.config.generatePrimary) {
@@ -1300,14 +1824,18 @@ export default class User extends AbleObject<UserData> {
1300
1824
  generatedPrimaryGroup = await tilmeld.Group.factory();
1301
1825
  generatedPrimaryGroup.groupname = this.$data.username;
1302
1826
  generatedPrimaryGroup.avatar = this.$data.avatar;
1303
- generatedPrimaryGroup.name = this.$data.name;
1304
- generatedPrimaryGroup.email = this.$data.email;
1827
+ if (tilmeld.config.userFields.includes('name')) {
1828
+ generatedPrimaryGroup.name = this.$data.name;
1829
+ }
1830
+ if (tilmeld.config.userFields.includes('email')) {
1831
+ generatedPrimaryGroup.email = this.$data.email;
1832
+ }
1305
1833
  const parent = await tnymph.getEntity(
1306
1834
  { class: tilmeld.Group },
1307
1835
  {
1308
1836
  type: '&',
1309
1837
  equal: ['defaultPrimary', true],
1310
- }
1838
+ },
1311
1839
  );
1312
1840
  if (parent != null) {
1313
1841
  generatedPrimaryGroup.parent = parent;
@@ -1329,7 +1857,7 @@ export default class User extends AbleObject<UserData> {
1329
1857
  {
1330
1858
  type: '&',
1331
1859
  equal: ['defaultPrimary', true],
1332
- }
1860
+ },
1333
1861
  );
1334
1862
  if (group != null) {
1335
1863
  this.$data.group = group;
@@ -1337,14 +1865,18 @@ export default class User extends AbleObject<UserData> {
1337
1865
  }
1338
1866
 
1339
1867
  // Add secondary groups.
1340
- if (tilmeld.config.verifyEmail && tilmeld.config.unverifiedAccess) {
1868
+ if (
1869
+ tilmeld.config.userFields.includes('email') &&
1870
+ tilmeld.config.verifyEmail &&
1871
+ tilmeld.config.unverifiedAccess
1872
+ ) {
1341
1873
  // Add the default secondaries for unverified users.
1342
1874
  this.$data.groups = await tnymph.getEntities(
1343
1875
  { class: tilmeld.Group },
1344
1876
  {
1345
1877
  type: '&',
1346
1878
  equal: ['unverifiedSecondary', true],
1347
- }
1879
+ },
1348
1880
  );
1349
1881
  } else {
1350
1882
  // Add the default secondaries.
@@ -1353,11 +1885,14 @@ export default class User extends AbleObject<UserData> {
1353
1885
  {
1354
1886
  type: '&',
1355
1887
  equal: ['defaultSecondary', true],
1356
- }
1888
+ },
1357
1889
  );
1358
1890
  }
1359
1891
 
1360
- if (tilmeld.config.verifyEmail) {
1892
+ if (
1893
+ tilmeld.config.userFields.includes('email') &&
1894
+ tilmeld.config.verifyEmail
1895
+ ) {
1361
1896
  // The user will be enabled after verifying their e-mail address.
1362
1897
  if (!tilmeld.config.unverifiedAccess) {
1363
1898
  this.$data.enabled = false;
@@ -1384,7 +1919,15 @@ export default class User extends AbleObject<UserData> {
1384
1919
  }
1385
1920
  }
1386
1921
 
1922
+ this.$nymph.config.debugLog(
1923
+ 'tilmeld',
1924
+ `Registering new user "${this.$data.username}".`,
1925
+ );
1387
1926
  if (await this.$saveSkipAC()) {
1927
+ this.$nymph.config.debugLog(
1928
+ 'tilmeld',
1929
+ `New user registered "${this.$data.username}".`,
1930
+ );
1388
1931
  // Send the new user registered email.
1389
1932
  if (tilmeld.config.userRegisteredRecipient != null) {
1390
1933
  await tilmeld.config.sendEmail(
@@ -1403,13 +1946,10 @@ export default class User extends AbleObject<UserData> {
1403
1946
  userPhone: this.$data.phone,
1404
1947
  },
1405
1948
  },
1406
- this
1949
+ this,
1407
1950
  );
1408
1951
  }
1409
1952
 
1410
- let message = '';
1411
- let loggedin = false;
1412
-
1413
1953
  // Save the primary group.
1414
1954
  if (generatedPrimaryGroup != null) {
1415
1955
  generatedPrimaryGroup.user = this;
@@ -1429,7 +1969,8 @@ export default class User extends AbleObject<UserData> {
1429
1969
  if (
1430
1970
  tilmeld.config.verifyEmail &&
1431
1971
  !tilmeld.config.unverifiedAccess &&
1432
- !madeAdmin
1972
+ !madeAdmin &&
1973
+ tilmeld.config.userFields.includes('email')
1433
1974
  ) {
1434
1975
  message +=
1435
1976
  `Almost there. An email has been sent to ${this.$data.email} ` +
@@ -1437,9 +1978,10 @@ export default class User extends AbleObject<UserData> {
1437
1978
  } else if (
1438
1979
  tilmeld.config.verifyEmail &&
1439
1980
  tilmeld.config.unverifiedAccess &&
1440
- !madeAdmin
1981
+ !madeAdmin &&
1982
+ tilmeld.config.userFields.includes('email')
1441
1983
  ) {
1442
- if (!tilmeld.login(this, true)) {
1984
+ if (!(await tilmeld.login(this, true))) {
1443
1985
  throw new Error('An error occurred trying to log you in.');
1444
1986
  }
1445
1987
  message +=
@@ -1448,7 +1990,7 @@ export default class User extends AbleObject<UserData> {
1448
1990
  'registration.';
1449
1991
  loggedin = true;
1450
1992
  } else {
1451
- if (!tilmeld.login(this, true)) {
1993
+ if (!(await tilmeld.login(this, true))) {
1452
1994
  throw new Error('An error occurred trying to log you in.');
1453
1995
  }
1454
1996
  message += "You're now registered and logged in!";
@@ -1464,21 +2006,11 @@ export default class User extends AbleObject<UserData> {
1464
2006
  });
1465
2007
  }
1466
2008
  }
1467
-
1468
- try {
1469
- await tnymph.commit(transaction);
1470
- this.$nymph = nymph;
1471
- this.$nymph.tilmeld?.fillSession(this);
1472
- } catch (e: any) {
1473
- throw e;
1474
- }
1475
-
1476
- return {
1477
- result: true,
1478
- loggedin,
1479
- message,
1480
- };
1481
2009
  } else {
2010
+ this.$nymph.config.debugError(
2011
+ 'tilmeld',
2012
+ `Error registering new user "${this.$data.username}".`,
2013
+ );
1482
2014
  await tnymph.rollback(transaction);
1483
2015
  this.$nymph = nymph;
1484
2016
  return {
@@ -1488,14 +2020,32 @@ export default class User extends AbleObject<UserData> {
1488
2020
  };
1489
2021
  }
1490
2022
  } catch (e: any) {
2023
+ this.$nymph.config.debugError(
2024
+ 'tilmeld',
2025
+ `Error registering new user "${this.$data.username}": ${e}`,
2026
+ );
1491
2027
  await tnymph.rollback(transaction);
1492
2028
  this.$nymph = nymph;
1493
2029
  throw e;
1494
2030
  }
2031
+
2032
+ try {
2033
+ await tnymph.commit(transaction);
2034
+ this.$nymph = nymph;
2035
+ await this.$nymph.tilmeld?.fillSession(this);
2036
+ } catch (e: any) {
2037
+ throw e;
2038
+ }
2039
+
2040
+ return {
2041
+ result: true,
2042
+ loggedin,
2043
+ message,
2044
+ };
1495
2045
  }
1496
2046
 
1497
2047
  public async $save() {
1498
- let tilmeld = this.$nymph.tilmeld as Tilmeld;
2048
+ let tilmeld = enforceTilmeld(this);
1499
2049
  if (this.$data.username == null || !this.$data.username.trim().length) {
1500
2050
  return false;
1501
2051
  }
@@ -1503,10 +2053,10 @@ export default class User extends AbleObject<UserData> {
1503
2053
  if (
1504
2054
  tilmeld.gatekeeper('tilmeld/admin') &&
1505
2055
  !tilmeld.gatekeeper('system/admin') &&
1506
- this.$gatekeeper('system/admin')
2056
+ this.$data.abilities?.includes('system/admin')
1507
2057
  ) {
1508
2058
  throw new BadDataError(
1509
- "You don't have the authority to modify system admins."
2059
+ "You don't have the authority to modify system admins.",
1510
2060
  );
1511
2061
  }
1512
2062
 
@@ -1528,49 +2078,88 @@ export default class User extends AbleObject<UserData> {
1528
2078
  this.$data.enabled = !!this.$data.enabled;
1529
2079
 
1530
2080
  // Clear empty values.
1531
- if (this.$data.nameFirst === '') {
2081
+ if (
2082
+ this.$data.nameFirst === '' ||
2083
+ !tilmeld.config.userFields.includes('name')
2084
+ ) {
1532
2085
  delete this.$data.nameFirst;
1533
2086
  }
1534
- if (this.$data.nameMiddle === '') {
2087
+ if (
2088
+ this.$data.nameMiddle === '' ||
2089
+ !tilmeld.config.userFields.includes('name')
2090
+ ) {
1535
2091
  delete this.$data.nameMiddle;
1536
2092
  }
1537
- if (this.$data.nameLast === '') {
2093
+ if (
2094
+ this.$data.nameLast === '' ||
2095
+ !tilmeld.config.userFields.includes('name')
2096
+ ) {
1538
2097
  delete this.$data.nameLast;
1539
2098
  }
1540
- if (this.$data.name === '') {
2099
+ if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
1541
2100
  delete this.$data.name;
1542
2101
  }
1543
- if (this.$data.email === '') {
2102
+ if (
2103
+ this.$data.email === '' ||
2104
+ !tilmeld.config.userFields.includes('email')
2105
+ ) {
1544
2106
  delete this.$data.email;
1545
2107
  }
1546
2108
  if (this.$data.avatar === '') {
1547
2109
  delete this.$data.avatar;
1548
2110
  }
1549
- if (this.$data.phone === '') {
2111
+ if (
2112
+ this.$data.phone === '' ||
2113
+ !tilmeld.config.userFields.includes('phone')
2114
+ ) {
1550
2115
  delete this.$data.phone;
1551
2116
  }
1552
- if (this.$data.secret === '') {
2117
+ if (
2118
+ this.$data.secret === '' ||
2119
+ !tilmeld.config.userFields.includes('email')
2120
+ ) {
1553
2121
  delete this.$data.secret;
1554
2122
  }
1555
- if (this.$data.emailChangeDate === 0) {
2123
+ if (
2124
+ this.$data.emailChangeDate === 0 ||
2125
+ !tilmeld.config.userFields.includes('email')
2126
+ ) {
1556
2127
  delete this.$data.emailChangeDate;
1557
2128
  }
1558
- if (this.$data.newEmailSecret === '') {
2129
+ if (
2130
+ this.$data.newEmailSecret === '' ||
2131
+ !tilmeld.config.userFields.includes('email')
2132
+ ) {
1559
2133
  delete this.$data.newEmailSecret;
1560
2134
  }
1561
- if (this.$data.newEmailAddress === '') {
2135
+ if (
2136
+ this.$data.newEmailAddress === '' ||
2137
+ !tilmeld.config.userFields.includes('email')
2138
+ ) {
1562
2139
  delete this.$data.newEmailAddress;
1563
2140
  }
1564
- if (this.$data.cancelEmailSecret === '') {
2141
+ if (
2142
+ this.$data.cancelEmailSecret === '' ||
2143
+ !tilmeld.config.userFields.includes('email')
2144
+ ) {
1565
2145
  delete this.$data.cancelEmailSecret;
1566
2146
  }
1567
- if (this.$data.cancelEmailAddress === '') {
2147
+ if (
2148
+ this.$data.cancelEmailAddress === '' ||
2149
+ !tilmeld.config.userFields.includes('email')
2150
+ ) {
1568
2151
  delete this.$data.cancelEmailAddress;
1569
2152
  }
1570
- if (this.$data.recoverSecret === '') {
2153
+ if (
2154
+ this.$data.recoverSecret === '' ||
2155
+ !tilmeld.config.userFields.includes('email')
2156
+ ) {
1571
2157
  delete this.$data.recoverSecret;
1572
2158
  }
1573
- if (this.$data.recoverSecretDate === 0) {
2159
+ if (
2160
+ this.$data.recoverSecretDate === 0 ||
2161
+ !tilmeld.config.userFields.includes('email')
2162
+ ) {
1574
2163
  delete this.$data.recoverSecretDate;
1575
2164
  }
1576
2165
  if (this.$data.password === '') {
@@ -1588,7 +2177,10 @@ export default class User extends AbleObject<UserData> {
1588
2177
  if (!unCheck.result) {
1589
2178
  throw new BadUsernameError(unCheck.message);
1590
2179
  }
1591
- if (!tilmeld.config.emailUsernames) {
2180
+ if (
2181
+ !tilmeld.config.emailUsernames &&
2182
+ tilmeld.config.userFields.includes('email')
2183
+ ) {
1592
2184
  const emCheck = await this.$checkEmail();
1593
2185
  if (!emCheck.result) {
1594
2186
  throw new BadEmailError(emCheck.message);
@@ -1596,7 +2188,10 @@ export default class User extends AbleObject<UserData> {
1596
2188
  }
1597
2189
 
1598
2190
  // Email changes.
1599
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
2191
+ if (
2192
+ !tilmeld.gatekeeper('tilmeld/admin') &&
2193
+ tilmeld.config.userFields.includes('email')
2194
+ ) {
1600
2195
  // The user isn't an admin, so email address changes should contain some
1601
2196
  // security measures.
1602
2197
  if (tilmeld.config.verifyEmail) {
@@ -1604,7 +2199,7 @@ export default class User extends AbleObject<UserData> {
1604
2199
  if (this.guid == null) {
1605
2200
  // If this is the first user, they'll be made an admin, so they don't
1606
2201
  // need to verify.
1607
- if ((this.$data.abilities?.indexOf('system/admin') ?? -1) === -1) {
2202
+ if (!this.$data.abilities?.includes('system/admin')) {
1608
2203
  this.$data.secret = nanoid();
1609
2204
  sendVerification = true;
1610
2205
  }
@@ -1624,10 +2219,10 @@ export default class User extends AbleObject<UserData> {
1624
2219
  new Date(
1625
2220
  strtotime(
1626
2221
  '+' + tilmeld.config.emailRateLimit,
1627
- Math.floor(this.$data.emailChangeDate / 1000)
1628
- ) * 1000
2222
+ Math.floor(this.$data.emailChangeDate / 1000),
2223
+ ) * 1000,
1629
2224
  ).toString() +
1630
- ' to change your email address again.'
2225
+ ' to change your email address again.',
1631
2226
  );
1632
2227
  } else {
1633
2228
  if (
@@ -1683,7 +2278,7 @@ export default class User extends AbleObject<UserData> {
1683
2278
  }
1684
2279
 
1685
2280
  try {
1686
- tilmeld.config.validatorUser(this);
2281
+ tilmeld.config.validatorUser(tilmeld, this);
1687
2282
  } catch (e: any) {
1688
2283
  throw new BadDataError(e?.message);
1689
2284
  }
@@ -1693,21 +2288,26 @@ export default class User extends AbleObject<UserData> {
1693
2288
  const nymph = this.$nymph;
1694
2289
  const tnymph = await this.$nymph.startTransaction(transaction);
1695
2290
  this.$nymph = tnymph;
1696
- tilmeld = this.$nymph.tilmeld as Tilmeld;
2291
+ tilmeld = enforceTilmeld(this);
1697
2292
 
1698
- if (
1699
- this.$data.group != null &&
1700
- this.$data.group.user != null &&
1701
- this.$is(this.$data.group.user)
1702
- ) {
2293
+ const group = this.$data.group;
2294
+ await group?.$wake();
2295
+ if (group != null && group.user != null && this.$is(group.user)) {
1703
2296
  try {
1704
2297
  // Update the user's generated primary group.
1705
- this.$data.group.groupname = this.$data.username;
1706
- this.$data.group.avatar = this.$data.avatar;
1707
- this.$data.group.email = this.$data.email;
1708
- this.$data.group.name = this.$data.name;
1709
- this.$data.group.phone = this.$data.phone;
1710
- await this.$data.group.$saveSkipAC();
2298
+ group.groupname = this.$data.username;
2299
+ group.avatar = this.$data.avatar;
2300
+ if (tilmeld.config.userFields.includes('email')) {
2301
+ group.email = this.$data.email;
2302
+ }
2303
+ if (tilmeld.config.userFields.includes('name')) {
2304
+ group.name = this.$data.name;
2305
+ }
2306
+ if (tilmeld.config.userFields.includes('phone')) {
2307
+ group.phone = this.$data.phone;
2308
+ }
2309
+ await group.$saveSkipAC();
2310
+ this.$data.group = group;
1711
2311
  } catch (e: any) {
1712
2312
  await tnymph.rollback(transaction);
1713
2313
  this.$nymph = nymph;
@@ -1744,9 +2344,11 @@ export default class User extends AbleObject<UserData> {
1744
2344
  this.$gatekeeperCache = undefined;
1745
2345
  await tnymph.commit(transaction);
1746
2346
 
1747
- if ((nymph.tilmeld as Tilmeld).User.current(true).$is(this)) {
2347
+ const tilmeld = enforceTilmeld(nymph);
2348
+
2349
+ if (tilmeld.User.current(true).$is(this)) {
1748
2350
  // Update the user in the session cache.
1749
- (nymph.tilmeld as Tilmeld).fillSession(this);
2351
+ await tilmeld.fillSession(this);
1750
2352
  }
1751
2353
  } else {
1752
2354
  await tnymph.rollback(transaction);
@@ -1772,16 +2374,16 @@ export default class User extends AbleObject<UserData> {
1772
2374
  }
1773
2375
 
1774
2376
  public async $delete() {
1775
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
2377
+ const tilmeld = enforceTilmeld(this);
1776
2378
  if (!tilmeld.gatekeeper('tilmeld/admin')) {
1777
2379
  throw new BadDataError("You don't have the authority to delete users.");
1778
2380
  }
1779
2381
  if (
1780
2382
  !tilmeld.gatekeeper('system/admin') &&
1781
- this.$gatekeeper('system/admin')
2383
+ this.$data.abilities?.includes('system/admin')
1782
2384
  ) {
1783
2385
  throw new BadDataError(
1784
- "You don't have the authority to delete system admins."
2386
+ "You don't have the authority to delete system admins.",
1785
2387
  );
1786
2388
  }
1787
2389
  if (tilmeld.User.current(true).$is(this)) {
@@ -1792,7 +2394,9 @@ export default class User extends AbleObject<UserData> {
1792
2394
 
1793
2395
  public static on<T extends EventType>(
1794
2396
  event: T,
1795
- callback: T extends 'beforeRegister'
2397
+ callback: T extends 'checkUsername'
2398
+ ? TilmeldCheckUsernameCallback
2399
+ : T extends 'beforeRegister'
1796
2400
  ? TilmeldBeforeRegisterCallback
1797
2401
  : T extends 'afterRegister'
1798
2402
  ? TilmeldAfterRegisterCallback
@@ -1804,9 +2408,11 @@ export default class User extends AbleObject<UserData> {
1804
2408
  ? TilmeldBeforeLogoutCallback
1805
2409
  : T extends 'afterLogout'
1806
2410
  ? TilmeldAfterLogoutCallback
1807
- : never
2411
+ : never,
1808
2412
  ) {
1809
- const prop = (event + 'Callbacks') as T extends 'beforeRegister'
2413
+ const prop = (event + 'Callbacks') as T extends 'checkUsername'
2414
+ ? 'checkUsernameCallbacks'
2415
+ : T extends 'beforeRegister'
1810
2416
  ? 'beforeRegisterCallbacks'
1811
2417
  : T extends 'afterRegister'
1812
2418
  ? 'afterRegisterCallbacks'
@@ -1829,7 +2435,9 @@ export default class User extends AbleObject<UserData> {
1829
2435
 
1830
2436
  public static off<T extends EventType>(
1831
2437
  event: T,
1832
- callback: T extends 'beforeRegister'
2438
+ callback: T extends 'checkUsername'
2439
+ ? TilmeldCheckUsernameCallback
2440
+ : T extends 'beforeRegister'
1833
2441
  ? TilmeldBeforeRegisterCallback
1834
2442
  : T extends 'afterRegister'
1835
2443
  ? TilmeldAfterRegisterCallback
@@ -1841,9 +2449,11 @@ export default class User extends AbleObject<UserData> {
1841
2449
  ? TilmeldBeforeLogoutCallback
1842
2450
  : T extends 'afterLogout'
1843
2451
  ? TilmeldAfterLogoutCallback
1844
- : never
2452
+ : never,
1845
2453
  ) {
1846
- const prop = (event + 'Callbacks') as T extends 'beforeRegister'
2454
+ const prop = (event + 'Callbacks') as T extends 'checkUsername'
2455
+ ? 'checkUsernameCallbacks'
2456
+ : T extends 'beforeRegister'
1847
2457
  ? 'beforeRegisterCallbacks'
1848
2458
  : T extends 'afterRegister'
1849
2459
  ? 'afterRegisterCallbacks'