@nymphjs/tilmeld 1.0.0-beta.11 → 1.0.0-beta.110

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