@nymphjs/tilmeld 1.0.0-beta.12 → 1.0.0-beta.121

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