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

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