@nymphjs/tilmeld 1.0.0-beta.8 → 1.0.0-beta.80

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 (57) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/README.md +14 -1
  3. package/dist/AbleObject.d.ts +15 -0
  4. package/dist/AbleObject.js +16 -1
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +130 -10
  7. package/dist/Group.js +311 -129
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.test.js +77 -2
  10. package/dist/Group.test.js.map +1 -1
  11. package/dist/Tilmeld.d.ts +237 -9
  12. package/dist/Tilmeld.js +569 -201
  13. package/dist/Tilmeld.js.map +1 -1
  14. package/dist/User.d.ts +344 -16
  15. package/dist/User.js +979 -313
  16. package/dist/User.js.map +1 -1
  17. package/dist/User.test.js +63 -2
  18. package/dist/User.test.js.map +1 -1
  19. package/dist/conf/d.d.ts +250 -3
  20. package/dist/conf/defaults.js +146 -98
  21. package/dist/conf/defaults.js.map +1 -1
  22. package/dist/enforceTilmeld.d.ts +3 -0
  23. package/dist/enforceTilmeld.js +22 -0
  24. package/dist/enforceTilmeld.js.map +1 -0
  25. package/dist/errors/AccessControlError.d.ts +2 -1
  26. package/dist/errors/AccessControlError.js +3 -2
  27. package/dist/errors/AccessControlError.js.map +1 -1
  28. package/dist/errors/BadDataError.d.ts +2 -1
  29. package/dist/errors/BadDataError.js +3 -2
  30. package/dist/errors/BadDataError.js.map +1 -1
  31. package/dist/errors/BadEmailError.d.ts +2 -1
  32. package/dist/errors/BadEmailError.js +3 -2
  33. package/dist/errors/BadEmailError.js.map +1 -1
  34. package/dist/errors/BadUsernameError.d.ts +2 -1
  35. package/dist/errors/BadUsernameError.js +3 -2
  36. package/dist/errors/BadUsernameError.js.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +1 -0
  39. package/dist/index.js.map +1 -1
  40. package/package.json +30 -26
  41. package/src/AbleObject.ts +2 -2
  42. package/src/Group.test.ts +102 -4
  43. package/src/Group.ts +274 -158
  44. package/src/Tilmeld.ts +383 -249
  45. package/src/User.test.ts +80 -4
  46. package/src/User.ts +1061 -382
  47. package/src/conf/d.ts +26 -13
  48. package/src/conf/defaults.ts +151 -104
  49. package/src/enforceTilmeld.ts +20 -0
  50. package/src/errors/AccessControlError.ts +4 -2
  51. package/src/errors/BadDataError.ts +4 -2
  52. package/src/errors/BadEmailError.ts +4 -2
  53. package/src/errors/BadUsernameError.ts +4 -2
  54. package/src/index.ts +2 -0
  55. package/src/tilmeldJoiProps.ts +9 -9
  56. package/tsconfig.json +3 -3
  57. package/typedoc.json +4 -0
package/dist/User.js CHANGED
@@ -3,18 +3,115 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const guid_1 = require("@nymphjs/guid");
6
7
  const strtotime_1 = __importDefault(require("locutus/php/datetime/strtotime"));
7
8
  const enc_base64_1 = __importDefault(require("crypto-js/enc-base64"));
8
9
  const sha256_1 = __importDefault(require("crypto-js/sha256"));
9
10
  const md5_1 = __importDefault(require("crypto-js/md5"));
10
11
  const lodash_1 = require("lodash");
11
- const guid_1 = require("@nymphjs/guid");
12
+ const otpauth_1 = require("otpauth");
13
+ const qrcode_1 = require("qrcode");
14
+ const enforceTilmeld_1 = require("./enforceTilmeld");
12
15
  const AbleObject_1 = __importDefault(require("./AbleObject"));
13
16
  const errors_1 = require("./errors");
17
+ /**
18
+ * A user data model.
19
+ *
20
+ * Written by Hunter Perrin for SciActive.
21
+ *
22
+ * @author Hunter Perrin <hperrin@gmail.com>
23
+ * @copyright SciActive Inc
24
+ * @see http://nymph.io/
25
+ */
14
26
  class User extends AbleObject_1.default {
15
- static async factory(guid) {
16
- return (await super.factory(guid));
17
- }
27
+ /**
28
+ * The instance of Tilmeld to use for queries.
29
+ */
30
+ static ETYPE = 'tilmeld_user';
31
+ static class = 'User';
32
+ static checkUsernameCallbacks = [];
33
+ static beforeRegisterCallbacks = [];
34
+ static afterRegisterCallbacks = [];
35
+ static beforeLoginCallbacks = [];
36
+ static afterLoginCallbacks = [];
37
+ static beforeLogoutCallbacks = [];
38
+ static afterLogoutCallbacks = [];
39
+ static DEFAULT_CLIENT_ENABLED_METHODS = [
40
+ '$checkUsername',
41
+ '$checkEmail',
42
+ '$checkPhone',
43
+ '$getAvatar',
44
+ '$register',
45
+ ];
46
+ static DEFAULT_PRIVATE_DATA = [
47
+ // The things that are commented are there to show what is _not_ private.
48
+ 'abilities',
49
+ // 'username',
50
+ // 'nameFirst',
51
+ // 'nameMiddle',
52
+ // 'nameLast',
53
+ // 'name',
54
+ 'email',
55
+ // 'avatar',
56
+ 'phone',
57
+ 'group',
58
+ 'groups',
59
+ 'inheritAbilities',
60
+ 'enabled',
61
+ 'secret',
62
+ 'emailChangeDate',
63
+ 'newEmailSecret',
64
+ 'newEmailAddress',
65
+ 'cancelEmailSecret',
66
+ 'cancelEmailAddress',
67
+ 'recoverSecret',
68
+ 'recoverSecretDate',
69
+ 'salt',
70
+ 'password',
71
+ 'passwordTemp',
72
+ 'revokeTokenDate',
73
+ 'totpSecret',
74
+ ];
75
+ static DEFAULT_ALLOWLIST_DATA = [];
76
+ $clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
77
+ static clientEnabledStaticMethods = [
78
+ 'current',
79
+ 'loginUser',
80
+ 'sendRecovery',
81
+ 'recover',
82
+ 'getClientConfig',
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;
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 = (0, enforceTilmeld_1.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 = (0, enforceTilmeld_1.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
  }
185
+ // Create a unique secret.
96
186
  getUser.recoverSecret = (0, guid_1.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 = (0, enforceTilmeld_1.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
+ (0, strtotime_1.default)('+' + 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,24 @@ class User extends AbleObject_1.default {
161
262
  }
162
263
  }
163
264
  static getClientConfig() {
265
+ const tilmeld = (0, enforceTilmeld_1.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
+ allowRegistration: tilmeld.config.allowRegistration,
271
+ allowUsernameChange: tilmeld.config.allowUsernameChange,
272
+ pwRecovery: tilmeld.config.pwRecovery,
273
+ verifyEmail: tilmeld.config.verifyEmail,
274
+ unverifiedAccess: tilmeld.config.unverifiedAccess,
173
275
  };
174
276
  }
175
277
  static async loginUser(data) {
278
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
176
279
  if (!('username' in data) || !data.username.length) {
177
280
  return { result: false, message: 'Incorrect login/password.' };
178
281
  }
179
- const user = await this.tilmeld.User.factoryUsername(data.username);
282
+ const user = await tilmeld.User.factoryUsername(data.username);
180
283
  const result = await user.$login(data);
181
284
  if (result.result) {
182
285
  user.$updateDataProtection();
@@ -184,20 +287,68 @@ class User extends AbleObject_1.default {
184
287
  }
185
288
  return result;
186
289
  }
290
+ constructor() {
291
+ super();
292
+ // Defaults.
293
+ this.$data.enabled = true;
294
+ this.$data.abilities = [];
295
+ this.$data.groups = [];
296
+ this.$data.inheritAbilities = true;
297
+ this.$updateDataProtection();
298
+ }
299
+ $setNymph(nymph) {
300
+ this.$nymph = nymph;
301
+ if (!this.$asleep()) {
302
+ if (this.$data.group && this.$data.group.$nymph !== nymph) {
303
+ this.$data.group.$setNymph(nymph);
304
+ }
305
+ for (const group of this.$data.groups ?? []) {
306
+ if (group && group.$nymph !== nymph) {
307
+ group.$setNymph(nymph);
308
+ }
309
+ }
310
+ }
311
+ }
312
+ async $getUniques() {
313
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
314
+ const uniques = [`u:${this.$data.username}`];
315
+ if (!tilmeld.config.emailUsernames &&
316
+ tilmeld.config.userFields.includes('email')) {
317
+ uniques.push(`e:${this.$data.email}`);
318
+ if (this.$data.newEmailAddress != null) {
319
+ uniques.push(`e:${this.$data.newEmailAddress}`);
320
+ }
321
+ }
322
+ return uniques;
323
+ }
187
324
  async $login(data) {
188
- const tilmeld = this.$nymph.tilmeld;
325
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
189
326
  if (this.guid == null) {
190
327
  return { result: false, message: 'Incorrect login/password.' };
191
328
  }
192
329
  if (!this.$data.enabled) {
193
330
  return { result: false, message: 'This user is disabled.' };
194
331
  }
195
- if (this.$gatekeeper()) {
332
+ if (await this.$gatekeeper()) {
196
333
  return { result: true, message: 'You are already logged in.' };
197
334
  }
198
335
  if (!this.$checkPassword(data.password)) {
199
336
  return { result: false, message: 'Incorrect login/password.' };
200
337
  }
338
+ if (this.$data.totpSecret != null) {
339
+ if (data.code == null ||
340
+ typeof data.code !== 'string' ||
341
+ data.code.length !== 6) {
342
+ return {
343
+ result: false,
344
+ needTOTP: true,
345
+ message: 'You need to provide a 2FA code.',
346
+ };
347
+ }
348
+ if (!this.$checkTOTPCode(data.code)) {
349
+ return { result: false, message: 'Incorrect 2FA code.' };
350
+ }
351
+ }
201
352
  try {
202
353
  for (let callback of this.constructor
203
354
  .beforeLoginCallbacks) {
@@ -212,7 +363,9 @@ class User extends AbleObject_1.default {
212
363
  message: e.message,
213
364
  };
214
365
  }
215
- if (!tilmeld.login(this, true)) {
366
+ this.$nymph.config.debugLog('tilmeld', `User login for "${this.$data.username}".`);
367
+ // Authentication was successful, attempt to login.
368
+ if (!(await tilmeld.login(this, true))) {
216
369
  return { result: false, message: 'Incorrect login/password.' };
217
370
  }
218
371
  for (let callback of this.constructor
@@ -221,10 +374,73 @@ class User extends AbleObject_1.default {
221
374
  await callback(this);
222
375
  }
223
376
  }
377
+ // Login was successful.
224
378
  return { result: true, message: 'You are logged in.' };
225
379
  }
380
+ async $switchUser(data) {
381
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
382
+ if (this.guid == null) {
383
+ return { result: false, message: 'Incorrect login/password.' };
384
+ }
385
+ let user = tilmeld.User.current();
386
+ if (!user || user.guid == null) {
387
+ return { result: false, message: "You're not logged in." };
388
+ }
389
+ const isCurrentUser = user != null && user.$is(this);
390
+ if (isCurrentUser) {
391
+ return { result: false, message: "You can't switch to yourself." };
392
+ }
393
+ const toAbilities = this.$data.abilities || [];
394
+ const fromAbilities = user.abilities || [];
395
+ if (!fromAbilities.includes('tilmeld/switch') &&
396
+ !fromAbilities.includes('system/admin')) {
397
+ return {
398
+ result: false,
399
+ message: "You don't have permission to switch users.",
400
+ };
401
+ }
402
+ if (toAbilities.includes('system/admin') ||
403
+ toAbilities.includes('tilmeld/admin')) {
404
+ return { result: false, message: "You can't switch to an admin." };
405
+ }
406
+ try {
407
+ for (let callback of this.constructor
408
+ .beforeLoginCallbacks) {
409
+ if (callback) {
410
+ await callback(this, {
411
+ username: this.$data.username || '',
412
+ password: '',
413
+ ...(data || {}),
414
+ });
415
+ }
416
+ }
417
+ }
418
+ catch (e) {
419
+ return {
420
+ result: false,
421
+ message: e.message,
422
+ };
423
+ }
424
+ this.$nymph.config.debugLog('tilmeld', `User login switch for "${user.username}" to "${this.$data.username}".`);
425
+ // Authentication was successful, attempt to login.
426
+ if (!(await tilmeld.loginSwitch(this, true))) {
427
+ return { result: false, message: 'Incorrect login/password.' };
428
+ }
429
+ for (let callback of this.constructor
430
+ .afterLoginCallbacks) {
431
+ if (callback) {
432
+ await callback(this);
433
+ }
434
+ }
435
+ // Login was successful.
436
+ return { result: true, message: 'You are logged in.' };
437
+ }
438
+ /**
439
+ * Log a user out of the system.
440
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
441
+ */
226
442
  async $logout() {
227
- const tilmeld = this.$nymph.tilmeld;
443
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
228
444
  try {
229
445
  for (let callback of this.constructor
230
446
  .beforeLogoutCallbacks) {
@@ -239,7 +455,8 @@ class User extends AbleObject_1.default {
239
455
  message: e.message,
240
456
  };
241
457
  }
242
- tilmeld.logout();
458
+ this.$nymph.config.debugLog('tilmeld', `User logout for "${this.$data.username}".`);
459
+ await tilmeld.logout();
243
460
  for (let callback of this.constructor
244
461
  .afterLogoutCallbacks) {
245
462
  if (callback) {
@@ -259,52 +476,87 @@ class User extends AbleObject_1.default {
259
476
  (0, md5_1.default)(this.$data.email.trim().toLowerCase()).toString() +
260
477
  '?d=identicon&s=40');
261
478
  }
479
+ $getGid() {
480
+ if (this.$data.group) {
481
+ return this.$data.group.guid;
482
+ }
483
+ return null;
484
+ }
485
+ $getGids() {
486
+ if (this.$data.groups) {
487
+ return this.$data.groups
488
+ .map((group) => group.guid)
489
+ .filter((guid) => guid != null);
490
+ }
491
+ return null;
492
+ }
493
+ /**
494
+ * Get the user's group descendants.
495
+ */
262
496
  async $getDescendantGroups() {
263
497
  if (this.$descendantGroups == null) {
264
498
  this.$descendantGroups = [];
265
499
  if (this.$data.group != null) {
500
+ await this.$data.group.$wake();
266
501
  this.$descendantGroups = await this.$data.group.$getDescendants();
267
502
  }
503
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
268
504
  for (let curGroup of this.$data.groups ?? []) {
269
505
  this.$descendantGroups = this.$descendantGroups?.concat(await curGroup.$getDescendants());
270
506
  }
271
507
  }
272
508
  return this.$descendantGroups;
273
509
  }
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
510
  $jsonAcceptData(input, allowConflict = false) {
287
- const tilmeld = this.$nymph.tilmeld;
288
- this.$referenceWake();
511
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
512
+ this.$check();
289
513
  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.");
514
+ input.data.abilities?.includes('system/admin') !==
515
+ this.$data.abilities?.includes('system/admin') &&
516
+ (!tilmeld.currentUser ||
517
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
518
+ throw new errors_1.BadDataError("You don't have the authority to grant or revoke system/admin.");
519
+ }
520
+ if ('abilities' in input.data &&
521
+ input.data.abilities?.includes('tilmeld/admin') !==
522
+ this.$data.abilities?.includes('tilmeld/admin') &&
523
+ (!tilmeld.currentUser ||
524
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
525
+ throw new errors_1.BadDataError("You don't have the authority to grant or revoke tilmeld/admin.");
526
+ }
527
+ if ('abilities' in input.data &&
528
+ input.data.abilities?.includes('tilmeld/switch') !==
529
+ this.$data.abilities?.includes('tilmeld/switch') &&
530
+ (!tilmeld.currentUser ||
531
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
532
+ throw new errors_1.BadDataError("You don't have the authority to grant or revoke tilmeld/switch.");
295
533
  }
296
534
  this.$originalEmail = this.$data.email;
297
535
  super.$jsonAcceptData(input, allowConflict);
298
536
  }
299
537
  $jsonAcceptPatch(patch, allowConflict = false) {
300
- const tilmeld = this.$nymph.tilmeld;
301
- this.$referenceWake();
538
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
539
+ this.$check();
302
540
  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.");
541
+ patch.set.abilities?.includes('system/admin') !==
542
+ this.$data.abilities?.includes('system/admin') &&
543
+ (!tilmeld.currentUser ||
544
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
545
+ throw new errors_1.BadDataError("You don't have the authority to grant or revoke system/admin.");
546
+ }
547
+ if ('abilities' in patch.set &&
548
+ patch.set.abilities?.includes('tilmeld/admin') !==
549
+ this.$data.abilities?.includes('tilmeld/admin') &&
550
+ (!tilmeld.currentUser ||
551
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
552
+ throw new errors_1.BadDataError("You don't have the authority to grant or revoke tilmeld/admin.");
553
+ }
554
+ if ('abilities' in patch.set &&
555
+ patch.set.abilities?.includes('tilmeld/switch') !==
556
+ this.$data.abilities?.includes('tilmeld/switch') &&
557
+ (!tilmeld.currentUser ||
558
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
559
+ throw new errors_1.BadDataError("You don't have the authority to grant or revoke tilmeld/switch.");
308
560
  }
309
561
  this.$originalEmail = this.$data.email;
310
562
  super.$jsonAcceptPatch(patch, allowConflict);
@@ -313,8 +565,13 @@ class User extends AbleObject_1.default {
313
565
  super.$putData(data, sdata);
314
566
  this.$updateDataProtection();
315
567
  }
568
+ /**
569
+ * Update the data protection arrays for a user.
570
+ *
571
+ * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
572
+ */
316
573
  $updateDataProtection(givenUser) {
317
- const tilmeld = this.$nymph.tilmeld;
574
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
318
575
  let user = givenUser ?? tilmeld.User.current();
319
576
  this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
320
577
  this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
@@ -323,75 +580,146 @@ class User extends AbleObject_1.default {
323
580
  this.$privateData.push('username');
324
581
  }
325
582
  const isCurrentUser = user != null && user.$is(this);
583
+ // While loading, accessing abilities on the current user will infinitely
584
+ // loop, but if this is the current user, we can use its abilities.
326
585
  const abilities = (isCurrentUser || user == null
327
586
  ? this.$data.abilities
328
587
  : user?.abilities) ?? [];
329
588
  const isNewUser = this.guid == null;
330
589
  if (isCurrentUser) {
590
+ // Users can check to see what abilities they have.
331
591
  this.$clientEnabledMethods.push('$gatekeeper');
332
592
  this.$clientEnabledMethods.push('$changePassword');
593
+ this.$clientEnabledMethods.push('$revokeCurrentTokens');
333
594
  this.$clientEnabledMethods.push('$logout');
334
595
  this.$clientEnabledMethods.push('$sendEmailVerification');
596
+ this.$clientEnabledMethods.push('$hasTOTPSecret');
597
+ this.$clientEnabledMethods.push('$getNewTOTPSecret');
598
+ this.$clientEnabledMethods.push('$saveTOTPSecret');
599
+ this.$clientEnabledMethods.push('$removeTOTPSecret');
335
600
  }
336
601
  if (user != null &&
337
- (abilities.indexOf('tilmeld/admin') !== -1 ||
338
- abilities.indexOf('system/admin') !== -1)) {
339
- this.$privateData = ['password', 'salt'];
602
+ (abilities.includes('tilmeld/admin') ||
603
+ abilities.includes('system/admin'))) {
604
+ // Users who can edit other users can see most of their data.
605
+ this.$privateData = ['password', 'salt', 'totpSecret'];
606
+ this.$clientEnabledMethods.push('$hasTOTPSecret');
607
+ this.$clientEnabledMethods.push('$removeTOTPSecret');
340
608
  this.$allowlistData = undefined;
341
609
  }
342
610
  else if (isCurrentUser || isNewUser) {
611
+ // Users can see their own data, and edit some of it.
343
612
  if (tilmeld.config.allowUsernameChange || isNewUser) {
344
613
  this.$allowlistData.push('username');
345
614
  }
346
615
  this.$allowlistData.push('avatar');
347
- if (tilmeld.config.userFields.indexOf('name') !== -1) {
616
+ if (tilmeld.config.userFields.includes('name')) {
348
617
  this.$allowlistData.push('nameFirst');
349
618
  this.$allowlistData.push('nameMiddle');
350
619
  this.$allowlistData.push('nameLast');
351
620
  this.$allowlistData.push('name');
352
621
  }
353
- if (tilmeld.config.userFields.indexOf('email') !== -1) {
622
+ if (tilmeld.config.userFields.includes('email')) {
354
623
  this.$allowlistData.push('email');
355
624
  }
356
- if (tilmeld.config.userFields.indexOf('phone') !== -1) {
625
+ if (tilmeld.config.userFields.includes('phone')) {
357
626
  this.$allowlistData.push('phone');
358
627
  }
359
628
  this.$privateData = [
629
+ // The things that are commented are there to show what is _not_ private.
630
+ // 'abilities',
631
+ // 'username',
632
+ // 'nameFirst',
633
+ // 'nameMiddle',
634
+ // 'nameLast',
635
+ // 'name',
636
+ // 'email',
637
+ // 'avatar',
638
+ // 'phone',
639
+ // 'group',
640
+ // 'groups',
641
+ // 'inheritAbilities',
642
+ 'enabled',
360
643
  'secret',
644
+ 'emailChangeDate',
361
645
  'newEmailSecret',
362
- 'cancelEmailAddress',
646
+ // 'newEmailAddress',
363
647
  'cancelEmailSecret',
364
- 'emailChangeDate',
648
+ 'cancelEmailAddress',
365
649
  'recoverSecret',
366
650
  'recoverSecretDate',
367
- 'password',
368
651
  'salt',
652
+ 'password',
653
+ 'passwordTemp',
654
+ // 'revokeTokenDate',
655
+ 'totpSecret',
369
656
  ];
370
657
  }
658
+ if (user != null &&
659
+ !isCurrentUser &&
660
+ (abilities.includes('tilmeld/switch') ||
661
+ abilities.includes('system/admin'))) {
662
+ // Users with this ability can switch to other users.
663
+ this.$clientEnabledMethods.push('$switchUser');
664
+ }
665
+ }
666
+ $updateGroupDataProtection() {
667
+ if (this.$data.group != null && !this.$data.group.$asleep()) {
668
+ this.$data.group.$updateDataProtection();
669
+ }
670
+ for (let group of this.$data.groups ?? []) {
671
+ if (!group.$asleep()) {
672
+ group.$updateDataProtection();
673
+ }
674
+ }
371
675
  }
372
- $gatekeeper(ability) {
373
- const tilmeld = this.$nymph.tilmeld;
676
+ /**
677
+ * Check to see if a user has an ability.
678
+ *
679
+ * This function will check both user and group abilities, if the user is
680
+ * marked to inherit the abilities of its group.
681
+ *
682
+ * If `ability` is undefined, it will check to see if the user is currently
683
+ * logged in.
684
+ *
685
+ * If the user has the "system/admin" ability, this function will return true.
686
+ *
687
+ * @param ability The ability.
688
+ * @returns True or false.
689
+ */
690
+ async $gatekeeper(ability) {
691
+ await this.$wake();
692
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
374
693
  if (ability == null) {
375
694
  return tilmeld.User.current()?.$is(this) ?? false;
376
695
  }
696
+ // Check the cache to see if we've already checked this user.
377
697
  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 ?? []);
698
+ this.$gatekeeperCache = await this.$getGatekeeperCache();
699
+ }
700
+ return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
701
+ !!this.$data.abilities?.includes('system/admin'));
702
+ }
703
+ /**
704
+ * Build a gatekeeper cache object.
705
+ */
706
+ async $getGatekeeperCache() {
707
+ await this.$wake();
708
+ let abilities = this.$data.abilities ?? [];
709
+ if (this.$data.inheritAbilities) {
710
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
711
+ for (let curGroup of this.$data.groups ?? []) {
712
+ if (curGroup.enabled) {
713
+ abilities = abilities.concat(curGroup.abilities ?? []);
389
714
  }
390
715
  }
391
- this.$gatekeeperCache = Object.fromEntries(abilities.map((ability) => [ability, true]));
716
+ await this.$data.group?.$wake();
717
+ const group = this.$data.group;
718
+ if (group != null && group.cdate != null && group.enabled) {
719
+ abilities = abilities.concat(group.abilities ?? []);
720
+ }
392
721
  }
393
- return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
394
- !!this.$gatekeeperCache['system/admin']);
722
+ return Object.fromEntries(abilities.map((ability) => [ability, true]));
395
723
  }
396
724
  $clearCache() {
397
725
  const ret = super.$clearCache();
@@ -399,8 +727,13 @@ class User extends AbleObject_1.default {
399
727
  this.$gatekeeperCache = undefined;
400
728
  return ret;
401
729
  }
730
+ /**
731
+ * Send the user email verification/change/cancellation links.
732
+ *
733
+ * @returns True on success, false on failure.
734
+ */
402
735
  async $sendEmailVerification() {
403
- const tilmeld = this.$nymph.tilmeld;
736
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
404
737
  if (this.guid == null) {
405
738
  return false;
406
739
  }
@@ -463,18 +796,33 @@ class User extends AbleObject_1.default {
463
796
  }
464
797
  return success;
465
798
  }
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;
799
+ /**
800
+ * Add the user to a (secondary) group.
801
+ *
802
+ * @param group The group.
803
+ * @returns True if the user is already in the group. The resulting array of groups if the user was not.
804
+ */
805
+ async $addGroup(group) {
806
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
807
+ let groups = this.$data.groups;
808
+ if (groups == null) {
809
+ groups = [];
810
+ }
811
+ if (!group.$inArray(groups)) {
812
+ groups.push(group);
813
+ this.$data.groups = groups;
814
+ return groups;
473
815
  }
474
816
  return true;
475
817
  }
818
+ /**
819
+ * Check the given password against the user's.
820
+ *
821
+ * @param password The password in question.
822
+ * @returns True if the passwords match, otherwise false.
823
+ */
476
824
  $checkPassword(password) {
477
- const tilmeld = this.$nymph.tilmeld;
825
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
478
826
  switch (tilmeld.config.pwMethod) {
479
827
  case 'plain':
480
828
  return this.$data.password === password;
@@ -486,10 +834,18 @@ class User extends AbleObject_1.default {
486
834
  (0, sha256_1.default)(password + this.$data.salt).toString(enc_base64_1.default));
487
835
  }
488
836
  }
489
- $delGroup(group) {
490
- if (this.$data.groups != null && group.$inArray(this.$data.groups)) {
837
+ /**
838
+ * Remove the user from a (secondary) group.
839
+ *
840
+ * @param group The group.
841
+ * @returns True if the user wasn't in the group. The resulting array of groups if the user was.
842
+ */
843
+ async $delGroup(group) {
844
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
845
+ let groups = this.$data.groups || [];
846
+ if (this.$data.groups != null && group.$inArray(groups)) {
491
847
  const newGroups = [];
492
- for (let curGroup of this.$data.groups) {
848
+ for (let curGroup of groups) {
493
849
  if (!group.$is(curGroup)) {
494
850
  newGroups.push(curGroup);
495
851
  }
@@ -499,7 +855,13 @@ class User extends AbleObject_1.default {
499
855
  }
500
856
  return true;
501
857
  }
502
- $inGroup(group) {
858
+ /**
859
+ * Check whether the user is in a (primary or secondary) group.
860
+ *
861
+ * @param group The group, or the group's GUID.
862
+ * @returns True or false.
863
+ */
864
+ async $inGroup(group) {
503
865
  if (typeof group === 'string') {
504
866
  if (this.$data.group?.guid === group) {
505
867
  return true;
@@ -513,35 +875,66 @@ class User extends AbleObject_1.default {
513
875
  }
514
876
  return (group.$is(this.$data.group) || group.$inArray(this.$data.groups ?? []));
515
877
  }
516
- $isDescendant(group) {
517
- if (this.$data.group?.cdate != null &&
518
- this.$data.group.$isDescendant(group)) {
878
+ /**
879
+ * Check whether the user is a descendant of a group.
880
+ *
881
+ * @param group The group, or the group's GUID.
882
+ * @returns True or false.
883
+ */
884
+ async $isDescendant(group) {
885
+ // Check to see if the user is in a descendant group of the given group.
886
+ await this.$data.group?.$wake();
887
+ if (this.$data.group != null &&
888
+ this.$data.group?.cdate != null &&
889
+ (await this.$data.group?.$isDescendant(group))) {
519
890
  return true;
520
891
  }
892
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
521
893
  for (let curGroup of this.$data.groups ?? []) {
522
- if (curGroup.$isDescendant(group)) {
894
+ if (await curGroup.$isDescendant(group)) {
523
895
  return true;
524
896
  }
525
897
  }
526
898
  return false;
527
899
  }
900
+ /**
901
+ * A frontend accessible method to change the user's password.
902
+ *
903
+ * @param data The input data from the client.
904
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
905
+ */
528
906
  async $changePassword(data) {
907
+ this.$nymph.config.debugLog('tilmeld', `Password change request for "${this.$data.username}".`);
529
908
  if (!('newPassword' in data) || !data.newPassword.length) {
530
909
  return { result: false, message: 'Please specify a password.' };
531
910
  }
532
911
  if (!this.$checkPassword(data.currentPassword ?? '')) {
912
+ this.$nymph.config.debugLog('tilmeld', `Incorrect password for change request for "${this.$data.username}".`);
533
913
  return { result: false, message: 'Incorrect password.' };
534
914
  }
535
915
  this.$data.passwordTemp = data.newPassword;
916
+ if (data.revokeCurrentTokens) {
917
+ this.$data.revokeTokenDate = Date.now();
918
+ }
536
919
  if (await this.$save()) {
920
+ if (data.revokeCurrentTokens) {
921
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
922
+ await tilmeld.login(this, true);
923
+ }
537
924
  return { result: true, message: 'Your password has been changed.' };
538
925
  }
539
926
  else {
540
927
  return { result: false, message: "Couldn't save new password." };
541
928
  }
542
929
  }
930
+ /**
931
+ * Change the user's password.
932
+ *
933
+ * @param password The new password.
934
+ * @returns The resulting password or hash which is stored in the entity.
935
+ */
543
936
  $password(password) {
544
- const tilmeld = this.$nymph.tilmeld;
937
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
545
938
  switch (tilmeld.config.pwMethod) {
546
939
  case 'plain':
547
940
  delete this.$data.salt;
@@ -559,14 +952,194 @@ class User extends AbleObject_1.default {
559
952
  }
560
953
  return this.$data.password;
561
954
  }
955
+ /**
956
+ * A frontend accessible method to revoke all currently issued tokens.
957
+ *
958
+ * @param data The input data from the client.
959
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
960
+ */
961
+ async $revokeCurrentTokens(data) {
962
+ if (!this.$checkPassword(data.password ?? '')) {
963
+ return { result: false, message: 'Incorrect password.' };
964
+ }
965
+ this.$data.revokeTokenDate = Date.now();
966
+ if (await this.$save()) {
967
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
968
+ await tilmeld.login(this, true);
969
+ return {
970
+ result: true,
971
+ message: 'You have logged out of all other sessions.',
972
+ };
973
+ }
974
+ else {
975
+ return { result: false, message: "Couldn't save revocation date." };
976
+ }
977
+ }
978
+ async $hasTOTPSecret() {
979
+ return this.$data.totpSecret != null;
980
+ }
981
+ /**
982
+ * Check the given code against the user's TOTP secret.
983
+ *
984
+ * @param code The code in question.
985
+ * @returns True if the code is valid, otherwise false.
986
+ */
987
+ $checkTOTPCode(code) {
988
+ if (this.$data.totpSecret == null) {
989
+ return false;
990
+ }
991
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
992
+ const totp = new otpauth_1.TOTP({
993
+ issuer: tilmeld.config.appName,
994
+ label: this.$data.username,
995
+ algorithm: 'SHA1',
996
+ digits: 6,
997
+ period: 30,
998
+ secret: this.$data.totpSecret,
999
+ });
1000
+ return totp.validate({ token: code, window: 2 }) != null;
1001
+ }
1002
+ /**
1003
+ * A frontend accessible method to generate a new TOTP secret.
1004
+ *
1005
+ * @returns An object with 'uri', 'qrcode', and 'secret'.
1006
+ */
1007
+ async $getNewTOTPSecret() {
1008
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
1009
+ if (this.$data.totpSecret != null) {
1010
+ throw new errors_1.BadDataError('You already have a 2FA secret.');
1011
+ }
1012
+ const secret = new otpauth_1.Secret();
1013
+ const totp = new otpauth_1.TOTP({
1014
+ issuer: tilmeld.config.appName,
1015
+ label: this.$data.username,
1016
+ algorithm: 'SHA1',
1017
+ digits: 6,
1018
+ period: 30,
1019
+ secret,
1020
+ });
1021
+ const uri = totp.toString();
1022
+ const qrcode = await (0, qrcode_1.toDataURL)(uri);
1023
+ return {
1024
+ uri,
1025
+ qrcode,
1026
+ secret: secret.base32,
1027
+ };
1028
+ }
1029
+ /**
1030
+ * A frontend accessible method to save a TOTP secret into the user's account.
1031
+ *
1032
+ * @param data The input data from the client.
1033
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1034
+ */
1035
+ async $saveTOTPSecret(data) {
1036
+ if (!this.$checkPassword(data.password ?? '')) {
1037
+ return { result: false, message: 'Incorrect password.' };
1038
+ }
1039
+ if (this.$data.totpSecret != null) {
1040
+ return { result: false, message: 'You already have a 2FA secret.' };
1041
+ }
1042
+ if (data.secret == null ||
1043
+ typeof data.secret !== 'string' ||
1044
+ data.secret === '') {
1045
+ return { result: false, message: '2FA secret is invalid.' };
1046
+ }
1047
+ if (data.code == null ||
1048
+ typeof data.code !== 'string' ||
1049
+ data.code.length !== 6) {
1050
+ return { result: false, message: '2FA code is invalid.' };
1051
+ }
1052
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
1053
+ const totp = new otpauth_1.TOTP({
1054
+ issuer: tilmeld.config.appName,
1055
+ label: this.$data.username,
1056
+ algorithm: 'SHA1',
1057
+ digits: 6,
1058
+ period: 30,
1059
+ secret: data.secret,
1060
+ });
1061
+ if (totp.validate({ token: data.code, window: 2 }) == null) {
1062
+ return { result: false, message: '2FA code is incorrect.' };
1063
+ }
1064
+ this.$data.totpSecret = data.secret;
1065
+ if (await this.$save()) {
1066
+ return { result: true, message: 'Your two factor secret is now set.' };
1067
+ }
1068
+ else {
1069
+ return { result: false, message: "Couldn't save two factor secret." };
1070
+ }
1071
+ }
1072
+ /**
1073
+ * A frontend accessible method to remove the TOTP secret from the user's
1074
+ * account.
1075
+ *
1076
+ * @param data The input data from the client.
1077
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1078
+ */
1079
+ async $removeTOTPSecret(data) {
1080
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
1081
+ if (data) {
1082
+ if (!this.$checkPassword(data.password ?? '')) {
1083
+ return { result: false, message: 'Incorrect password.' };
1084
+ }
1085
+ if (data.code == null ||
1086
+ typeof data.code !== 'string' ||
1087
+ data.code.length !== 6) {
1088
+ return { result: false, message: '2FA code is invalid.' };
1089
+ }
1090
+ if (this.$data.totpSecret == null) {
1091
+ return { result: false, message: "You don't have a 2FA secret." };
1092
+ }
1093
+ const totp = new otpauth_1.TOTP({
1094
+ issuer: tilmeld.config.appName,
1095
+ label: this.$data.username,
1096
+ algorithm: 'SHA1',
1097
+ digits: 6,
1098
+ period: 30,
1099
+ secret: this.$data.totpSecret,
1100
+ });
1101
+ if (totp.validate({ token: data.code, window: 2 }) == null) {
1102
+ return { result: false, message: '2FA code is incorrect.' };
1103
+ }
1104
+ }
1105
+ else if (!tilmeld.gatekeeper('tilmeld/admin')) {
1106
+ return {
1107
+ result: false,
1108
+ message: 'You must provide your password and 2FA code.',
1109
+ };
1110
+ }
1111
+ delete this.$data.totpSecret;
1112
+ if (await this.$save()) {
1113
+ return {
1114
+ result: true,
1115
+ message: 'Two factor secret has been removed.',
1116
+ };
1117
+ }
1118
+ else {
1119
+ return { result: false, message: "Couldn't remove two factor secret." };
1120
+ }
1121
+ }
1122
+ /**
1123
+ * Check that a username is valid.
1124
+ *
1125
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1126
+ */
562
1127
  async $checkUsername() {
563
- const tilmeld = this.$nymph.tilmeld;
1128
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
1129
+ this.$nymph.config.debugInfo('tilmeld', `Username check "${this.$data.username}".`);
564
1130
  if (!tilmeld.config.emailUsernames) {
565
1131
  if (this.$data.username == null || !this.$data.username.length) {
566
1132
  return { result: false, message: 'Please specify a username.' };
567
1133
  }
568
- if (tilmeld.config.maxUsernameLength > 0 &&
569
- this.$data.username.length > tilmeld.config.maxUsernameLength) {
1134
+ if (this.$data.username.length < tilmeld.config.minUsernameLength) {
1135
+ return {
1136
+ result: false,
1137
+ message: 'Usernames must be at least ' +
1138
+ tilmeld.config.minUsernameLength +
1139
+ ' characters.',
1140
+ };
1141
+ }
1142
+ if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
570
1143
  return {
571
1144
  result: false,
572
1145
  message: 'Usernames must not exceed ' +
@@ -600,6 +1173,17 @@ class User extends AbleObject_1.default {
600
1173
  if (test != null) {
601
1174
  return { result: false, message: 'That username is taken.' };
602
1175
  }
1176
+ for (let callback of this.constructor
1177
+ .checkUsernameCallbacks) {
1178
+ if (callback) {
1179
+ const result = await callback(this, {
1180
+ username: this.$data.username,
1181
+ });
1182
+ if (!result.result) {
1183
+ return { message: 'Username is not available.', ...result };
1184
+ }
1185
+ }
1186
+ }
603
1187
  return {
604
1188
  result: true,
605
1189
  message: this.guid != null ? 'Username is valid.' : 'Username is available!',
@@ -610,8 +1194,15 @@ class User extends AbleObject_1.default {
610
1194
  if (this.$data.username == null || !this.$data.username.length) {
611
1195
  return { result: false, message: 'Please specify an email.' };
612
1196
  }
613
- if (tilmeld.config.maxUsernameLength > 0 &&
614
- this.$data.username.length > tilmeld.config.maxUsernameLength) {
1197
+ if (this.$data.username.length < tilmeld.config.minUsernameLength) {
1198
+ return {
1199
+ result: false,
1200
+ message: 'Emails must be at least ' +
1201
+ tilmeld.config.minUsernameLength +
1202
+ ' characters.',
1203
+ };
1204
+ }
1205
+ if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
615
1206
  return {
616
1207
  result: false,
617
1208
  message: 'Emails must not exceed ' +
@@ -622,8 +1213,14 @@ class User extends AbleObject_1.default {
622
1213
  return await this.$checkEmail();
623
1214
  }
624
1215
  }
1216
+ /**
1217
+ * Check that an email is unique.
1218
+ *
1219
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1220
+ */
625
1221
  async $checkEmail() {
626
- const tilmeld = this.$nymph.tilmeld;
1222
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
1223
+ this.$nymph.config.debugInfo('tilmeld', `Email check "${this.$data.email}".`);
627
1224
  if (this.$data.email == null || !this.$data.email.length) {
628
1225
  if (tilmeld.config.verifyEmail) {
629
1226
  return { result: false, message: 'Please specify an email.' };
@@ -657,8 +1254,14 @@ class User extends AbleObject_1.default {
657
1254
  message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
658
1255
  };
659
1256
  }
1257
+ /**
1258
+ * Check that a phone number is unique.
1259
+ *
1260
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
1261
+ */
660
1262
  async $checkPhone() {
661
- const tilmeld = this.$nymph.tilmeld;
1263
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
1264
+ this.$nymph.config.debugInfo('tilmeld', `Phone check "${this.$data.phone}".`);
662
1265
  if (this.$data.phone == null || !this.$data.phone.length) {
663
1266
  return { result: false, message: 'Please specify a phone number.' };
664
1267
  }
@@ -686,7 +1289,7 @@ class User extends AbleObject_1.default {
686
1289
  };
687
1290
  }
688
1291
  async $register(data) {
689
- let tilmeld = this.$nymph.tilmeld;
1292
+ let tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
690
1293
  if (!tilmeld.config.allowRegistration) {
691
1294
  return {
692
1295
  result: false,
@@ -701,21 +1304,6 @@ class User extends AbleObject_1.default {
701
1304
  message: 'This is already a registered user.',
702
1305
  };
703
1306
  }
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
1307
  if (!('password' in data) || !data.password.length) {
720
1308
  return {
721
1309
  result: false,
@@ -728,7 +1316,7 @@ class User extends AbleObject_1.default {
728
1316
  return { ...unCheck, loggedin: false };
729
1317
  }
730
1318
  this.$password(data.password);
731
- if (tilmeld.config.regFields.indexOf('name') !== -1) {
1319
+ if (tilmeld.config.regFields.includes('name')) {
732
1320
  this.$data.name =
733
1321
  this.$data.nameFirst +
734
1322
  (this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
@@ -740,59 +1328,35 @@ class User extends AbleObject_1.default {
740
1328
  if (tilmeld.config.emailUsernames) {
741
1329
  this.$data.email = this.$data.username;
742
1330
  }
1331
+ // Start transaction.
743
1332
  const transaction = 'tilmeld-register-' + (0, guid_1.nanoid)();
744
1333
  const nymph = this.$nymph;
745
1334
  const tnymph = await this.$nymph.startTransaction(transaction);
746
- this.$nymph = tnymph;
747
- tilmeld = this.$nymph.tilmeld;
1335
+ this.$setNymph(tnymph);
1336
+ tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
1337
+ let message = '';
1338
+ let loggedin = false;
748
1339
  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;
1340
+ try {
1341
+ for (let callback of this.constructor
1342
+ .beforeRegisterCallbacks) {
1343
+ if (callback) {
1344
+ await callback(this, data);
1345
+ }
781
1346
  }
782
1347
  }
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
- });
1348
+ catch (e) {
1349
+ await tnymph.rollback(transaction);
1350
+ this.$setNymph(nymph);
1351
+ return {
1352
+ result: false,
1353
+ loggedin: false,
1354
+ message: e.message,
1355
+ };
794
1356
  }
795
- if (tilmeld.config.verifyEmail) {
1357
+ if (tilmeld.config.userFields.includes('email') &&
1358
+ tilmeld.config.verifyEmail) {
1359
+ // The user will be enabled after verifying their e-mail address.
796
1360
  if (!tilmeld.config.unverifiedAccess) {
797
1361
  this.$data.enabled = false;
798
1362
  }
@@ -800,6 +1364,8 @@ class User extends AbleObject_1.default {
800
1364
  else {
801
1365
  this.$data.enabled = true;
802
1366
  }
1367
+ // If createAdmin is true and there are no other users, grant
1368
+ // "system/admin".
803
1369
  let madeAdmin = false;
804
1370
  if (tilmeld.config.createAdmin) {
805
1371
  const otherUsers = await tnymph.getEntities({
@@ -808,13 +1374,17 @@ class User extends AbleObject_1.default {
808
1374
  limit: 1,
809
1375
  return: 'guid',
810
1376
  });
1377
+ // Make sure it's not just null, cause that means an error.
811
1378
  if (!otherUsers.length) {
812
1379
  this.$grant('system/admin');
813
1380
  this.$data.enabled = true;
814
1381
  madeAdmin = true;
815
1382
  }
816
1383
  }
1384
+ this.$nymph.config.debugLog('tilmeld', `Registering new user "${this.$data.username}".`);
817
1385
  if (await this.$saveSkipAC()) {
1386
+ this.$nymph.config.debugLog('tilmeld', `New user registered "${this.$data.username}".`);
1387
+ // Send the new user registered email.
818
1388
  if (tilmeld.config.userRegisteredRecipient != null) {
819
1389
  await tilmeld.config.sendEmail(tilmeld, {
820
1390
  template: 'UserRegistered',
@@ -831,31 +1401,20 @@ class User extends AbleObject_1.default {
831
1401
  },
832
1402
  }, this);
833
1403
  }
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
- }
1404
+ // Finish up.
848
1405
  if (tilmeld.config.verifyEmail &&
849
1406
  !tilmeld.config.unverifiedAccess &&
850
- !madeAdmin) {
1407
+ !madeAdmin &&
1408
+ tilmeld.config.userFields.includes('email')) {
851
1409
  message +=
852
1410
  `Almost there. An email has been sent to ${this.$data.email} ` +
853
1411
  'with a verification link for you to finish registration.';
854
1412
  }
855
1413
  else if (tilmeld.config.verifyEmail &&
856
1414
  tilmeld.config.unverifiedAccess &&
857
- !madeAdmin) {
858
- if (!tilmeld.login(this, true)) {
1415
+ !madeAdmin &&
1416
+ tilmeld.config.userFields.includes('email')) {
1417
+ if (!(await tilmeld.login(this, true))) {
859
1418
  throw new Error('An error occurred trying to log you in.');
860
1419
  }
861
1420
  message +=
@@ -865,7 +1424,7 @@ class User extends AbleObject_1.default {
865
1424
  loggedin = true;
866
1425
  }
867
1426
  else {
868
- if (!tilmeld.login(this, true)) {
1427
+ if (!(await tilmeld.login(this, true))) {
869
1428
  throw new Error('An error occurred trying to log you in.');
870
1429
  }
871
1430
  message += "You're now registered and logged in!";
@@ -880,23 +1439,11 @@ class User extends AbleObject_1.default {
880
1439
  });
881
1440
  }
882
1441
  }
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
1442
  }
897
1443
  else {
1444
+ this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}".`);
898
1445
  await tnymph.rollback(transaction);
899
- this.$nymph = nymph;
1446
+ this.$setNymph(nymph);
900
1447
  return {
901
1448
  result: false,
902
1449
  loggedin: false,
@@ -905,22 +1452,38 @@ class User extends AbleObject_1.default {
905
1452
  }
906
1453
  }
907
1454
  catch (e) {
1455
+ this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}": ${e}`);
908
1456
  await tnymph.rollback(transaction);
909
- this.$nymph = nymph;
1457
+ this.$setNymph(nymph);
910
1458
  throw e;
911
1459
  }
1460
+ try {
1461
+ await tnymph.commit(transaction);
1462
+ this.$setNymph(nymph);
1463
+ await this.$nymph.tilmeld?.fillSession(this);
1464
+ }
1465
+ catch (e) {
1466
+ throw e;
1467
+ }
1468
+ return {
1469
+ result: true,
1470
+ loggedin,
1471
+ message,
1472
+ };
912
1473
  }
913
1474
  async $save() {
914
- let tilmeld = this.$nymph.tilmeld;
1475
+ let tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
915
1476
  if (this.$data.username == null || !this.$data.username.trim().length) {
916
1477
  return false;
917
1478
  }
918
- if (tilmeld.gatekeeper('tilmeld/admin') &&
1479
+ if (!this.$skipAcWhenSaving &&
1480
+ tilmeld.gatekeeper('tilmeld/admin') &&
919
1481
  !tilmeld.gatekeeper('system/admin') &&
920
- this.$gatekeeper('system/admin')) {
1482
+ this.$data.abilities?.includes('system/admin')) {
921
1483
  throw new errors_1.BadDataError("You don't have the authority to modify system admins.");
922
1484
  }
923
1485
  let sendVerification = false;
1486
+ // Formatting.
924
1487
  this.$data.username = this.$data.username.trim();
925
1488
  if (tilmeld.config.emailUsernames) {
926
1489
  this.$data.email = this.$data.username;
@@ -934,49 +1497,63 @@ class User extends AbleObject_1.default {
934
1497
  (this.$data.nameMiddle ? ' ' + this.$data.nameMiddle : '') +
935
1498
  (this.$data.nameLast ? ' ' + this.$data.nameLast : '');
936
1499
  this.$data.enabled = !!this.$data.enabled;
937
- if (this.$data.nameFirst === '') {
1500
+ // Clear empty values.
1501
+ if (this.$data.nameFirst === '' ||
1502
+ !tilmeld.config.userFields.includes('name')) {
938
1503
  delete this.$data.nameFirst;
939
1504
  }
940
- if (this.$data.nameMiddle === '') {
1505
+ if (this.$data.nameMiddle === '' ||
1506
+ !tilmeld.config.userFields.includes('name')) {
941
1507
  delete this.$data.nameMiddle;
942
1508
  }
943
- if (this.$data.nameLast === '') {
1509
+ if (this.$data.nameLast === '' ||
1510
+ !tilmeld.config.userFields.includes('name')) {
944
1511
  delete this.$data.nameLast;
945
1512
  }
946
- if (this.$data.name === '') {
1513
+ if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
947
1514
  delete this.$data.name;
948
1515
  }
949
- if (this.$data.email === '') {
1516
+ if (this.$data.email === '' ||
1517
+ !tilmeld.config.userFields.includes('email')) {
950
1518
  delete this.$data.email;
951
1519
  }
952
1520
  if (this.$data.avatar === '') {
953
1521
  delete this.$data.avatar;
954
1522
  }
955
- if (this.$data.phone === '') {
1523
+ if (this.$data.phone === '' ||
1524
+ !tilmeld.config.userFields.includes('phone')) {
956
1525
  delete this.$data.phone;
957
1526
  }
958
- if (this.$data.secret === '') {
1527
+ if (this.$data.secret === '' ||
1528
+ !tilmeld.config.userFields.includes('email')) {
959
1529
  delete this.$data.secret;
960
1530
  }
961
- if (this.$data.emailChangeDate === 0) {
1531
+ if (this.$data.emailChangeDate === 0 ||
1532
+ !tilmeld.config.userFields.includes('email')) {
962
1533
  delete this.$data.emailChangeDate;
963
1534
  }
964
- if (this.$data.newEmailSecret === '') {
1535
+ if (this.$data.newEmailSecret === '' ||
1536
+ !tilmeld.config.userFields.includes('email')) {
965
1537
  delete this.$data.newEmailSecret;
966
1538
  }
967
- if (this.$data.newEmailAddress === '') {
1539
+ if (this.$data.newEmailAddress === '' ||
1540
+ !tilmeld.config.userFields.includes('email')) {
968
1541
  delete this.$data.newEmailAddress;
969
1542
  }
970
- if (this.$data.cancelEmailSecret === '') {
1543
+ if (this.$data.cancelEmailSecret === '' ||
1544
+ !tilmeld.config.userFields.includes('email')) {
971
1545
  delete this.$data.cancelEmailSecret;
972
1546
  }
973
- if (this.$data.cancelEmailAddress === '') {
1547
+ if (this.$data.cancelEmailAddress === '' ||
1548
+ !tilmeld.config.userFields.includes('email')) {
974
1549
  delete this.$data.cancelEmailAddress;
975
1550
  }
976
- if (this.$data.recoverSecret === '') {
1551
+ if (this.$data.recoverSecret === '' ||
1552
+ !tilmeld.config.userFields.includes('email')) {
977
1553
  delete this.$data.recoverSecret;
978
1554
  }
979
- if (this.$data.recoverSecretDate === 0) {
1555
+ if (this.$data.recoverSecretDate === 0 ||
1556
+ !tilmeld.config.userFields.includes('email')) {
980
1557
  delete this.$data.recoverSecretDate;
981
1558
  }
982
1559
  if (this.$data.password === '') {
@@ -988,26 +1565,36 @@ class User extends AbleObject_1.default {
988
1565
  if (this.$data.inheritAbilities === false) {
989
1566
  delete this.$data.inheritAbilities;
990
1567
  }
1568
+ // Verification.
991
1569
  const unCheck = await this.$checkUsername();
992
1570
  if (!unCheck.result) {
993
1571
  throw new errors_1.BadUsernameError(unCheck.message);
994
1572
  }
995
- if (!tilmeld.config.emailUsernames) {
1573
+ if (!tilmeld.config.emailUsernames &&
1574
+ tilmeld.config.userFields.includes('email')) {
996
1575
  const emCheck = await this.$checkEmail();
997
1576
  if (!emCheck.result) {
998
1577
  throw new errors_1.BadEmailError(emCheck.message);
999
1578
  }
1000
1579
  }
1001
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
1580
+ // Email changes.
1581
+ if (!tilmeld.gatekeeper('tilmeld/admin') &&
1582
+ tilmeld.config.userFields.includes('email')) {
1583
+ // The user isn't an admin, so email address changes should contain some
1584
+ // security measures.
1002
1585
  if (tilmeld.config.verifyEmail) {
1586
+ // The user needs to verify this new email address.
1003
1587
  if (this.guid == null) {
1004
- if ((this.$data.abilities?.indexOf('system/admin') ?? -1) === -1) {
1588
+ // If this is the first user, they'll be made an admin, so they don't
1589
+ // need to verify.
1590
+ if (!this.$data.abilities?.includes('system/admin')) {
1005
1591
  this.$data.secret = (0, guid_1.nanoid)();
1006
1592
  sendVerification = true;
1007
1593
  }
1008
1594
  }
1009
1595
  else if (this.$originalEmail != null &&
1010
1596
  this.$originalEmail !== this.$data.email) {
1597
+ // The user already has an old email address.
1011
1598
  if (tilmeld.config.emailRateLimit !== '' &&
1012
1599
  this.$data.emailChangeDate != null &&
1013
1600
  this.$data.emailChangeDate >=
@@ -1019,13 +1606,18 @@ class User extends AbleObject_1.default {
1019
1606
  else {
1020
1607
  if (this.$data.newEmailSecret == null &&
1021
1608
  tilmeld.config.emailRateLimit !== '' &&
1609
+ // Make sure the user has at least the rate limit time to cancel
1610
+ // an email change.
1022
1611
  (this.$data.emailChangeDate == null ||
1023
1612
  this.$data.emailChangeDate <
1024
1613
  (0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
1614
+ // Save the old email in case the cancel change link is clicked.
1025
1615
  this.$data.cancelEmailAddress = this.$originalEmail;
1026
1616
  this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
1027
1617
  this.$data.emailChangeDate = Date.now();
1028
1618
  }
1619
+ // Save the new address and reset to the old one (until they verify
1620
+ // it).
1029
1621
  this.$data.newEmailAddress = this.$data.email;
1030
1622
  this.$data.email = this.$originalEmail;
1031
1623
  if (tilmeld.config.emailUsernames) {
@@ -1039,9 +1631,13 @@ class User extends AbleObject_1.default {
1039
1631
  else if (this.guid != null &&
1040
1632
  this.$originalEmail != null &&
1041
1633
  this.$originalEmail !== this.$data.email &&
1634
+ // Make sure the user has at least the rate limit time to cancel an
1635
+ // email change.
1042
1636
  (this.$data.emailChangeDate == null ||
1043
1637
  this.$data.emailChangeDate <
1044
1638
  (0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
1639
+ // The user doesn't need to verify their new email address, but should
1640
+ // be able to cancel the email change from their old address.
1045
1641
  this.$data.cancelEmailAddress = this.$originalEmail;
1046
1642
  this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
1047
1643
  sendVerification = true;
@@ -1054,70 +1650,165 @@ class User extends AbleObject_1.default {
1054
1650
  if (this.$data.password == null) {
1055
1651
  throw new errors_1.BadDataError('A password is required.');
1056
1652
  }
1057
- try {
1058
- tilmeld.config.validatorUser(this);
1059
- }
1060
- catch (e) {
1061
- throw new errors_1.BadDataError(e?.message);
1062
- }
1653
+ // Start transaction.
1063
1654
  const transaction = 'tilmeld-save-' + (0, guid_1.nanoid)();
1064
1655
  const nymph = this.$nymph;
1065
1656
  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)) {
1657
+ this.$setNymph(tnymph);
1658
+ tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
1659
+ // If the primary group is generated now, the user assigned to it won't have
1660
+ // a guid yet, so it needs to be assigned after the user is saved.
1661
+ let saveGeneratedPrimaryGroupAgain = false;
1662
+ try {
1663
+ let group = this.$data.group;
1664
+ await group?.$wake();
1665
+ if (group == null && this.guid == null) {
1666
+ if (tilmeld.config.generatePrimary) {
1667
+ // Generate a new primary group for the user.
1668
+ group = await tilmeld.Group.factory();
1669
+ // This user doesn't have a guid yet, so that's why we set
1670
+ // saveGeneratedPrimaryGroupAgain.
1671
+ group.user = this;
1672
+ saveGeneratedPrimaryGroupAgain = true;
1673
+ const parent = await tnymph.getEntity({ class: tilmeld.Group }, {
1674
+ type: '&',
1675
+ equal: ['defaultPrimary', true],
1676
+ });
1677
+ if (parent != null) {
1678
+ group.parent = parent;
1679
+ }
1680
+ }
1681
+ else {
1682
+ // Add the default primary.
1683
+ const group = await tnymph.getEntity({ class: tilmeld.Group }, {
1684
+ type: '&',
1685
+ equal: ['defaultPrimary', true],
1686
+ });
1687
+ if (group != null) {
1688
+ this.$data.group = group;
1689
+ }
1690
+ }
1691
+ }
1692
+ if (group != null && group.user != null && this.$is(group.user)) {
1693
+ // Update the user's generated primary group.
1694
+ let changed = false;
1695
+ if (group.groupname !== this.$data.username) {
1696
+ group.groupname = this.$data.username;
1697
+ changed = true;
1698
+ }
1699
+ if (group.avatar !== this.$data.avatar) {
1700
+ group.avatar = this.$data.avatar;
1701
+ changed = true;
1702
+ }
1703
+ if (tilmeld.config.userFields.includes('email') &&
1704
+ group.email !== this.$data.email) {
1705
+ group.email = this.$data.email;
1706
+ changed = true;
1707
+ }
1708
+ if (tilmeld.config.userFields.includes('name') &&
1709
+ group.name !== this.$data.name) {
1710
+ group.name = this.$data.name;
1711
+ changed = true;
1712
+ }
1713
+ if (tilmeld.config.userFields.includes('phone') &&
1714
+ group.phone !== this.$data.phone) {
1715
+ group.phone = this.$data.phone;
1716
+ changed = true;
1717
+ }
1718
+ if (changed) {
1719
+ if (!(await group.$saveSkipAC())) {
1720
+ throw Error('Error updating primary group for user.');
1721
+ }
1722
+ }
1723
+ this.$data.group = group;
1724
+ }
1725
+ if (this.$data.groups == null || this.$data.groups.length === 0) {
1726
+ // Add secondary groups.
1727
+ if (tilmeld.config.userFields.includes('email') &&
1728
+ tilmeld.config.verifyEmail &&
1729
+ tilmeld.config.unverifiedAccess &&
1730
+ this.$data.secret != null) {
1731
+ // Add the default secondaries for unverified users.
1732
+ this.$data.groups = await tnymph.getEntities({ class: tilmeld.Group }, {
1733
+ type: '&',
1734
+ equal: ['unverifiedSecondary', true],
1735
+ });
1736
+ }
1737
+ else {
1738
+ // Add the default secondaries.
1739
+ this.$data.groups = await tnymph.getEntities({ class: tilmeld.Group }, {
1740
+ type: '&',
1741
+ equal: ['defaultSecondary', true],
1742
+ });
1743
+ }
1744
+ }
1071
1745
  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();
1746
+ tilmeld.config.validatorUser(tilmeld, this);
1078
1747
  }
1079
1748
  catch (e) {
1080
- await tnymph.rollback(transaction);
1081
- this.$nymph = nymph;
1082
- throw e;
1749
+ throw new errors_1.BadDataError(e?.message);
1083
1750
  }
1084
1751
  }
1752
+ catch (e) {
1753
+ await tnymph.rollback(transaction);
1754
+ this.$setNymph(nymph);
1755
+ throw e;
1756
+ }
1085
1757
  let ret;
1086
1758
  let preGuid = this.guid;
1087
1759
  let preCdate = this.cdate;
1088
1760
  let preMdate = this.mdate;
1089
1761
  try {
1090
1762
  ret = await super.$save();
1763
+ if (ret && saveGeneratedPrimaryGroupAgain) {
1764
+ const group = this.$data.group;
1765
+ if (group == null) {
1766
+ throw Error('Generated primary group not found.');
1767
+ }
1768
+ await group.$wake();
1769
+ // Now this user has a GUID, so saving it on the group will succeed.
1770
+ group.user = this;
1771
+ if (!(await group.$saveSkipAC())) {
1772
+ throw Error("Generated primary group couldn't be saved.");
1773
+ }
1774
+ this.$data.group = group;
1775
+ }
1091
1776
  }
1092
1777
  catch (e) {
1093
1778
  await tnymph.rollback(transaction);
1094
- this.$nymph = nymph;
1779
+ this.$setNymph(nymph);
1095
1780
  throw e;
1096
1781
  }
1097
1782
  if (ret) {
1098
1783
  if (sendVerification) {
1784
+ // The email has changed, so send a new verification email.
1099
1785
  if (!(await this.$sendEmailVerification())) {
1100
1786
  await tnymph.rollback(transaction);
1101
1787
  this.guid = preGuid;
1102
1788
  this.cdate = preCdate;
1103
1789
  this.mdate = preMdate;
1104
- this.$nymph = nymph;
1790
+ this.$setNymph(nymph);
1105
1791
  throw new Error("Couldn't send verification email.");
1106
1792
  }
1107
1793
  }
1108
1794
  this.$descendantGroups = undefined;
1109
1795
  this.$gatekeeperCache = undefined;
1110
1796
  await tnymph.commit(transaction);
1111
- if (nymph.tilmeld.User.current(true).$is(this)) {
1112
- nymph.tilmeld.fillSession(this);
1797
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
1798
+ if (tilmeld.User.current(true).$is(this)) {
1799
+ // Update the user in the session cache.
1800
+ await tilmeld.fillSession(this);
1113
1801
  }
1114
1802
  }
1115
1803
  else {
1116
1804
  await tnymph.rollback(transaction);
1117
1805
  }
1118
- this.$nymph = nymph;
1806
+ this.$setNymph(nymph);
1119
1807
  return ret;
1120
1808
  }
1809
+ /**
1810
+ * This should *never* be accessible on the client.
1811
+ */
1121
1812
  async $saveSkipAC() {
1122
1813
  this.$skipAcWhenSaving = true;
1123
1814
  return await this.$save();
@@ -1130,12 +1821,13 @@ class User extends AbleObject_1.default {
1130
1821
  return false;
1131
1822
  }
1132
1823
  async $delete() {
1133
- const tilmeld = this.$nymph.tilmeld;
1134
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
1824
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
1825
+ if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
1135
1826
  throw new errors_1.BadDataError("You don't have the authority to delete users.");
1136
1827
  }
1137
- if (!tilmeld.gatekeeper('system/admin') &&
1138
- this.$gatekeeper('system/admin')) {
1828
+ if (!this.$skipAcWhenDeleting &&
1829
+ !tilmeld.gatekeeper('system/admin') &&
1830
+ this.$data.abilities?.includes('system/admin')) {
1139
1831
  throw new errors_1.BadDataError("You don't have the authority to delete system admins.");
1140
1832
  }
1141
1833
  if (tilmeld.User.current(true).$is(this)) {
@@ -1143,11 +1835,26 @@ class User extends AbleObject_1.default {
1143
1835
  }
1144
1836
  return await super.$delete();
1145
1837
  }
1838
+ /*
1839
+ * This should *never* be accessible on the client.
1840
+ */
1841
+ async $deleteSkipAC() {
1842
+ this.$skipAcWhenDeleting = true;
1843
+ return await this.$delete();
1844
+ }
1845
+ $tilmeldDeleteSkipAC() {
1846
+ if (this.$skipAcWhenDeleting) {
1847
+ this.$skipAcWhenDeleting = false;
1848
+ return true;
1849
+ }
1850
+ return false;
1851
+ }
1146
1852
  static on(event, callback) {
1147
1853
  const prop = (event + 'Callbacks');
1148
1854
  if (!(prop in this)) {
1149
1855
  throw new Error('Invalid event type.');
1150
1856
  }
1857
+ // @ts-ignore: The callback should always be the right type here.
1151
1858
  this[prop].push(callback);
1152
1859
  return () => this.off(event, callback);
1153
1860
  }
@@ -1156,55 +1863,14 @@ class User extends AbleObject_1.default {
1156
1863
  if (!(prop in this)) {
1157
1864
  return false;
1158
1865
  }
1866
+ // @ts-ignore: The callback should always be the right type here.
1159
1867
  const i = this[prop].indexOf(callback);
1160
1868
  if (i > -1) {
1869
+ // @ts-ignore: The callback should always be the right type here.
1161
1870
  this[prop].splice(i, 1);
1162
1871
  }
1163
1872
  return true;
1164
1873
  }
1165
1874
  }
1166
1875
  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
1876
  //# sourceMappingURL=User.js.map