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

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