@nymphjs/tilmeld 1.0.0-beta.9 → 1.0.0-beta.91

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 (78) hide show
  1. package/CHANGELOG.md +414 -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 +181 -15
  7. package/dist/Group.js +551 -192
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.test.js +83 -13
  10. package/dist/Group.test.js.map +1 -1
  11. package/dist/Tilmeld.d.ts +241 -12
  12. package/dist/Tilmeld.js +636 -254
  13. package/dist/Tilmeld.js.map +1 -1
  14. package/dist/Tilmeld.types.d.ts +2 -2
  15. package/dist/Tilmeld.types.js +1 -2
  16. package/dist/User.d.ts +395 -22
  17. package/dist/User.js +1316 -365
  18. package/dist/User.js.map +1 -1
  19. package/dist/User.test.js +132 -13
  20. package/dist/User.test.js.map +1 -1
  21. package/dist/conf/d.d.ts +287 -7
  22. package/dist/conf/d.js +1 -2
  23. package/dist/conf/defaults.d.ts +1 -1
  24. package/dist/conf/defaults.js +188 -141
  25. package/dist/conf/defaults.js.map +1 -1
  26. package/dist/conf/index.d.ts +2 -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/tilmeldJoiProps.js +13 -18
  55. package/dist/tilmeldJoiProps.js.map +1 -1
  56. package/jest.config.js +11 -2
  57. package/package.json +34 -31
  58. package/src/AbleObject.ts +2 -2
  59. package/src/Group.test.ts +104 -6
  60. package/src/Group.ts +661 -226
  61. package/src/Tilmeld.ts +420 -254
  62. package/src/Tilmeld.types.ts +2 -2
  63. package/src/User.test.ts +163 -6
  64. package/src/User.ts +1557 -454
  65. package/src/conf/d.ts +63 -17
  66. package/src/conf/defaults.ts +162 -109
  67. package/src/conf/index.ts +2 -2
  68. package/src/enforceTilmeld.ts +20 -0
  69. package/src/errors/AccessControlError.ts +4 -2
  70. package/src/errors/BadDataError.ts +4 -2
  71. package/src/errors/BadEmailError.ts +4 -2
  72. package/src/errors/BadUsernameError.ts +4 -2
  73. package/src/errors/index.ts +6 -6
  74. package/src/index.ts +12 -10
  75. package/src/tilmeldJoiProps.ts +11 -11
  76. package/src/type/locutus.d.ts +3 -0
  77. package/tsconfig.json +5 -3
  78. package/typedoc.json +4 -0
package/dist/User.js CHANGED
@@ -1,20 +1,121 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const strtotime_1 = __importDefault(require("locutus/php/datetime/strtotime"));
7
- const enc_base64_1 = __importDefault(require("crypto-js/enc-base64"));
8
- const sha256_1 = __importDefault(require("crypto-js/sha256"));
9
- const md5_1 = __importDefault(require("crypto-js/md5"));
10
- const lodash_1 = require("lodash");
11
- const guid_1 = require("@nymphjs/guid");
12
- const AbleObject_1 = __importDefault(require("./AbleObject"));
13
- const errors_1 = require("./errors");
14
- class User extends AbleObject_1.default {
15
- static async factory(guid) {
16
- return (await super.factory(guid));
17
- }
1
+ import crypto from 'node:crypto';
2
+ import { humanSecret, nanoid } from '@nymphjs/guid';
3
+ import strtotime from 'locutus/php/datetime/strtotime.js';
4
+ import { difference, xor } from 'lodash-es';
5
+ import { TOTP, Secret } from 'otpauth';
6
+ import { toDataURL } from 'qrcode';
7
+ import { splitn } from '@sciactive/splitn';
8
+ import { enforceTilmeld } from './enforceTilmeld.js';
9
+ import AbleObject from './AbleObject.js';
10
+ import GroupClass from './Group.js';
11
+ import { AccessControlError, BadDataError, BadEmailError, BadUsernameError, EmailChangeRateLimitExceededError, } from './errors/index.js';
12
+ /**
13
+ * A user data model.
14
+ *
15
+ * Written by Hunter Perrin for SciActive.
16
+ *
17
+ * @author Hunter Perrin <hperrin@gmail.com>
18
+ * @copyright SciActive Inc
19
+ * @see http://nymph.io/
20
+ */
21
+ export default class User extends AbleObject {
22
+ /**
23
+ * The instance of Tilmeld to use for queries.
24
+ */
25
+ static ETYPE = 'tilmeld_user';
26
+ static class = 'User';
27
+ static checkUsernameCallbacks = [];
28
+ static beforeRegisterCallbacks = [];
29
+ static afterRegisterCallbacks = [];
30
+ static beforeSaveCallbacks = [];
31
+ static afterSaveCallbacks = [];
32
+ static beforeLoginCallbacks = [];
33
+ static afterLoginCallbacks = [];
34
+ static beforeLogoutCallbacks = [];
35
+ static afterLogoutCallbacks = [];
36
+ static beforeDeleteCallbacks = [];
37
+ static afterDeleteCallbacks = [];
38
+ static DEFAULT_CLIENT_ENABLED_METHODS = [
39
+ '$checkUsername',
40
+ '$checkEmail',
41
+ '$checkPhone',
42
+ '$getAvatar',
43
+ '$register',
44
+ ];
45
+ static DEFAULT_PRIVATE_DATA = [
46
+ // The things that are commented are there to show what is _not_ private.
47
+ 'abilities',
48
+ // 'username',
49
+ // 'nameFirst',
50
+ // 'nameMiddle',
51
+ // 'nameLast',
52
+ // 'name',
53
+ 'email',
54
+ // 'avatar',
55
+ 'phone',
56
+ 'group',
57
+ 'groups',
58
+ 'inheritAbilities',
59
+ 'enabled',
60
+ 'secret',
61
+ 'emailChangeDate',
62
+ 'newEmailSecret',
63
+ 'newEmailAddress',
64
+ 'cancelEmailSecret',
65
+ 'cancelEmailAddress',
66
+ 'recoverSecret',
67
+ 'recoverSecretDate',
68
+ 'salt',
69
+ 'password',
70
+ 'passwordTemp',
71
+ 'revokeTokenDate',
72
+ 'totpSecret',
73
+ ];
74
+ static DEFAULT_ALLOWLIST_DATA = [];
75
+ $clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
76
+ static clientEnabledStaticMethods = [
77
+ 'current',
78
+ 'loginUser',
79
+ 'sendRecovery',
80
+ 'recover',
81
+ 'getClientConfig',
82
+ 'getDomainUsers',
83
+ ];
84
+ $privateData = [...User.DEFAULT_PRIVATE_DATA];
85
+ static searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
86
+ $allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
87
+ $allowlistTags = [];
88
+ /**
89
+ * Gatekeeper ability cache.
90
+ *
91
+ * Gatekeeper will cache the user's abilities that it calculates, so it can
92
+ * check faster if that user has been checked before.
93
+ */
94
+ $gatekeeperCache;
95
+ /**
96
+ * This should only be used by the backend.
97
+ */
98
+ $skipAcWhenSaving = false;
99
+ /**
100
+ * This should only be used by the backend.
101
+ */
102
+ $skipAcWhenDeleting = false;
103
+ /**
104
+ * The user's group descendants.
105
+ */
106
+ $descendantGroups;
107
+ /**
108
+ * Used to save the current email address to send verification if it changes
109
+ * from the frontend.
110
+ *
111
+ * If you are changing a user's email address and want to bypass email
112
+ * verification, don't set this.
113
+ */
114
+ $originalEmail;
115
+ /**
116
+ * Used to save the current username for domain admin permissions.
117
+ */
118
+ $originalUsername;
18
119
  static async factoryUsername(username) {
19
120
  const entity = new this();
20
121
  if (username != null) {
@@ -30,32 +131,22 @@ class User extends AbleObject_1.default {
30
131
  }
31
132
  return entity;
32
133
  }
33
- static factorySync(guid) {
34
- return super.factorySync(guid);
35
- }
36
- constructor(guid) {
37
- super(guid);
38
- this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
39
- this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
40
- this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
41
- this.$allowlistTags = [];
42
- this.$skipAcWhenSaving = false;
43
- if (this.guid == null) {
44
- this.$data.enabled = true;
45
- this.$data.abilities = [];
46
- this.$data.groups = [];
47
- this.$data.inheritAbilities = true;
48
- this.$updateDataProtection();
49
- }
50
- }
51
134
  static current(returnObjectIfNotExist) {
52
- if (this.tilmeld.currentUser == null) {
135
+ const tilmeld = enforceTilmeld(this);
136
+ if (tilmeld.currentUser == null) {
53
137
  return returnObjectIfNotExist ? this.factorySync() : null;
54
138
  }
55
- return this.tilmeld.currentUser;
139
+ return tilmeld.currentUser;
56
140
  }
141
+ /**
142
+ * Send an account recovery link.
143
+ *
144
+ * @param data The input data from the client.
145
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
146
+ */
57
147
  static async sendRecovery(data) {
58
- if (!this.tilmeld.config.pwRecovery) {
148
+ const tilmeld = enforceTilmeld(this);
149
+ if (!tilmeld.config.pwRecovery) {
59
150
  return {
60
151
  result: false,
61
152
  message: 'Account recovery is not allowed.',
@@ -63,9 +154,9 @@ class User extends AbleObject_1.default {
63
154
  }
64
155
  let user;
65
156
  const options = {};
66
- if (!this.tilmeld.config.emailUsernames &&
67
- data.recoveryType === 'username') {
68
- const getUser = await this.nymph.getEntity({ class: this.tilmeld.User, skipAc: true }, {
157
+ if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
158
+ // Create a username recovery email.
159
+ const getUser = await this.nymph.getEntity({ class: tilmeld.User, skipAc: true }, {
69
160
  type: '&',
70
161
  ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
71
162
  });
@@ -75,6 +166,7 @@ class User extends AbleObject_1.default {
75
166
  message: 'Requested account is not accessible.',
76
167
  };
77
168
  }
169
+ // Send the recovery email.
78
170
  user = getUser;
79
171
  options.template = 'RecoverUsername';
80
172
  options.message = {
@@ -86,18 +178,21 @@ class User extends AbleObject_1.default {
86
178
  options.locals = {};
87
179
  }
88
180
  else if (data.recoveryType === 'password') {
89
- const getUser = await this.tilmeld.User.factoryUsername(data.account);
181
+ // Create a password recovery email.
182
+ const getUser = await tilmeld.User.factoryUsername(data.account);
90
183
  if (getUser.guid == null) {
91
184
  return {
92
185
  result: false,
93
186
  message: 'Requested account is not accessible.',
94
187
  };
95
188
  }
96
- getUser.recoverSecret = (0, guid_1.humanSecret)();
189
+ // Create a unique secret.
190
+ getUser.recoverSecret = humanSecret();
97
191
  getUser.recoverSecretDate = Date.now();
98
192
  if (!(await getUser.$saveSkipAC())) {
99
193
  return { result: false, message: "Couldn't save recovery secret." };
100
194
  }
195
+ // Send the recovery email.
101
196
  user = getUser;
102
197
  options.template = 'RecoverPassword';
103
198
  options.message = {
@@ -108,13 +203,15 @@ class User extends AbleObject_1.default {
108
203
  };
109
204
  options.locals = {
110
205
  recoverCode: getUser.recoverSecret,
111
- timeLimit: this.tilmeld.config.pwRecoveryTimeLimit,
206
+ timeLimit: tilmeld.config.pwRecoveryTimeLimit,
112
207
  };
113
208
  }
114
209
  else {
115
210
  return { result: false, message: 'Invalid recovery type.' };
116
211
  }
117
- if (await this.tilmeld.config.sendEmail(this.tilmeld, options, user)) {
212
+ this.nymph.config.debugLog('tilmeld', `User account recovery (${options.template}) request for "${user.username}".`);
213
+ // Send the email.
214
+ if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
118
215
  return {
119
216
  result: true,
120
217
  message: "We've sent an email to your registered address. Please " +
@@ -125,18 +222,25 @@ class User extends AbleObject_1.default {
125
222
  return { result: false, message: "Couldn't send recovery email." };
126
223
  }
127
224
  }
225
+ /**
226
+ * Recover account details.
227
+ *
228
+ * @param data The input data from the client.
229
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
230
+ */
128
231
  static async recover(data) {
129
- if (!this.tilmeld.config.pwRecovery) {
232
+ const tilmeld = enforceTilmeld(this);
233
+ if (!tilmeld.config.pwRecovery) {
130
234
  return {
131
235
  result: false,
132
236
  message: 'Account recovery is not allowed.',
133
237
  };
134
238
  }
135
- const user = await this.tilmeld.User.factoryUsername(data.username);
239
+ const user = await tilmeld.User.factoryUsername(data.username);
136
240
  if (user.guid == null ||
137
241
  user.recoverSecret == null ||
138
242
  data.secret !== user.recoverSecret ||
139
- (0, strtotime_1.default)('+' + this.tilmeld.config.pwRecoveryTimeLimit, Math.floor((user.recoverSecretDate ?? 0) / 1000)) *
243
+ strtotime('+' + tilmeld.config.pwRecoveryTimeLimit, Math.floor((user.recoverSecretDate ?? 0) / 1000)) *
140
244
  1000 <
141
245
  Date.now()) {
142
246
  return {
@@ -147,6 +251,7 @@ class User extends AbleObject_1.default {
147
251
  if (!('password' in data) || !data.password.length) {
148
252
  return { result: false, message: 'Password cannot be empty.' };
149
253
  }
254
+ this.nymph.config.debugLog('tilmeld', `User account recovery for "${user.username}".`);
150
255
  user.$password(data.password);
151
256
  delete user.recoverSecret;
152
257
  delete user.recoverSecretDate;
@@ -161,22 +266,25 @@ class User extends AbleObject_1.default {
161
266
  }
162
267
  }
163
268
  static getClientConfig() {
269
+ const tilmeld = enforceTilmeld(this);
164
270
  return {
165
- regFields: this.tilmeld.config.regFields,
166
- userFields: this.tilmeld.config.userFields,
167
- emailUsernames: this.tilmeld.config.emailUsernames,
168
- allowRegistration: this.tilmeld.config.allowRegistration,
169
- allowUsernameChange: this.tilmeld.config.allowUsernameChange,
170
- pwRecovery: this.tilmeld.config.pwRecovery,
171
- verifyEmail: this.tilmeld.config.verifyEmail,
172
- unverifiedAccess: this.tilmeld.config.unverifiedAccess,
271
+ regFields: tilmeld.config.regFields,
272
+ userFields: tilmeld.config.userFields,
273
+ emailUsernames: tilmeld.config.emailUsernames,
274
+ domainSupport: tilmeld.config.domainSupport,
275
+ allowRegistration: tilmeld.config.allowRegistration,
276
+ allowUsernameChange: tilmeld.config.allowUsernameChange,
277
+ pwRecovery: tilmeld.config.pwRecovery,
278
+ verifyEmail: tilmeld.config.verifyEmail,
279
+ unverifiedAccess: tilmeld.config.unverifiedAccess,
173
280
  };
174
281
  }
175
282
  static async loginUser(data) {
283
+ const tilmeld = enforceTilmeld(this);
176
284
  if (!('username' in data) || !data.username.length) {
177
285
  return { result: false, message: 'Incorrect login/password.' };
178
286
  }
179
- const user = await this.tilmeld.User.factoryUsername(data.username);
287
+ const user = await tilmeld.User.factoryUsername(data.username);
180
288
  const result = await user.$login(data);
181
289
  if (result.result) {
182
290
  user.$updateDataProtection();
@@ -184,20 +292,113 @@ class User extends AbleObject_1.default {
184
292
  }
185
293
  return result;
186
294
  }
295
+ /**
296
+ * Get the users in a domain. This is only accessible to the domain's admins.
297
+ */
298
+ static async getDomainUsers(domain, options) {
299
+ const tilmeld = enforceTilmeld(this);
300
+ if (!tilmeld.config.domainSupport) {
301
+ throw new AccessControlError('Domain support is not enabled.');
302
+ }
303
+ if (!domain) {
304
+ throw new BadDataError('Domain is required.');
305
+ }
306
+ domain = domain.toLowerCase();
307
+ if (!tilmeld.config.validDomainRegex.test(domain)) {
308
+ throw new BadDataError(tilmeld.config.validDomainRegexNotice);
309
+ }
310
+ if (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
311
+ throw new AccessControlError('You do not have the authority to manage this domain.');
312
+ }
313
+ return await this.nymph.getEntities({
314
+ class: this.nymph.getEntityClass(User),
315
+ ...(options?.limit != null
316
+ ? {
317
+ limit: options.limit,
318
+ }
319
+ : {}),
320
+ ...(options?.offset != null
321
+ ? {
322
+ offset: options.offset,
323
+ }
324
+ : {}),
325
+ ...(options?.sort != null
326
+ ? {
327
+ sort: options.sort,
328
+ }
329
+ : {}),
330
+ ...(options?.reverse != null
331
+ ? {
332
+ reverse: options.reverse,
333
+ }
334
+ : {}),
335
+ }, {
336
+ type: '&',
337
+ like: ['username', `%@${domain.replace(/([\\%_])/g, (s) => `\\${s}`)}`],
338
+ });
339
+ }
340
+ constructor() {
341
+ super();
342
+ // Defaults.
343
+ this.$data.enabled = true;
344
+ this.$data.abilities = [];
345
+ this.$data.groups = [];
346
+ this.$data.inheritAbilities = true;
347
+ this.$updateDataProtection();
348
+ }
349
+ $setNymph(nymph) {
350
+ this.$nymph = nymph;
351
+ if (!this.$asleep()) {
352
+ if (this.$data.group && this.$data.group.$nymph !== nymph) {
353
+ this.$data.group.$setNymph(nymph);
354
+ }
355
+ for (const group of this.$data.groups ?? []) {
356
+ if (group && group.$nymph !== nymph) {
357
+ group.$setNymph(nymph);
358
+ }
359
+ }
360
+ }
361
+ }
362
+ async $getUniques() {
363
+ const tilmeld = enforceTilmeld(this);
364
+ const uniques = [`u:${this.$data.username}`];
365
+ if (!tilmeld.config.emailUsernames &&
366
+ tilmeld.config.userFields.includes('email')) {
367
+ uniques.push(`e:${this.$data.email}`);
368
+ if (this.$data.newEmailAddress != null) {
369
+ uniques.push(`e:${this.$data.newEmailAddress}`);
370
+ }
371
+ }
372
+ return uniques;
373
+ }
187
374
  async $login(data) {
188
- const tilmeld = this.$nymph.tilmeld;
375
+ const tilmeld = enforceTilmeld(this);
189
376
  if (this.guid == null) {
190
377
  return { result: false, message: 'Incorrect login/password.' };
191
378
  }
192
379
  if (!this.$data.enabled) {
193
380
  return { result: false, message: 'This user is disabled.' };
194
381
  }
195
- if (this.$gatekeeper()) {
382
+ if (await this.$gatekeeper()) {
196
383
  return { result: true, message: 'You are already logged in.' };
197
384
  }
198
385
  if (!this.$checkPassword(data.password)) {
199
386
  return { result: false, message: 'Incorrect login/password.' };
200
387
  }
388
+ if (this.$data.totpSecret != null) {
389
+ if (data.code == null ||
390
+ typeof data.code !== 'string' ||
391
+ data.code.length !== 6) {
392
+ return {
393
+ result: false,
394
+ needTOTP: true,
395
+ message: 'You need to provide a 2FA code.',
396
+ };
397
+ }
398
+ if (!this.$checkTOTPCode(data.code)) {
399
+ return { result: false, message: 'Incorrect 2FA code.' };
400
+ }
401
+ }
201
402
  try {
202
403
  for (let callback of this.constructor
203
404
  .beforeLoginCallbacks) {
@@ -212,7 +413,9 @@ class User extends AbleObject_1.default {
212
413
  message: e.message,
213
414
  };
214
415
  }
215
- if (!tilmeld.login(this, true)) {
416
+ this.$nymph.config.debugLog('tilmeld', `User login for "${this.$data.username}".`);
417
+ // Authentication was successful, attempt to login.
418
+ if (!(await tilmeld.login(this, true))) {
216
419
  return { result: false, message: 'Incorrect login/password.' };
217
420
  }
218
421
  for (let callback of this.constructor
@@ -221,10 +424,73 @@ class User extends AbleObject_1.default {
221
424
  await callback(this);
222
425
  }
223
426
  }
427
+ // Login was successful.
224
428
  return { result: true, message: 'You are logged in.' };
225
429
  }
430
+ async $switchUser(data) {
431
+ const tilmeld = enforceTilmeld(this);
432
+ if (this.guid == null) {
433
+ return { result: false, message: 'Incorrect login/password.' };
434
+ }
435
+ let user = tilmeld.User.current();
436
+ if (!user || user.guid == null) {
437
+ return { result: false, message: "You're not logged in." };
438
+ }
439
+ const isCurrentUser = user != null && user.$is(this);
440
+ if (isCurrentUser) {
441
+ return { result: false, message: "You can't switch to yourself." };
442
+ }
443
+ const toAbilities = this.$data.abilities || [];
444
+ const fromAbilities = user.abilities || [];
445
+ if (!fromAbilities.includes('tilmeld/switch') &&
446
+ !fromAbilities.includes('system/admin')) {
447
+ return {
448
+ result: false,
449
+ message: "You don't have permission to switch users.",
450
+ };
451
+ }
452
+ if (toAbilities.includes('system/admin') ||
453
+ toAbilities.includes('tilmeld/admin')) {
454
+ return { result: false, message: "You can't switch to an admin." };
455
+ }
456
+ try {
457
+ for (let callback of this.constructor
458
+ .beforeLoginCallbacks) {
459
+ if (callback) {
460
+ await callback(this, {
461
+ username: this.$data.username || '',
462
+ password: '',
463
+ ...(data || {}),
464
+ });
465
+ }
466
+ }
467
+ }
468
+ catch (e) {
469
+ return {
470
+ result: false,
471
+ message: e.message,
472
+ };
473
+ }
474
+ this.$nymph.config.debugLog('tilmeld', `User login switch for "${user.username}" to "${this.$data.username}".`);
475
+ // Authentication was successful, attempt to login.
476
+ if (!(await tilmeld.loginSwitch(this, true))) {
477
+ return { result: false, message: 'Incorrect login/password.' };
478
+ }
479
+ for (let callback of this.constructor
480
+ .afterLoginCallbacks) {
481
+ if (callback) {
482
+ await callback(this);
483
+ }
484
+ }
485
+ // Login was successful.
486
+ return { result: true, message: 'You are logged in.' };
487
+ }
488
+ /**
489
+ * Log a user out of the system.
490
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
491
+ */
226
492
  async $logout() {
227
- const tilmeld = this.$nymph.tilmeld;
493
+ const tilmeld = enforceTilmeld(this);
228
494
  try {
229
495
  for (let callback of this.constructor
230
496
  .beforeLogoutCallbacks) {
@@ -239,7 +505,8 @@ class User extends AbleObject_1.default {
239
505
  message: e.message,
240
506
  };
241
507
  }
242
- tilmeld.logout();
508
+ this.$nymph.config.debugLog('tilmeld', `User logout for "${this.$data.username}".`);
509
+ await tilmeld.logout();
243
510
  for (let callback of this.constructor
244
511
  .afterLogoutCallbacks) {
245
512
  if (callback) {
@@ -256,66 +523,142 @@ class User extends AbleObject_1.default {
256
523
  return 'https://secure.gravatar.com/avatar/?d=mm&s=40';
257
524
  }
258
525
  return ('https://secure.gravatar.com/avatar/' +
259
- (0, md5_1.default)(this.$data.email.trim().toLowerCase()).toString() +
526
+ crypto
527
+ .createHash('sha256')
528
+ .update(this.$data.email.trim().toLowerCase())
529
+ .digest('hex')
530
+ .toLowerCase() +
260
531
  '?d=identicon&s=40');
261
532
  }
533
+ $getGid() {
534
+ if (this.$data.group) {
535
+ return this.$data.group.guid;
536
+ }
537
+ return null;
538
+ }
539
+ $getGids() {
540
+ if (this.$data.groups) {
541
+ return this.$data.groups
542
+ .map((group) => group.guid)
543
+ .filter((guid) => guid != null);
544
+ }
545
+ return null;
546
+ }
547
+ /**
548
+ * Get the user's group descendants.
549
+ */
262
550
  async $getDescendantGroups() {
263
551
  if (this.$descendantGroups == null) {
264
552
  this.$descendantGroups = [];
265
553
  if (this.$data.group != null) {
554
+ await this.$data.group.$wake();
266
555
  this.$descendantGroups = await this.$data.group.$getDescendants();
267
556
  }
557
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
268
558
  for (let curGroup of this.$data.groups ?? []) {
269
559
  this.$descendantGroups = this.$descendantGroups?.concat(await curGroup.$getDescendants());
270
560
  }
271
561
  }
272
562
  return this.$descendantGroups;
273
563
  }
274
- $getDescendantGroupsSync() {
275
- if (this.$descendantGroups == null) {
276
- this.$descendantGroups = [];
277
- if (this.$data.group != null) {
278
- this.$descendantGroups = this.$data.group.$getDescendantsSync();
279
- }
280
- for (let curGroup of this.$data.groups ?? []) {
281
- this.$descendantGroups = this.$descendantGroups?.concat(curGroup.$getDescendantsSync());
282
- }
283
- }
284
- return this.$descendantGroups;
285
- }
286
564
  $jsonAcceptData(input, allowConflict = false) {
287
- const tilmeld = this.$nymph.tilmeld;
288
- this.$referenceWake();
289
- if ('abilities' in input.data &&
290
- input.data.abilities.indexOf('system/admin') !== -1 &&
291
- this.$data.abilities?.indexOf('system/admin') === -1 &&
292
- tilmeld.gatekeeper('tilmeld/admin') &&
293
- !tilmeld.gatekeeper('system/admin')) {
294
- throw new errors_1.BadDataError("You don't have the authority to make this user a system admin.");
565
+ const tilmeld = enforceTilmeld(this);
566
+ this.$check();
567
+ if ('abilities' in input.data) {
568
+ this.$checkIncomingAbilities(input.data.abilities);
569
+ }
570
+ // If the user is a new domain user, the data protection needs to be updated
571
+ // before the data is accepted, or else some things won't be accepted
572
+ // correctly (enabled, passwordTemp).
573
+ let reloadDataProtection = false;
574
+ if (tilmeld.config.domainSupport) {
575
+ let [_username, domain] = splitn(input.data.username ?? '', '@', 2);
576
+ // Lowercase the domain part.
577
+ if (domain) {
578
+ domain = domain.toLowerCase();
579
+ }
580
+ const user = tilmeld.User.current();
581
+ const isCurrentUser = user != null && user.$is(this);
582
+ // While loading, accessing abilities on the current user will infinitely
583
+ // loop, but if this is the current user, we can use its abilities.
584
+ const abilities = (isCurrentUser || user == null
585
+ ? this.$data.abilities
586
+ : user?.abilities) ?? [];
587
+ const isNewUser = input.guid == null;
588
+ const isDomainUser = !isCurrentUser &&
589
+ user != null &&
590
+ domain &&
591
+ abilities.includes(`tilmeld/domain/${domain}/admin`);
592
+ reloadDataProtection = !!(isNewUser && isDomainUser);
295
593
  }
296
- this.$originalEmail = this.$data.email;
297
594
  super.$jsonAcceptData(input, allowConflict);
595
+ if (reloadDataProtection) {
596
+ // Load it again after data protection is run.
597
+ this.$updateDataProtection();
598
+ super.$jsonAcceptData(input, allowConflict);
599
+ }
298
600
  }
299
601
  $jsonAcceptPatch(patch, allowConflict = false) {
300
- const tilmeld = this.$nymph.tilmeld;
301
- this.$referenceWake();
302
- if ('abilities' in patch.set &&
303
- patch.set.abilities.indexOf('system/admin') !== -1 &&
304
- this.$data.abilities?.indexOf('system/admin') === -1 &&
305
- tilmeld.gatekeeper('tilmeld/admin') &&
306
- !tilmeld.gatekeeper('system/admin')) {
307
- throw new errors_1.BadDataError("You don't have the authority to make this user a system admin.");
602
+ this.$check();
603
+ if ('abilities' in patch.set) {
604
+ this.$checkIncomingAbilities(patch.set.abilities);
308
605
  }
309
- this.$originalEmail = this.$data.email;
310
606
  super.$jsonAcceptPatch(patch, allowConflict);
311
607
  }
312
- $putData(data, sdata) {
313
- super.$putData(data, sdata);
608
+ $checkIncomingAbilities(incoming) {
609
+ const tilmeld = enforceTilmeld(this);
610
+ if (!Array.isArray(incoming) ||
611
+ incoming.find((ability) => typeof ability !== 'string')) {
612
+ throw new BadDataError('Abilities must be an array of strings.');
613
+ }
614
+ if (incoming.includes('system/admin') !==
615
+ this.$data.abilities?.includes('system/admin') &&
616
+ (!tilmeld.currentUser ||
617
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
618
+ throw new BadDataError("You don't have the authority to grant or revoke system/admin.");
619
+ }
620
+ if (incoming.includes('tilmeld/admin') !==
621
+ this.$data.abilities?.includes('tilmeld/admin') &&
622
+ (!tilmeld.currentUser ||
623
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
624
+ throw new BadDataError("You don't have the authority to grant or revoke tilmeld/admin.");
625
+ }
626
+ if (incoming.includes('tilmeld/switch') !==
627
+ this.$data.abilities?.includes('tilmeld/switch') &&
628
+ (!tilmeld.currentUser ||
629
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
630
+ throw new BadDataError("You don't have the authority to grant or revoke tilmeld/switch.");
631
+ }
632
+ if (xor(incoming.filter((ability) => ability.startsWith('tilmeld/')), this.$data.abilities?.filter((ability) => ability.startsWith('tilmeld/')) ?? []).length &&
633
+ (!tilmeld.currentUser ||
634
+ (!tilmeld.currentUser.abilities?.includes('system/admin') &&
635
+ !tilmeld.currentUser.abilities?.includes('tilmeld/admin')))) {
636
+ throw new BadDataError("You don't have the authority to grant or revoke Tilmeld abilities.");
637
+ }
638
+ }
639
+ $putData(data, sdata, source) {
640
+ super.$putData(data, sdata, source);
641
+ if (source === 'server') {
642
+ this.$originalEmail = this.$data.email;
643
+ this.$originalUsername = this.$data.username;
644
+ }
314
645
  this.$updateDataProtection();
315
646
  }
647
+ /**
648
+ * Update the data protection arrays for a user.
649
+ *
650
+ * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
651
+ */
316
652
  $updateDataProtection(givenUser) {
317
- const tilmeld = this.$nymph.tilmeld;
653
+ const tilmeld = enforceTilmeld(this);
318
654
  let user = givenUser ?? tilmeld.User.current();
655
+ let [_username, domain] = tilmeld.config.domainSupport
656
+ ? splitn(this.$data.username ?? '', '@', 2)
657
+ : [this.$data.username, null];
658
+ // Lowercase the domain part.
659
+ if (domain) {
660
+ domain = domain.toLowerCase();
661
+ }
319
662
  this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
320
663
  this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
321
664
  this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
@@ -323,75 +666,211 @@ class User extends AbleObject_1.default {
323
666
  this.$privateData.push('username');
324
667
  }
325
668
  const isCurrentUser = user != null && user.$is(this);
669
+ // While loading, accessing abilities on the current user will infinitely
670
+ // loop, but if this is the current user, we can use its abilities.
326
671
  const abilities = (isCurrentUser || user == null
327
672
  ? this.$data.abilities
328
673
  : user?.abilities) ?? [];
329
674
  const isNewUser = this.guid == null;
675
+ const isDomainUser = tilmeld.config.domainSupport &&
676
+ !isCurrentUser &&
677
+ user != null &&
678
+ domain &&
679
+ abilities.includes(`tilmeld/domain/${domain}/admin`);
330
680
  if (isCurrentUser) {
681
+ // Users can check to see what abilities they have.
331
682
  this.$clientEnabledMethods.push('$gatekeeper');
332
683
  this.$clientEnabledMethods.push('$changePassword');
684
+ this.$clientEnabledMethods.push('$revokeCurrentTokens');
333
685
  this.$clientEnabledMethods.push('$logout');
334
686
  this.$clientEnabledMethods.push('$sendEmailVerification');
687
+ this.$clientEnabledMethods.push('$hasTOTPSecret');
688
+ this.$clientEnabledMethods.push('$getNewTOTPSecret');
689
+ this.$clientEnabledMethods.push('$saveTOTPSecret');
690
+ this.$clientEnabledMethods.push('$removeTOTPSecret');
691
+ }
692
+ if (isDomainUser) {
693
+ // Domain admins can check to see what abilities their users have.
694
+ this.$clientEnabledMethods.push('$gatekeeper');
695
+ this.$clientEnabledMethods.push('$changePassword');
696
+ this.$clientEnabledMethods.push('$revokeCurrentTokens');
697
+ this.$clientEnabledMethods.push('$hasTOTPSecret');
698
+ this.$clientEnabledMethods.push('$getNewTOTPSecret');
699
+ this.$clientEnabledMethods.push('$saveTOTPSecret');
700
+ this.$clientEnabledMethods.push('$removeTOTPSecret');
335
701
  }
336
702
  if (user != null &&
337
- (abilities.indexOf('tilmeld/admin') !== -1 ||
338
- abilities.indexOf('system/admin') !== -1)) {
339
- this.$privateData = ['password', 'salt'];
703
+ (abilities.includes('tilmeld/admin') ||
704
+ abilities.includes('system/admin'))) {
705
+ // Users who can edit other users can see most of their data.
706
+ this.$privateData = ['password', 'salt', 'totpSecret'];
707
+ this.$clientEnabledMethods.push('$hasTOTPSecret');
708
+ this.$clientEnabledMethods.push('$removeTOTPSecret');
340
709
  this.$allowlistData = undefined;
341
710
  }
342
- else if (isCurrentUser || isNewUser) {
711
+ else if (isDomainUser) {
712
+ // Domain admins can see their users' data, and edit some of it.
343
713
  if (tilmeld.config.allowUsernameChange || isNewUser) {
344
714
  this.$allowlistData.push('username');
345
715
  }
346
716
  this.$allowlistData.push('avatar');
347
- if (tilmeld.config.userFields.indexOf('name') !== -1) {
717
+ if (tilmeld.config.userFields.includes('name')) {
348
718
  this.$allowlistData.push('nameFirst');
349
719
  this.$allowlistData.push('nameMiddle');
350
720
  this.$allowlistData.push('nameLast');
351
721
  this.$allowlistData.push('name');
352
722
  }
353
- if (tilmeld.config.userFields.indexOf('email') !== -1) {
723
+ if (tilmeld.config.userFields.includes('email')) {
354
724
  this.$allowlistData.push('email');
355
725
  }
356
- if (tilmeld.config.userFields.indexOf('phone') !== -1) {
726
+ if (tilmeld.config.userFields.includes('phone')) {
357
727
  this.$allowlistData.push('phone');
358
728
  }
729
+ this.$allowlistData.push('enabled');
730
+ this.$allowlistData.push('passwordTemp');
359
731
  this.$privateData = [
732
+ // The things commented are there to show what is _not_ private.
733
+ // 'abilities',
734
+ // 'username',
735
+ // 'nameFirst',
736
+ // 'nameMiddle',
737
+ // 'nameLast',
738
+ // 'name',
739
+ // 'email',
740
+ // 'avatar',
741
+ // 'phone',
742
+ // 'group',
743
+ // 'groups',
744
+ // 'inheritAbilities',
745
+ // 'enabled',
360
746
  'secret',
747
+ 'emailChangeDate',
361
748
  'newEmailSecret',
362
- 'cancelEmailAddress',
749
+ // 'newEmailAddress',
363
750
  'cancelEmailSecret',
364
- 'emailChangeDate',
751
+ 'cancelEmailAddress',
365
752
  'recoverSecret',
366
753
  'recoverSecretDate',
754
+ 'salt',
367
755
  'password',
756
+ // 'passwordTemp',
757
+ // 'revokeTokenDate',
758
+ 'totpSecret',
759
+ ];
760
+ }
761
+ else if (isCurrentUser || isNewUser) {
762
+ // Users can see their own data, and edit some of it.
763
+ if (tilmeld.config.allowUsernameChange || isNewUser) {
764
+ this.$allowlistData.push('username');
765
+ }
766
+ this.$allowlistData.push('avatar');
767
+ if (tilmeld.config.userFields.includes('name')) {
768
+ this.$allowlistData.push('nameFirst');
769
+ this.$allowlistData.push('nameMiddle');
770
+ this.$allowlistData.push('nameLast');
771
+ this.$allowlistData.push('name');
772
+ }
773
+ if (tilmeld.config.userFields.includes('email')) {
774
+ this.$allowlistData.push('email');
775
+ }
776
+ if (tilmeld.config.userFields.includes('phone')) {
777
+ this.$allowlistData.push('phone');
778
+ }
779
+ this.$privateData = [
780
+ // The things commented are there to show what is _not_ private.
781
+ // 'abilities',
782
+ // 'username',
783
+ // 'nameFirst',
784
+ // 'nameMiddle',
785
+ // 'nameLast',
786
+ // 'name',
787
+ // 'email',
788
+ // 'avatar',
789
+ // 'phone',
790
+ // 'group',
791
+ // 'groups',
792
+ // 'inheritAbilities',
793
+ 'enabled',
794
+ 'secret',
795
+ 'emailChangeDate',
796
+ 'newEmailSecret',
797
+ // 'newEmailAddress',
798
+ 'cancelEmailSecret',
799
+ 'cancelEmailAddress',
800
+ 'recoverSecret',
801
+ 'recoverSecretDate',
368
802
  'salt',
803
+ 'password',
804
+ 'passwordTemp',
805
+ // 'revokeTokenDate',
806
+ 'totpSecret',
369
807
  ];
370
808
  }
809
+ if (user != null &&
810
+ !isCurrentUser &&
811
+ (abilities.includes('tilmeld/switch') ||
812
+ abilities.includes('system/admin'))) {
813
+ // Users with this ability can switch to other users.
814
+ this.$clientEnabledMethods.push('$switchUser');
815
+ }
371
816
  }
372
- $gatekeeper(ability) {
373
- const tilmeld = this.$nymph.tilmeld;
817
+ $updateGroupDataProtection() {
818
+ if (this.$data.group != null && !this.$data.group.$asleep()) {
819
+ this.$data.group.$updateDataProtection();
820
+ }
821
+ for (let group of this.$data.groups ?? []) {
822
+ if (!group.$asleep()) {
823
+ group.$updateDataProtection();
824
+ }
825
+ }
826
+ }
827
+ /**
828
+ * Check to see if a user has an ability.
829
+ *
830
+ * This function will check both user and group abilities, if the user is
831
+ * marked to inherit the abilities of its group.
832
+ *
833
+ * If `ability` is undefined, it will check to see if the user is currently
834
+ * logged in.
835
+ *
836
+ * If the user has the "system/admin" ability, this function will return true.
837
+ *
838
+ * @param ability The ability.
839
+ * @returns True or false.
840
+ */
841
+ async $gatekeeper(ability) {
842
+ await this.$wake();
843
+ const tilmeld = enforceTilmeld(this);
374
844
  if (ability == null) {
375
845
  return tilmeld.User.current()?.$is(this) ?? false;
376
846
  }
847
+ // Check the cache to see if we've already checked this user.
377
848
  if (this.$gatekeeperCache == null) {
378
- let abilities = this.$data.abilities ?? [];
379
- if (this.$data.inheritAbilities) {
380
- for (let curGroup of this.$data.groups ?? []) {
381
- if (curGroup.enabled) {
382
- abilities = abilities.concat(curGroup.abilities ?? []);
383
- }
384
- }
385
- if (this.$data.group != null &&
386
- this.$data.group.cdate != null &&
387
- this.$data.group.enabled) {
388
- abilities = abilities.concat(this.$data.group.abilities ?? []);
849
+ this.$gatekeeperCache = await this.$getGatekeeperCache();
850
+ }
851
+ return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
852
+ !!this.$data.abilities?.includes('system/admin'));
853
+ }
854
+ /**
855
+ * Build a gatekeeper cache object.
856
+ */
857
+ async $getGatekeeperCache() {
858
+ await this.$wake();
859
+ let abilities = this.$data.abilities ?? [];
860
+ if (this.$data.inheritAbilities) {
861
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
862
+ for (let curGroup of this.$data.groups ?? []) {
863
+ if (curGroup.enabled) {
864
+ abilities = abilities.concat(curGroup.abilities ?? []);
389
865
  }
390
866
  }
391
- this.$gatekeeperCache = Object.fromEntries(abilities.map((ability) => [ability, true]));
867
+ await this.$data.group?.$wake();
868
+ const group = this.$data.group;
869
+ if (group != null && group.cdate != null && group.enabled) {
870
+ abilities = abilities.concat(group.abilities ?? []);
871
+ }
392
872
  }
393
- return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
394
- !!this.$gatekeeperCache['system/admin']);
873
+ return Object.fromEntries(abilities.map((ability) => [ability, true]));
395
874
  }
396
875
  $clearCache() {
397
876
  const ret = super.$clearCache();
@@ -399,8 +878,13 @@ class User extends AbleObject_1.default {
399
878
  this.$gatekeeperCache = undefined;
400
879
  return ret;
401
880
  }
881
+ /**
882
+ * Send the user email verification/change/cancellation links.
883
+ *
884
+ * @returns True on success, false on failure.
885
+ */
402
886
  async $sendEmailVerification() {
403
- const tilmeld = this.$nymph.tilmeld;
887
+ const tilmeld = enforceTilmeld(this);
404
888
  if (this.guid == null) {
405
889
  return false;
406
890
  }
@@ -463,33 +947,60 @@ class User extends AbleObject_1.default {
463
947
  }
464
948
  return success;
465
949
  }
466
- $addGroup(group) {
467
- if (this.$data.groups == null) {
468
- this.$data.groups = [];
469
- }
470
- if (!group.$inArray(this.$data.groups)) {
471
- this.$data.groups.push(group);
472
- return this.$data.groups;
950
+ /**
951
+ * Add the user to a (secondary) group.
952
+ *
953
+ * @param group The group.
954
+ * @returns True if the user is already in the group. The resulting array of groups if the user was not.
955
+ */
956
+ async $addGroup(group) {
957
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
958
+ let groups = this.$data.groups;
959
+ if (groups == null) {
960
+ groups = [];
961
+ }
962
+ if (!group.$inArray(groups)) {
963
+ groups.push(group);
964
+ this.$data.groups = groups;
965
+ return groups;
473
966
  }
474
967
  return true;
475
968
  }
969
+ /**
970
+ * Check the given password against the user's.
971
+ *
972
+ * @param password The password in question.
973
+ * @returns True if the passwords match, otherwise false.
974
+ */
476
975
  $checkPassword(password) {
477
- const tilmeld = this.$nymph.tilmeld;
976
+ const tilmeld = enforceTilmeld(this);
478
977
  switch (tilmeld.config.pwMethod) {
479
978
  case 'plain':
480
979
  return this.$data.password === password;
481
980
  case 'digest':
482
- return this.$data.password === (0, sha256_1.default)(password).toString(enc_base64_1.default);
981
+ return (this.$data.password ===
982
+ crypto.createHash('sha256').update(password).digest('base64'));
483
983
  case 'salt':
484
984
  default:
485
985
  return (this.$data.password ===
486
- (0, sha256_1.default)(password + this.$data.salt).toString(enc_base64_1.default));
986
+ crypto
987
+ .createHash('sha256')
988
+ .update(password + this.$data.salt)
989
+ .digest('base64'));
487
990
  }
488
991
  }
489
- $delGroup(group) {
490
- if (this.$data.groups != null && group.$inArray(this.$data.groups)) {
992
+ /**
993
+ * Remove the user from a (secondary) group.
994
+ *
995
+ * @param group The group.
996
+ * @returns True if the user wasn't in the group. The resulting array of groups if the user was.
997
+ */
998
+ async $delGroup(group) {
999
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
1000
+ let groups = this.$data.groups || [];
1001
+ if (this.$data.groups != null && group.$inArray(groups)) {
491
1002
  const newGroups = [];
492
- for (let curGroup of this.$data.groups) {
1003
+ for (let curGroup of groups) {
493
1004
  if (!group.$is(curGroup)) {
494
1005
  newGroups.push(curGroup);
495
1006
  }
@@ -499,7 +1010,13 @@ class User extends AbleObject_1.default {
499
1010
  }
500
1011
  return true;
501
1012
  }
502
- $inGroup(group) {
1013
+ /**
1014
+ * Check whether the user is in a (primary or secondary) group.
1015
+ *
1016
+ * @param group The group, or the group's GUID.
1017
+ * @returns True or false.
1018
+ */
1019
+ async $inGroup(group) {
503
1020
  if (typeof group === 'string') {
504
1021
  if (this.$data.group?.guid === group) {
505
1022
  return true;
@@ -513,35 +1030,66 @@ class User extends AbleObject_1.default {
513
1030
  }
514
1031
  return (group.$is(this.$data.group) || group.$inArray(this.$data.groups ?? []));
515
1032
  }
516
- $isDescendant(group) {
517
- if (this.$data.group?.cdate != null &&
518
- this.$data.group.$isDescendant(group)) {
1033
+ /**
1034
+ * Check whether the user is a descendant of a group.
1035
+ *
1036
+ * @param group The group, or the group's GUID.
1037
+ * @returns True or false.
1038
+ */
1039
+ async $isDescendant(group) {
1040
+ // Check to see if the user is in a descendant group of the given group.
1041
+ await this.$data.group?.$wake();
1042
+ if (this.$data.group != null &&
1043
+ this.$data.group?.cdate != null &&
1044
+ (await this.$data.group?.$isDescendant(group))) {
519
1045
  return true;
520
1046
  }
1047
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
521
1048
  for (let curGroup of this.$data.groups ?? []) {
522
- if (curGroup.$isDescendant(group)) {
1049
+ if (await curGroup.$isDescendant(group)) {
523
1050
  return true;
524
1051
  }
525
1052
  }
526
1053
  return false;
527
1054
  }
1055
+ /**
1056
+ * A frontend accessible method to change the user's password.
1057
+ *
1058
+ * @param data The input data from the client.
1059
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1060
+ */
528
1061
  async $changePassword(data) {
1062
+ this.$nymph.config.debugLog('tilmeld', `Password change request for "${this.$data.username}".`);
529
1063
  if (!('newPassword' in data) || !data.newPassword.length) {
530
1064
  return { result: false, message: 'Please specify a password.' };
531
1065
  }
532
1066
  if (!this.$checkPassword(data.currentPassword ?? '')) {
1067
+ this.$nymph.config.debugLog('tilmeld', `Incorrect password for change request for "${this.$data.username}".`);
533
1068
  return { result: false, message: 'Incorrect password.' };
534
1069
  }
535
1070
  this.$data.passwordTemp = data.newPassword;
1071
+ if (data.revokeCurrentTokens) {
1072
+ this.$data.revokeTokenDate = Date.now();
1073
+ }
536
1074
  if (await this.$save()) {
1075
+ if (data.revokeCurrentTokens) {
1076
+ const tilmeld = enforceTilmeld(this);
1077
+ await tilmeld.login(this, true);
1078
+ }
537
1079
  return { result: true, message: 'Your password has been changed.' };
538
1080
  }
539
1081
  else {
540
1082
  return { result: false, message: "Couldn't save new password." };
541
1083
  }
542
1084
  }
1085
+ /**
1086
+ * Change the user's password.
1087
+ *
1088
+ * @param password The new password.
1089
+ * @returns The resulting password or hash which is stored in the entity.
1090
+ */
543
1091
  $password(password) {
544
- const tilmeld = this.$nymph.tilmeld;
1092
+ const tilmeld = enforceTilmeld(this);
545
1093
  switch (tilmeld.config.pwMethod) {
546
1094
  case 'plain':
547
1095
  delete this.$data.salt;
@@ -549,24 +1097,221 @@ class User extends AbleObject_1.default {
549
1097
  break;
550
1098
  case 'digest':
551
1099
  delete this.$data.salt;
552
- this.$data.password = (0, sha256_1.default)(password).toString(enc_base64_1.default);
1100
+ this.$data.password = crypto
1101
+ .createHash('sha256')
1102
+ .update(password)
1103
+ .digest('base64');
553
1104
  break;
554
1105
  case 'salt':
555
1106
  default:
556
- this.$data.salt = (0, guid_1.nanoid)();
557
- this.$data.password = (0, sha256_1.default)(password + this.$data.salt).toString(enc_base64_1.default);
1107
+ this.$data.salt = nanoid();
1108
+ this.$data.password = crypto
1109
+ .createHash('sha256')
1110
+ .update(password + this.$data.salt)
1111
+ .digest('base64');
558
1112
  break;
559
1113
  }
560
1114
  return this.$data.password;
561
1115
  }
1116
+ /**
1117
+ * A frontend accessible method to revoke all currently issued tokens.
1118
+ *
1119
+ * @param data The input data from the client.
1120
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1121
+ */
1122
+ async $revokeCurrentTokens(data) {
1123
+ if (!this.$checkPassword(data.password ?? '')) {
1124
+ return { result: false, message: 'Incorrect password.' };
1125
+ }
1126
+ this.$data.revokeTokenDate = Date.now();
1127
+ if (await this.$save()) {
1128
+ const tilmeld = enforceTilmeld(this);
1129
+ await tilmeld.login(this, true);
1130
+ return {
1131
+ result: true,
1132
+ message: 'You have logged out of all other sessions.',
1133
+ };
1134
+ }
1135
+ else {
1136
+ return { result: false, message: "Couldn't save revocation date." };
1137
+ }
1138
+ }
1139
+ async $hasTOTPSecret() {
1140
+ return this.$data.totpSecret != null;
1141
+ }
1142
+ /**
1143
+ * Check the given code against the user's TOTP secret.
1144
+ *
1145
+ * @param code The code in question.
1146
+ * @returns True if the code is valid, otherwise false.
1147
+ */
1148
+ $checkTOTPCode(code) {
1149
+ if (this.$data.totpSecret == null) {
1150
+ return false;
1151
+ }
1152
+ const tilmeld = enforceTilmeld(this);
1153
+ const totp = new TOTP({
1154
+ issuer: tilmeld.config.appName,
1155
+ label: this.$data.username,
1156
+ algorithm: 'SHA1',
1157
+ digits: 6,
1158
+ period: 30,
1159
+ secret: this.$data.totpSecret,
1160
+ });
1161
+ return totp.validate({ token: code, window: 2 }) != null;
1162
+ }
1163
+ /**
1164
+ * A frontend accessible method to generate a new TOTP secret.
1165
+ *
1166
+ * @returns An object with 'uri', 'qrcode', and 'secret'.
1167
+ */
1168
+ async $getNewTOTPSecret() {
1169
+ const tilmeld = enforceTilmeld(this);
1170
+ if (this.$data.totpSecret != null) {
1171
+ throw new BadDataError('You already have a 2FA secret.');
1172
+ }
1173
+ const secret = new Secret();
1174
+ const totp = new TOTP({
1175
+ issuer: tilmeld.config.appName,
1176
+ label: this.$data.username,
1177
+ algorithm: 'SHA1',
1178
+ digits: 6,
1179
+ period: 30,
1180
+ secret,
1181
+ });
1182
+ const uri = totp.toString();
1183
+ const qrcode = await toDataURL(uri);
1184
+ return {
1185
+ uri,
1186
+ qrcode,
1187
+ secret: secret.base32,
1188
+ };
1189
+ }
1190
+ /**
1191
+ * A frontend accessible method to save a TOTP secret into the user's account.
1192
+ *
1193
+ * @param data The input data from the client.
1194
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1195
+ */
1196
+ async $saveTOTPSecret(data) {
1197
+ if (!this.$checkPassword(data.password ?? '')) {
1198
+ return { result: false, message: 'Incorrect password.' };
1199
+ }
1200
+ if (this.$data.totpSecret != null) {
1201
+ return { result: false, message: 'You already have a 2FA secret.' };
1202
+ }
1203
+ if (data.secret == null ||
1204
+ typeof data.secret !== 'string' ||
1205
+ data.secret === '') {
1206
+ return { result: false, message: '2FA secret is invalid.' };
1207
+ }
1208
+ if (data.code == null ||
1209
+ typeof data.code !== 'string' ||
1210
+ data.code.length !== 6) {
1211
+ return { result: false, message: '2FA code is invalid.' };
1212
+ }
1213
+ const tilmeld = enforceTilmeld(this);
1214
+ const totp = new TOTP({
1215
+ issuer: tilmeld.config.appName,
1216
+ label: this.$data.username,
1217
+ algorithm: 'SHA1',
1218
+ digits: 6,
1219
+ period: 30,
1220
+ secret: data.secret,
1221
+ });
1222
+ if (totp.validate({ token: data.code, window: 2 }) == null) {
1223
+ return { result: false, message: '2FA code is incorrect.' };
1224
+ }
1225
+ this.$data.totpSecret = data.secret;
1226
+ if (await this.$save()) {
1227
+ return { result: true, message: 'Your two factor secret is now set.' };
1228
+ }
1229
+ else {
1230
+ return { result: false, message: "Couldn't save two factor secret." };
1231
+ }
1232
+ }
1233
+ /**
1234
+ * A frontend accessible method to remove the TOTP secret from the user's
1235
+ * account.
1236
+ *
1237
+ * @param data The input data from the client.
1238
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1239
+ */
1240
+ async $removeTOTPSecret(data) {
1241
+ const tilmeld = enforceTilmeld(this);
1242
+ if (data) {
1243
+ if (!this.$checkPassword(data.password ?? '')) {
1244
+ return { result: false, message: 'Incorrect password.' };
1245
+ }
1246
+ if (data.code == null ||
1247
+ typeof data.code !== 'string' ||
1248
+ data.code.length !== 6) {
1249
+ return { result: false, message: '2FA code is invalid.' };
1250
+ }
1251
+ if (this.$data.totpSecret == null) {
1252
+ return { result: false, message: "You don't have a 2FA secret." };
1253
+ }
1254
+ const totp = new TOTP({
1255
+ issuer: tilmeld.config.appName,
1256
+ label: this.$data.username,
1257
+ algorithm: 'SHA1',
1258
+ digits: 6,
1259
+ period: 30,
1260
+ secret: this.$data.totpSecret,
1261
+ });
1262
+ if (totp.validate({ token: data.code, window: 2 }) == null) {
1263
+ return { result: false, message: '2FA code is incorrect.' };
1264
+ }
1265
+ }
1266
+ else if (!tilmeld.gatekeeper('tilmeld/admin')) {
1267
+ return {
1268
+ result: false,
1269
+ message: 'You must provide your password and 2FA code.',
1270
+ };
1271
+ }
1272
+ delete this.$data.totpSecret;
1273
+ if (await this.$save()) {
1274
+ return {
1275
+ result: true,
1276
+ message: 'Two factor secret has been removed.',
1277
+ };
1278
+ }
1279
+ else {
1280
+ return { result: false, message: "Couldn't remove two factor secret." };
1281
+ }
1282
+ }
1283
+ /**
1284
+ * Check that a username is valid.
1285
+ *
1286
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1287
+ */
562
1288
  async $checkUsername() {
563
- const tilmeld = this.$nymph.tilmeld;
1289
+ const tilmeld = enforceTilmeld(this);
1290
+ this.$nymph.config.debugInfo('tilmeld', `Username check "${this.$data.username}".`);
564
1291
  if (!tilmeld.config.emailUsernames) {
565
- if (this.$data.username == null || !this.$data.username.length) {
1292
+ let [username, domain] = tilmeld.config.domainSupport
1293
+ ? splitn(this.$data.username ?? '', '@', 2)
1294
+ : [this.$data.username, null];
1295
+ // Lowercase the domain part.
1296
+ if (domain) {
1297
+ domain = domain.toLowerCase();
1298
+ this.$data.username = `${username}@${domain}`;
1299
+ }
1300
+ if (this.$data.username == null ||
1301
+ !this.$data.username.length ||
1302
+ username == null ||
1303
+ !username.length) {
566
1304
  return { result: false, message: 'Please specify a username.' };
567
1305
  }
568
- if (tilmeld.config.maxUsernameLength > 0 &&
569
- this.$data.username.length > tilmeld.config.maxUsernameLength) {
1306
+ if (username.length < tilmeld.config.minUsernameLength) {
1307
+ return {
1308
+ result: false,
1309
+ message: 'Usernames must be at least ' +
1310
+ tilmeld.config.minUsernameLength +
1311
+ ' characters.',
1312
+ };
1313
+ }
1314
+ if (username.length > tilmeld.config.maxUsernameLength) {
570
1315
  return {
571
1316
  result: false,
572
1317
  message: 'Usernames must not exceed ' +
@@ -574,18 +1319,34 @@ class User extends AbleObject_1.default {
574
1319
  ' characters.',
575
1320
  };
576
1321
  }
577
- if ((0, lodash_1.difference)(this.$data.username.split(''), tilmeld.config.validChars.split('')).length) {
1322
+ if (difference(username.split(''), tilmeld.config.validChars.split(''))
1323
+ .length) {
578
1324
  return {
579
1325
  result: false,
580
1326
  message: tilmeld.config.validCharsNotice,
581
1327
  };
582
1328
  }
583
- if (!tilmeld.config.validRegex.test(this.$data.username)) {
1329
+ if (!tilmeld.config.validRegex.test(username)) {
584
1330
  return {
585
1331
  result: false,
586
1332
  message: tilmeld.config.validRegexNotice,
587
1333
  };
588
1334
  }
1335
+ if (domain != null) {
1336
+ if (!tilmeld.config.validDomainRegex.test(domain)) {
1337
+ return {
1338
+ result: false,
1339
+ message: tilmeld.config.validDomainRegexNotice,
1340
+ };
1341
+ }
1342
+ if (!this.$is(tilmeld.currentUser) &&
1343
+ !tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
1344
+ return {
1345
+ result: false,
1346
+ message: "You don't have the authority to manage users on this domain.",
1347
+ };
1348
+ }
1349
+ }
589
1350
  const selector = {
590
1351
  type: '&',
591
1352
  ilike: [
@@ -600,6 +1361,17 @@ class User extends AbleObject_1.default {
600
1361
  if (test != null) {
601
1362
  return { result: false, message: 'That username is taken.' };
602
1363
  }
1364
+ for (let callback of this.constructor
1365
+ .checkUsernameCallbacks) {
1366
+ if (callback) {
1367
+ const result = await callback(this, {
1368
+ username: this.$data.username,
1369
+ });
1370
+ if (!result.result) {
1371
+ return { message: 'Username is not available.', ...result };
1372
+ }
1373
+ }
1374
+ }
603
1375
  return {
604
1376
  result: true,
605
1377
  message: this.guid != null ? 'Username is valid.' : 'Username is available!',
@@ -610,8 +1382,15 @@ class User extends AbleObject_1.default {
610
1382
  if (this.$data.username == null || !this.$data.username.length) {
611
1383
  return { result: false, message: 'Please specify an email.' };
612
1384
  }
613
- if (tilmeld.config.maxUsernameLength > 0 &&
614
- this.$data.username.length > tilmeld.config.maxUsernameLength) {
1385
+ if (this.$data.username.length < tilmeld.config.minUsernameLength) {
1386
+ return {
1387
+ result: false,
1388
+ message: 'Emails must be at least ' +
1389
+ tilmeld.config.minUsernameLength +
1390
+ ' characters.',
1391
+ };
1392
+ }
1393
+ if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
615
1394
  return {
616
1395
  result: false,
617
1396
  message: 'Emails must not exceed ' +
@@ -622,8 +1401,14 @@ class User extends AbleObject_1.default {
622
1401
  return await this.$checkEmail();
623
1402
  }
624
1403
  }
1404
+ /**
1405
+ * Check that an email is unique.
1406
+ *
1407
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1408
+ */
625
1409
  async $checkEmail() {
626
- const tilmeld = this.$nymph.tilmeld;
1410
+ const tilmeld = enforceTilmeld(this);
1411
+ this.$nymph.config.debugInfo('tilmeld', `Email check "${this.$data.email}".`);
627
1412
  if (this.$data.email == null || !this.$data.email.length) {
628
1413
  if (tilmeld.config.verifyEmail) {
629
1414
  return { result: false, message: 'Please specify an email.' };
@@ -657,8 +1442,14 @@ class User extends AbleObject_1.default {
657
1442
  message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
658
1443
  };
659
1444
  }
1445
+ /**
1446
+ * Check that a phone number is unique.
1447
+ *
1448
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1449
+ */
660
1450
  async $checkPhone() {
661
- const tilmeld = this.$nymph.tilmeld;
1451
+ const tilmeld = enforceTilmeld(this);
1452
+ this.$nymph.config.debugInfo('tilmeld', `Phone check "${this.$data.phone}".`);
662
1453
  if (this.$data.phone == null || !this.$data.phone.length) {
663
1454
  return { result: false, message: 'Please specify a phone number.' };
664
1455
  }
@@ -686,7 +1477,7 @@ class User extends AbleObject_1.default {
686
1477
  };
687
1478
  }
688
1479
  async $register(data) {
689
- let tilmeld = this.$nymph.tilmeld;
1480
+ let tilmeld = enforceTilmeld(this);
690
1481
  if (!tilmeld.config.allowRegistration) {
691
1482
  return {
692
1483
  result: false,
@@ -701,21 +1492,6 @@ class User extends AbleObject_1.default {
701
1492
  message: 'This is already a registered user.',
702
1493
  };
703
1494
  }
704
- try {
705
- for (let callback of this.constructor
706
- .beforeRegisterCallbacks) {
707
- if (callback) {
708
- await callback(this, data);
709
- }
710
- }
711
- }
712
- catch (e) {
713
- return {
714
- result: false,
715
- loggedin: false,
716
- message: e.message,
717
- };
718
- }
719
1495
  if (!('password' in data) || !data.password.length) {
720
1496
  return {
721
1497
  result: false,
@@ -728,7 +1504,7 @@ class User extends AbleObject_1.default {
728
1504
  return { ...unCheck, loggedin: false };
729
1505
  }
730
1506
  this.$password(data.password);
731
- if (tilmeld.config.regFields.indexOf('name') !== -1) {
1507
+ if (tilmeld.config.regFields.includes('name')) {
732
1508
  this.$data.name =
733
1509
  this.$data.nameFirst +
734
1510
  (this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
@@ -740,59 +1516,35 @@ class User extends AbleObject_1.default {
740
1516
  if (tilmeld.config.emailUsernames) {
741
1517
  this.$data.email = this.$data.username;
742
1518
  }
743
- const transaction = 'tilmeld-register-' + (0, guid_1.nanoid)();
1519
+ // Start transaction.
1520
+ const transaction = 'tilmeld-register-' + nanoid();
744
1521
  const nymph = this.$nymph;
745
1522
  const tnymph = await this.$nymph.startTransaction(transaction);
746
- this.$nymph = tnymph;
747
- tilmeld = this.$nymph.tilmeld;
1523
+ this.$setNymph(tnymph);
1524
+ tilmeld = enforceTilmeld(this);
1525
+ let message = '';
1526
+ let loggedin = false;
748
1527
  try {
749
- let generatedPrimaryGroup = null;
750
- if (tilmeld.config.generatePrimary) {
751
- generatedPrimaryGroup = await tilmeld.Group.factory();
752
- generatedPrimaryGroup.groupname = this.$data.username;
753
- generatedPrimaryGroup.avatar = this.$data.avatar;
754
- generatedPrimaryGroup.name = this.$data.name;
755
- generatedPrimaryGroup.email = this.$data.email;
756
- const parent = await tnymph.getEntity({ class: tilmeld.Group }, {
757
- type: '&',
758
- equal: ['defaultPrimary', true],
759
- });
760
- if (parent != null) {
761
- generatedPrimaryGroup.parent = parent;
762
- }
763
- if (!(await generatedPrimaryGroup.$saveSkipAC())) {
764
- await tnymph.rollback(transaction);
765
- this.$nymph = nymph;
766
- return {
767
- result: false,
768
- loggedin: false,
769
- message: 'Error creating primary group for user.',
770
- };
771
- }
772
- this.$data.group = generatedPrimaryGroup;
773
- }
774
- else {
775
- const group = await tnymph.getEntity({ class: tilmeld.Group }, {
776
- type: '&',
777
- equal: ['defaultPrimary', true],
778
- });
779
- if (group != null) {
780
- this.$data.group = group;
1528
+ try {
1529
+ for (let callback of this.constructor
1530
+ .beforeRegisterCallbacks) {
1531
+ if (callback) {
1532
+ await callback(this, data);
1533
+ }
781
1534
  }
782
1535
  }
783
- if (tilmeld.config.verifyEmail && tilmeld.config.unverifiedAccess) {
784
- this.$data.groups = await tnymph.getEntities({ class: tilmeld.Group }, {
785
- type: '&',
786
- equal: ['unverifiedSecondary', true],
787
- });
788
- }
789
- else {
790
- this.$data.groups = await tnymph.getEntities({ class: tilmeld.Group }, {
791
- type: '&',
792
- equal: ['defaultSecondary', true],
793
- });
1536
+ catch (e) {
1537
+ await tnymph.rollback(transaction);
1538
+ this.$setNymph(nymph);
1539
+ return {
1540
+ result: false,
1541
+ loggedin: false,
1542
+ message: e.message,
1543
+ };
794
1544
  }
795
- if (tilmeld.config.verifyEmail) {
1545
+ if (tilmeld.config.userFields.includes('email') &&
1546
+ tilmeld.config.verifyEmail) {
1547
+ // The user will be enabled after verifying their e-mail address.
796
1548
  if (!tilmeld.config.unverifiedAccess) {
797
1549
  this.$data.enabled = false;
798
1550
  }
@@ -800,6 +1552,8 @@ class User extends AbleObject_1.default {
800
1552
  else {
801
1553
  this.$data.enabled = true;
802
1554
  }
1555
+ // If createAdmin is true and there are no other users, grant
1556
+ // "system/admin".
803
1557
  let madeAdmin = false;
804
1558
  if (tilmeld.config.createAdmin) {
805
1559
  const otherUsers = await tnymph.getEntities({
@@ -808,13 +1562,17 @@ class User extends AbleObject_1.default {
808
1562
  limit: 1,
809
1563
  return: 'guid',
810
1564
  });
1565
+ // Make sure it's not just null, cause that means an error.
811
1566
  if (!otherUsers.length) {
812
1567
  this.$grant('system/admin');
813
1568
  this.$data.enabled = true;
814
1569
  madeAdmin = true;
815
1570
  }
816
1571
  }
1572
+ this.$nymph.config.debugLog('tilmeld', `Registering new user "${this.$data.username}".`);
817
1573
  if (await this.$saveSkipAC()) {
1574
+ this.$nymph.config.debugLog('tilmeld', `New user registered "${this.$data.username}".`);
1575
+ // Send the new user registered email.
818
1576
  if (tilmeld.config.userRegisteredRecipient != null) {
819
1577
  await tilmeld.config.sendEmail(tilmeld, {
820
1578
  template: 'UserRegistered',
@@ -831,31 +1589,20 @@ class User extends AbleObject_1.default {
831
1589
  },
832
1590
  }, this);
833
1591
  }
834
- let message = '';
835
- let loggedin = false;
836
- if (generatedPrimaryGroup != null) {
837
- generatedPrimaryGroup.user = this;
838
- if (!(await generatedPrimaryGroup.$saveSkipAC())) {
839
- await tnymph.rollback(transaction);
840
- this.$nymph = nymph;
841
- return {
842
- result: false,
843
- loggedin: false,
844
- message: 'Your primary group could not be assigned. Please try again later.',
845
- };
846
- }
847
- }
1592
+ // Finish up.
848
1593
  if (tilmeld.config.verifyEmail &&
849
1594
  !tilmeld.config.unverifiedAccess &&
850
- !madeAdmin) {
1595
+ !madeAdmin &&
1596
+ tilmeld.config.userFields.includes('email')) {
851
1597
  message +=
852
1598
  `Almost there. An email has been sent to ${this.$data.email} ` +
853
1599
  'with a verification link for you to finish registration.';
854
1600
  }
855
1601
  else if (tilmeld.config.verifyEmail &&
856
1602
  tilmeld.config.unverifiedAccess &&
857
- !madeAdmin) {
858
- if (!tilmeld.login(this, true)) {
1603
+ !madeAdmin &&
1604
+ tilmeld.config.userFields.includes('email')) {
1605
+ if (!(await tilmeld.login(this, true))) {
859
1606
  throw new Error('An error occurred trying to log you in.');
860
1607
  }
861
1608
  message +=
@@ -865,7 +1612,7 @@ class User extends AbleObject_1.default {
865
1612
  loggedin = true;
866
1613
  }
867
1614
  else {
868
- if (!tilmeld.login(this, true)) {
1615
+ if (!(await tilmeld.login(this, true))) {
869
1616
  throw new Error('An error occurred trying to log you in.');
870
1617
  }
871
1618
  message += "You're now registered and logged in!";
@@ -880,23 +1627,11 @@ class User extends AbleObject_1.default {
880
1627
  });
881
1628
  }
882
1629
  }
883
- try {
884
- await tnymph.commit(transaction);
885
- this.$nymph = nymph;
886
- this.$nymph.tilmeld?.fillSession(this);
887
- }
888
- catch (e) {
889
- throw e;
890
- }
891
- return {
892
- result: true,
893
- loggedin,
894
- message,
895
- };
896
1630
  }
897
1631
  else {
1632
+ this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}".`);
898
1633
  await tnymph.rollback(transaction);
899
- this.$nymph = nymph;
1634
+ this.$setNymph(nymph);
900
1635
  return {
901
1636
  result: false,
902
1637
  loggedin: false,
@@ -905,22 +1640,69 @@ class User extends AbleObject_1.default {
905
1640
  }
906
1641
  }
907
1642
  catch (e) {
1643
+ this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}": ${e}`);
908
1644
  await tnymph.rollback(transaction);
909
- this.$nymph = nymph;
1645
+ this.$setNymph(nymph);
1646
+ throw e;
1647
+ }
1648
+ try {
1649
+ await tnymph.commit(transaction);
1650
+ this.$setNymph(nymph);
1651
+ await this.$nymph.tilmeld?.fillSession(this);
1652
+ }
1653
+ catch (e) {
910
1654
  throw e;
911
1655
  }
1656
+ return {
1657
+ result: true,
1658
+ loggedin,
1659
+ message,
1660
+ };
912
1661
  }
913
1662
  async $save() {
914
- let tilmeld = this.$nymph.tilmeld;
1663
+ let tilmeld = enforceTilmeld(this);
915
1664
  if (this.$data.username == null || !this.$data.username.trim().length) {
916
1665
  return false;
917
1666
  }
918
- if (tilmeld.gatekeeper('tilmeld/admin') &&
1667
+ if (!this.$skipAcWhenSaving &&
1668
+ tilmeld.gatekeeper('tilmeld/admin') &&
919
1669
  !tilmeld.gatekeeper('system/admin') &&
920
- this.$gatekeeper('system/admin')) {
921
- throw new errors_1.BadDataError("You don't have the authority to modify system admins.");
1670
+ this.$data.abilities?.includes('system/admin')) {
1671
+ throw new AccessControlError("You don't have the authority to modify system admins.");
1672
+ }
1673
+ // Lowercase the domain part.
1674
+ if (tilmeld.config.domainSupport) {
1675
+ const [username, domain] = splitn(this.$data.username ?? '', '@', 2);
1676
+ if (domain) {
1677
+ this.$data.username = `${username}@${domain.toLowerCase()}`;
1678
+ }
1679
+ }
1680
+ if (tilmeld.config.domainSupport) {
1681
+ const [_username, domain] = splitn(this.$data.username ?? '', '@', 2);
1682
+ const [_originalUsername, originalDomain] = splitn(this.$originalUsername ?? '', '@', 2);
1683
+ if (this.guid != null &&
1684
+ ((originalDomain == null && domain != null) ||
1685
+ (domain == null && originalDomain != null))) {
1686
+ throw new AccessControlError("Users can't switch to or from having a domain once created.");
1687
+ }
1688
+ if (!this.$skipAcWhenSaving &&
1689
+ !tilmeld.gatekeeper('tilmeld/admin') &&
1690
+ !tilmeld.gatekeeper('system/admin')) {
1691
+ if (domain != null &&
1692
+ !this.$is(tilmeld.currentUser) &&
1693
+ !tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
1694
+ throw new AccessControlError("You don't have the authority to modify users on this domain.");
1695
+ }
1696
+ if (originalDomain != null &&
1697
+ originalDomain !== domain &&
1698
+ (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
1699
+ !tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))) {
1700
+ throw new AccessControlError("You don't have the authority to modify users on this domain.");
1701
+ }
1702
+ }
922
1703
  }
923
1704
  let sendVerification = false;
1705
+ // Formatting.
924
1706
  this.$data.username = this.$data.username.trim();
925
1707
  if (tilmeld.config.emailUsernames) {
926
1708
  this.$data.email = this.$data.username;
@@ -934,49 +1716,63 @@ class User extends AbleObject_1.default {
934
1716
  (this.$data.nameMiddle ? ' ' + this.$data.nameMiddle : '') +
935
1717
  (this.$data.nameLast ? ' ' + this.$data.nameLast : '');
936
1718
  this.$data.enabled = !!this.$data.enabled;
937
- if (this.$data.nameFirst === '') {
1719
+ // Clear empty values.
1720
+ if (this.$data.nameFirst === '' ||
1721
+ !tilmeld.config.userFields.includes('name')) {
938
1722
  delete this.$data.nameFirst;
939
1723
  }
940
- if (this.$data.nameMiddle === '') {
1724
+ if (this.$data.nameMiddle === '' ||
1725
+ !tilmeld.config.userFields.includes('name')) {
941
1726
  delete this.$data.nameMiddle;
942
1727
  }
943
- if (this.$data.nameLast === '') {
1728
+ if (this.$data.nameLast === '' ||
1729
+ !tilmeld.config.userFields.includes('name')) {
944
1730
  delete this.$data.nameLast;
945
1731
  }
946
- if (this.$data.name === '') {
1732
+ if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
947
1733
  delete this.$data.name;
948
1734
  }
949
- if (this.$data.email === '') {
1735
+ if (this.$data.email === '' ||
1736
+ !tilmeld.config.userFields.includes('email')) {
950
1737
  delete this.$data.email;
951
1738
  }
952
1739
  if (this.$data.avatar === '') {
953
1740
  delete this.$data.avatar;
954
1741
  }
955
- if (this.$data.phone === '') {
1742
+ if (this.$data.phone === '' ||
1743
+ !tilmeld.config.userFields.includes('phone')) {
956
1744
  delete this.$data.phone;
957
1745
  }
958
- if (this.$data.secret === '') {
1746
+ if (this.$data.secret === '' ||
1747
+ !tilmeld.config.userFields.includes('email')) {
959
1748
  delete this.$data.secret;
960
1749
  }
961
- if (this.$data.emailChangeDate === 0) {
1750
+ if (this.$data.emailChangeDate === 0 ||
1751
+ !tilmeld.config.userFields.includes('email')) {
962
1752
  delete this.$data.emailChangeDate;
963
1753
  }
964
- if (this.$data.newEmailSecret === '') {
1754
+ if (this.$data.newEmailSecret === '' ||
1755
+ !tilmeld.config.userFields.includes('email')) {
965
1756
  delete this.$data.newEmailSecret;
966
1757
  }
967
- if (this.$data.newEmailAddress === '') {
1758
+ if (this.$data.newEmailAddress === '' ||
1759
+ !tilmeld.config.userFields.includes('email')) {
968
1760
  delete this.$data.newEmailAddress;
969
1761
  }
970
- if (this.$data.cancelEmailSecret === '') {
1762
+ if (this.$data.cancelEmailSecret === '' ||
1763
+ !tilmeld.config.userFields.includes('email')) {
971
1764
  delete this.$data.cancelEmailSecret;
972
1765
  }
973
- if (this.$data.cancelEmailAddress === '') {
1766
+ if (this.$data.cancelEmailAddress === '' ||
1767
+ !tilmeld.config.userFields.includes('email')) {
974
1768
  delete this.$data.cancelEmailAddress;
975
1769
  }
976
- if (this.$data.recoverSecret === '') {
1770
+ if (this.$data.recoverSecret === '' ||
1771
+ !tilmeld.config.userFields.includes('email')) {
977
1772
  delete this.$data.recoverSecret;
978
1773
  }
979
- if (this.$data.recoverSecretDate === 0) {
1774
+ if (this.$data.recoverSecretDate === 0 ||
1775
+ !tilmeld.config.userFields.includes('email')) {
980
1776
  delete this.$data.recoverSecretDate;
981
1777
  }
982
1778
  if (this.$data.password === '') {
@@ -988,50 +1784,65 @@ class User extends AbleObject_1.default {
988
1784
  if (this.$data.inheritAbilities === false) {
989
1785
  delete this.$data.inheritAbilities;
990
1786
  }
1787
+ // Verification.
991
1788
  const unCheck = await this.$checkUsername();
992
1789
  if (!unCheck.result) {
993
- throw new errors_1.BadUsernameError(unCheck.message);
1790
+ throw new BadUsernameError(unCheck.message);
994
1791
  }
995
- if (!tilmeld.config.emailUsernames) {
1792
+ if (!tilmeld.config.emailUsernames &&
1793
+ tilmeld.config.userFields.includes('email')) {
996
1794
  const emCheck = await this.$checkEmail();
997
1795
  if (!emCheck.result) {
998
- throw new errors_1.BadEmailError(emCheck.message);
1796
+ throw new BadEmailError(emCheck.message);
999
1797
  }
1000
1798
  }
1001
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
1799
+ // Email changes.
1800
+ if (!tilmeld.gatekeeper('tilmeld/admin') &&
1801
+ tilmeld.config.userFields.includes('email')) {
1802
+ // The user isn't an admin, so email address changes should contain some
1803
+ // security measures.
1002
1804
  if (tilmeld.config.verifyEmail) {
1805
+ // The user needs to verify this new email address.
1003
1806
  if (this.guid == null) {
1004
- if ((this.$data.abilities?.indexOf('system/admin') ?? -1) === -1) {
1005
- this.$data.secret = (0, guid_1.nanoid)();
1807
+ // If this is the first user, they'll be made an admin, so they don't
1808
+ // need to verify.
1809
+ if (!this.$data.abilities?.includes('system/admin')) {
1810
+ this.$data.secret = nanoid();
1006
1811
  sendVerification = true;
1007
1812
  }
1008
1813
  }
1009
1814
  else if (this.$originalEmail != null &&
1010
1815
  this.$originalEmail !== this.$data.email) {
1816
+ // The user already has an old email address.
1011
1817
  if (tilmeld.config.emailRateLimit !== '' &&
1012
1818
  this.$data.emailChangeDate != null &&
1013
1819
  this.$data.emailChangeDate >=
1014
- (0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000) {
1015
- throw new errors_1.EmailChangeRateLimitExceededError('You already changed your email address recently. Please wait until ' +
1016
- new Date((0, strtotime_1.default)('+' + tilmeld.config.emailRateLimit, Math.floor(this.$data.emailChangeDate / 1000)) * 1000).toString() +
1820
+ strtotime('-' + tilmeld.config.emailRateLimit) * 1000) {
1821
+ throw new EmailChangeRateLimitExceededError('You already changed your email address recently. Please wait until ' +
1822
+ new Date(strtotime('+' + tilmeld.config.emailRateLimit, Math.floor(this.$data.emailChangeDate / 1000)) * 1000).toString() +
1017
1823
  ' to change your email address again.');
1018
1824
  }
1019
1825
  else {
1020
1826
  if (this.$data.newEmailSecret == null &&
1021
1827
  tilmeld.config.emailRateLimit !== '' &&
1828
+ // Make sure the user has at least the rate limit time to cancel
1829
+ // an email change.
1022
1830
  (this.$data.emailChangeDate == null ||
1023
1831
  this.$data.emailChangeDate <
1024
- (0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
1832
+ strtotime('-' + tilmeld.config.emailRateLimit) * 1000)) {
1833
+ // Save the old email in case the cancel change link is clicked.
1025
1834
  this.$data.cancelEmailAddress = this.$originalEmail;
1026
- this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
1835
+ this.$data.cancelEmailSecret = nanoid();
1027
1836
  this.$data.emailChangeDate = Date.now();
1028
1837
  }
1838
+ // Save the new address and reset to the old one (until they verify
1839
+ // it).
1029
1840
  this.$data.newEmailAddress = this.$data.email;
1030
1841
  this.$data.email = this.$originalEmail;
1031
1842
  if (tilmeld.config.emailUsernames) {
1032
1843
  this.$data.username = this.$data.email;
1033
1844
  }
1034
- this.$data.newEmailSecret = (0, guid_1.nanoid)();
1845
+ this.$data.newEmailSecret = nanoid();
1035
1846
  sendVerification = true;
1036
1847
  }
1037
1848
  }
@@ -1039,11 +1850,15 @@ class User extends AbleObject_1.default {
1039
1850
  else if (this.guid != null &&
1040
1851
  this.$originalEmail != null &&
1041
1852
  this.$originalEmail !== this.$data.email &&
1853
+ // Make sure the user has at least the rate limit time to cancel an
1854
+ // email change.
1042
1855
  (this.$data.emailChangeDate == null ||
1043
1856
  this.$data.emailChangeDate <
1044
- (0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
1857
+ strtotime('-' + tilmeld.config.emailRateLimit) * 1000)) {
1858
+ // The user doesn't need to verify their new email address, but should
1859
+ // be able to cancel the email change from their old address.
1045
1860
  this.$data.cancelEmailAddress = this.$originalEmail;
1046
- this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
1861
+ this.$data.cancelEmailSecret = nanoid();
1047
1862
  sendVerification = true;
1048
1863
  }
1049
1864
  }
@@ -1052,72 +1867,178 @@ class User extends AbleObject_1.default {
1052
1867
  }
1053
1868
  delete this.$data.passwordTemp;
1054
1869
  if (this.$data.password == null) {
1055
- throw new errors_1.BadDataError('A password is required.');
1056
- }
1057
- try {
1058
- tilmeld.config.validatorUser(this);
1059
- }
1060
- catch (e) {
1061
- throw new errors_1.BadDataError(e?.message);
1870
+ throw new BadDataError('A password is required.');
1062
1871
  }
1063
- const transaction = 'tilmeld-save-' + (0, guid_1.nanoid)();
1872
+ // Start transaction.
1873
+ const transaction = 'tilmeld-save-user-' + (this.guid || nanoid());
1064
1874
  const nymph = this.$nymph;
1065
1875
  const tnymph = await this.$nymph.startTransaction(transaction);
1066
- this.$nymph = tnymph;
1067
- tilmeld = this.$nymph.tilmeld;
1068
- if (this.$data.group != null &&
1069
- this.$data.group.user != null &&
1070
- this.$is(this.$data.group.user)) {
1876
+ this.$setNymph(tnymph);
1877
+ tilmeld = enforceTilmeld(this);
1878
+ const Group = tnymph.getEntityClass(GroupClass);
1879
+ try {
1880
+ let group = this.$data.group;
1881
+ await group?.$wake();
1882
+ if (group == null && this.guid == null) {
1883
+ if (tilmeld.config.generatePrimary) {
1884
+ // Generate a new primary group for the user.
1885
+ group = await Group.factory();
1886
+ group.user = this;
1887
+ const parent = await tnymph.getEntity({ class: Group }, {
1888
+ type: '&',
1889
+ equal: ['defaultPrimary', true],
1890
+ });
1891
+ if (parent != null) {
1892
+ group.parent = parent;
1893
+ }
1894
+ }
1895
+ else {
1896
+ // Add the default primary.
1897
+ const group = await tnymph.getEntity({ class: Group }, {
1898
+ type: '&',
1899
+ equal: ['defaultPrimary', true],
1900
+ });
1901
+ if (group != null) {
1902
+ this.$data.group = group;
1903
+ }
1904
+ }
1905
+ }
1906
+ if (group != null && group.user != null && this.$is(group.user)) {
1907
+ // Update the user's generated primary group.
1908
+ let changed = false;
1909
+ if (group.groupname !== this.$data.username) {
1910
+ group.groupname = this.$data.username;
1911
+ changed = true;
1912
+ }
1913
+ if (group.avatar !== this.$data.avatar) {
1914
+ group.avatar = this.$data.avatar;
1915
+ changed = true;
1916
+ }
1917
+ if (tilmeld.config.userFields.includes('email') &&
1918
+ group.email !== this.$data.email) {
1919
+ group.email = this.$data.email;
1920
+ changed = true;
1921
+ }
1922
+ if (tilmeld.config.userFields.includes('name') &&
1923
+ group.name !== this.$data.name) {
1924
+ group.name = this.$data.name;
1925
+ changed = true;
1926
+ }
1927
+ if (tilmeld.config.userFields.includes('phone') &&
1928
+ group.phone !== this.$data.phone) {
1929
+ group.phone = this.$data.phone;
1930
+ changed = true;
1931
+ }
1932
+ if (changed || group.guid == null) {
1933
+ if (!(await group.$saveSkipAC())) {
1934
+ throw Error('Error updating primary group for user.');
1935
+ }
1936
+ }
1937
+ this.$data.group = group;
1938
+ }
1939
+ if (this.$data.groups == null || this.$data.groups.length === 0) {
1940
+ // Add secondary groups.
1941
+ if (tilmeld.config.userFields.includes('email') &&
1942
+ tilmeld.config.verifyEmail &&
1943
+ tilmeld.config.unverifiedAccess &&
1944
+ this.$data.secret != null) {
1945
+ // Add the default secondaries for unverified users.
1946
+ this.$data.groups = await tnymph.getEntities({ class: Group }, {
1947
+ type: '&',
1948
+ equal: ['unverifiedSecondary', true],
1949
+ });
1950
+ }
1951
+ else {
1952
+ // Add the default secondaries.
1953
+ this.$data.groups = await tnymph.getEntities({ class: Group }, {
1954
+ type: '&',
1955
+ equal: ['defaultSecondary', true],
1956
+ });
1957
+ }
1958
+ }
1071
1959
  try {
1072
- this.$data.group.groupname = this.$data.username;
1073
- this.$data.group.avatar = this.$data.avatar;
1074
- this.$data.group.email = this.$data.email;
1075
- this.$data.group.name = this.$data.name;
1076
- this.$data.group.phone = this.$data.phone;
1077
- await this.$data.group.$saveSkipAC();
1960
+ tilmeld.config.validatorUser(tilmeld, this);
1078
1961
  }
1079
1962
  catch (e) {
1080
- await tnymph.rollback(transaction);
1081
- this.$nymph = nymph;
1082
- throw e;
1963
+ throw new BadDataError(e?.message);
1083
1964
  }
1084
1965
  }
1085
- let ret;
1966
+ catch (e) {
1967
+ await tnymph.rollback(transaction);
1968
+ this.$setNymph(nymph);
1969
+ throw e;
1970
+ }
1971
+ let ret = false;
1086
1972
  let preGuid = this.guid;
1087
1973
  let preCdate = this.cdate;
1088
1974
  let preMdate = this.mdate;
1089
1975
  try {
1976
+ for (let callback of this.constructor
1977
+ .beforeSaveCallbacks) {
1978
+ if (callback) {
1979
+ await callback(this);
1980
+ }
1981
+ }
1090
1982
  ret = await super.$save();
1091
1983
  }
1092
1984
  catch (e) {
1093
1985
  await tnymph.rollback(transaction);
1094
- this.$nymph = nymph;
1986
+ this.guid = preGuid;
1987
+ this.cdate = preCdate;
1988
+ this.mdate = preMdate;
1989
+ this.$setNymph(nymph);
1095
1990
  throw e;
1096
1991
  }
1097
1992
  if (ret) {
1098
1993
  if (sendVerification) {
1994
+ // The email has changed, so send a new verification email.
1099
1995
  if (!(await this.$sendEmailVerification())) {
1100
1996
  await tnymph.rollback(transaction);
1101
1997
  this.guid = preGuid;
1102
1998
  this.cdate = preCdate;
1103
1999
  this.mdate = preMdate;
1104
- this.$nymph = nymph;
2000
+ this.$setNymph(nymph);
1105
2001
  throw new Error("Couldn't send verification email.");
1106
2002
  }
1107
2003
  }
1108
2004
  this.$descendantGroups = undefined;
1109
2005
  this.$gatekeeperCache = undefined;
1110
- await tnymph.commit(transaction);
1111
- if (nymph.tilmeld.User.current(true).$is(this)) {
1112
- nymph.tilmeld.fillSession(this);
2006
+ try {
2007
+ for (let callback of this.constructor
2008
+ .afterSaveCallbacks) {
2009
+ if (callback) {
2010
+ await callback(this);
2011
+ }
2012
+ }
2013
+ }
2014
+ catch (e) {
2015
+ await tnymph.rollback(transaction);
2016
+ this.guid = preGuid;
2017
+ this.cdate = preCdate;
2018
+ this.mdate = preMdate;
2019
+ this.$setNymph(nymph);
2020
+ throw e;
1113
2021
  }
2022
+ ret = await tnymph.commit(transaction);
1114
2023
  }
1115
2024
  else {
1116
2025
  await tnymph.rollback(transaction);
1117
2026
  }
1118
- this.$nymph = nymph;
2027
+ this.$setNymph(nymph);
2028
+ if (ret) {
2029
+ this.$originalEmail = this.$data.email;
2030
+ this.$originalUsername = this.$data.username;
2031
+ const tilmeld = enforceTilmeld(nymph);
2032
+ if (tilmeld.User.current(true).$is(this)) {
2033
+ // Update the user in the session cache.
2034
+ await tilmeld.fillSession(this);
2035
+ }
2036
+ }
1119
2037
  return ret;
1120
2038
  }
2039
+ /**
2040
+ * This should *never* be accessible on the client.
2041
+ */
1121
2042
  async $saveSkipAC() {
1122
2043
  this.$skipAcWhenSaving = true;
1123
2044
  return await this.$save();
@@ -1130,24 +2051,96 @@ class User extends AbleObject_1.default {
1130
2051
  return false;
1131
2052
  }
1132
2053
  async $delete() {
1133
- const tilmeld = this.$nymph.tilmeld;
1134
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
1135
- throw new errors_1.BadDataError("You don't have the authority to delete users.");
2054
+ const tilmeld = enforceTilmeld(this);
2055
+ if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
2056
+ throw new AccessControlError("You don't have the authority to delete users.");
1136
2057
  }
1137
- if (!tilmeld.gatekeeper('system/admin') &&
1138
- this.$gatekeeper('system/admin')) {
1139
- throw new errors_1.BadDataError("You don't have the authority to delete system admins.");
2058
+ if (!this.$skipAcWhenDeleting &&
2059
+ !tilmeld.gatekeeper('system/admin') &&
2060
+ this.$data.abilities?.includes('system/admin')) {
2061
+ throw new AccessControlError("You don't have the authority to delete system admins.");
1140
2062
  }
1141
- if (tilmeld.User.current(true).$is(this)) {
1142
- await this.$logout();
2063
+ if (this.$data.username !== this.$originalUsername) {
2064
+ throw new AccessControlError("You can't delete a user while switching their username.");
1143
2065
  }
1144
- return await super.$delete();
2066
+ const transaction = 'tilmeld-delete-user-' + this.guid;
2067
+ const nymph = this.$nymph;
2068
+ const tnymph = await nymph.startTransaction(transaction);
2069
+ this.$setNymph(tnymph);
2070
+ let success = false;
2071
+ try {
2072
+ for (let callback of this.constructor
2073
+ .beforeDeleteCallbacks) {
2074
+ if (callback) {
2075
+ await callback(this);
2076
+ }
2077
+ }
2078
+ if (tilmeld.User.current(true).$is(this)) {
2079
+ await this.$logout();
2080
+ }
2081
+ if (this.$data.group != null && this.$is(this.$data.group.user)) {
2082
+ // Read the skip ac value here, because super.$delete changes it.
2083
+ const $skipAcWhenDeleting = this.$skipAcWhenDeleting;
2084
+ // Delete the user.
2085
+ success = await super.$delete();
2086
+ if (success) {
2087
+ // Delete the generated group.
2088
+ success = $skipAcWhenDeleting
2089
+ ? await this.$data.group.$deleteSkipAC()
2090
+ : await this.$data.group.$delete();
2091
+ }
2092
+ }
2093
+ else {
2094
+ success = await super.$delete();
2095
+ }
2096
+ }
2097
+ catch (e) {
2098
+ await tnymph.rollback(transaction);
2099
+ this.$setNymph(nymph);
2100
+ throw e;
2101
+ }
2102
+ if (success) {
2103
+ try {
2104
+ for (let callback of this.constructor
2105
+ .afterDeleteCallbacks) {
2106
+ if (callback) {
2107
+ await callback(this);
2108
+ }
2109
+ }
2110
+ }
2111
+ catch (e) {
2112
+ await tnymph.rollback(transaction);
2113
+ this.$setNymph(nymph);
2114
+ throw e;
2115
+ }
2116
+ success = await tnymph.commit(transaction);
2117
+ }
2118
+ else {
2119
+ await tnymph.rollback(transaction);
2120
+ }
2121
+ this.$setNymph(nymph);
2122
+ return success;
2123
+ }
2124
+ /*
2125
+ * This should *never* be accessible on the client.
2126
+ */
2127
+ async $deleteSkipAC() {
2128
+ this.$skipAcWhenDeleting = true;
2129
+ return await this.$delete();
2130
+ }
2131
+ $tilmeldDeleteSkipAC() {
2132
+ if (this.$skipAcWhenDeleting) {
2133
+ this.$skipAcWhenDeleting = false;
2134
+ return true;
2135
+ }
2136
+ return false;
1145
2137
  }
1146
2138
  static on(event, callback) {
1147
2139
  const prop = (event + 'Callbacks');
1148
2140
  if (!(prop in this)) {
1149
2141
  throw new Error('Invalid event type.');
1150
2142
  }
2143
+ // @ts-ignore: The callback should always be the right type here.
1151
2144
  this[prop].push(callback);
1152
2145
  return () => this.off(event, callback);
1153
2146
  }
@@ -1156,55 +2149,13 @@ class User extends AbleObject_1.default {
1156
2149
  if (!(prop in this)) {
1157
2150
  return false;
1158
2151
  }
2152
+ // @ts-ignore: The callback should always be the right type here.
1159
2153
  const i = this[prop].indexOf(callback);
1160
2154
  if (i > -1) {
2155
+ // @ts-ignore: The callback should always be the right type here.
1161
2156
  this[prop].splice(i, 1);
1162
2157
  }
1163
2158
  return true;
1164
2159
  }
1165
2160
  }
1166
- exports.default = User;
1167
- User.ETYPE = 'tilmeld_user';
1168
- User.class = 'User';
1169
- User.beforeRegisterCallbacks = [];
1170
- User.afterRegisterCallbacks = [];
1171
- User.beforeLoginCallbacks = [];
1172
- User.afterLoginCallbacks = [];
1173
- User.beforeLogoutCallbacks = [];
1174
- User.afterLogoutCallbacks = [];
1175
- User.DEFAULT_CLIENT_ENABLED_METHODS = [
1176
- '$checkUsername',
1177
- '$checkEmail',
1178
- '$checkPhone',
1179
- '$getAvatar',
1180
- '$register',
1181
- ];
1182
- User.DEFAULT_PRIVATE_DATA = [
1183
- 'email',
1184
- 'originalEmail',
1185
- 'phone',
1186
- 'group',
1187
- 'groups',
1188
- 'abilities',
1189
- 'inheritAbilities',
1190
- 'recoverSecret',
1191
- 'recoverSecretDate',
1192
- 'password',
1193
- 'salt',
1194
- 'secret',
1195
- 'newEmailAddress',
1196
- 'newEmailSecret',
1197
- 'cancelEmailAddress',
1198
- 'cancelEmailSecret',
1199
- 'emailChangeDate',
1200
- ];
1201
- User.DEFAULT_ALLOWLIST_DATA = [];
1202
- User.clientEnabledStaticMethods = [
1203
- 'current',
1204
- 'loginUser',
1205
- 'sendRecovery',
1206
- 'recover',
1207
- 'getClientConfig',
1208
- ];
1209
- User.searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
1210
2161
  //# sourceMappingURL=User.js.map