@nymphjs/tilmeld 1.0.0-beta.10 → 1.0.0-beta.101

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 +459 -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 +652 -253
  13. package/dist/Tilmeld.js.map +1 -1
  14. package/dist/Tilmeld.types.d.ts +2 -2
  15. package/dist/Tilmeld.types.js +1 -2
  16. package/dist/User.d.ts +395 -22
  17. package/dist/User.js +1355 -368
  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 +441 -254
  62. package/src/Tilmeld.types.ts +2 -2
  63. package/src/User.test.ts +160 -7
  64. package/src/User.ts +1596 -457
  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,22 +266,25 @@ class User extends AbleObject_1.default {
161
266
  }
162
267
  }
163
268
  static getClientConfig() {
269
+ const tilmeld = enforceTilmeld(this);
164
270
  return {
165
- regFields: this.tilmeld.config.regFields,
166
- userFields: this.tilmeld.config.userFields,
167
- emailUsernames: this.tilmeld.config.emailUsernames,
168
- allowRegistration: this.tilmeld.config.allowRegistration,
169
- allowUsernameChange: this.tilmeld.config.allowUsernameChange,
170
- pwRecovery: this.tilmeld.config.pwRecovery,
171
- verifyEmail: this.tilmeld.config.verifyEmail,
172
- unverifiedAccess: this.tilmeld.config.unverifiedAccess,
271
+ regFields: tilmeld.config.regFields,
272
+ userFields: tilmeld.config.userFields,
273
+ emailUsernames: tilmeld.config.emailUsernames,
274
+ domainSupport: tilmeld.config.domainSupport,
275
+ allowRegistration: tilmeld.config.allowRegistration,
276
+ allowUsernameChange: tilmeld.config.allowUsernameChange,
277
+ pwRecovery: tilmeld.config.pwRecovery,
278
+ verifyEmail: tilmeld.config.verifyEmail,
279
+ unverifiedAccess: tilmeld.config.unverifiedAccess,
173
280
  };
174
281
  }
175
282
  static async loginUser(data) {
283
+ const tilmeld = enforceTilmeld(this);
176
284
  if (!('username' in data) || !data.username.length) {
177
285
  return { result: false, message: 'Incorrect login/password.' };
178
286
  }
179
- const user = await this.tilmeld.User.factoryUsername(data.username);
287
+ const user = await tilmeld.User.factoryUsername(data.username);
180
288
  const result = await user.$login(data);
181
289
  if (result.result) {
182
290
  user.$updateDataProtection();
@@ -184,19 +292,115 @@ class User extends AbleObject_1.default {
184
292
  }
185
293
  return result;
186
294
  }
295
+ /**
296
+ * Get the users in a domain. This is only accessible to the domain's admins.
297
+ */
298
+ static async getDomainUsers(domain, options) {
299
+ const tilmeld = enforceTilmeld(this);
300
+ if (!tilmeld.config.domainSupport) {
301
+ throw new AccessControlError('Domain support is not enabled.');
302
+ }
303
+ if (!domain) {
304
+ throw new BadDataError('Domain is required.');
305
+ }
306
+ domain = domain.toLowerCase();
307
+ if (!tilmeld.config.validDomainRegex.test(domain)) {
308
+ throw new BadDataError(tilmeld.config.validDomainRegexNotice);
309
+ }
310
+ if (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
311
+ throw new AccessControlError('You do not have the authority to manage this domain.');
312
+ }
313
+ return await this.nymph.getEntities({
314
+ class: this.nymph.getEntityClass(User),
315
+ ...(options?.limit != null
316
+ ? {
317
+ limit: options.limit,
318
+ }
319
+ : {}),
320
+ ...(options?.offset != null
321
+ ? {
322
+ offset: options.offset,
323
+ }
324
+ : {}),
325
+ ...(options?.sort != null
326
+ ? {
327
+ sort: options.sort,
328
+ }
329
+ : {}),
330
+ ...(options?.reverse != null
331
+ ? {
332
+ reverse: options.reverse,
333
+ }
334
+ : {}),
335
+ }, {
336
+ type: '&',
337
+ like: ['username', `%@${domain.replace(/([\\%_])/g, (s) => `\\${s}`)}`],
338
+ });
339
+ }
340
+ constructor() {
341
+ super();
342
+ // Defaults.
343
+ this.$data.enabled = true;
344
+ this.$data.abilities = [];
345
+ this.$data.groups = [];
346
+ this.$data.inheritAbilities = true;
347
+ this.$updateDataProtection();
348
+ }
349
+ $setNymph(nymph) {
350
+ this.$nymph = nymph;
351
+ if (!this.$asleep()) {
352
+ if (this.$data.group && this.$data.group.$nymph !== nymph) {
353
+ this.$data.group.$setNymph(nymph);
354
+ }
355
+ for (const group of this.$data.groups ?? []) {
356
+ if (group && group.$nymph !== nymph) {
357
+ group.$setNymph(nymph);
358
+ }
359
+ }
360
+ }
361
+ }
362
+ async $getUniques() {
363
+ const tilmeld = enforceTilmeld(this);
364
+ const uniques = [`u:${this.$data.username}`];
365
+ if (!tilmeld.config.emailUsernames &&
366
+ tilmeld.config.userFields.includes('email')) {
367
+ uniques.push(`e:${this.$data.email}`);
368
+ if (this.$data.newEmailAddress != null) {
369
+ uniques.push(`e:${this.$data.newEmailAddress}`);
370
+ }
371
+ }
372
+ return uniques;
373
+ }
187
374
  async $login(data) {
188
- const tilmeld = this.$nymph.tilmeld;
375
+ const tilmeld = enforceTilmeld(this);
189
376
  if (this.guid == null) {
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)) {
199
- return { result: false, message: 'Incorrect login/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
+ }
200
404
  }
201
405
  try {
202
406
  for (let callback of this.constructor
@@ -212,7 +416,67 @@ class User extends AbleObject_1.default {
212
416
  message: e.message,
213
417
  };
214
418
  }
215
- if (!tilmeld.login(this, true)) {
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) {
436
+ return { result: false, message: 'Incorrect login/password.' };
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
+ }
459
+ try {
460
+ for (let callback of this.constructor
461
+ .beforeLoginCallbacks) {
462
+ if (callback) {
463
+ await callback(this, {
464
+ username: this.$data.username || '',
465
+ password: '',
466
+ ...(data || {}),
467
+ });
468
+ }
469
+ }
470
+ }
471
+ catch (e) {
472
+ return {
473
+ result: false,
474
+ message: e.message,
475
+ };
476
+ }
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
@@ -221,10 +485,15 @@ class User extends AbleObject_1.default {
221
485
  await callback(this);
222
486
  }
223
487
  }
488
+ // Login was successful.
224
489
  return { result: true, message: 'You are logged in.' };
225
490
  }
491
+ /**
492
+ * Log a user out of the system.
493
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
494
+ */
226
495
  async $logout() {
227
- const tilmeld = this.$nymph.tilmeld;
496
+ const tilmeld = enforceTilmeld(this);
228
497
  try {
229
498
  for (let callback of this.constructor
230
499
  .beforeLogoutCallbacks) {
@@ -239,7 +508,8 @@ 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) {
@@ -256,66 +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 ('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.");
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);
295
596
  }
296
- this.$originalEmail = this.$data.email;
297
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
+ }
298
603
  }
299
604
  $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.");
605
+ this.$check();
606
+ if ('abilities' in patch.set) {
607
+ this.$checkIncomingAbilities(patch.set.abilities);
308
608
  }
309
- this.$originalEmail = this.$data.email;
310
609
  super.$jsonAcceptPatch(patch, allowConflict);
311
610
  }
312
- $putData(data, sdata) {
313
- 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
+ }
314
648
  this.$updateDataProtection();
315
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
+ */
316
655
  $updateDataProtection(givenUser) {
317
- const tilmeld = this.$nymph.tilmeld;
656
+ const tilmeld = enforceTilmeld(this);
318
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
+ }
319
665
  this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
320
666
  this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
321
667
  this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
@@ -323,75 +669,211 @@ class User extends AbleObject_1.default {
323
669
  this.$privateData.push('username');
324
670
  }
325
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.
326
674
  const abilities = (isCurrentUser || user == null
327
675
  ? this.$data.abilities
328
676
  : user?.abilities) ?? [];
329
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`);
330
683
  if (isCurrentUser) {
684
+ // Users can check to see what abilities they have.
331
685
  this.$clientEnabledMethods.push('$gatekeeper');
332
686
  this.$clientEnabledMethods.push('$changePassword');
687
+ this.$clientEnabledMethods.push('$revokeCurrentTokens');
333
688
  this.$clientEnabledMethods.push('$logout');
334
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');
335
704
  }
336
705
  if (user != null &&
337
- (abilities.indexOf('tilmeld/admin') !== -1 ||
338
- abilities.indexOf('system/admin') !== -1)) {
339
- 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');
340
712
  this.$allowlistData = undefined;
341
713
  }
342
- else if (isCurrentUser || isNewUser) {
714
+ else if (isDomainUser) {
715
+ // Domain admins can see their users' data, and edit some of it.
343
716
  if (tilmeld.config.allowUsernameChange || isNewUser) {
344
717
  this.$allowlistData.push('username');
345
718
  }
346
719
  this.$allowlistData.push('avatar');
347
- if (tilmeld.config.userFields.indexOf('name') !== -1) {
720
+ if (tilmeld.config.userFields.includes('name')) {
348
721
  this.$allowlistData.push('nameFirst');
349
722
  this.$allowlistData.push('nameMiddle');
350
723
  this.$allowlistData.push('nameLast');
351
724
  this.$allowlistData.push('name');
352
725
  }
353
- if (tilmeld.config.userFields.indexOf('email') !== -1) {
726
+ if (tilmeld.config.userFields.includes('email')) {
354
727
  this.$allowlistData.push('email');
355
728
  }
356
- if (tilmeld.config.userFields.indexOf('phone') !== -1) {
729
+ if (tilmeld.config.userFields.includes('phone')) {
357
730
  this.$allowlistData.push('phone');
358
731
  }
732
+ this.$allowlistData.push('enabled');
733
+ this.$allowlistData.push('passwordTemp');
359
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',
360
749
  'secret',
750
+ 'emailChangeDate',
361
751
  'newEmailSecret',
362
- 'cancelEmailAddress',
752
+ // 'newEmailAddress',
363
753
  'cancelEmailSecret',
364
- 'emailChangeDate',
754
+ 'cancelEmailAddress',
365
755
  'recoverSecret',
366
756
  'recoverSecretDate',
757
+ 'salt',
367
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',
368
805
  'salt',
806
+ 'password',
807
+ 'passwordTemp',
808
+ // 'revokeTokenDate',
809
+ 'totpSecret',
369
810
  ];
370
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
+ }
371
829
  }
372
- $gatekeeper(ability) {
373
- 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);
374
847
  if (ability == null) {
375
848
  return tilmeld.User.current()?.$is(this) ?? false;
376
849
  }
850
+ // Check the cache to see if we've already checked this user.
377
851
  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 ?? []);
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 ?? []);
389
868
  }
390
869
  }
391
- 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
+ }
392
875
  }
393
- return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
394
- !!this.$gatekeeperCache['system/admin']);
876
+ return Object.fromEntries(abilities.map((ability) => [ability, true]));
395
877
  }
396
878
  $clearCache() {
397
879
  const ret = super.$clearCache();
@@ -399,8 +881,13 @@ class User extends AbleObject_1.default {
399
881
  this.$gatekeeperCache = undefined;
400
882
  return ret;
401
883
  }
884
+ /**
885
+ * Send the user email verification/change/cancellation links.
886
+ *
887
+ * @returns True on success, false on failure.
888
+ */
402
889
  async $sendEmailVerification() {
403
- const tilmeld = this.$nymph.tilmeld;
890
+ const tilmeld = enforceTilmeld(this);
404
891
  if (this.guid == null) {
405
892
  return false;
406
893
  }
@@ -463,33 +950,60 @@ class User extends AbleObject_1.default {
463
950
  }
464
951
  return success;
465
952
  }
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;
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;
473
969
  }
474
970
  return true;
475
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
+ */
476
978
  $checkPassword(password) {
477
- const tilmeld = this.$nymph.tilmeld;
979
+ const tilmeld = enforceTilmeld(this);
478
980
  switch (tilmeld.config.pwMethod) {
479
981
  case 'plain':
480
982
  return this.$data.password === password;
481
983
  case 'digest':
482
- 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'));
483
986
  case 'salt':
484
987
  default:
485
988
  return (this.$data.password ===
486
- (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'));
487
993
  }
488
994
  }
489
- $delGroup(group) {
490
- 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)) {
491
1005
  const newGroups = [];
492
- for (let curGroup of this.$data.groups) {
1006
+ for (let curGroup of groups) {
493
1007
  if (!group.$is(curGroup)) {
494
1008
  newGroups.push(curGroup);
495
1009
  }
@@ -499,7 +1013,13 @@ class User extends AbleObject_1.default {
499
1013
  }
500
1014
  return true;
501
1015
  }
502
- $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) {
503
1023
  if (typeof group === 'string') {
504
1024
  if (this.$data.group?.guid === group) {
505
1025
  return true;
@@ -513,35 +1033,66 @@ class User extends AbleObject_1.default {
513
1033
  }
514
1034
  return (group.$is(this.$data.group) || group.$inArray(this.$data.groups ?? []));
515
1035
  }
516
- $isDescendant(group) {
517
- if (this.$data.group?.cdate != null &&
518
- 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))) {
519
1048
  return true;
520
1049
  }
1050
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
521
1051
  for (let curGroup of this.$data.groups ?? []) {
522
- if (curGroup.$isDescendant(group)) {
1052
+ if (await curGroup.$isDescendant(group)) {
523
1053
  return true;
524
1054
  }
525
1055
  }
526
1056
  return false;
527
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
+ */
528
1064
  async $changePassword(data) {
1065
+ this.$nymph.config.debugLog('tilmeld', `Password change request for "${this.$data.username}".`);
529
1066
  if (!('newPassword' in data) || !data.newPassword.length) {
530
1067
  return { result: false, message: 'Please specify a password.' };
531
1068
  }
532
1069
  if (!this.$checkPassword(data.currentPassword ?? '')) {
1070
+ this.$nymph.config.debugLog('tilmeld', `Incorrect password for change request for "${this.$data.username}".`);
533
1071
  return { result: false, message: 'Incorrect password.' };
534
1072
  }
535
1073
  this.$data.passwordTemp = data.newPassword;
1074
+ if (data.revokeCurrentTokens) {
1075
+ this.$data.revokeTokenDate = Date.now();
1076
+ }
536
1077
  if (await this.$save()) {
1078
+ if (data.revokeCurrentTokens) {
1079
+ const tilmeld = enforceTilmeld(this);
1080
+ await tilmeld.login(this, true);
1081
+ }
537
1082
  return { result: true, message: 'Your password has been changed.' };
538
1083
  }
539
1084
  else {
540
1085
  return { result: false, message: "Couldn't save new password." };
541
1086
  }
542
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
+ */
543
1094
  $password(password) {
544
- const tilmeld = this.$nymph.tilmeld;
1095
+ const tilmeld = enforceTilmeld(this);
545
1096
  switch (tilmeld.config.pwMethod) {
546
1097
  case 'plain':
547
1098
  delete this.$data.salt;
@@ -549,24 +1100,221 @@ class User extends AbleObject_1.default {
549
1100
  break;
550
1101
  case 'digest':
551
1102
  delete this.$data.salt;
552
- 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');
553
1107
  break;
554
1108
  case 'salt':
555
1109
  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);
1110
+ this.$data.salt = nanoid();
1111
+ this.$data.password = crypto
1112
+ .createHash('sha256')
1113
+ .update(password + this.$data.salt)
1114
+ .digest('base64');
558
1115
  break;
559
1116
  }
560
1117
  return this.$data.password;
561
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
+ */
562
1291
  async $checkUsername() {
563
- const tilmeld = this.$nymph.tilmeld;
1292
+ const tilmeld = enforceTilmeld(this);
1293
+ this.$nymph.config.debugInfo('tilmeld', `Username check "${this.$data.username}".`);
564
1294
  if (!tilmeld.config.emailUsernames) {
565
- 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) {
566
1307
  return { result: false, message: 'Please specify a username.' };
567
1308
  }
568
- if (tilmeld.config.maxUsernameLength > 0 &&
569
- 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) {
570
1318
  return {
571
1319
  result: false,
572
1320
  message: 'Usernames must not exceed ' +
@@ -574,18 +1322,35 @@ class User extends AbleObject_1.default {
574
1322
  ' characters.',
575
1323
  };
576
1324
  }
577
- 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) {
578
1327
  return {
579
1328
  result: false,
580
1329
  message: tilmeld.config.validCharsNotice,
581
1330
  };
582
1331
  }
583
- if (!tilmeld.config.validRegex.test(this.$data.username)) {
1332
+ if (!tilmeld.config.validRegex.test(username)) {
584
1333
  return {
585
1334
  result: false,
586
1335
  message: tilmeld.config.validRegexNotice,
587
1336
  };
588
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
+ }
589
1354
  const selector = {
590
1355
  type: '&',
591
1356
  ilike: [
@@ -600,6 +1365,17 @@ class User extends AbleObject_1.default {
600
1365
  if (test != null) {
601
1366
  return { result: false, message: 'That username is taken.' };
602
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
+ }
603
1379
  return {
604
1380
  result: true,
605
1381
  message: this.guid != null ? 'Username is valid.' : 'Username is available!',
@@ -610,8 +1386,15 @@ class User extends AbleObject_1.default {
610
1386
  if (this.$data.username == null || !this.$data.username.length) {
611
1387
  return { result: false, message: 'Please specify an email.' };
612
1388
  }
613
- if (tilmeld.config.maxUsernameLength > 0 &&
614
- 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) {
615
1398
  return {
616
1399
  result: false,
617
1400
  message: 'Emails must not exceed ' +
@@ -622,8 +1405,14 @@ class User extends AbleObject_1.default {
622
1405
  return await this.$checkEmail();
623
1406
  }
624
1407
  }
1408
+ /**
1409
+ * Check that an email is unique.
1410
+ *
1411
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1412
+ */
625
1413
  async $checkEmail() {
626
- const tilmeld = this.$nymph.tilmeld;
1414
+ const tilmeld = enforceTilmeld(this);
1415
+ this.$nymph.config.debugInfo('tilmeld', `Email check "${this.$data.email}".`);
627
1416
  if (this.$data.email == null || !this.$data.email.length) {
628
1417
  if (tilmeld.config.verifyEmail) {
629
1418
  return { result: false, message: 'Please specify an email.' };
@@ -657,8 +1446,14 @@ class User extends AbleObject_1.default {
657
1446
  message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
658
1447
  };
659
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
+ */
660
1454
  async $checkPhone() {
661
- const tilmeld = this.$nymph.tilmeld;
1455
+ const tilmeld = enforceTilmeld(this);
1456
+ this.$nymph.config.debugInfo('tilmeld', `Phone check "${this.$data.phone}".`);
662
1457
  if (this.$data.phone == null || !this.$data.phone.length) {
663
1458
  return { result: false, message: 'Please specify a phone number.' };
664
1459
  }
@@ -686,7 +1481,7 @@ class User extends AbleObject_1.default {
686
1481
  };
687
1482
  }
688
1483
  async $register(data) {
689
- let tilmeld = this.$nymph.tilmeld;
1484
+ let tilmeld = enforceTilmeld(this);
690
1485
  if (!tilmeld.config.allowRegistration) {
691
1486
  return {
692
1487
  result: false,
@@ -701,21 +1496,6 @@ class User extends AbleObject_1.default {
701
1496
  message: 'This is already a registered user.',
702
1497
  };
703
1498
  }
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
1499
  if (!('password' in data) || !data.password.length) {
720
1500
  return {
721
1501
  result: false,
@@ -728,7 +1508,7 @@ class User extends AbleObject_1.default {
728
1508
  return { ...unCheck, loggedin: false };
729
1509
  }
730
1510
  this.$password(data.password);
731
- if (tilmeld.config.regFields.indexOf('name') !== -1) {
1511
+ if (tilmeld.config.regFields.includes('name')) {
732
1512
  this.$data.name =
733
1513
  this.$data.nameFirst +
734
1514
  (this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
@@ -740,59 +1520,35 @@ class User extends AbleObject_1.default {
740
1520
  if (tilmeld.config.emailUsernames) {
741
1521
  this.$data.email = this.$data.username;
742
1522
  }
743
- const transaction = 'tilmeld-register-' + (0, guid_1.nanoid)();
1523
+ // Start transaction.
1524
+ const transaction = 'tilmeld-register-' + nanoid();
744
1525
  const nymph = this.$nymph;
745
1526
  const tnymph = await this.$nymph.startTransaction(transaction);
746
- this.$nymph = tnymph;
747
- tilmeld = this.$nymph.tilmeld;
1527
+ this.$setNymph(tnymph);
1528
+ tilmeld = enforceTilmeld(this);
1529
+ let message = '';
1530
+ let loggedin = false;
748
1531
  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;
1532
+ try {
1533
+ for (let callback of this.constructor
1534
+ .beforeRegisterCallbacks) {
1535
+ if (callback) {
1536
+ await callback(this, data);
1537
+ }
781
1538
  }
782
1539
  }
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
- });
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
+ };
794
1548
  }
795
- 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.
796
1552
  if (!tilmeld.config.unverifiedAccess) {
797
1553
  this.$data.enabled = false;
798
1554
  }
@@ -800,6 +1556,8 @@ class User extends AbleObject_1.default {
800
1556
  else {
801
1557
  this.$data.enabled = true;
802
1558
  }
1559
+ // If createAdmin is true and there are no other users, grant
1560
+ // "system/admin".
803
1561
  let madeAdmin = false;
804
1562
  if (tilmeld.config.createAdmin) {
805
1563
  const otherUsers = await tnymph.getEntities({
@@ -808,13 +1566,26 @@ class User extends AbleObject_1.default {
808
1566
  limit: 1,
809
1567
  return: 'guid',
810
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
+ }
811
1579
  if (!otherUsers.length) {
812
1580
  this.$grant('system/admin');
813
1581
  this.$data.enabled = true;
814
1582
  madeAdmin = true;
815
1583
  }
816
1584
  }
1585
+ this.$nymph.config.debugLog('tilmeld', `Registering new user "${this.$data.username}".`);
817
1586
  if (await this.$saveSkipAC()) {
1587
+ this.$nymph.config.debugLog('tilmeld', `New user registered "${this.$data.username}".`);
1588
+ // Send the new user registered email.
818
1589
  if (tilmeld.config.userRegisteredRecipient != null) {
819
1590
  await tilmeld.config.sendEmail(tilmeld, {
820
1591
  template: 'UserRegistered',
@@ -831,31 +1602,20 @@ class User extends AbleObject_1.default {
831
1602
  },
832
1603
  }, this);
833
1604
  }
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
- }
1605
+ // Finish up.
848
1606
  if (tilmeld.config.verifyEmail &&
849
1607
  !tilmeld.config.unverifiedAccess &&
850
- !madeAdmin) {
1608
+ !madeAdmin &&
1609
+ tilmeld.config.userFields.includes('email')) {
851
1610
  message +=
852
1611
  `Almost there. An email has been sent to ${this.$data.email} ` +
853
1612
  'with a verification link for you to finish registration.';
854
1613
  }
855
1614
  else if (tilmeld.config.verifyEmail &&
856
1615
  tilmeld.config.unverifiedAccess &&
857
- !madeAdmin) {
858
- if (!tilmeld.login(this, true)) {
1616
+ !madeAdmin &&
1617
+ tilmeld.config.userFields.includes('email')) {
1618
+ if (!(await tilmeld.login(this, true))) {
859
1619
  throw new Error('An error occurred trying to log you in.');
860
1620
  }
861
1621
  message +=
@@ -865,7 +1625,7 @@ class User extends AbleObject_1.default {
865
1625
  loggedin = true;
866
1626
  }
867
1627
  else {
868
- if (!tilmeld.login(this, true)) {
1628
+ if (!(await tilmeld.login(this, true))) {
869
1629
  throw new Error('An error occurred trying to log you in.');
870
1630
  }
871
1631
  message += "You're now registered and logged in!";
@@ -880,23 +1640,11 @@ class User extends AbleObject_1.default {
880
1640
  });
881
1641
  }
882
1642
  }
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
1643
  }
897
1644
  else {
1645
+ this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}".`);
898
1646
  await tnymph.rollback(transaction);
899
- this.$nymph = nymph;
1647
+ this.$setNymph(nymph);
900
1648
  return {
901
1649
  result: false,
902
1650
  loggedin: false,
@@ -905,22 +1653,69 @@ class User extends AbleObject_1.default {
905
1653
  }
906
1654
  }
907
1655
  catch (e) {
1656
+ this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}": ${e}`);
908
1657
  await tnymph.rollback(transaction);
909
- 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) {
910
1667
  throw e;
911
1668
  }
1669
+ return {
1670
+ result: true,
1671
+ loggedin,
1672
+ message,
1673
+ };
912
1674
  }
913
1675
  async $save() {
914
- let tilmeld = this.$nymph.tilmeld;
1676
+ let tilmeld = enforceTilmeld(this);
915
1677
  if (this.$data.username == null || !this.$data.username.trim().length) {
916
1678
  return false;
917
1679
  }
918
- if (tilmeld.gatekeeper('tilmeld/admin') &&
1680
+ if (!this.$skipAcWhenSaving &&
1681
+ tilmeld.gatekeeper('tilmeld/admin') &&
919
1682
  !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.");
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
+ }
922
1716
  }
923
1717
  let sendVerification = false;
1718
+ // Formatting.
924
1719
  this.$data.username = this.$data.username.trim();
925
1720
  if (tilmeld.config.emailUsernames) {
926
1721
  this.$data.email = this.$data.username;
@@ -934,49 +1729,63 @@ class User extends AbleObject_1.default {
934
1729
  (this.$data.nameMiddle ? ' ' + this.$data.nameMiddle : '') +
935
1730
  (this.$data.nameLast ? ' ' + this.$data.nameLast : '');
936
1731
  this.$data.enabled = !!this.$data.enabled;
937
- if (this.$data.nameFirst === '') {
1732
+ // Clear empty values.
1733
+ if (this.$data.nameFirst === '' ||
1734
+ !tilmeld.config.userFields.includes('name')) {
938
1735
  delete this.$data.nameFirst;
939
1736
  }
940
- if (this.$data.nameMiddle === '') {
1737
+ if (this.$data.nameMiddle === '' ||
1738
+ !tilmeld.config.userFields.includes('name')) {
941
1739
  delete this.$data.nameMiddle;
942
1740
  }
943
- if (this.$data.nameLast === '') {
1741
+ if (this.$data.nameLast === '' ||
1742
+ !tilmeld.config.userFields.includes('name')) {
944
1743
  delete this.$data.nameLast;
945
1744
  }
946
- if (this.$data.name === '') {
1745
+ if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
947
1746
  delete this.$data.name;
948
1747
  }
949
- if (this.$data.email === '') {
1748
+ if (this.$data.email === '' ||
1749
+ !tilmeld.config.userFields.includes('email')) {
950
1750
  delete this.$data.email;
951
1751
  }
952
1752
  if (this.$data.avatar === '') {
953
1753
  delete this.$data.avatar;
954
1754
  }
955
- if (this.$data.phone === '') {
1755
+ if (this.$data.phone === '' ||
1756
+ !tilmeld.config.userFields.includes('phone')) {
956
1757
  delete this.$data.phone;
957
1758
  }
958
- if (this.$data.secret === '') {
1759
+ if (this.$data.secret === '' ||
1760
+ !tilmeld.config.userFields.includes('email')) {
959
1761
  delete this.$data.secret;
960
1762
  }
961
- if (this.$data.emailChangeDate === 0) {
1763
+ if (this.$data.emailChangeDate === 0 ||
1764
+ !tilmeld.config.userFields.includes('email')) {
962
1765
  delete this.$data.emailChangeDate;
963
1766
  }
964
- if (this.$data.newEmailSecret === '') {
1767
+ if (this.$data.newEmailSecret === '' ||
1768
+ !tilmeld.config.userFields.includes('email')) {
965
1769
  delete this.$data.newEmailSecret;
966
1770
  }
967
- if (this.$data.newEmailAddress === '') {
1771
+ if (this.$data.newEmailAddress === '' ||
1772
+ !tilmeld.config.userFields.includes('email')) {
968
1773
  delete this.$data.newEmailAddress;
969
1774
  }
970
- if (this.$data.cancelEmailSecret === '') {
1775
+ if (this.$data.cancelEmailSecret === '' ||
1776
+ !tilmeld.config.userFields.includes('email')) {
971
1777
  delete this.$data.cancelEmailSecret;
972
1778
  }
973
- if (this.$data.cancelEmailAddress === '') {
1779
+ if (this.$data.cancelEmailAddress === '' ||
1780
+ !tilmeld.config.userFields.includes('email')) {
974
1781
  delete this.$data.cancelEmailAddress;
975
1782
  }
976
- if (this.$data.recoverSecret === '') {
1783
+ if (this.$data.recoverSecret === '' ||
1784
+ !tilmeld.config.userFields.includes('email')) {
977
1785
  delete this.$data.recoverSecret;
978
1786
  }
979
- if (this.$data.recoverSecretDate === 0) {
1787
+ if (this.$data.recoverSecretDate === 0 ||
1788
+ !tilmeld.config.userFields.includes('email')) {
980
1789
  delete this.$data.recoverSecretDate;
981
1790
  }
982
1791
  if (this.$data.password === '') {
@@ -988,50 +1797,65 @@ class User extends AbleObject_1.default {
988
1797
  if (this.$data.inheritAbilities === false) {
989
1798
  delete this.$data.inheritAbilities;
990
1799
  }
1800
+ // Verification.
991
1801
  const unCheck = await this.$checkUsername();
992
1802
  if (!unCheck.result) {
993
- throw new errors_1.BadUsernameError(unCheck.message);
1803
+ throw new BadUsernameError(unCheck.message);
994
1804
  }
995
- if (!tilmeld.config.emailUsernames) {
1805
+ if (!tilmeld.config.emailUsernames &&
1806
+ tilmeld.config.userFields.includes('email')) {
996
1807
  const emCheck = await this.$checkEmail();
997
1808
  if (!emCheck.result) {
998
- throw new errors_1.BadEmailError(emCheck.message);
1809
+ throw new BadEmailError(emCheck.message);
999
1810
  }
1000
1811
  }
1001
- 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.
1002
1817
  if (tilmeld.config.verifyEmail) {
1818
+ // The user needs to verify this new email address.
1003
1819
  if (this.guid == null) {
1004
- if ((this.$data.abilities?.indexOf('system/admin') ?? -1) === -1) {
1005
- 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();
1006
1824
  sendVerification = true;
1007
1825
  }
1008
1826
  }
1009
1827
  else if (this.$originalEmail != null &&
1010
1828
  this.$originalEmail !== this.$data.email) {
1829
+ // The user already has an old email address.
1011
1830
  if (tilmeld.config.emailRateLimit !== '' &&
1012
1831
  this.$data.emailChangeDate != null &&
1013
1832
  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() +
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() +
1017
1836
  ' to change your email address again.');
1018
1837
  }
1019
1838
  else {
1020
1839
  if (this.$data.newEmailSecret == null &&
1021
1840
  tilmeld.config.emailRateLimit !== '' &&
1841
+ // Make sure the user has at least the rate limit time to cancel
1842
+ // an email change.
1022
1843
  (this.$data.emailChangeDate == null ||
1023
1844
  this.$data.emailChangeDate <
1024
- (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.
1025
1847
  this.$data.cancelEmailAddress = this.$originalEmail;
1026
- this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
1848
+ this.$data.cancelEmailSecret = nanoid();
1027
1849
  this.$data.emailChangeDate = Date.now();
1028
1850
  }
1851
+ // Save the new address and reset to the old one (until they verify
1852
+ // it).
1029
1853
  this.$data.newEmailAddress = this.$data.email;
1030
1854
  this.$data.email = this.$originalEmail;
1031
1855
  if (tilmeld.config.emailUsernames) {
1032
1856
  this.$data.username = this.$data.email;
1033
1857
  }
1034
- this.$data.newEmailSecret = (0, guid_1.nanoid)();
1858
+ this.$data.newEmailSecret = nanoid();
1035
1859
  sendVerification = true;
1036
1860
  }
1037
1861
  }
@@ -1039,11 +1863,15 @@ class User extends AbleObject_1.default {
1039
1863
  else if (this.guid != null &&
1040
1864
  this.$originalEmail != null &&
1041
1865
  this.$originalEmail !== this.$data.email &&
1866
+ // Make sure the user has at least the rate limit time to cancel an
1867
+ // email change.
1042
1868
  (this.$data.emailChangeDate == null ||
1043
1869
  this.$data.emailChangeDate <
1044
- (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.
1045
1873
  this.$data.cancelEmailAddress = this.$originalEmail;
1046
- this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
1874
+ this.$data.cancelEmailSecret = nanoid();
1047
1875
  sendVerification = true;
1048
1876
  }
1049
1877
  }
@@ -1052,72 +1880,188 @@ class User extends AbleObject_1.default {
1052
1880
  }
1053
1881
  delete this.$data.passwordTemp;
1054
1882
  if (this.$data.password == null) {
1055
- throw new errors_1.BadDataError('A password is required.');
1056
- }
1057
- try {
1058
- tilmeld.config.validatorUser(this);
1059
- }
1060
- catch (e) {
1061
- throw new errors_1.BadDataError(e?.message);
1883
+ throw new BadDataError('A password is required.');
1062
1884
  }
1063
- const transaction = 'tilmeld-save-' + (0, guid_1.nanoid)();
1885
+ // Start transaction.
1886
+ const transaction = 'tilmeld-save-user-' + (this.guid || nanoid());
1064
1887
  const nymph = this.$nymph;
1065
1888
  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)) {
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
+ }
1071
1982
  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();
1983
+ tilmeld.config.validatorUser(tilmeld, this);
1078
1984
  }
1079
1985
  catch (e) {
1080
- await tnymph.rollback(transaction);
1081
- this.$nymph = nymph;
1082
- throw e;
1986
+ throw new BadDataError(e?.message);
1083
1987
  }
1084
1988
  }
1085
- let ret;
1989
+ catch (e) {
1990
+ await tnymph.rollback(transaction);
1991
+ this.$setNymph(nymph);
1992
+ throw e;
1993
+ }
1994
+ let ret = false;
1086
1995
  let preGuid = this.guid;
1087
1996
  let preCdate = this.cdate;
1088
1997
  let preMdate = this.mdate;
1089
1998
  try {
1999
+ for (let callback of this.constructor
2000
+ .beforeSaveCallbacks) {
2001
+ if (callback) {
2002
+ await callback(this);
2003
+ }
2004
+ }
1090
2005
  ret = await super.$save();
1091
2006
  }
1092
2007
  catch (e) {
1093
2008
  await tnymph.rollback(transaction);
1094
- this.$nymph = nymph;
2009
+ this.guid = preGuid;
2010
+ this.cdate = preCdate;
2011
+ this.mdate = preMdate;
2012
+ this.$setNymph(nymph);
1095
2013
  throw e;
1096
2014
  }
1097
2015
  if (ret) {
1098
2016
  if (sendVerification) {
2017
+ // The email has changed, so send a new verification email.
1099
2018
  if (!(await this.$sendEmailVerification())) {
1100
2019
  await tnymph.rollback(transaction);
1101
2020
  this.guid = preGuid;
1102
2021
  this.cdate = preCdate;
1103
2022
  this.mdate = preMdate;
1104
- this.$nymph = nymph;
2023
+ this.$setNymph(nymph);
1105
2024
  throw new Error("Couldn't send verification email.");
1106
2025
  }
1107
2026
  }
1108
2027
  this.$descendantGroups = undefined;
1109
2028
  this.$gatekeeperCache = undefined;
1110
- await tnymph.commit(transaction);
1111
- if (nymph.tilmeld.User.current(true).$is(this)) {
1112
- 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;
1113
2044
  }
2045
+ ret = await tnymph.commit(transaction);
1114
2046
  }
1115
2047
  else {
1116
2048
  await tnymph.rollback(transaction);
1117
2049
  }
1118
- 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
+ }
1119
2060
  return ret;
1120
2061
  }
2062
+ /**
2063
+ * This should *never* be accessible on the client.
2064
+ */
1121
2065
  async $saveSkipAC() {
1122
2066
  this.$skipAcWhenSaving = true;
1123
2067
  return await this.$save();
@@ -1130,24 +2074,109 @@ class User extends AbleObject_1.default {
1130
2074
  return false;
1131
2075
  }
1132
2076
  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.");
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.");
2080
+ }
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.");
2088
+ }
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
+ }
2132
+ }
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);
1136
2153
  }
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.");
2154
+ else {
2155
+ await tnymph.rollback(transaction);
1140
2156
  }
1141
- if (tilmeld.User.current(true).$is(this)) {
1142
- await this.$logout();
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;
1143
2171
  }
1144
- return await super.$delete();
2172
+ return false;
1145
2173
  }
1146
2174
  static on(event, callback) {
1147
2175
  const prop = (event + 'Callbacks');
1148
2176
  if (!(prop in this)) {
1149
2177
  throw new Error('Invalid event type.');
1150
2178
  }
2179
+ // @ts-ignore: The callback should always be the right type here.
1151
2180
  this[prop].push(callback);
1152
2181
  return () => this.off(event, callback);
1153
2182
  }
@@ -1156,55 +2185,13 @@ class User extends AbleObject_1.default {
1156
2185
  if (!(prop in this)) {
1157
2186
  return false;
1158
2187
  }
2188
+ // @ts-ignore: The callback should always be the right type here.
1159
2189
  const i = this[prop].indexOf(callback);
1160
2190
  if (i > -1) {
2191
+ // @ts-ignore: The callback should always be the right type here.
1161
2192
  this[prop].splice(i, 1);
1162
2193
  }
1163
2194
  return true;
1164
2195
  }
1165
2196
  }
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
2197
  //# sourceMappingURL=User.js.map