@nymphjs/tilmeld 1.0.0-beta.1 → 1.0.0-beta.100

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