@nymphjs/tilmeld 1.0.0-beta.122 → 1.0.0-beta.124

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