@nymphjs/tilmeld 1.0.0-beta.12 → 1.0.0-beta.121

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 (93) hide show
  1. package/CHANGELOG.md +550 -0
  2. package/README.md +15 -2
  3. package/dist/AbleObject.d.ts +15 -0
  4. package/dist/AbleObject.js +18 -6
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +182 -15
  7. package/dist/Group.js +571 -191
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.testArtifacts.d.ts +3 -0
  10. package/dist/Group.testArtifacts.js +120 -0
  11. package/dist/Group.testArtifacts.js.map +1 -0
  12. package/dist/Tilmeld.d.ts +241 -12
  13. package/dist/Tilmeld.js +721 -273
  14. package/dist/Tilmeld.js.map +1 -1
  15. package/dist/Tilmeld.types.d.ts +5 -5
  16. package/dist/Tilmeld.types.js +1 -2
  17. package/dist/User.d.ts +396 -22
  18. package/dist/User.js +1386 -367
  19. package/dist/User.js.map +1 -1
  20. package/dist/User.testArtifacts.d.ts +3 -0
  21. package/dist/User.testArtifacts.js +175 -0
  22. package/dist/User.testArtifacts.js.map +1 -0
  23. package/dist/conf/defaults.d.ts +1 -1
  24. package/dist/conf/defaults.js +196 -141
  25. package/dist/conf/defaults.js.map +1 -1
  26. package/dist/conf/index.d.ts +355 -2
  27. package/dist/conf/index.js +2 -8
  28. package/dist/conf/index.js.map +1 -1
  29. package/dist/enforceTilmeld.d.ts +3 -0
  30. package/dist/enforceTilmeld.js +18 -0
  31. package/dist/enforceTilmeld.js.map +1 -0
  32. package/dist/errors/AccessControlError.d.ts +2 -1
  33. package/dist/errors/AccessControlError.js +3 -6
  34. package/dist/errors/AccessControlError.js.map +1 -1
  35. package/dist/errors/BadDataError.d.ts +2 -1
  36. package/dist/errors/BadDataError.js +3 -6
  37. package/dist/errors/BadDataError.js.map +1 -1
  38. package/dist/errors/BadEmailError.d.ts +2 -1
  39. package/dist/errors/BadEmailError.js +3 -6
  40. package/dist/errors/BadEmailError.js.map +1 -1
  41. package/dist/errors/BadUsernameError.d.ts +2 -1
  42. package/dist/errors/BadUsernameError.js +3 -6
  43. package/dist/errors/BadUsernameError.js.map +1 -1
  44. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js +1 -5
  45. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js.map +1 -1
  46. package/dist/errors/EmailChangeRateLimitExceededError.js +1 -5
  47. package/dist/errors/EmailChangeRateLimitExceededError.js.map +1 -1
  48. package/dist/errors/index.d.ts +6 -6
  49. package/dist/errors/index.js +6 -22
  50. package/dist/errors/index.js.map +1 -1
  51. package/dist/index.d.ts +11 -10
  52. package/dist/index.js +16 -35
  53. package/dist/index.js.map +1 -1
  54. package/dist/testArtifacts.d.ts +19 -0
  55. package/dist/testArtifacts.js +1173 -0
  56. package/dist/testArtifacts.js.map +1 -0
  57. package/dist/tilmeldJoiProps.d.ts +3 -3
  58. package/dist/tilmeldJoiProps.js +13 -18
  59. package/dist/tilmeldJoiProps.js.map +1 -1
  60. package/jest.config.js +31 -5
  61. package/package.json +41 -34
  62. package/src/AbleObject.ts +2 -2
  63. package/src/Group.testArtifacts.ts +159 -0
  64. package/src/Group.ts +684 -225
  65. package/src/Tilmeld.ts +547 -283
  66. package/src/Tilmeld.types.ts +5 -5
  67. package/src/User.testArtifacts.ts +226 -0
  68. package/src/User.ts +1635 -461
  69. package/src/conf/defaults.ts +172 -109
  70. package/src/conf/index.ts +370 -2
  71. package/src/enforceTilmeld.ts +20 -0
  72. package/src/errors/AccessControlError.ts +4 -2
  73. package/src/errors/BadDataError.ts +4 -2
  74. package/src/errors/BadEmailError.ts +4 -2
  75. package/src/errors/BadUsernameError.ts +4 -2
  76. package/src/errors/index.ts +6 -6
  77. package/src/index.ts +12 -10
  78. package/src/testArtifacts.ts +1979 -0
  79. package/src/tilmeldJoiProps.ts +8 -23
  80. package/tsconfig.json +6 -3
  81. package/typedoc.json +4 -0
  82. package/dist/Group.test.d.ts +0 -1
  83. package/dist/Group.test.js +0 -48
  84. package/dist/Group.test.js.map +0 -1
  85. package/dist/User.test.d.ts +0 -1
  86. package/dist/User.test.js +0 -51
  87. package/dist/User.test.js.map +0 -1
  88. package/dist/conf/d.d.ts +0 -55
  89. package/dist/conf/d.js +0 -3
  90. package/dist/conf/d.js.map +0 -1
  91. package/src/Group.test.ts +0 -58
  92. package/src/User.test.ts +0 -61
  93. package/src/conf/d.ts +0 -303
package/src/User.ts CHANGED
@@ -1,49 +1,87 @@
1
- import {
1
+ import crypto from 'node:crypto';
2
+ import type {
3
+ Nymph,
2
4
  EntityData,
3
5
  EntityJson,
4
6
  EntityPatch,
5
7
  Selector,
6
8
  SerializedEntityData,
7
9
  } from '@nymphjs/nymph';
8
- import { EmailOptions } from 'email-templates';
9
- import strtotime from 'locutus/php/datetime/strtotime';
10
- import Base64 from 'crypto-js/enc-base64';
11
- import sha256 from 'crypto-js/sha256';
12
- import md5 from 'crypto-js/md5';
13
- import { difference } from 'lodash';
14
10
  import { humanSecret, nanoid } from '@nymphjs/guid';
15
-
16
- import type Tilmeld from './Tilmeld';
17
- import AbleObject from './AbleObject';
18
- import Group, { GroupData } from './Group';
11
+ import type { EmailOptions } from 'email-templates';
12
+ import { strtotime } from 'locutus/php/datetime/index';
13
+ import { difference, xor } from 'lodash-es';
14
+ import { TOTP, Secret } from 'otpauth';
15
+ import { toDataURL } from 'qrcode';
16
+ import { splitn } from '@sciactive/splitn';
17
+
18
+ import { enforceTilmeld } from './enforceTilmeld.js';
19
+ import AbleObject from './AbleObject.js';
20
+ import GroupClass from './Group.js';
21
+ import type { GroupData } from './Group.js';
19
22
  import {
23
+ AccessControlError,
20
24
  BadDataError,
21
25
  BadEmailError,
22
26
  BadUsernameError,
23
27
  EmailChangeRateLimitExceededError,
24
- } from './errors';
28
+ } from './errors/index.js';
25
29
 
26
30
  export type EventType =
31
+ | 'checkUsername'
27
32
  | 'beforeRegister'
28
33
  | 'afterRegister'
34
+ | 'beforeSave'
35
+ | 'afterSave'
29
36
  | 'beforeLogin'
30
37
  | 'afterLogin'
31
38
  | 'beforeLogout'
32
- | 'afterLogout';
39
+ | 'afterLogout'
40
+ | 'beforeDelete'
41
+ | 'afterDelete';
42
+ /**
43
+ * This is run when the user has entered an otherwise valid username into the
44
+ * signup form. It should return a result, which when false will stop the
45
+ * process and return the included message, disallowing the username.
46
+ */
47
+ export type TilmeldCheckUsernameCallback = (
48
+ user: User & UserData,
49
+ data: { username: string },
50
+ ) => Promise<{ result: boolean; message?: string }>;
33
51
  /**
34
- * Theses are run before the user data checks, so the only checks before are
52
+ * This is run before the user data checks, so the only checks before are
35
53
  * whether registration is allowed and whether the user is already registered.
36
54
  */
37
55
  export type TilmeldBeforeRegisterCallback = (
38
56
  user: User & UserData,
39
- data: { password: string; additionalData?: { [k: string]: any } }
57
+ data: { password: string; additionalData?: { [k: string]: any } },
40
58
  ) => Promise<void>;
59
+ /**
60
+ * This is run before the transaction is committed, and you can perform
61
+ * additional functions on the transaction, which is available in `user.$nymph`.
62
+ */
41
63
  export type TilmeldAfterRegisterCallback = (
42
64
  user: User & UserData,
43
- result: { loggedin: boolean; message: string }
65
+ result: { loggedin: boolean; message: string },
66
+ ) => Promise<void>;
67
+ /**
68
+ * This is run just before the user is saved, so all checks have passed.
69
+ *
70
+ * The transaction is available in `user.$nymph`.
71
+ */
72
+ export type TilmeldBeforeSaveCallback = (
73
+ user: User & UserData,
44
74
  ) => Promise<void>;
45
75
  /**
46
- * These are run after the authentication checks, but before the login action.
76
+ * This is run before the transaction is committed, and you can perform
77
+ * additional functions on the transaction, which is available in `user.$nymph`.
78
+ *
79
+ * This is run after a new email verification email has been sent, so throwing
80
+ * an error will cause confusion for the user if they've changed their email.
81
+ */
82
+ export type TilmeldAfterSaveCallback = (user: User & UserData) => Promise<void>;
83
+ /**
84
+ * This is run after the authentication checks, but before the login action.
47
85
  */
48
86
  export type TilmeldBeforeLoginCallback = (
49
87
  user: User & UserData,
@@ -51,20 +89,32 @@ export type TilmeldBeforeLoginCallback = (
51
89
  username: string;
52
90
  password: string;
53
91
  additionalData?: { [k: string]: any };
54
- }
92
+ },
55
93
  ) => Promise<void>;
56
- /**
57
- * This is run before the transaction is committed, and you can perform
58
- * additional functions on the transaction, which is available in `user.$nymph`.
59
- */
60
94
  export type TilmeldAfterLoginCallback = (
61
- user: User & UserData
95
+ user: User & UserData,
62
96
  ) => Promise<void>;
63
97
  export type TilmeldBeforeLogoutCallback = (
64
- user: User & UserData
98
+ user: User & UserData,
65
99
  ) => Promise<void>;
66
100
  export type TilmeldAfterLogoutCallback = (
67
- user: User & UserData
101
+ user: User & UserData,
102
+ ) => Promise<void>;
103
+ /**
104
+ * This is run before the user is logged out (if the current user is being
105
+ * deleted).
106
+ *
107
+ * The transaction is available in `user.$nymph`.
108
+ */
109
+ export type TilmeldBeforeDeleteCallback = (
110
+ user: User & UserData,
111
+ ) => Promise<void>;
112
+ /**
113
+ * This is run before the transaction is committed, and you can perform
114
+ * additional functions on the transaction, which is available in `user.$nymph`.
115
+ */
116
+ export type TilmeldAfterDeleteCallback = (
117
+ user: User & UserData,
68
118
  ) => Promise<void>;
69
119
 
70
120
  export type UserData = {
@@ -108,11 +158,11 @@ export type UserData = {
108
158
  /**
109
159
  * The user's primary group.
110
160
  */
111
- group?: Group & GroupData;
161
+ group?: GroupClass & GroupData;
112
162
  /**
113
163
  * The user's secondary groups.
114
164
  */
115
- groups?: (Group & GroupData)[];
165
+ groups?: (GroupClass & GroupData)[];
116
166
  /**
117
167
  * Whether the user should inherit the abilities of his groups.
118
168
  */
@@ -135,7 +185,8 @@ export type UserData = {
135
185
  */
136
186
  newEmailSecret?: string;
137
187
  /**
138
- * The new email address.
188
+ * If the user has changed their email address, this is the new one, awaiting
189
+ * verification.
139
190
  */
140
191
  newEmailAddress?: string;
141
192
  /**
@@ -167,6 +218,15 @@ export type UserData = {
167
218
  * database.
168
219
  */
169
220
  passwordTemp?: string;
221
+ /**
222
+ * If set, this timestamp is the cutoff point for JWT issue dates. Any token
223
+ * issued before this date will not authenticate the user.
224
+ */
225
+ revokeTokenDate?: number;
226
+ /**
227
+ * Two factor auth secret.
228
+ */
229
+ totpSecret?: string;
170
230
  };
171
231
 
172
232
  /**
@@ -174,7 +234,7 @@ export type UserData = {
174
234
  *
175
235
  * Written by Hunter Perrin for SciActive.
176
236
  *
177
- * @author Hunter Perrin <hperrin@gmail.com>
237
+ * @author Hunter Perrin <hperrin@port87.com>
178
238
  * @copyright SciActive Inc
179
239
  * @see http://nymph.io/
180
240
  */
@@ -182,15 +242,19 @@ export default class User extends AbleObject<UserData> {
182
242
  /**
183
243
  * The instance of Tilmeld to use for queries.
184
244
  */
185
- static tilmeld: Tilmeld;
186
245
  static ETYPE = 'tilmeld_user';
187
246
  static class = 'User';
247
+ private static checkUsernameCallbacks: TilmeldCheckUsernameCallback[] = [];
188
248
  private static beforeRegisterCallbacks: TilmeldBeforeRegisterCallback[] = [];
189
249
  private static afterRegisterCallbacks: TilmeldAfterRegisterCallback[] = [];
250
+ private static beforeSaveCallbacks: TilmeldBeforeSaveCallback[] = [];
251
+ private static afterSaveCallbacks: TilmeldAfterSaveCallback[] = [];
190
252
  private static beforeLoginCallbacks: TilmeldBeforeLoginCallback[] = [];
191
253
  private static afterLoginCallbacks: TilmeldAfterLoginCallback[] = [];
192
254
  private static beforeLogoutCallbacks: TilmeldBeforeLogoutCallback[] = [];
193
255
  private static afterLogoutCallbacks: TilmeldAfterLogoutCallback[] = [];
256
+ private static beforeDeleteCallbacks: TilmeldBeforeDeleteCallback[] = [];
257
+ private static afterDeleteCallbacks: TilmeldAfterDeleteCallback[] = [];
194
258
 
195
259
  private static DEFAULT_CLIENT_ENABLED_METHODS = [
196
260
  '$checkUsername',
@@ -200,23 +264,33 @@ export default class User extends AbleObject<UserData> {
200
264
  '$register',
201
265
  ];
202
266
  private static DEFAULT_PRIVATE_DATA = [
267
+ // The things that are commented are there to show what is _not_ private.
268
+ 'abilities',
269
+ // 'username',
270
+ // 'nameFirst',
271
+ // 'nameMiddle',
272
+ // 'nameLast',
273
+ // 'name',
203
274
  'email',
204
- 'originalEmail',
275
+ // 'avatar',
205
276
  'phone',
206
277
  'group',
207
278
  'groups',
208
- 'abilities',
209
279
  'inheritAbilities',
210
- 'recoverSecret',
211
- 'recoverSecretDate',
212
- 'password',
213
- 'salt',
280
+ 'enabled',
214
281
  'secret',
215
- 'newEmailAddress',
282
+ 'emailChangeDate',
216
283
  'newEmailSecret',
217
- 'cancelEmailAddress',
284
+ 'newEmailAddress',
218
285
  'cancelEmailSecret',
219
- 'emailChangeDate',
286
+ 'cancelEmailAddress',
287
+ 'recoverSecret',
288
+ 'recoverSecretDate',
289
+ 'salt',
290
+ 'password',
291
+ 'passwordTemp',
292
+ 'revokeTokenDate',
293
+ 'totpSecret',
220
294
  ];
221
295
  private static DEFAULT_ALLOWLIST_DATA: string[] = [];
222
296
 
@@ -227,6 +301,7 @@ export default class User extends AbleObject<UserData> {
227
301
  'sendRecovery',
228
302
  'recover',
229
303
  'getClientConfig',
304
+ 'getDomainUsers',
230
305
  ];
231
306
  protected $privateData = [...User.DEFAULT_PRIVATE_DATA];
232
307
  public static searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
@@ -241,13 +316,17 @@ export default class User extends AbleObject<UserData> {
241
316
  */
242
317
  private $gatekeeperCache?: { [k: string]: true };
243
318
  /**
244
- * This is explicitly used only during the registration proccess.
319
+ * This should only be used by the backend.
245
320
  */
246
321
  private $skipAcWhenSaving = false;
322
+ /**
323
+ * This should only be used by the backend.
324
+ */
325
+ private $skipAcWhenDeleting = false;
247
326
  /**
248
327
  * The user's group descendants.
249
328
  */
250
- private $descendantGroups?: (Group & GroupData)[];
329
+ private $descendantGroups?: (GroupClass & GroupData)[];
251
330
  /**
252
331
  * Used to save the current email address to send verification if it changes
253
332
  * from the frontend.
@@ -256,9 +335,37 @@ export default class User extends AbleObject<UserData> {
256
335
  * verification, don't set this.
257
336
  */
258
337
  public $originalEmail?: string;
338
+ /**
339
+ * Used to save the current username for domain admin permissions.
340
+ */
341
+ public $originalUsername?: string;
259
342
 
260
- static async factory(guid?: string): Promise<User & UserData> {
261
- return (await super.factory(guid)) as User & UserData;
343
+ public static getFTSText(name: string, value: any) {
344
+ if (
345
+ [
346
+ 'abilities',
347
+ 'group',
348
+ 'groups',
349
+ 'inheritAbilities',
350
+ 'enabled',
351
+ 'secret',
352
+ 'emailChangeDate',
353
+ 'newEmailSecret',
354
+ 'newEmailAddress',
355
+ 'cancelEmailSecret',
356
+ 'cancelEmailAddress',
357
+ 'recoverSecret',
358
+ 'recoverSecretDate',
359
+ 'salt',
360
+ 'password',
361
+ 'passwordTemp',
362
+ 'revokeTokenDate',
363
+ 'totpSecret',
364
+ ].indexOf(name) !== -1
365
+ ) {
366
+ return null;
367
+ }
368
+ return super.getFTSText(name, value);
262
369
  }
263
370
 
264
371
  static async factoryUsername(username?: string): Promise<User & UserData> {
@@ -271,7 +378,7 @@ export default class User extends AbleObject<UserData> {
271
378
  {
272
379
  type: '&',
273
380
  ilike: ['username', username.replace(/([\\%_])/g, (s) => `\\${s}`)],
274
- }
381
+ },
275
382
  );
276
383
  if (entity != null) {
277
384
  return entity;
@@ -280,34 +387,18 @@ export default class User extends AbleObject<UserData> {
280
387
  return entity;
281
388
  }
282
389
 
283
- static factorySync(guid?: string): User & UserData {
284
- return super.factorySync(guid) as User & UserData;
285
- }
286
-
287
- constructor(guid?: string) {
288
- super(guid);
289
-
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
- }
298
- }
299
-
300
390
  public static current(returnObjectIfNotExist: true): User & UserData;
301
391
  public static current(
302
- returnObjectIfNotExist?: false
392
+ returnObjectIfNotExist?: false,
303
393
  ): (User & UserData) | null;
304
394
  public static current(
305
- returnObjectIfNotExist?: boolean
395
+ returnObjectIfNotExist?: boolean,
306
396
  ): (User & UserData) | null {
307
- if (this.tilmeld.currentUser == null) {
397
+ const tilmeld = enforceTilmeld(this);
398
+ if (tilmeld.currentUser == null) {
308
399
  return returnObjectIfNotExist ? this.factorySync() : null;
309
400
  }
310
- return this.tilmeld.currentUser;
401
+ return tilmeld.currentUser;
311
402
  }
312
403
 
313
404
  /**
@@ -320,7 +411,8 @@ export default class User extends AbleObject<UserData> {
320
411
  recoveryType: 'username' | 'password';
321
412
  account: string;
322
413
  }): Promise<{ result: boolean; message: string }> {
323
- if (!this.tilmeld.config.pwRecovery) {
414
+ const tilmeld = enforceTilmeld(this);
415
+ if (!tilmeld.config.pwRecovery) {
324
416
  return {
325
417
  result: false,
326
418
  message: 'Account recovery is not allowed.',
@@ -330,18 +422,15 @@ export default class User extends AbleObject<UserData> {
330
422
  let user: User & UserData;
331
423
  const options: EmailOptions = {};
332
424
 
333
- if (
334
- !this.tilmeld.config.emailUsernames &&
335
- data.recoveryType === 'username'
336
- ) {
425
+ if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
337
426
  // Create a username recovery email.
338
427
 
339
428
  const getUser = await this.nymph.getEntity(
340
- { class: this.tilmeld.User, skipAc: true },
429
+ { class: tilmeld.User, skipAc: true },
341
430
  {
342
431
  type: '&',
343
432
  ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
344
- }
433
+ },
345
434
  );
346
435
 
347
436
  if (getUser == null) {
@@ -364,7 +453,7 @@ export default class User extends AbleObject<UserData> {
364
453
  } else if (data.recoveryType === 'password') {
365
454
  // Create a password recovery email.
366
455
 
367
- const getUser = await this.tilmeld.User.factoryUsername(data.account);
456
+ const getUser = await tilmeld.User.factoryUsername(data.account);
368
457
 
369
458
  if (getUser.guid == null) {
370
459
  return {
@@ -391,14 +480,19 @@ export default class User extends AbleObject<UserData> {
391
480
  };
392
481
  options.locals = {
393
482
  recoverCode: getUser.recoverSecret,
394
- timeLimit: this.tilmeld.config.pwRecoveryTimeLimit,
483
+ timeLimit: tilmeld.config.pwRecoveryTimeLimit,
395
484
  };
396
485
  } else {
397
486
  return { result: false, message: 'Invalid recovery type.' };
398
487
  }
399
488
 
489
+ this.nymph.config.debugLog(
490
+ 'tilmeld',
491
+ `User account recovery (${options.template}) request for "${user.username}".`,
492
+ );
493
+
400
494
  // Send the email.
401
- if (await this.tilmeld.config.sendEmail(this.tilmeld, options, user)) {
495
+ if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
402
496
  return {
403
497
  result: true,
404
498
  message:
@@ -421,23 +515,24 @@ export default class User extends AbleObject<UserData> {
421
515
  secret: string;
422
516
  password: string;
423
517
  }): Promise<{ result: boolean; message: string }> {
424
- if (!this.tilmeld.config.pwRecovery) {
518
+ const tilmeld = enforceTilmeld(this);
519
+ if (!tilmeld.config.pwRecovery) {
425
520
  return {
426
521
  result: false,
427
522
  message: 'Account recovery is not allowed.',
428
523
  };
429
524
  }
430
525
 
431
- const user = await this.tilmeld.User.factoryUsername(data.username);
526
+ const user = await tilmeld.User.factoryUsername(data.username);
432
527
 
433
528
  if (
434
529
  user.guid == null ||
435
530
  user.recoverSecret == null ||
436
531
  data.secret !== user.recoverSecret ||
437
- strtotime(
438
- '+' + this.tilmeld.config.pwRecoveryTimeLimit,
439
- Math.floor((user.recoverSecretDate ?? 0) / 1000)
440
- ) *
532
+ (strtotime(
533
+ '+' + tilmeld.config.pwRecoveryTimeLimit,
534
+ Math.floor((user.recoverSecretDate ?? 0) / 1000),
535
+ ) || 0) *
441
536
  1000 <
442
537
  Date.now()
443
538
  ) {
@@ -451,6 +546,11 @@ export default class User extends AbleObject<UserData> {
451
546
  return { result: false, message: 'Password cannot be empty.' };
452
547
  }
453
548
 
549
+ this.nymph.config.debugLog(
550
+ 'tilmeld',
551
+ `User account recovery for "${user.username}".`,
552
+ );
553
+
454
554
  user.$password(data.password);
455
555
  delete user.recoverSecret;
456
556
  delete user.recoverSecretDate;
@@ -466,27 +566,31 @@ export default class User extends AbleObject<UserData> {
466
566
  }
467
567
 
468
568
  public static getClientConfig() {
569
+ const tilmeld = enforceTilmeld(this);
469
570
  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,
571
+ regFields: tilmeld.config.regFields,
572
+ userFields: tilmeld.config.userFields,
573
+ emailUsernames: tilmeld.config.emailUsernames,
574
+ domainSupport: tilmeld.config.domainSupport,
575
+ allowRegistration: tilmeld.config.allowRegistration,
576
+ allowUsernameChange: tilmeld.config.allowUsernameChange,
577
+ pwRecovery: tilmeld.config.pwRecovery,
578
+ verifyEmail: tilmeld.config.verifyEmail,
579
+ unverifiedAccess: tilmeld.config.unverifiedAccess,
478
580
  };
479
581
  }
480
582
 
481
583
  public static async loginUser(data: {
482
584
  username: string;
483
585
  password: string;
586
+ code?: string;
484
587
  additionalData?: { [k: string]: any };
485
588
  }) {
589
+ const tilmeld = enforceTilmeld(this);
486
590
  if (!('username' in data) || !data.username.length) {
487
591
  return { result: false, message: 'Incorrect login/password.' };
488
592
  }
489
- const user = await this.tilmeld.User.factoryUsername(data.username);
593
+ const user = await tilmeld.User.factoryUsername(data.username);
490
594
  const result: { result: boolean; message: string; user?: User & UserData } =
491
595
  await user.$login(data);
492
596
  if (result.result) {
@@ -496,23 +600,150 @@ export default class User extends AbleObject<UserData> {
496
600
  return result;
497
601
  }
498
602
 
603
+ /**
604
+ * Get the users in a domain. This is only accessible to the domain's admins.
605
+ */
606
+ public static async getDomainUsers(
607
+ domain: string,
608
+ options?: {
609
+ limit?: number;
610
+ offset?: number;
611
+ sort?: string;
612
+ reverse?: boolean;
613
+ },
614
+ ) {
615
+ const tilmeld = enforceTilmeld(this);
616
+
617
+ if (!tilmeld.config.domainSupport) {
618
+ throw new AccessControlError('Domain support is not enabled.');
619
+ }
620
+
621
+ if (!domain) {
622
+ throw new BadDataError('Domain is required.');
623
+ }
624
+
625
+ domain = domain.toLowerCase();
626
+ if (!tilmeld.config.validDomainRegex.test(domain)) {
627
+ throw new BadDataError(tilmeld.config.validDomainRegexNotice);
628
+ }
629
+
630
+ if (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
631
+ throw new AccessControlError(
632
+ 'You do not have the authority to manage this domain.',
633
+ );
634
+ }
635
+
636
+ return await this.nymph.getEntities(
637
+ {
638
+ class: this.nymph.getEntityClass(User),
639
+ ...(options?.limit != null
640
+ ? {
641
+ limit: options.limit,
642
+ }
643
+ : {}),
644
+ ...(options?.offset != null
645
+ ? {
646
+ offset: options.offset,
647
+ }
648
+ : {}),
649
+ ...(options?.sort != null
650
+ ? {
651
+ sort: options.sort,
652
+ }
653
+ : {}),
654
+ ...(options?.reverse != null
655
+ ? {
656
+ reverse: options.reverse,
657
+ }
658
+ : {}),
659
+ },
660
+ {
661
+ type: '&',
662
+ like: ['username', `%@${domain.replace(/([\\%_])/g, (s) => `\\${s}`)}`],
663
+ },
664
+ );
665
+ }
666
+
667
+ constructor() {
668
+ super();
669
+
670
+ // Defaults.
671
+ this.$data.enabled = true;
672
+ this.$data.abilities = [];
673
+ this.$data.groups = [];
674
+ this.$data.inheritAbilities = true;
675
+ this.$updateDataProtection();
676
+ }
677
+
678
+ $setNymph(nymph: Nymph) {
679
+ this.$nymph = nymph;
680
+ if (!this.$asleep()) {
681
+ if (this.$data.group && this.$data.group.$nymph !== nymph) {
682
+ this.$data.group.$setNymph(nymph);
683
+ }
684
+ for (const group of this.$data.groups ?? []) {
685
+ if (group && group.$nymph !== nymph) {
686
+ group.$setNymph(nymph);
687
+ }
688
+ }
689
+ }
690
+ }
691
+
692
+ async $getUniques(): Promise<string[]> {
693
+ const tilmeld = enforceTilmeld(this);
694
+ const uniques = [`u:${this.$data.username}`];
695
+ if (
696
+ !tilmeld.config.emailUsernames &&
697
+ tilmeld.config.userFields.includes('email')
698
+ ) {
699
+ uniques.push(`e:${this.$data.email}`);
700
+
701
+ if (this.$data.newEmailAddress != null) {
702
+ uniques.push(`e:${this.$data.newEmailAddress}`);
703
+ }
704
+ }
705
+ return uniques;
706
+ }
707
+
499
708
  public async $login(data: {
500
709
  username: string;
501
710
  password: string;
711
+ code?: string;
502
712
  additionalData?: { [k: string]: any };
503
713
  }) {
504
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
714
+ const tilmeld = enforceTilmeld(this);
505
715
  if (this.guid == null) {
716
+ await tilmeld.config.failedLoginAttempt(null, data);
717
+ return { result: false, message: 'Incorrect login/password.' };
718
+ }
719
+ if (!this.$checkPassword(data.password)) {
720
+ await tilmeld.config.failedLoginAttempt(this, data);
506
721
  return { result: false, message: 'Incorrect login/password.' };
507
722
  }
508
723
  if (!this.$data.enabled) {
509
- return { result: false, message: 'This user is disabled.' };
724
+ return { result: false, message: 'Your account is disabled.' };
510
725
  }
511
- if (this.$gatekeeper()) {
726
+ if (await this.$gatekeeper()) {
512
727
  return { result: true, message: 'You are already logged in.' };
513
728
  }
514
- if (!this.$checkPassword(data.password)) {
515
- return { result: false, message: 'Incorrect login/password.' };
729
+
730
+ if (this.$data.totpSecret != null) {
731
+ if (
732
+ data.code == null ||
733
+ typeof data.code !== 'string' ||
734
+ data.code.length !== 6
735
+ ) {
736
+ return {
737
+ result: false,
738
+ needTOTP: true,
739
+ message: 'You need to provide a 2FA code.',
740
+ };
741
+ }
742
+
743
+ if (!this.$checkTOTPCode(data.code)) {
744
+ await tilmeld.config.failedLoginAttempt(this, data);
745
+ return { result: false, message: 'Incorrect 2FA code.' };
746
+ }
516
747
  }
517
748
 
518
749
  try {
@@ -529,8 +760,90 @@ export default class User extends AbleObject<UserData> {
529
760
  };
530
761
  }
531
762
 
763
+ this.$nymph.config.debugLog(
764
+ 'tilmeld',
765
+ `User login for "${this.$data.username}".`,
766
+ );
767
+
768
+ // Authentication was successful, attempt to login.
769
+ if (!(await tilmeld.login(this, true))) {
770
+ return { result: false, message: 'Incorrect login/password.' };
771
+ }
772
+
773
+ for (let callback of (this.constructor as typeof User)
774
+ .afterLoginCallbacks) {
775
+ if (callback) {
776
+ await callback(this);
777
+ }
778
+ }
779
+
780
+ // Login was successful.
781
+ return { result: true, message: 'You are logged in.' };
782
+ }
783
+
784
+ public async $switchUser(data?: { additionalData?: { [k: string]: any } }) {
785
+ const tilmeld = enforceTilmeld(this);
786
+ if (this.guid == null) {
787
+ return { result: false, message: 'Incorrect login/password.' };
788
+ }
789
+
790
+ let user = tilmeld.User.current();
791
+
792
+ if (!user || user.guid == null) {
793
+ return { result: false, message: "You're not logged in." };
794
+ }
795
+
796
+ const isCurrentUser = user != null && user.$is(this);
797
+
798
+ if (isCurrentUser) {
799
+ return { result: false, message: "You can't switch to yourself." };
800
+ }
801
+
802
+ const toAbilities = this.$data.abilities || [];
803
+ const fromAbilities = user.abilities || [];
804
+
805
+ if (
806
+ !fromAbilities.includes('tilmeld/switch') &&
807
+ !fromAbilities.includes('system/admin')
808
+ ) {
809
+ return {
810
+ result: false,
811
+ message: "You don't have permission to switch users.",
812
+ };
813
+ }
814
+
815
+ if (
816
+ toAbilities.includes('system/admin') ||
817
+ toAbilities.includes('tilmeld/admin')
818
+ ) {
819
+ return { result: false, message: "You can't switch to an admin." };
820
+ }
821
+
822
+ try {
823
+ for (let callback of (this.constructor as typeof User)
824
+ .beforeLoginCallbacks) {
825
+ if (callback) {
826
+ await callback(this, {
827
+ username: this.$data.username || '',
828
+ password: '',
829
+ ...(data || {}),
830
+ });
831
+ }
832
+ }
833
+ } catch (e: any) {
834
+ return {
835
+ result: false,
836
+ message: e.message,
837
+ };
838
+ }
839
+
840
+ this.$nymph.config.debugLog(
841
+ 'tilmeld',
842
+ `User login switch for "${user.username}" to "${this.$data.username}".`,
843
+ );
844
+
532
845
  // Authentication was successful, attempt to login.
533
- if (!tilmeld.login(this, true)) {
846
+ if (!(await tilmeld.loginSwitch(this, true))) {
534
847
  return { result: false, message: 'Incorrect login/password.' };
535
848
  }
536
849
 
@@ -550,7 +863,7 @@ export default class User extends AbleObject<UserData> {
550
863
  * @returns An object with a boolean 'result' entry and a 'message' entry.
551
864
  */
552
865
  public async $logout() {
553
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
866
+ const tilmeld = enforceTilmeld(this);
554
867
 
555
868
  try {
556
869
  for (let callback of (this.constructor as typeof User)
@@ -566,7 +879,12 @@ export default class User extends AbleObject<UserData> {
566
879
  };
567
880
  }
568
881
 
569
- tilmeld.logout();
882
+ this.$nymph.config.debugLog(
883
+ 'tilmeld',
884
+ `User logout for "${this.$data.username}".`,
885
+ );
886
+
887
+ await tilmeld.logout();
570
888
 
571
889
  for (let callback of (this.constructor as typeof User)
572
890
  .afterLogoutCallbacks) {
@@ -587,89 +905,183 @@ export default class User extends AbleObject<UserData> {
587
905
  }
588
906
  return (
589
907
  'https://secure.gravatar.com/avatar/' +
590
- md5(this.$data.email.trim().toLowerCase()).toString() +
908
+ crypto
909
+ .createHash('sha256')
910
+ .update(this.$data.email.trim().toLowerCase())
911
+ .digest('hex')
912
+ .toLowerCase() +
591
913
  '?d=identicon&s=40'
592
914
  );
593
915
  }
594
916
 
917
+ public $getGid() {
918
+ if (this.$data.group) {
919
+ return this.$data.group.guid;
920
+ }
921
+ return null;
922
+ }
923
+
924
+ public $getGids() {
925
+ if (this.$data.groups) {
926
+ return this.$data.groups
927
+ .map((group) => group.guid)
928
+ .filter((guid) => guid != null) as string[];
929
+ }
930
+ return null;
931
+ }
932
+
595
933
  /**
596
934
  * Get the user's group descendants.
597
935
  */
598
- public async $getDescendantGroups(): Promise<(Group & GroupData)[]> {
936
+ public async $getDescendantGroups(): Promise<(GroupClass & GroupData)[]> {
599
937
  if (this.$descendantGroups == null) {
600
938
  this.$descendantGroups = [];
601
939
  if (this.$data.group != null) {
940
+ await this.$data.group.$wake();
602
941
  this.$descendantGroups = await this.$data.group.$getDescendants();
603
942
  }
943
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
604
944
  for (let curGroup of this.$data.groups ?? []) {
605
945
  this.$descendantGroups = this.$descendantGroups?.concat(
606
- await curGroup.$getDescendants()
946
+ await curGroup.$getDescendants(),
607
947
  );
608
948
  }
609
949
  }
610
950
  return this.$descendantGroups;
611
951
  }
612
952
 
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
- );
953
+ public $jsonAcceptData(input: EntityJson, allowConflict = false) {
954
+ const tilmeld = enforceTilmeld(this);
955
+ this.$check();
956
+
957
+ if ('abilities' in input.data) {
958
+ this.$checkIncomingAbilities(input.data.abilities);
959
+ }
960
+
961
+ // If the user is a new domain user, the data protection needs to be updated
962
+ // before the data is accepted, or else some things won't be accepted
963
+ // correctly (enabled, passwordTemp).
964
+ let reloadDataProtection = false;
965
+ if (tilmeld.config.domainSupport) {
966
+ let [_username, domain] = splitn(input.data.username ?? '', '@', 2);
967
+ // Lowercase the domain part.
968
+ if (domain) {
969
+ domain = domain.toLowerCase();
626
970
  }
971
+ const user = tilmeld.User.current();
972
+ const isCurrentUser = user != null && user.$is(this);
973
+ // While loading, accessing abilities on the current user will infinitely
974
+ // loop, but if this is the current user, we can use its abilities.
975
+ const abilities =
976
+ (isCurrentUser || user == null
977
+ ? this.$data.abilities
978
+ : user?.abilities) ?? [];
979
+ const isNewUser = input.guid == null;
980
+ const isDomainUser =
981
+ !isCurrentUser &&
982
+ user != null &&
983
+ domain &&
984
+ abilities.includes(`tilmeld/domain/${domain}/admin`);
985
+
986
+ reloadDataProtection = !!(isNewUser && isDomainUser);
987
+ }
988
+
989
+ super.$jsonAcceptData(input, allowConflict);
990
+
991
+ if (reloadDataProtection) {
992
+ // Load it again after data protection is run.
993
+ this.$updateDataProtection();
994
+ super.$jsonAcceptData(input, allowConflict);
627
995
  }
628
- return this.$descendantGroups;
629
996
  }
630
997
 
631
- public $jsonAcceptData(input: EntityJson, allowConflict = false) {
632
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
633
- this.$referenceWake();
998
+ public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
999
+ this.$check();
1000
+
1001
+ if ('abilities' in patch.set) {
1002
+ this.$checkIncomingAbilities(patch.set.abilities);
1003
+ }
1004
+
1005
+ super.$jsonAcceptPatch(patch, allowConflict);
1006
+ }
1007
+
1008
+ private $checkIncomingAbilities(incoming: any) {
1009
+ const tilmeld = enforceTilmeld(this);
634
1010
 
635
1011
  if (
636
- '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')
1012
+ !Array.isArray(incoming) ||
1013
+ incoming.find((ability) => typeof ability !== 'string')
1014
+ ) {
1015
+ throw new BadDataError('Abilities must be an array of strings.');
1016
+ }
1017
+
1018
+ if (
1019
+ incoming.includes('system/admin') !==
1020
+ this.$data.abilities?.includes('system/admin') &&
1021
+ (!tilmeld.currentUser ||
1022
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
641
1023
  ) {
642
1024
  throw new BadDataError(
643
- "You don't have the authority to make this user a system admin."
1025
+ "You don't have the authority to grant or revoke system/admin.",
644
1026
  );
645
1027
  }
646
1028
 
647
- this.$originalEmail = this.$data.email;
648
- super.$jsonAcceptData(input, allowConflict);
649
- }
650
-
651
- public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
652
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
653
- this.$referenceWake();
1029
+ if (
1030
+ incoming.includes('tilmeld/admin') !==
1031
+ this.$data.abilities?.includes('tilmeld/admin') &&
1032
+ (!tilmeld.currentUser ||
1033
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
1034
+ ) {
1035
+ throw new BadDataError(
1036
+ "You don't have the authority to grant or revoke tilmeld/admin.",
1037
+ );
1038
+ }
654
1039
 
655
1040
  if (
656
- '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')
1041
+ incoming.includes('tilmeld/switch') !==
1042
+ this.$data.abilities?.includes('tilmeld/switch') &&
1043
+ (!tilmeld.currentUser ||
1044
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
661
1045
  ) {
662
1046
  throw new BadDataError(
663
- "You don't have the authority to make this user a system admin."
1047
+ "You don't have the authority to grant or revoke tilmeld/switch.",
664
1048
  );
665
1049
  }
666
1050
 
667
- this.$originalEmail = this.$data.email;
668
- super.$jsonAcceptPatch(patch, allowConflict);
1051
+ if (
1052
+ xor(
1053
+ incoming.filter((ability: string) => ability.startsWith('tilmeld/')),
1054
+ this.$data.abilities?.filter((ability: string) =>
1055
+ ability.startsWith('tilmeld/'),
1056
+ ) ?? [],
1057
+ ).length &&
1058
+ (!tilmeld.currentUser ||
1059
+ (!tilmeld.currentUser.abilities?.includes('system/admin') &&
1060
+ !tilmeld.currentUser.abilities?.includes('tilmeld/admin')))
1061
+ ) {
1062
+ throw new BadDataError(
1063
+ "You don't have the authority to grant or revoke Tilmeld abilities.",
1064
+ );
1065
+ }
669
1066
  }
670
1067
 
671
- public $putData(data: EntityData, sdata?: SerializedEntityData) {
672
- super.$putData(data, sdata);
1068
+ public $putData(
1069
+ data: EntityData,
1070
+ sdata?: SerializedEntityData,
1071
+ source?: 'server',
1072
+ ) {
1073
+ super.$putData(data, sdata, source);
1074
+ // TODO: Fix these being added by the drivers.
1075
+ // @ts-ignore
1076
+ delete this.$data.acRead;
1077
+ // @ts-ignore
1078
+ delete this.$data.acWrite;
1079
+ // @ts-ignore
1080
+ delete this.$data.acFull;
1081
+ if (source === 'server') {
1082
+ this.$originalEmail = this.$data.email;
1083
+ this.$originalUsername = this.$data.username;
1084
+ }
673
1085
  this.$updateDataProtection();
674
1086
  }
675
1087
 
@@ -679,8 +1091,15 @@ export default class User extends AbleObject<UserData> {
679
1091
  * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
680
1092
  */
681
1093
  public $updateDataProtection(givenUser?: User & UserData) {
682
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1094
+ const tilmeld = enforceTilmeld(this);
683
1095
  let user = givenUser ?? tilmeld.User.current();
1096
+ let [_username, domain] = tilmeld.config.domainSupport
1097
+ ? splitn(this.$originalUsername ?? this.$data.username ?? '', '@', 2)
1098
+ : [this.$originalUsername ?? this.$data.username, null];
1099
+ // Lowercase the domain part.
1100
+ if (domain) {
1101
+ domain = domain.toLowerCase();
1102
+ }
684
1103
 
685
1104
  this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
686
1105
  this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
@@ -698,53 +1117,165 @@ export default class User extends AbleObject<UserData> {
698
1117
  ? this.$data.abilities
699
1118
  : user?.abilities) ?? [];
700
1119
  const isNewUser = this.guid == null;
1120
+ const isDomainUser =
1121
+ tilmeld.config.domainSupport &&
1122
+ !isCurrentUser &&
1123
+ user != null &&
1124
+ domain &&
1125
+ abilities.includes(`tilmeld/domain/${domain}/admin`);
701
1126
 
702
1127
  if (isCurrentUser) {
703
1128
  // Users can check to see what abilities they have.
704
1129
  this.$clientEnabledMethods.push('$gatekeeper');
705
1130
  this.$clientEnabledMethods.push('$changePassword');
1131
+ this.$clientEnabledMethods.push('$revokeCurrentTokens');
706
1132
  this.$clientEnabledMethods.push('$logout');
707
1133
  this.$clientEnabledMethods.push('$sendEmailVerification');
1134
+ this.$clientEnabledMethods.push('$hasTOTPSecret');
1135
+ this.$clientEnabledMethods.push('$getNewTOTPSecret');
1136
+ this.$clientEnabledMethods.push('$saveTOTPSecret');
1137
+ this.$clientEnabledMethods.push('$removeTOTPSecret');
1138
+ }
1139
+
1140
+ if (isDomainUser) {
1141
+ // Domain admins can check to see what abilities their users have.
1142
+ this.$clientEnabledMethods.push('$gatekeeper');
1143
+ this.$clientEnabledMethods.push('$changePassword');
1144
+ this.$clientEnabledMethods.push('$revokeCurrentTokens');
1145
+ this.$clientEnabledMethods.push('$hasTOTPSecret');
1146
+ this.$clientEnabledMethods.push('$getNewTOTPSecret');
1147
+ this.$clientEnabledMethods.push('$saveTOTPSecret');
1148
+ this.$clientEnabledMethods.push('$removeTOTPSecret');
708
1149
  }
709
1150
 
710
1151
  if (
711
1152
  user != null &&
712
- (abilities.indexOf('tilmeld/admin') !== -1 ||
713
- abilities.indexOf('system/admin') !== -1)
1153
+ (abilities.includes('tilmeld/admin') ||
1154
+ abilities.includes('system/admin'))
714
1155
  ) {
715
1156
  // Users who can edit other users can see most of their data.
716
- this.$privateData = ['password', 'salt'];
1157
+ this.$privateData = ['password', 'salt', 'totpSecret'];
1158
+ this.$clientEnabledMethods.push('$hasTOTPSecret');
1159
+ this.$clientEnabledMethods.push('$removeTOTPSecret');
717
1160
  this.$allowlistData = undefined;
1161
+ } else if (isDomainUser) {
1162
+ // Domain admins can see their users' data, and edit some of it.
1163
+ if (tilmeld.config.allowUsernameChange || isNewUser) {
1164
+ this.$allowlistData.push('username');
1165
+ }
1166
+ this.$allowlistData.push('avatar');
1167
+ if (tilmeld.config.userFields.includes('name')) {
1168
+ this.$allowlistData.push('nameFirst');
1169
+ this.$allowlistData.push('nameMiddle');
1170
+ this.$allowlistData.push('nameLast');
1171
+ this.$allowlistData.push('name');
1172
+ }
1173
+ if (tilmeld.config.userFields.includes('email')) {
1174
+ this.$allowlistData.push('email');
1175
+ }
1176
+ if (tilmeld.config.userFields.includes('phone')) {
1177
+ this.$allowlistData.push('phone');
1178
+ }
1179
+ this.$allowlistData.push('enabled');
1180
+ this.$allowlistData.push('passwordTemp');
1181
+ this.$privateData = [
1182
+ // The things commented are there to show what is _not_ private.
1183
+ // 'abilities',
1184
+ // 'username',
1185
+ // 'nameFirst',
1186
+ // 'nameMiddle',
1187
+ // 'nameLast',
1188
+ // 'name',
1189
+ // 'email',
1190
+ // 'avatar',
1191
+ // 'phone',
1192
+ // 'group',
1193
+ // 'groups',
1194
+ // 'inheritAbilities',
1195
+ // 'enabled',
1196
+ 'secret',
1197
+ 'emailChangeDate',
1198
+ 'newEmailSecret',
1199
+ // 'newEmailAddress',
1200
+ 'cancelEmailSecret',
1201
+ 'cancelEmailAddress',
1202
+ 'recoverSecret',
1203
+ 'recoverSecretDate',
1204
+ 'salt',
1205
+ 'password',
1206
+ // 'passwordTemp',
1207
+ // 'revokeTokenDate',
1208
+ 'totpSecret',
1209
+ ];
718
1210
  } else if (isCurrentUser || isNewUser) {
719
1211
  // Users can see their own data, and edit some of it.
720
1212
  if (tilmeld.config.allowUsernameChange || isNewUser) {
721
1213
  this.$allowlistData.push('username');
722
1214
  }
723
1215
  this.$allowlistData.push('avatar');
724
- if (tilmeld.config.userFields.indexOf('name') !== -1) {
1216
+ if (tilmeld.config.userFields.includes('name')) {
725
1217
  this.$allowlistData.push('nameFirst');
726
1218
  this.$allowlistData.push('nameMiddle');
727
1219
  this.$allowlistData.push('nameLast');
728
1220
  this.$allowlistData.push('name');
729
1221
  }
730
- if (tilmeld.config.userFields.indexOf('email') !== -1) {
1222
+ if (tilmeld.config.userFields.includes('email')) {
731
1223
  this.$allowlistData.push('email');
732
1224
  }
733
- if (tilmeld.config.userFields.indexOf('phone') !== -1) {
1225
+ if (tilmeld.config.userFields.includes('phone')) {
734
1226
  this.$allowlistData.push('phone');
735
1227
  }
736
1228
  this.$privateData = [
1229
+ // The things commented are there to show what is _not_ private.
1230
+ // 'abilities',
1231
+ // 'username',
1232
+ // 'nameFirst',
1233
+ // 'nameMiddle',
1234
+ // 'nameLast',
1235
+ // 'name',
1236
+ // 'email',
1237
+ // 'avatar',
1238
+ // 'phone',
1239
+ // 'group',
1240
+ // 'groups',
1241
+ // 'inheritAbilities',
1242
+ 'enabled',
737
1243
  'secret',
1244
+ 'emailChangeDate',
738
1245
  'newEmailSecret',
739
- 'cancelEmailAddress',
1246
+ // 'newEmailAddress',
740
1247
  'cancelEmailSecret',
741
- 'emailChangeDate',
1248
+ 'cancelEmailAddress',
742
1249
  'recoverSecret',
743
1250
  'recoverSecretDate',
744
- 'password',
745
1251
  'salt',
1252
+ 'password',
1253
+ 'passwordTemp',
1254
+ // 'revokeTokenDate',
1255
+ 'totpSecret',
746
1256
  ];
747
1257
  }
1258
+
1259
+ if (
1260
+ user != null &&
1261
+ !isCurrentUser &&
1262
+ (abilities.includes('tilmeld/switch') ||
1263
+ abilities.includes('system/admin'))
1264
+ ) {
1265
+ // Users with this ability can switch to other users.
1266
+ this.$clientEnabledMethods.push('$switchUser');
1267
+ }
1268
+ }
1269
+
1270
+ public $updateGroupDataProtection() {
1271
+ if (this.$data.group != null && !this.$data.group.$asleep()) {
1272
+ this.$data.group.$updateDataProtection();
1273
+ }
1274
+ for (let group of this.$data.groups ?? []) {
1275
+ if (!group.$asleep()) {
1276
+ group.$updateDataProtection();
1277
+ }
1278
+ }
748
1279
  }
749
1280
 
750
1281
  /**
@@ -761,35 +1292,43 @@ export default class User extends AbleObject<UserData> {
761
1292
  * @param ability The ability.
762
1293
  * @returns True or false.
763
1294
  */
764
- public $gatekeeper(ability?: string): boolean {
765
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1295
+ public async $gatekeeper(ability?: string): Promise<boolean> {
1296
+ await this.$wake();
1297
+ const tilmeld = enforceTilmeld(this);
766
1298
  if (ability == null) {
767
1299
  return tilmeld.User.current()?.$is(this) ?? false;
768
1300
  }
769
1301
  // Check the cache to see if we've already checked this user.
770
1302
  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
- );
1303
+ this.$gatekeeperCache = await this.$getGatekeeperCache();
789
1304
  }
790
1305
  return (
791
1306
  (ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
792
- !!this.$gatekeeperCache['system/admin']
1307
+ !!this.$data.abilities?.includes('system/admin')
1308
+ );
1309
+ }
1310
+
1311
+ /**
1312
+ * Build a gatekeeper cache object.
1313
+ */
1314
+ public async $getGatekeeperCache() {
1315
+ await this.$wake();
1316
+ let abilities = this.$data.abilities ?? [];
1317
+ if (this.$data.inheritAbilities) {
1318
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
1319
+ for (let curGroup of this.$data.groups ?? []) {
1320
+ if (curGroup.enabled) {
1321
+ abilities = abilities.concat(curGroup.abilities ?? []);
1322
+ }
1323
+ }
1324
+ await this.$data.group?.$wake();
1325
+ const group = this.$data.group;
1326
+ if (group != null && group.cdate != null && group.enabled) {
1327
+ abilities = abilities.concat(group.abilities ?? []);
1328
+ }
1329
+ }
1330
+ return Object.fromEntries(
1331
+ abilities.map((ability) => [ability, true as true]),
793
1332
  );
794
1333
  }
795
1334
 
@@ -806,7 +1345,7 @@ export default class User extends AbleObject<UserData> {
806
1345
  * @returns True on success, false on failure.
807
1346
  */
808
1347
  public async $sendEmailVerification() {
809
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1348
+ const tilmeld = enforceTilmeld(this);
810
1349
  if (this.guid == null) {
811
1350
  return false;
812
1351
  }
@@ -818,7 +1357,7 @@ export default class User extends AbleObject<UserData> {
818
1357
 
819
1358
  if (this.$data.secret != null) {
820
1359
  const link = `${verifyUrl}?action=verify&id=${encodeURIComponent(
821
- this.guid
1360
+ this.guid,
822
1361
  )}&secret=${encodeURIComponent(this.$data.secret)}`;
823
1362
  success =
824
1363
  success &&
@@ -836,13 +1375,13 @@ export default class User extends AbleObject<UserData> {
836
1375
  verifyLink: link,
837
1376
  },
838
1377
  },
839
- this
1378
+ this,
840
1379
  ));
841
1380
  }
842
1381
 
843
1382
  if (this.$data.newEmailSecret != null) {
844
1383
  const link = `${verifyUrl}?action=verifychange&id=${encodeURIComponent(
845
- this.guid
1384
+ this.guid,
846
1385
  )}&secret=${encodeURIComponent(this.$data.newEmailSecret)}`;
847
1386
  success =
848
1387
  success &&
@@ -862,13 +1401,13 @@ export default class User extends AbleObject<UserData> {
862
1401
  newEmail: this.$data.newEmailAddress,
863
1402
  },
864
1403
  },
865
- this
1404
+ this,
866
1405
  ));
867
1406
  }
868
1407
 
869
1408
  if (this.$data.cancelEmailSecret != null) {
870
1409
  const link = `${verifyUrl}?action=cancelchange&id=${encodeURIComponent(
871
- this.guid
1410
+ this.guid,
872
1411
  )}&secret=${encodeURIComponent(this.$data.cancelEmailSecret)}`;
873
1412
  success =
874
1413
  success &&
@@ -888,7 +1427,7 @@ export default class User extends AbleObject<UserData> {
888
1427
  newEmail: this.$data.newEmailAddress ?? this.$data.email,
889
1428
  },
890
1429
  },
891
- this
1430
+ this,
892
1431
  ));
893
1432
  }
894
1433
 
@@ -901,13 +1440,16 @@ export default class User extends AbleObject<UserData> {
901
1440
  * @param group The group.
902
1441
  * @returns True if the user is already in the group. The resulting array of groups if the user was not.
903
1442
  */
904
- public $addGroup(group: Group & GroupData) {
905
- if (this.$data.groups == null) {
906
- this.$data.groups = [];
1443
+ public async $addGroup(group: GroupClass & GroupData) {
1444
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
1445
+ let groups = this.$data.groups;
1446
+ if (groups == null) {
1447
+ groups = [];
907
1448
  }
908
- if (!group.$inArray(this.$data.groups)) {
909
- this.$data.groups.push(group);
910
- return this.$data.groups;
1449
+ if (!group.$inArray(groups)) {
1450
+ groups.push(group);
1451
+ this.$data.groups = groups;
1452
+ return groups;
911
1453
  }
912
1454
  return true;
913
1455
  }
@@ -919,17 +1461,23 @@ export default class User extends AbleObject<UserData> {
919
1461
  * @returns True if the passwords match, otherwise false.
920
1462
  */
921
1463
  public $checkPassword(password: string) {
922
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1464
+ const tilmeld = enforceTilmeld(this);
923
1465
  switch (tilmeld.config.pwMethod) {
924
1466
  case 'plain':
925
1467
  return this.$data.password === password;
926
1468
  case 'digest':
927
- return this.$data.password === sha256(password).toString(Base64);
1469
+ return (
1470
+ this.$data.password ===
1471
+ crypto.createHash('sha256').update(password).digest('base64')
1472
+ );
928
1473
  case 'salt':
929
1474
  default:
930
1475
  return (
931
1476
  this.$data.password ===
932
- sha256(password + this.$data.salt).toString(Base64)
1477
+ crypto
1478
+ .createHash('sha256')
1479
+ .update(password + this.$data.salt)
1480
+ .digest('base64')
933
1481
  );
934
1482
  }
935
1483
  }
@@ -940,10 +1488,12 @@ export default class User extends AbleObject<UserData> {
940
1488
  * @param group The group.
941
1489
  * @returns True if the user wasn't in the group. The resulting array of groups if the user was.
942
1490
  */
943
- public $delGroup(group: Group & GroupData) {
944
- if (this.$data.groups != null && group.$inArray(this.$data.groups)) {
945
- const newGroups: (Group & GroupData)[] = [];
946
- for (let curGroup of this.$data.groups) {
1491
+ public async $delGroup(group: GroupClass & GroupData) {
1492
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
1493
+ let groups = this.$data.groups || [];
1494
+ if (this.$data.groups != null && group.$inArray(groups)) {
1495
+ const newGroups: (GroupClass & GroupData)[] = [];
1496
+ for (let curGroup of groups) {
947
1497
  if (!group.$is(curGroup)) {
948
1498
  newGroups.push(curGroup);
949
1499
  }
@@ -960,7 +1510,7 @@ export default class User extends AbleObject<UserData> {
960
1510
  * @param group The group, or the group's GUID.
961
1511
  * @returns True or false.
962
1512
  */
963
- public $inGroup(group: (Group & GroupData) | string) {
1513
+ public async $inGroup(group: (GroupClass & GroupData) | string) {
964
1514
  if (typeof group === 'string') {
965
1515
  if (this.$data.group?.guid === group) {
966
1516
  return true;
@@ -983,16 +1533,19 @@ export default class User extends AbleObject<UserData> {
983
1533
  * @param group The group, or the group's GUID.
984
1534
  * @returns True or false.
985
1535
  */
986
- public $isDescendant(group: (Group & GroupData) | string) {
1536
+ public async $isDescendant(group: (GroupClass & GroupData) | string) {
987
1537
  // Check to see if the user is in a descendant group of the given group.
1538
+ await this.$data.group?.$wake();
988
1539
  if (
1540
+ this.$data.group != null &&
989
1541
  this.$data.group?.cdate != null &&
990
- this.$data.group.$isDescendant(group)
1542
+ (await this.$data.group?.$isDescendant(group))
991
1543
  ) {
992
1544
  return true;
993
1545
  }
1546
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
994
1547
  for (let curGroup of this.$data.groups ?? []) {
995
- if (curGroup.$isDescendant(group)) {
1548
+ if (await curGroup.$isDescendant(group)) {
996
1549
  return true;
997
1550
  }
998
1551
  }
@@ -1008,15 +1561,31 @@ export default class User extends AbleObject<UserData> {
1008
1561
  public async $changePassword(data: {
1009
1562
  newPassword: string;
1010
1563
  currentPassword: string;
1564
+ revokeCurrentTokens?: boolean;
1011
1565
  }): Promise<{ result: boolean; message: string }> {
1566
+ this.$nymph.config.debugLog(
1567
+ 'tilmeld',
1568
+ `Password change request for "${this.$data.username}".`,
1569
+ );
1012
1570
  if (!('newPassword' in data) || !data.newPassword.length) {
1013
1571
  return { result: false, message: 'Please specify a password.' };
1014
1572
  }
1015
1573
  if (!this.$checkPassword(data.currentPassword ?? '')) {
1574
+ this.$nymph.config.debugLog(
1575
+ 'tilmeld',
1576
+ `Incorrect password for change request for "${this.$data.username}".`,
1577
+ );
1016
1578
  return { result: false, message: 'Incorrect password.' };
1017
1579
  }
1018
1580
  this.$data.passwordTemp = data.newPassword;
1581
+ if (data.revokeCurrentTokens) {
1582
+ this.$data.revokeTokenDate = Date.now();
1583
+ }
1019
1584
  if (await this.$save()) {
1585
+ if (data.revokeCurrentTokens) {
1586
+ const tilmeld = enforceTilmeld(this);
1587
+ await tilmeld.login(this, true);
1588
+ }
1020
1589
  return { result: true, message: 'Your password has been changed.' };
1021
1590
  } else {
1022
1591
  return { result: false, message: "Couldn't save new password." };
@@ -1030,7 +1599,7 @@ export default class User extends AbleObject<UserData> {
1030
1599
  * @returns The resulting password or hash which is stored in the entity.
1031
1600
  */
1032
1601
  public $password(password: string) {
1033
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1602
+ const tilmeld = enforceTilmeld(this);
1034
1603
  switch (tilmeld.config.pwMethod) {
1035
1604
  case 'plain':
1036
1605
  delete this.$data.salt;
@@ -1038,34 +1607,275 @@ export default class User extends AbleObject<UserData> {
1038
1607
  break;
1039
1608
  case 'digest':
1040
1609
  delete this.$data.salt;
1041
- this.$data.password = sha256(password).toString(Base64);
1610
+ this.$data.password = crypto
1611
+ .createHash('sha256')
1612
+ .update(password)
1613
+ .digest('base64');
1042
1614
  break;
1043
1615
  case 'salt':
1044
1616
  default:
1045
1617
  this.$data.salt = nanoid();
1046
- this.$data.password = sha256(password + this.$data.salt).toString(
1047
- Base64
1048
- );
1618
+ this.$data.password = crypto
1619
+ .createHash('sha256')
1620
+ .update(password + this.$data.salt)
1621
+ .digest('base64');
1049
1622
  break;
1050
1623
  }
1051
1624
  return this.$data.password;
1052
1625
  }
1053
1626
 
1627
+ /**
1628
+ * A frontend accessible method to revoke all currently issued tokens.
1629
+ *
1630
+ * @param data The input data from the client.
1631
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1632
+ */
1633
+ public async $revokeCurrentTokens(data: {
1634
+ password: string;
1635
+ }): Promise<{ result: boolean; message: string }> {
1636
+ if (!this.$checkPassword(data.password ?? '')) {
1637
+ return { result: false, message: 'Incorrect password.' };
1638
+ }
1639
+
1640
+ this.$data.revokeTokenDate = Date.now();
1641
+
1642
+ if (await this.$save()) {
1643
+ const tilmeld = enforceTilmeld(this);
1644
+ await tilmeld.login(this, true);
1645
+ return {
1646
+ result: true,
1647
+ message: 'You have logged out of all other sessions.',
1648
+ };
1649
+ } else {
1650
+ return { result: false, message: "Couldn't save revocation date." };
1651
+ }
1652
+ }
1653
+
1654
+ public async $hasTOTPSecret() {
1655
+ return this.$data.totpSecret != null;
1656
+ }
1657
+
1658
+ /**
1659
+ * Check the given code against the user's TOTP secret.
1660
+ *
1661
+ * @param code The code in question.
1662
+ * @returns True if the code is valid, otherwise false.
1663
+ */
1664
+ public $checkTOTPCode(code: string) {
1665
+ if (this.$data.totpSecret == null) {
1666
+ return false;
1667
+ }
1668
+
1669
+ const tilmeld = enforceTilmeld(this);
1670
+
1671
+ const totp = new TOTP({
1672
+ issuer: tilmeld.config.appName,
1673
+ label: this.$data.username,
1674
+ algorithm: 'SHA1',
1675
+ digits: 6,
1676
+ period: 30,
1677
+ secret: this.$data.totpSecret,
1678
+ });
1679
+
1680
+ return totp.validate({ token: code, window: 2 }) != null;
1681
+ }
1682
+
1683
+ /**
1684
+ * A frontend accessible method to generate a new TOTP secret.
1685
+ *
1686
+ * @returns An object with 'uri', 'qrcode', and 'secret'.
1687
+ */
1688
+ public async $getNewTOTPSecret() {
1689
+ const tilmeld = enforceTilmeld(this);
1690
+
1691
+ if (this.$data.totpSecret != null) {
1692
+ throw new BadDataError('You already have a 2FA secret.');
1693
+ }
1694
+
1695
+ const secret = new Secret();
1696
+
1697
+ const totp = new TOTP({
1698
+ issuer: tilmeld.config.appName,
1699
+ label: this.$data.username,
1700
+ algorithm: 'SHA1',
1701
+ digits: 6,
1702
+ period: 30,
1703
+ secret,
1704
+ });
1705
+
1706
+ const uri = totp.toString();
1707
+
1708
+ const qrcode = await toDataURL(uri);
1709
+
1710
+ return {
1711
+ uri,
1712
+ qrcode,
1713
+ secret: secret.base32,
1714
+ };
1715
+ }
1716
+
1717
+ /**
1718
+ * A frontend accessible method to save a TOTP secret into the user's account.
1719
+ *
1720
+ * @param data The input data from the client.
1721
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1722
+ */
1723
+ public async $saveTOTPSecret(data: {
1724
+ password: string;
1725
+ secret: string;
1726
+ code: string;
1727
+ }): Promise<{ result: boolean; message: string }> {
1728
+ if (!this.$checkPassword(data.password ?? '')) {
1729
+ return { result: false, message: 'Incorrect password.' };
1730
+ }
1731
+
1732
+ if (this.$data.totpSecret != null) {
1733
+ return { result: false, message: 'You already have a 2FA secret.' };
1734
+ }
1735
+
1736
+ if (
1737
+ data.secret == null ||
1738
+ typeof data.secret !== 'string' ||
1739
+ data.secret === ''
1740
+ ) {
1741
+ return { result: false, message: '2FA secret is invalid.' };
1742
+ }
1743
+
1744
+ if (
1745
+ data.code == null ||
1746
+ typeof data.code !== 'string' ||
1747
+ data.code.length !== 6
1748
+ ) {
1749
+ return { result: false, message: '2FA code is invalid.' };
1750
+ }
1751
+
1752
+ const tilmeld = enforceTilmeld(this);
1753
+
1754
+ const totp = new TOTP({
1755
+ issuer: tilmeld.config.appName,
1756
+ label: this.$data.username,
1757
+ algorithm: 'SHA1',
1758
+ digits: 6,
1759
+ period: 30,
1760
+ secret: data.secret,
1761
+ });
1762
+
1763
+ if (totp.validate({ token: data.code, window: 2 }) == null) {
1764
+ return { result: false, message: '2FA code is incorrect.' };
1765
+ }
1766
+
1767
+ this.$data.totpSecret = data.secret;
1768
+
1769
+ if (await this.$save()) {
1770
+ return { result: true, message: 'Your two factor secret is now set.' };
1771
+ } else {
1772
+ return { result: false, message: "Couldn't save two factor secret." };
1773
+ }
1774
+ }
1775
+
1776
+ /**
1777
+ * A frontend accessible method to remove the TOTP secret from the user's
1778
+ * account.
1779
+ *
1780
+ * @param data The input data from the client.
1781
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1782
+ */
1783
+ public async $removeTOTPSecret(data?: {
1784
+ password: string;
1785
+ code: string;
1786
+ }): Promise<{ result: boolean; message: string }> {
1787
+ const tilmeld = enforceTilmeld(this);
1788
+
1789
+ if (data) {
1790
+ if (!this.$checkPassword(data.password ?? '')) {
1791
+ return { result: false, message: 'Incorrect password.' };
1792
+ }
1793
+
1794
+ if (
1795
+ data.code == null ||
1796
+ typeof data.code !== 'string' ||
1797
+ data.code.length !== 6
1798
+ ) {
1799
+ return { result: false, message: '2FA code is invalid.' };
1800
+ }
1801
+
1802
+ if (this.$data.totpSecret == null) {
1803
+ return { result: false, message: "You don't have a 2FA secret." };
1804
+ }
1805
+
1806
+ const totp = new TOTP({
1807
+ issuer: tilmeld.config.appName,
1808
+ label: this.$data.username,
1809
+ algorithm: 'SHA1',
1810
+ digits: 6,
1811
+ period: 30,
1812
+ secret: this.$data.totpSecret,
1813
+ });
1814
+
1815
+ if (totp.validate({ token: data.code, window: 2 }) == null) {
1816
+ return { result: false, message: '2FA code is incorrect.' };
1817
+ }
1818
+ } else if (!tilmeld.gatekeeper('tilmeld/admin')) {
1819
+ return {
1820
+ result: false,
1821
+ message: 'You must provide your password and 2FA code.',
1822
+ };
1823
+ }
1824
+
1825
+ delete this.$data.totpSecret;
1826
+
1827
+ if (await this.$save()) {
1828
+ return {
1829
+ result: true,
1830
+ message: 'Two factor secret has been removed.',
1831
+ };
1832
+ } else {
1833
+ return { result: false, message: "Couldn't remove two factor secret." };
1834
+ }
1835
+ }
1836
+
1054
1837
  /**
1055
1838
  * Check that a username is valid.
1056
1839
  *
1057
1840
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1058
1841
  */
1059
1842
  public async $checkUsername(): Promise<{ result: boolean; message: string }> {
1060
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1843
+ const tilmeld = enforceTilmeld(this);
1844
+
1845
+ this.$nymph.config.debugInfo(
1846
+ 'tilmeld',
1847
+ `Username check "${this.$data.username}".`,
1848
+ );
1849
+
1061
1850
  if (!tilmeld.config.emailUsernames) {
1062
- if (this.$data.username == null || !this.$data.username.length) {
1063
- return { result: false, message: 'Please specify a username.' };
1851
+ let [username, domain] = tilmeld.config.domainSupport
1852
+ ? splitn(this.$data.username ?? '', '@', 2)
1853
+ : [this.$data.username, null];
1854
+
1855
+ // Lowercase the domain part.
1856
+ if (domain) {
1857
+ domain = domain.toLowerCase();
1858
+ this.$data.username = `${username}@${domain}`;
1064
1859
  }
1860
+
1065
1861
  if (
1066
- tilmeld.config.maxUsernameLength > 0 &&
1067
- this.$data.username.length > tilmeld.config.maxUsernameLength
1862
+ this.$data.username == null ||
1863
+ !this.$data.username.length ||
1864
+ username == null ||
1865
+ !username.length
1068
1866
  ) {
1867
+ return { result: false, message: 'Please specify a username.' };
1868
+ }
1869
+ if (username.length < tilmeld.config.minUsernameLength) {
1870
+ return {
1871
+ result: false,
1872
+ message:
1873
+ 'Usernames must be at least ' +
1874
+ tilmeld.config.minUsernameLength +
1875
+ ' characters.',
1876
+ };
1877
+ }
1878
+ if (username.length > tilmeld.config.maxUsernameLength) {
1069
1879
  return {
1070
1880
  result: false,
1071
1881
  message:
@@ -1076,22 +1886,39 @@ export default class User extends AbleObject<UserData> {
1076
1886
  }
1077
1887
 
1078
1888
  if (
1079
- difference(
1080
- this.$data.username.split(''),
1081
- tilmeld.config.validChars.split('')
1082
- ).length
1889
+ difference(username.split(''), tilmeld.config.validChars.split(''))
1890
+ .length
1083
1891
  ) {
1084
1892
  return {
1085
1893
  result: false,
1086
1894
  message: tilmeld.config.validCharsNotice,
1087
1895
  };
1088
1896
  }
1089
- if (!tilmeld.config.validRegex.test(this.$data.username)) {
1897
+ if (!tilmeld.config.validRegex.test(username)) {
1090
1898
  return {
1091
1899
  result: false,
1092
1900
  message: tilmeld.config.validRegexNotice,
1093
1901
  };
1094
1902
  }
1903
+ if (domain != null) {
1904
+ if (!tilmeld.config.validDomainRegex.test(domain)) {
1905
+ return {
1906
+ result: false,
1907
+ message: tilmeld.config.validDomainRegexNotice,
1908
+ };
1909
+ }
1910
+ if (
1911
+ !this.$skipAcWhenSaving &&
1912
+ !this.$is(tilmeld.currentUser) &&
1913
+ !tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
1914
+ ) {
1915
+ return {
1916
+ result: false,
1917
+ message:
1918
+ "You don't have the authority to manage users on this domain.",
1919
+ };
1920
+ }
1921
+ }
1095
1922
 
1096
1923
  const selector: Selector = {
1097
1924
  type: '&',
@@ -1105,12 +1932,24 @@ export default class User extends AbleObject<UserData> {
1105
1932
  }
1106
1933
  const test = await this.$nymph.getEntity(
1107
1934
  { class: tilmeld.User, skipAc: true },
1108
- selector
1935
+ selector,
1109
1936
  );
1110
1937
  if (test != null) {
1111
1938
  return { result: false, message: 'That username is taken.' };
1112
1939
  }
1113
1940
 
1941
+ for (let callback of (this.constructor as typeof User)
1942
+ .checkUsernameCallbacks) {
1943
+ if (callback) {
1944
+ const result = await callback(this, {
1945
+ username: this.$data.username,
1946
+ });
1947
+ if (!result.result) {
1948
+ return { message: 'Username is not available.', ...result };
1949
+ }
1950
+ }
1951
+ }
1952
+
1114
1953
  return {
1115
1954
  result: true,
1116
1955
  message:
@@ -1121,10 +1960,16 @@ export default class User extends AbleObject<UserData> {
1121
1960
  if (this.$data.username == null || !this.$data.username.length) {
1122
1961
  return { result: false, message: 'Please specify an email.' };
1123
1962
  }
1124
- if (
1125
- tilmeld.config.maxUsernameLength > 0 &&
1126
- this.$data.username.length > tilmeld.config.maxUsernameLength
1127
- ) {
1963
+ if (this.$data.username.length < tilmeld.config.minUsernameLength) {
1964
+ return {
1965
+ result: false,
1966
+ message:
1967
+ 'Emails must be at least ' +
1968
+ tilmeld.config.minUsernameLength +
1969
+ ' characters.',
1970
+ };
1971
+ }
1972
+ if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
1128
1973
  return {
1129
1974
  result: false,
1130
1975
  message:
@@ -1144,7 +1989,13 @@ export default class User extends AbleObject<UserData> {
1144
1989
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1145
1990
  */
1146
1991
  public async $checkEmail(): Promise<{ result: boolean; message: string }> {
1147
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1992
+ const tilmeld = enforceTilmeld(this);
1993
+
1994
+ this.$nymph.config.debugInfo(
1995
+ 'tilmeld',
1996
+ `Email check "${this.$data.email}".`,
1997
+ );
1998
+
1148
1999
  if (this.$data.email == null || !this.$data.email.length) {
1149
2000
  if (tilmeld.config.verifyEmail) {
1150
2001
  return { result: false, message: 'Please specify an email.' };
@@ -1167,7 +2018,7 @@ export default class User extends AbleObject<UserData> {
1167
2018
  }
1168
2019
  const test = await this.$nymph.getEntity(
1169
2020
  { class: tilmeld.User, skipAc: true },
1170
- selector
2021
+ selector,
1171
2022
  );
1172
2023
  if (test != null) {
1173
2024
  return {
@@ -1189,7 +2040,13 @@ export default class User extends AbleObject<UserData> {
1189
2040
  * @returns An object with a boolean 'result' entry and a 'message' entry.
1190
2041
  */
1191
2042
  public async $checkPhone(): Promise<{ result: boolean; message: string }> {
1192
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
2043
+ const tilmeld = enforceTilmeld(this);
2044
+
2045
+ this.$nymph.config.debugInfo(
2046
+ 'tilmeld',
2047
+ `Phone check "${this.$data.phone}".`,
2048
+ );
2049
+
1193
2050
  if (this.$data.phone == null || !this.$data.phone.length) {
1194
2051
  return { result: false, message: 'Please specify a phone number.' };
1195
2052
  }
@@ -1211,7 +2068,7 @@ export default class User extends AbleObject<UserData> {
1211
2068
  }
1212
2069
  const test = await this.$nymph.getEntity(
1213
2070
  { class: tilmeld.User, skipAc: true },
1214
- selector
2071
+ selector,
1215
2072
  );
1216
2073
  if (test != null) {
1217
2074
  return { result: false, message: 'Phone number is in use.' };
@@ -1228,7 +2085,7 @@ export default class User extends AbleObject<UserData> {
1228
2085
  password: string;
1229
2086
  additionalData?: { [k: string]: any };
1230
2087
  }): Promise<{ result: boolean; loggedin: boolean; message: string }> {
1231
- let tilmeld = this.$nymph.tilmeld as Tilmeld;
2088
+ let tilmeld = enforceTilmeld(this);
1232
2089
  if (!tilmeld.config.allowRegistration) {
1233
2090
  return {
1234
2091
  result: false,
@@ -1244,21 +2101,6 @@ export default class User extends AbleObject<UserData> {
1244
2101
  };
1245
2102
  }
1246
2103
 
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
2104
  if (!('password' in data) || !data.password.length) {
1263
2105
  return {
1264
2106
  result: false,
@@ -1272,7 +2114,7 @@ export default class User extends AbleObject<UserData> {
1272
2114
  }
1273
2115
 
1274
2116
  this.$password(data.password);
1275
- if (tilmeld.config.regFields.indexOf('name') !== -1) {
2117
+ if (tilmeld.config.regFields.includes('name')) {
1276
2118
  this.$data.name =
1277
2119
  this.$data.nameFirst +
1278
2120
  (this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
@@ -1289,75 +2131,34 @@ export default class User extends AbleObject<UserData> {
1289
2131
  const transaction = 'tilmeld-register-' + nanoid();
1290
2132
  const nymph = this.$nymph;
1291
2133
  const tnymph = await this.$nymph.startTransaction(transaction);
1292
- this.$nymph = tnymph;
1293
- tilmeld = this.$nymph.tilmeld as Tilmeld;
2134
+ this.$setNymph(tnymph);
2135
+ tilmeld = enforceTilmeld(this);
2136
+
2137
+ let message = '';
2138
+ let loggedin = false;
1294
2139
 
1295
2140
  try {
1296
- // Add primary group.
1297
- let generatedPrimaryGroup: (Group & GroupData) | null = null;
1298
- if (tilmeld.config.generatePrimary) {
1299
- // Generate a new primary group for the user.
1300
- generatedPrimaryGroup = await tilmeld.Group.factory();
1301
- generatedPrimaryGroup.groupname = this.$data.username;
1302
- generatedPrimaryGroup.avatar = this.$data.avatar;
1303
- generatedPrimaryGroup.name = this.$data.name;
1304
- generatedPrimaryGroup.email = this.$data.email;
1305
- const parent = await tnymph.getEntity(
1306
- { class: tilmeld.Group },
1307
- {
1308
- type: '&',
1309
- equal: ['defaultPrimary', true],
1310
- }
1311
- );
1312
- if (parent != null) {
1313
- generatedPrimaryGroup.parent = parent;
1314
- }
1315
- if (!(await generatedPrimaryGroup.$saveSkipAC())) {
1316
- await tnymph.rollback(transaction);
1317
- this.$nymph = nymph;
1318
- return {
1319
- result: false,
1320
- loggedin: false,
1321
- message: 'Error creating primary group for user.',
1322
- };
1323
- }
1324
- this.$data.group = generatedPrimaryGroup;
1325
- } else {
1326
- // Add the default primary.
1327
- const group = await tnymph.getEntity(
1328
- { class: tilmeld.Group },
1329
- {
1330
- type: '&',
1331
- equal: ['defaultPrimary', true],
2141
+ try {
2142
+ for (let callback of (this.constructor as typeof User)
2143
+ .beforeRegisterCallbacks) {
2144
+ if (callback) {
2145
+ await callback(this, data);
1332
2146
  }
1333
- );
1334
- if (group != null) {
1335
- this.$data.group = group;
1336
2147
  }
2148
+ } catch (e: any) {
2149
+ await tnymph.rollback(transaction);
2150
+ this.$setNymph(nymph);
2151
+ return {
2152
+ result: false,
2153
+ loggedin: false,
2154
+ message: e.message,
2155
+ };
1337
2156
  }
1338
2157
 
1339
- // Add secondary groups.
1340
- if (tilmeld.config.verifyEmail && tilmeld.config.unverifiedAccess) {
1341
- // Add the default secondaries for unverified users.
1342
- this.$data.groups = await tnymph.getEntities(
1343
- { class: tilmeld.Group },
1344
- {
1345
- type: '&',
1346
- equal: ['unverifiedSecondary', true],
1347
- }
1348
- );
1349
- } else {
1350
- // Add the default secondaries.
1351
- this.$data.groups = await tnymph.getEntities(
1352
- { class: tilmeld.Group },
1353
- {
1354
- type: '&',
1355
- equal: ['defaultSecondary', true],
1356
- }
1357
- );
1358
- }
1359
-
1360
- if (tilmeld.config.verifyEmail) {
2158
+ if (
2159
+ tilmeld.config.userFields.includes('email') &&
2160
+ tilmeld.config.verifyEmail
2161
+ ) {
1361
2162
  // The user will be enabled after verifying their e-mail address.
1362
2163
  if (!tilmeld.config.unverifiedAccess) {
1363
2164
  this.$data.enabled = false;
@@ -1366,7 +2167,7 @@ export default class User extends AbleObject<UserData> {
1366
2167
  this.$data.enabled = true;
1367
2168
  }
1368
2169
 
1369
- // If create_admin is true and there are no other users, grant
2170
+ // If createAdmin is true and there are no other users, grant
1370
2171
  // "system/admin".
1371
2172
  let madeAdmin = false;
1372
2173
  if (tilmeld.config.createAdmin) {
@@ -1377,6 +2178,15 @@ export default class User extends AbleObject<UserData> {
1377
2178
  return: 'guid',
1378
2179
  });
1379
2180
  // Make sure it's not just null, cause that means an error.
2181
+ if (otherUsers == null) {
2182
+ await tnymph.rollback(transaction);
2183
+ this.$setNymph(nymph);
2184
+ return {
2185
+ result: false,
2186
+ loggedin: false,
2187
+ message: 'An error occurred.',
2188
+ };
2189
+ }
1380
2190
  if (!otherUsers.length) {
1381
2191
  this.$grant('system/admin');
1382
2192
  this.$data.enabled = true;
@@ -1384,7 +2194,15 @@ export default class User extends AbleObject<UserData> {
1384
2194
  }
1385
2195
  }
1386
2196
 
2197
+ this.$nymph.config.debugLog(
2198
+ 'tilmeld',
2199
+ `Registering new user "${this.$data.username}".`,
2200
+ );
1387
2201
  if (await this.$saveSkipAC()) {
2202
+ this.$nymph.config.debugLog(
2203
+ 'tilmeld',
2204
+ `New user registered "${this.$data.username}".`,
2205
+ );
1388
2206
  // Send the new user registered email.
1389
2207
  if (tilmeld.config.userRegisteredRecipient != null) {
1390
2208
  await tilmeld.config.sendEmail(
@@ -1403,33 +2221,16 @@ export default class User extends AbleObject<UserData> {
1403
2221
  userPhone: this.$data.phone,
1404
2222
  },
1405
2223
  },
1406
- this
2224
+ this,
1407
2225
  );
1408
2226
  }
1409
2227
 
1410
- let message = '';
1411
- let loggedin = false;
1412
-
1413
- // Save the primary group.
1414
- if (generatedPrimaryGroup != null) {
1415
- generatedPrimaryGroup.user = this;
1416
- if (!(await generatedPrimaryGroup.$saveSkipAC())) {
1417
- await tnymph.rollback(transaction);
1418
- this.$nymph = nymph;
1419
- return {
1420
- result: false,
1421
- loggedin: false,
1422
- message:
1423
- 'Your primary group could not be assigned. Please try again later.',
1424
- };
1425
- }
1426
- }
1427
-
1428
2228
  // Finish up.
1429
2229
  if (
1430
2230
  tilmeld.config.verifyEmail &&
1431
2231
  !tilmeld.config.unverifiedAccess &&
1432
- !madeAdmin
2232
+ !madeAdmin &&
2233
+ tilmeld.config.userFields.includes('email')
1433
2234
  ) {
1434
2235
  message +=
1435
2236
  `Almost there. An email has been sent to ${this.$data.email} ` +
@@ -1437,9 +2238,10 @@ export default class User extends AbleObject<UserData> {
1437
2238
  } else if (
1438
2239
  tilmeld.config.verifyEmail &&
1439
2240
  tilmeld.config.unverifiedAccess &&
1440
- !madeAdmin
2241
+ !madeAdmin &&
2242
+ tilmeld.config.userFields.includes('email')
1441
2243
  ) {
1442
- if (!tilmeld.login(this, true)) {
2244
+ if (!(await tilmeld.login(this, true))) {
1443
2245
  throw new Error('An error occurred trying to log you in.');
1444
2246
  }
1445
2247
  message +=
@@ -1448,7 +2250,7 @@ export default class User extends AbleObject<UserData> {
1448
2250
  'registration.';
1449
2251
  loggedin = true;
1450
2252
  } else {
1451
- if (!tilmeld.login(this, true)) {
2253
+ if (!(await tilmeld.login(this, true))) {
1452
2254
  throw new Error('An error occurred trying to log you in.');
1453
2255
  }
1454
2256
  message += "You're now registered and logged in!";
@@ -1464,23 +2266,13 @@ export default class User extends AbleObject<UserData> {
1464
2266
  });
1465
2267
  }
1466
2268
  }
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
2269
  } else {
2270
+ this.$nymph.config.debugError(
2271
+ 'tilmeld',
2272
+ `Error registering new user "${this.$data.username}".`,
2273
+ );
1482
2274
  await tnymph.rollback(transaction);
1483
- this.$nymph = nymph;
2275
+ this.$setNymph(nymph);
1484
2276
  return {
1485
2277
  result: false,
1486
2278
  loggedin: false,
@@ -1488,28 +2280,100 @@ export default class User extends AbleObject<UserData> {
1488
2280
  };
1489
2281
  }
1490
2282
  } catch (e: any) {
2283
+ this.$nymph.config.debugError(
2284
+ 'tilmeld',
2285
+ `Error registering new user "${this.$data.username}": ${e}`,
2286
+ );
1491
2287
  await tnymph.rollback(transaction);
1492
- this.$nymph = nymph;
2288
+ this.$setNymph(nymph);
1493
2289
  throw e;
1494
2290
  }
2291
+
2292
+ try {
2293
+ await tnymph.commit(transaction);
2294
+ this.$setNymph(nymph);
2295
+ await this.$nymph.tilmeld?.fillSession(this);
2296
+ } catch (e: any) {
2297
+ throw e;
2298
+ }
2299
+
2300
+ return {
2301
+ result: true,
2302
+ loggedin,
2303
+ message,
2304
+ };
1495
2305
  }
1496
2306
 
1497
2307
  public async $save() {
1498
- let tilmeld = this.$nymph.tilmeld as Tilmeld;
2308
+ let tilmeld = enforceTilmeld(this);
1499
2309
  if (this.$data.username == null || !this.$data.username.trim().length) {
1500
2310
  return false;
1501
2311
  }
1502
2312
 
1503
2313
  if (
2314
+ !this.$skipAcWhenSaving &&
1504
2315
  tilmeld.gatekeeper('tilmeld/admin') &&
1505
2316
  !tilmeld.gatekeeper('system/admin') &&
1506
- this.$gatekeeper('system/admin')
2317
+ this.$data.abilities?.includes('system/admin')
1507
2318
  ) {
1508
- throw new BadDataError(
1509
- "You don't have the authority to modify system admins."
2319
+ throw new AccessControlError(
2320
+ "You don't have the authority to modify system admins.",
1510
2321
  );
1511
2322
  }
1512
2323
 
2324
+ // Lowercase the domain part.
2325
+ if (tilmeld.config.domainSupport) {
2326
+ const [username, domain] = splitn(this.$data.username ?? '', '@', 2);
2327
+ if (domain) {
2328
+ this.$data.username = `${username}@${domain.toLowerCase()}`;
2329
+ }
2330
+ }
2331
+
2332
+ if (tilmeld.config.domainSupport) {
2333
+ const [_username, domain] = splitn(this.$data.username ?? '', '@', 2);
2334
+ const [_originalUsername, originalDomain] = splitn(
2335
+ this.$originalUsername ?? '',
2336
+ '@',
2337
+ 2,
2338
+ );
2339
+
2340
+ if (
2341
+ this.guid != null &&
2342
+ ((originalDomain == null && domain != null) ||
2343
+ (domain == null && originalDomain != null))
2344
+ ) {
2345
+ throw new AccessControlError(
2346
+ "Users can't switch to or from having a domain once created.",
2347
+ );
2348
+ }
2349
+
2350
+ if (
2351
+ !this.$skipAcWhenSaving &&
2352
+ !tilmeld.gatekeeper('tilmeld/admin') &&
2353
+ !tilmeld.gatekeeper('system/admin')
2354
+ ) {
2355
+ if (
2356
+ domain != null &&
2357
+ !this.$is(tilmeld.currentUser) &&
2358
+ !tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
2359
+ ) {
2360
+ throw new AccessControlError(
2361
+ "You don't have the authority to modify users on this domain.",
2362
+ );
2363
+ }
2364
+ if (
2365
+ originalDomain != null &&
2366
+ originalDomain !== domain &&
2367
+ (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
2368
+ !tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))
2369
+ ) {
2370
+ throw new AccessControlError(
2371
+ "You don't have the authority to modify users on this domain.",
2372
+ );
2373
+ }
2374
+ }
2375
+ }
2376
+
1513
2377
  let sendVerification = false;
1514
2378
 
1515
2379
  // Formatting.
@@ -1528,49 +2392,88 @@ export default class User extends AbleObject<UserData> {
1528
2392
  this.$data.enabled = !!this.$data.enabled;
1529
2393
 
1530
2394
  // Clear empty values.
1531
- if (this.$data.nameFirst === '') {
2395
+ if (
2396
+ this.$data.nameFirst === '' ||
2397
+ !tilmeld.config.userFields.includes('name')
2398
+ ) {
1532
2399
  delete this.$data.nameFirst;
1533
2400
  }
1534
- if (this.$data.nameMiddle === '') {
2401
+ if (
2402
+ this.$data.nameMiddle === '' ||
2403
+ !tilmeld.config.userFields.includes('name')
2404
+ ) {
1535
2405
  delete this.$data.nameMiddle;
1536
2406
  }
1537
- if (this.$data.nameLast === '') {
2407
+ if (
2408
+ this.$data.nameLast === '' ||
2409
+ !tilmeld.config.userFields.includes('name')
2410
+ ) {
1538
2411
  delete this.$data.nameLast;
1539
2412
  }
1540
- if (this.$data.name === '') {
2413
+ if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
1541
2414
  delete this.$data.name;
1542
2415
  }
1543
- if (this.$data.email === '') {
2416
+ if (
2417
+ this.$data.email === '' ||
2418
+ !tilmeld.config.userFields.includes('email')
2419
+ ) {
1544
2420
  delete this.$data.email;
1545
2421
  }
1546
2422
  if (this.$data.avatar === '') {
1547
2423
  delete this.$data.avatar;
1548
2424
  }
1549
- if (this.$data.phone === '') {
2425
+ if (
2426
+ this.$data.phone === '' ||
2427
+ !tilmeld.config.userFields.includes('phone')
2428
+ ) {
1550
2429
  delete this.$data.phone;
1551
2430
  }
1552
- if (this.$data.secret === '') {
2431
+ if (
2432
+ this.$data.secret === '' ||
2433
+ !tilmeld.config.userFields.includes('email')
2434
+ ) {
1553
2435
  delete this.$data.secret;
1554
2436
  }
1555
- if (this.$data.emailChangeDate === 0) {
2437
+ if (
2438
+ this.$data.emailChangeDate === 0 ||
2439
+ !tilmeld.config.userFields.includes('email')
2440
+ ) {
1556
2441
  delete this.$data.emailChangeDate;
1557
2442
  }
1558
- if (this.$data.newEmailSecret === '') {
2443
+ if (
2444
+ this.$data.newEmailSecret === '' ||
2445
+ !tilmeld.config.userFields.includes('email')
2446
+ ) {
1559
2447
  delete this.$data.newEmailSecret;
1560
2448
  }
1561
- if (this.$data.newEmailAddress === '') {
2449
+ if (
2450
+ this.$data.newEmailAddress === '' ||
2451
+ !tilmeld.config.userFields.includes('email')
2452
+ ) {
1562
2453
  delete this.$data.newEmailAddress;
1563
2454
  }
1564
- if (this.$data.cancelEmailSecret === '') {
2455
+ if (
2456
+ this.$data.cancelEmailSecret === '' ||
2457
+ !tilmeld.config.userFields.includes('email')
2458
+ ) {
1565
2459
  delete this.$data.cancelEmailSecret;
1566
2460
  }
1567
- if (this.$data.cancelEmailAddress === '') {
2461
+ if (
2462
+ this.$data.cancelEmailAddress === '' ||
2463
+ !tilmeld.config.userFields.includes('email')
2464
+ ) {
1568
2465
  delete this.$data.cancelEmailAddress;
1569
2466
  }
1570
- if (this.$data.recoverSecret === '') {
2467
+ if (
2468
+ this.$data.recoverSecret === '' ||
2469
+ !tilmeld.config.userFields.includes('email')
2470
+ ) {
1571
2471
  delete this.$data.recoverSecret;
1572
2472
  }
1573
- if (this.$data.recoverSecretDate === 0) {
2473
+ if (
2474
+ this.$data.recoverSecretDate === 0 ||
2475
+ !tilmeld.config.userFields.includes('email')
2476
+ ) {
1574
2477
  delete this.$data.recoverSecretDate;
1575
2478
  }
1576
2479
  if (this.$data.password === '') {
@@ -1588,7 +2491,10 @@ export default class User extends AbleObject<UserData> {
1588
2491
  if (!unCheck.result) {
1589
2492
  throw new BadUsernameError(unCheck.message);
1590
2493
  }
1591
- if (!tilmeld.config.emailUsernames) {
2494
+ if (
2495
+ !tilmeld.config.emailUsernames &&
2496
+ tilmeld.config.userFields.includes('email')
2497
+ ) {
1592
2498
  const emCheck = await this.$checkEmail();
1593
2499
  if (!emCheck.result) {
1594
2500
  throw new BadEmailError(emCheck.message);
@@ -1596,7 +2502,10 @@ export default class User extends AbleObject<UserData> {
1596
2502
  }
1597
2503
 
1598
2504
  // Email changes.
1599
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
2505
+ if (
2506
+ !tilmeld.gatekeeper('tilmeld/admin') &&
2507
+ tilmeld.config.userFields.includes('email')
2508
+ ) {
1600
2509
  // The user isn't an admin, so email address changes should contain some
1601
2510
  // security measures.
1602
2511
  if (tilmeld.config.verifyEmail) {
@@ -1604,7 +2513,7 @@ export default class User extends AbleObject<UserData> {
1604
2513
  if (this.guid == null) {
1605
2514
  // If this is the first user, they'll be made an admin, so they don't
1606
2515
  // need to verify.
1607
- if ((this.$data.abilities?.indexOf('system/admin') ?? -1) === -1) {
2516
+ if (!this.$data.abilities?.includes('system/admin')) {
1608
2517
  this.$data.secret = nanoid();
1609
2518
  sendVerification = true;
1610
2519
  }
@@ -1617,17 +2526,17 @@ export default class User extends AbleObject<UserData> {
1617
2526
  tilmeld.config.emailRateLimit !== '' &&
1618
2527
  this.$data.emailChangeDate != null &&
1619
2528
  this.$data.emailChangeDate >=
1620
- strtotime('-' + tilmeld.config.emailRateLimit) * 1000
2529
+ (strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000
1621
2530
  ) {
1622
2531
  throw new EmailChangeRateLimitExceededError(
1623
2532
  'You already changed your email address recently. Please wait until ' +
1624
2533
  new Date(
1625
- strtotime(
2534
+ (strtotime(
1626
2535
  '+' + tilmeld.config.emailRateLimit,
1627
- Math.floor(this.$data.emailChangeDate / 1000)
1628
- ) * 1000
2536
+ Math.floor(this.$data.emailChangeDate / 1000),
2537
+ ) || 0) * 1000,
1629
2538
  ).toString() +
1630
- ' to change your email address again.'
2539
+ ' to change your email address again.',
1631
2540
  );
1632
2541
  } else {
1633
2542
  if (
@@ -1637,7 +2546,7 @@ export default class User extends AbleObject<UserData> {
1637
2546
  // an email change.
1638
2547
  (this.$data.emailChangeDate == null ||
1639
2548
  this.$data.emailChangeDate <
1640
- strtotime('-' + tilmeld.config.emailRateLimit) * 1000)
2549
+ (strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000)
1641
2550
  ) {
1642
2551
  // Save the old email in case the cancel change link is clicked.
1643
2552
  this.$data.cancelEmailAddress = this.$originalEmail;
@@ -1663,7 +2572,7 @@ export default class User extends AbleObject<UserData> {
1663
2572
  // email change.
1664
2573
  (this.$data.emailChangeDate == null ||
1665
2574
  this.$data.emailChangeDate <
1666
- strtotime('-' + tilmeld.config.emailRateLimit) * 1000)
2575
+ (strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000)
1667
2576
  ) {
1668
2577
  // The user doesn't need to verify their new email address, but should
1669
2578
  // be able to cancel the email change from their old address.
@@ -1682,51 +2591,157 @@ export default class User extends AbleObject<UserData> {
1682
2591
  throw new BadDataError('A password is required.');
1683
2592
  }
1684
2593
 
1685
- try {
1686
- tilmeld.config.validatorUser(this);
1687
- } catch (e: any) {
1688
- throw new BadDataError(e?.message);
1689
- }
1690
-
1691
2594
  // Start transaction.
1692
- const transaction = 'tilmeld-save-' + nanoid();
2595
+ const transaction = 'tilmeld-save-user-' + (this.guid || nanoid());
1693
2596
  const nymph = this.$nymph;
1694
2597
  const tnymph = await this.$nymph.startTransaction(transaction);
1695
- this.$nymph = tnymph;
1696
- tilmeld = this.$nymph.tilmeld as Tilmeld;
2598
+ this.$setNymph(tnymph);
2599
+ tilmeld = enforceTilmeld(this);
2600
+ const Group = tnymph.getEntityClass(GroupClass);
1697
2601
 
1698
- if (
1699
- this.$data.group != null &&
1700
- this.$data.group.user != null &&
1701
- this.$is(this.$data.group.user)
1702
- ) {
1703
- try {
2602
+ try {
2603
+ let group = this.$data.group;
2604
+ if (group != null) {
2605
+ // Make sure to get fresh group data.
2606
+ if (group.$asleep()) {
2607
+ await group.$wake();
2608
+ } else {
2609
+ if (!(await group.$refresh())) {
2610
+ throw new Error("Couldn't retrieve primary group info.");
2611
+ }
2612
+ }
2613
+ }
2614
+ if (group == null && this.guid == null) {
2615
+ if (tilmeld.config.generatePrimary) {
2616
+ // Generate a new primary group for the user.
2617
+ group = await Group.factory();
2618
+ group.user = this;
2619
+ const parent = await tnymph.getEntity(
2620
+ { class: Group },
2621
+ {
2622
+ type: '&',
2623
+ equal: ['defaultPrimary', true],
2624
+ },
2625
+ );
2626
+ if (parent != null) {
2627
+ group.parent = parent;
2628
+ }
2629
+ } else {
2630
+ // Add the default primary.
2631
+ const group = await tnymph.getEntity(
2632
+ { class: Group },
2633
+ {
2634
+ type: '&',
2635
+ equal: ['defaultPrimary', true],
2636
+ },
2637
+ );
2638
+ if (group != null) {
2639
+ this.$data.group = group;
2640
+ }
2641
+ }
2642
+ }
2643
+ if (group != null && group.user != null && this.$is(group.user)) {
1704
2644
  // 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();
2645
+ let changed = false;
2646
+ if (group.groupname !== this.$data.username) {
2647
+ group.groupname = this.$data.username;
2648
+ changed = true;
2649
+ }
2650
+ if (group.avatar !== this.$data.avatar) {
2651
+ group.avatar = this.$data.avatar;
2652
+ changed = true;
2653
+ }
2654
+ if (
2655
+ tilmeld.config.userFields.includes('email') &&
2656
+ group.email !== this.$data.email
2657
+ ) {
2658
+ group.email = this.$data.email;
2659
+ changed = true;
2660
+ }
2661
+ if (
2662
+ tilmeld.config.userFields.includes('name') &&
2663
+ group.name !== this.$data.name
2664
+ ) {
2665
+ group.name = this.$data.name;
2666
+ changed = true;
2667
+ }
2668
+ if (
2669
+ tilmeld.config.userFields.includes('phone') &&
2670
+ group.phone !== this.$data.phone
2671
+ ) {
2672
+ group.phone = this.$data.phone;
2673
+ changed = true;
2674
+ }
2675
+ if (changed || group.guid == null) {
2676
+ if (!(await group.$saveSkipAC())) {
2677
+ throw Error('Error updating primary group for user.');
2678
+ }
2679
+ }
2680
+ this.$data.group = group;
2681
+ }
2682
+
2683
+ if (this.$data.groups == null || this.$data.groups.length === 0) {
2684
+ // Add secondary groups.
2685
+ if (
2686
+ tilmeld.config.userFields.includes('email') &&
2687
+ tilmeld.config.verifyEmail &&
2688
+ tilmeld.config.unverifiedAccess &&
2689
+ this.$data.secret != null
2690
+ ) {
2691
+ // Add the default secondaries for unverified users.
2692
+ this.$data.groups = await tnymph.getEntities(
2693
+ { class: Group },
2694
+ {
2695
+ type: '&',
2696
+ equal: ['unverifiedSecondary', true],
2697
+ },
2698
+ );
2699
+ } else {
2700
+ // Add the default secondaries.
2701
+ this.$data.groups = await tnymph.getEntities(
2702
+ { class: Group },
2703
+ {
2704
+ type: '&',
2705
+ equal: ['defaultSecondary', true],
2706
+ },
2707
+ );
2708
+ }
2709
+ }
2710
+
2711
+ try {
2712
+ tilmeld.config.validatorUser(tilmeld, this);
1711
2713
  } catch (e: any) {
1712
- await tnymph.rollback(transaction);
1713
- this.$nymph = nymph;
1714
- throw e;
2714
+ throw new BadDataError(e?.message);
1715
2715
  }
2716
+ } catch (e: any) {
2717
+ await tnymph.rollback(transaction);
2718
+ this.$setNymph(nymph);
2719
+ throw e;
1716
2720
  }
1717
2721
 
1718
- let ret: boolean;
2722
+ let ret = false;
1719
2723
  let preGuid = this.guid;
1720
2724
  let preCdate = this.cdate;
1721
2725
  let preMdate = this.mdate;
1722
2726
 
1723
2727
  try {
2728
+ for (let callback of (this.constructor as typeof User)
2729
+ .beforeSaveCallbacks) {
2730
+ if (callback) {
2731
+ await callback(this);
2732
+ }
2733
+ }
2734
+
1724
2735
  ret = await super.$save();
1725
2736
  } catch (e: any) {
1726
2737
  await tnymph.rollback(transaction);
1727
- this.$nymph = nymph;
2738
+ this.guid = preGuid;
2739
+ this.cdate = preCdate;
2740
+ this.mdate = preMdate;
2741
+ this.$setNymph(nymph);
1728
2742
  throw e;
1729
2743
  }
2744
+
1730
2745
  if (ret) {
1731
2746
  if (sendVerification) {
1732
2747
  // The email has changed, so send a new verification email.
@@ -1735,23 +2750,48 @@ export default class User extends AbleObject<UserData> {
1735
2750
  this.guid = preGuid;
1736
2751
  this.cdate = preCdate;
1737
2752
  this.mdate = preMdate;
1738
- this.$nymph = nymph;
2753
+ this.$setNymph(nymph);
1739
2754
  throw new Error("Couldn't send verification email.");
1740
2755
  }
1741
2756
  }
1742
2757
 
1743
2758
  this.$descendantGroups = undefined;
1744
2759
  this.$gatekeeperCache = undefined;
1745
- await tnymph.commit(transaction);
1746
2760
 
1747
- if ((nymph.tilmeld as Tilmeld).User.current(true).$is(this)) {
1748
- // Update the user in the session cache.
1749
- (nymph.tilmeld as Tilmeld).fillSession(this);
2761
+ try {
2762
+ for (let callback of (this.constructor as typeof User)
2763
+ .afterSaveCallbacks) {
2764
+ if (callback) {
2765
+ await callback(this);
2766
+ }
2767
+ }
2768
+ } catch (e: any) {
2769
+ await tnymph.rollback(transaction);
2770
+ this.guid = preGuid;
2771
+ this.cdate = preCdate;
2772
+ this.mdate = preMdate;
2773
+ this.$setNymph(nymph);
2774
+ throw e;
1750
2775
  }
2776
+
2777
+ ret = await tnymph.commit(transaction);
1751
2778
  } else {
1752
2779
  await tnymph.rollback(transaction);
1753
2780
  }
1754
- this.$nymph = nymph;
2781
+ this.$setNymph(nymph);
2782
+
2783
+ if (ret) {
2784
+ this.$originalEmail = this.$data.email;
2785
+ this.$originalUsername = this.$data.username;
2786
+
2787
+ const tilmeld = enforceTilmeld(nymph);
2788
+
2789
+ if (tilmeld.User.current(true).$is(this)) {
2790
+ // Update the user in the session cache.
2791
+ await tilmeld.fillSession(this);
2792
+ }
2793
+ }
2794
+
1755
2795
  return ret;
1756
2796
  }
1757
2797
 
@@ -1772,53 +2812,167 @@ export default class User extends AbleObject<UserData> {
1772
2812
  }
1773
2813
 
1774
2814
  public async $delete() {
1775
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1776
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
1777
- throw new BadDataError("You don't have the authority to delete users.");
2815
+ const tilmeld = enforceTilmeld(this);
2816
+ if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
2817
+ throw new AccessControlError(
2818
+ "You don't have the authority to delete users.",
2819
+ );
1778
2820
  }
1779
2821
  if (
2822
+ !this.$skipAcWhenDeleting &&
1780
2823
  !tilmeld.gatekeeper('system/admin') &&
1781
- this.$gatekeeper('system/admin')
2824
+ this.$data.abilities?.includes('system/admin')
1782
2825
  ) {
1783
- throw new BadDataError(
1784
- "You don't have the authority to delete system admins."
2826
+ throw new AccessControlError(
2827
+ "You don't have the authority to delete system admins.",
2828
+ );
2829
+ }
2830
+ if (this.$data.username !== this.$originalUsername) {
2831
+ throw new AccessControlError(
2832
+ "You can't delete a user while switching their username.",
1785
2833
  );
1786
2834
  }
1787
- if (tilmeld.User.current(true).$is(this)) {
1788
- await this.$logout();
2835
+
2836
+ const transaction = 'tilmeld-delete-user-' + this.guid;
2837
+ const nymph = this.$nymph;
2838
+ const tnymph = await nymph.startTransaction(transaction);
2839
+ this.$setNymph(tnymph);
2840
+
2841
+ let success = false;
2842
+
2843
+ try {
2844
+ if (!(await this.$refresh())) {
2845
+ throw new BadDataError('User could not be refreshed.');
2846
+ }
2847
+
2848
+ if (this.$data.group != null) {
2849
+ if (this.$data.group.$asleep()) {
2850
+ await this.$data.group.$wake();
2851
+ } else {
2852
+ if (!(await this.$data.group.$refresh())) {
2853
+ throw new BadDataError('Group could not be refreshed.');
2854
+ }
2855
+ }
2856
+ }
2857
+
2858
+ for (let callback of (this.constructor as typeof User)
2859
+ .beforeDeleteCallbacks) {
2860
+ if (callback) {
2861
+ await callback(this);
2862
+ }
2863
+ }
2864
+
2865
+ if (tilmeld.User.current(true).$is(this)) {
2866
+ await this.$logout();
2867
+ }
2868
+
2869
+ if (this.$data.group != null && this.$is(this.$data.group.user)) {
2870
+ // Read the skip ac value here, because super.$delete changes it.
2871
+ const $skipAcWhenDeleting = this.$skipAcWhenDeleting;
2872
+
2873
+ // Delete the user.
2874
+ success = await super.$delete();
2875
+ if (success) {
2876
+ // Delete the generated group.
2877
+ success = $skipAcWhenDeleting
2878
+ ? await this.$data.group.$deleteSkipAC()
2879
+ : await this.$data.group.$delete();
2880
+ }
2881
+ } else {
2882
+ success = await super.$delete();
2883
+ }
2884
+ } catch (e: any) {
2885
+ await tnymph.rollback(transaction);
2886
+ this.$setNymph(nymph);
2887
+ throw e;
2888
+ }
2889
+
2890
+ if (success) {
2891
+ try {
2892
+ for (let callback of (this.constructor as typeof User)
2893
+ .afterDeleteCallbacks) {
2894
+ if (callback) {
2895
+ await callback(this);
2896
+ }
2897
+ }
2898
+ } catch (e: any) {
2899
+ await tnymph.rollback(transaction);
2900
+ this.$setNymph(nymph);
2901
+ throw e;
2902
+ }
2903
+ success = await tnymph.commit(transaction);
2904
+ } else {
2905
+ await tnymph.rollback(transaction);
1789
2906
  }
1790
- return await super.$delete();
2907
+ this.$setNymph(nymph);
2908
+ return success;
2909
+ }
2910
+
2911
+ /*
2912
+ * This should *never* be accessible on the client.
2913
+ */
2914
+ async $deleteSkipAC() {
2915
+ this.$skipAcWhenDeleting = true;
2916
+ return await this.$delete();
2917
+ }
2918
+
2919
+ $tilmeldDeleteSkipAC() {
2920
+ if (this.$skipAcWhenDeleting) {
2921
+ this.$skipAcWhenDeleting = false;
2922
+ return true;
2923
+ }
2924
+ return false;
1791
2925
  }
1792
2926
 
1793
2927
  public static on<T extends EventType>(
1794
2928
  event: T,
1795
- callback: T extends 'beforeRegister'
1796
- ? TilmeldBeforeRegisterCallback
1797
- : T extends 'afterRegister'
1798
- ? TilmeldAfterRegisterCallback
1799
- : T extends 'beforeLogin'
1800
- ? TilmeldBeforeLoginCallback
1801
- : T extends 'afterLogin'
1802
- ? TilmeldAfterLoginCallback
1803
- : T extends 'beforeLogout'
1804
- ? TilmeldBeforeLogoutCallback
1805
- : T extends 'afterLogout'
1806
- ? TilmeldAfterLogoutCallback
1807
- : never
2929
+ callback: T extends 'checkUsername'
2930
+ ? TilmeldCheckUsernameCallback
2931
+ : T extends 'beforeRegister'
2932
+ ? TilmeldBeforeRegisterCallback
2933
+ : T extends 'afterRegister'
2934
+ ? TilmeldAfterRegisterCallback
2935
+ : T extends 'beforeSave'
2936
+ ? TilmeldBeforeSaveCallback
2937
+ : T extends 'afterSave'
2938
+ ? TilmeldAfterSaveCallback
2939
+ : T extends 'beforeLogin'
2940
+ ? TilmeldBeforeLoginCallback
2941
+ : T extends 'afterLogin'
2942
+ ? TilmeldAfterLoginCallback
2943
+ : T extends 'beforeLogout'
2944
+ ? TilmeldBeforeLogoutCallback
2945
+ : T extends 'afterLogout'
2946
+ ? TilmeldAfterLogoutCallback
2947
+ : T extends 'beforeDelete'
2948
+ ? TilmeldBeforeDeleteCallback
2949
+ : T extends 'afterDelete'
2950
+ ? TilmeldAfterDeleteCallback
2951
+ : never,
1808
2952
  ) {
1809
- const prop = (event + 'Callbacks') as T extends 'beforeRegister'
1810
- ? 'beforeRegisterCallbacks'
1811
- : T extends 'afterRegister'
1812
- ? 'afterRegisterCallbacks'
1813
- : T extends 'beforeLogin'
1814
- ? 'beforeLoginCallbacks'
1815
- : T extends 'afterLogin'
1816
- ? 'afterLoginCallbacks'
1817
- : T extends 'beforeLogout'
1818
- ? 'beforeLogoutCallbacks'
1819
- : T extends 'afterLogout'
1820
- ? 'afterLogoutCallbacks'
1821
- : never;
2953
+ const prop = (event + 'Callbacks') as T extends 'checkUsername'
2954
+ ? 'checkUsernameCallbacks'
2955
+ : T extends 'beforeRegister'
2956
+ ? 'beforeRegisterCallbacks'
2957
+ : T extends 'afterRegister'
2958
+ ? 'afterRegisterCallbacks'
2959
+ : T extends 'beforeSave'
2960
+ ? 'beforeSaveCallbacks'
2961
+ : T extends 'afterSave'
2962
+ ? 'afterSaveCallbacks'
2963
+ : T extends 'beforeLogin'
2964
+ ? 'beforeLoginCallbacks'
2965
+ : T extends 'afterLogin'
2966
+ ? 'afterLoginCallbacks'
2967
+ : T extends 'beforeLogout'
2968
+ ? 'beforeLogoutCallbacks'
2969
+ : T extends 'afterLogout'
2970
+ ? 'afterLogoutCallbacks'
2971
+ : T extends 'beforeDelete'
2972
+ ? 'beforeDeleteCallbacks'
2973
+ : T extends 'afterDelete'
2974
+ ? 'afterDeleteCallbacks'
2975
+ : never;
1822
2976
  if (!(prop in this)) {
1823
2977
  throw new Error('Invalid event type.');
1824
2978
  }
@@ -1829,33 +2983,53 @@ export default class User extends AbleObject<UserData> {
1829
2983
 
1830
2984
  public static off<T extends EventType>(
1831
2985
  event: T,
1832
- callback: T extends 'beforeRegister'
1833
- ? TilmeldBeforeRegisterCallback
1834
- : T extends 'afterRegister'
1835
- ? TilmeldAfterRegisterCallback
1836
- : T extends 'beforeLogin'
1837
- ? TilmeldBeforeLoginCallback
1838
- : T extends 'afterLogin'
1839
- ? TilmeldAfterLoginCallback
1840
- : T extends 'beforeLogout'
1841
- ? TilmeldBeforeLogoutCallback
1842
- : T extends 'afterLogout'
1843
- ? TilmeldAfterLogoutCallback
1844
- : never
2986
+ callback: T extends 'checkUsername'
2987
+ ? TilmeldCheckUsernameCallback
2988
+ : T extends 'beforeRegister'
2989
+ ? TilmeldBeforeRegisterCallback
2990
+ : T extends 'afterRegister'
2991
+ ? TilmeldAfterRegisterCallback
2992
+ : T extends 'beforeSave'
2993
+ ? TilmeldBeforeSaveCallback
2994
+ : T extends 'afterSave'
2995
+ ? TilmeldAfterSaveCallback
2996
+ : T extends 'beforeLogin'
2997
+ ? TilmeldBeforeLoginCallback
2998
+ : T extends 'afterLogin'
2999
+ ? TilmeldAfterLoginCallback
3000
+ : T extends 'beforeLogout'
3001
+ ? TilmeldBeforeLogoutCallback
3002
+ : T extends 'afterLogout'
3003
+ ? TilmeldAfterLogoutCallback
3004
+ : T extends 'beforeDelete'
3005
+ ? TilmeldBeforeDeleteCallback
3006
+ : T extends 'afterDelete'
3007
+ ? TilmeldAfterDeleteCallback
3008
+ : never,
1845
3009
  ) {
1846
- const prop = (event + 'Callbacks') as T extends 'beforeRegister'
1847
- ? 'beforeRegisterCallbacks'
1848
- : T extends 'afterRegister'
1849
- ? 'afterRegisterCallbacks'
1850
- : T extends 'beforeLogin'
1851
- ? 'beforeLoginCallbacks'
1852
- : T extends 'afterLogin'
1853
- ? 'afterLoginCallbacks'
1854
- : T extends 'beforeLogout'
1855
- ? 'beforeLogoutCallbacks'
1856
- : T extends 'afterLogout'
1857
- ? 'afterLogoutCallbacks'
1858
- : never;
3010
+ const prop = (event + 'Callbacks') as T extends 'checkUsername'
3011
+ ? 'checkUsernameCallbacks'
3012
+ : T extends 'beforeRegister'
3013
+ ? 'beforeRegisterCallbacks'
3014
+ : T extends 'afterRegister'
3015
+ ? 'afterRegisterCallbacks'
3016
+ : T extends 'beforeSave'
3017
+ ? 'beforeSaveCallbacks'
3018
+ : T extends 'afterSave'
3019
+ ? 'afterSaveCallbacks'
3020
+ : T extends 'beforeLogin'
3021
+ ? 'beforeLoginCallbacks'
3022
+ : T extends 'afterLogin'
3023
+ ? 'afterLoginCallbacks'
3024
+ : T extends 'beforeLogout'
3025
+ ? 'beforeLogoutCallbacks'
3026
+ : T extends 'afterLogout'
3027
+ ? 'afterLogoutCallbacks'
3028
+ : T extends 'beforeDelete'
3029
+ ? 'beforeDeleteCallbacks'
3030
+ : T extends 'afterDelete'
3031
+ ? 'afterDeleteCallbacks'
3032
+ : never;
1859
3033
  if (!(prop in this)) {
1860
3034
  return false;
1861
3035
  }