@nymphjs/tilmeld 1.0.0-beta.13 → 1.0.0-beta.130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +598 -0
  2. package/README.md +21 -5
  3. package/dist/AbleObject.d.ts +15 -0
  4. package/dist/AbleObject.js +18 -6
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +186 -19
  7. package/dist/Group.js +817 -440
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.testArtifacts.d.ts +3 -0
  10. package/dist/Group.testArtifacts.js +120 -0
  11. package/dist/Group.testArtifacts.js.map +1 -0
  12. package/dist/Tilmeld.d.ts +241 -12
  13. package/dist/Tilmeld.js +725 -286
  14. package/dist/Tilmeld.js.map +1 -1
  15. package/dist/Tilmeld.types.d.ts +5 -5
  16. package/dist/Tilmeld.types.js +1 -2
  17. package/dist/User.d.ts +399 -26
  18. package/dist/User.js +1955 -1055
  19. package/dist/User.js.map +1 -1
  20. package/dist/User.testArtifacts.d.ts +3 -0
  21. package/dist/User.testArtifacts.js +175 -0
  22. package/dist/User.testArtifacts.js.map +1 -0
  23. package/dist/conf/defaults.d.ts +1 -1
  24. package/dist/conf/defaults.js +196 -141
  25. package/dist/conf/defaults.js.map +1 -1
  26. package/dist/conf/index.d.ts +355 -2
  27. package/dist/conf/index.js +2 -8
  28. package/dist/conf/index.js.map +1 -1
  29. package/dist/decorators/index.d.ts +2 -0
  30. package/dist/decorators/index.js +3 -0
  31. package/dist/decorators/index.js.map +1 -0
  32. package/dist/decorators/requireAbility.d.ts +10 -0
  33. package/dist/decorators/requireAbility.js +21 -0
  34. package/dist/decorators/requireAbility.js.map +1 -0
  35. package/dist/decorators/requireAuthentication.d.ts +8 -0
  36. package/dist/decorators/requireAuthentication.js +16 -0
  37. package/dist/decorators/requireAuthentication.js.map +1 -0
  38. package/dist/enforceTilmeld.d.ts +3 -0
  39. package/dist/enforceTilmeld.js +18 -0
  40. package/dist/enforceTilmeld.js.map +1 -0
  41. package/dist/errors/AccessControlError.d.ts +2 -1
  42. package/dist/errors/AccessControlError.js +3 -6
  43. package/dist/errors/AccessControlError.js.map +1 -1
  44. package/dist/errors/BadDataError.d.ts +2 -1
  45. package/dist/errors/BadDataError.js +3 -6
  46. package/dist/errors/BadDataError.js.map +1 -1
  47. package/dist/errors/BadEmailError.d.ts +2 -1
  48. package/dist/errors/BadEmailError.js +3 -6
  49. package/dist/errors/BadEmailError.js.map +1 -1
  50. package/dist/errors/BadUsernameError.d.ts +2 -1
  51. package/dist/errors/BadUsernameError.js +3 -6
  52. package/dist/errors/BadUsernameError.js.map +1 -1
  53. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js +1 -5
  54. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js.map +1 -1
  55. package/dist/errors/EmailChangeRateLimitExceededError.js +1 -5
  56. package/dist/errors/EmailChangeRateLimitExceededError.js.map +1 -1
  57. package/dist/errors/index.d.ts +6 -6
  58. package/dist/errors/index.js +6 -22
  59. package/dist/errors/index.js.map +1 -1
  60. package/dist/index.d.ts +12 -10
  61. package/dist/index.js +17 -35
  62. package/dist/index.js.map +1 -1
  63. package/dist/testArtifacts.d.ts +23 -0
  64. package/dist/testArtifacts.js +1317 -0
  65. package/dist/testArtifacts.js.map +1 -0
  66. package/dist/tilmeldJoiProps.d.ts +3 -3
  67. package/dist/tilmeldJoiProps.js +13 -18
  68. package/dist/tilmeldJoiProps.js.map +1 -1
  69. package/jest.config.js +31 -5
  70. package/package.json +49 -36
  71. package/src/AbleObject.ts +2 -2
  72. package/src/Group.testArtifacts.ts +159 -0
  73. package/src/Group.ts +647 -238
  74. package/src/Tilmeld.ts +550 -315
  75. package/src/Tilmeld.types.ts +5 -5
  76. package/src/User.testArtifacts.ts +226 -0
  77. package/src/User.ts +1657 -634
  78. package/src/conf/defaults.ts +172 -109
  79. package/src/conf/index.ts +370 -2
  80. package/src/decorators/index.ts +2 -0
  81. package/src/decorators/requireAbility.ts +26 -0
  82. package/src/decorators/requireAuthentication.ts +19 -0
  83. package/src/enforceTilmeld.ts +20 -0
  84. package/src/errors/AccessControlError.ts +4 -2
  85. package/src/errors/BadDataError.ts +4 -2
  86. package/src/errors/BadEmailError.ts +4 -2
  87. package/src/errors/BadUsernameError.ts +4 -2
  88. package/src/errors/index.ts +6 -6
  89. package/src/index.ts +13 -10
  90. package/src/testArtifacts.ts +2098 -0
  91. package/src/tilmeldJoiProps.ts +8 -23
  92. package/tsconfig.json +6 -3
  93. package/typedoc.json +4 -0
  94. package/dist/Group.test.d.ts +0 -1
  95. package/dist/Group.test.js +0 -48
  96. package/dist/Group.test.js.map +0 -1
  97. package/dist/User.test.d.ts +0 -1
  98. package/dist/User.test.js +0 -51
  99. package/dist/User.test.js.map +0 -1
  100. package/dist/conf/d.d.ts +0 -55
  101. package/dist/conf/d.js +0 -3
  102. package/dist/conf/d.js.map +0 -1
  103. package/src/Group.test.ts +0 -58
  104. package/src/User.test.ts +0 -61
  105. package/src/conf/d.ts +0 -303
package/dist/User.js CHANGED
@@ -1,1210 +1,2110 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const strtotime_1 = __importDefault(require("locutus/php/datetime/strtotime"));
7
- const enc_base64_1 = __importDefault(require("crypto-js/enc-base64"));
8
- const sha256_1 = __importDefault(require("crypto-js/sha256"));
9
- const md5_1 = __importDefault(require("crypto-js/md5"));
10
- const lodash_1 = require("lodash");
11
- const guid_1 = require("@nymphjs/guid");
12
- const AbleObject_1 = __importDefault(require("./AbleObject"));
13
- const errors_1 = require("./errors");
14
- class User extends AbleObject_1.default {
15
- static async factory(guid) {
16
- return (await super.factory(guid));
17
- }
18
- static async factoryUsername(username) {
19
- const entity = new this();
20
- if (username != null) {
21
- const entity = await this.nymph.getEntity({
22
- class: this,
23
- }, {
24
- type: '&',
25
- ilike: ['username', username.replace(/([\\%_])/g, (s) => `\\${s}`)],
26
- });
27
- if (entity != null) {
28
- return entity;
29
- }
30
- }
31
- return entity;
32
- }
33
- static factorySync(guid) {
34
- return super.factorySync(guid);
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);
35
5
  }
36
- constructor(guid) {
37
- super(guid);
38
- this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
39
- this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
40
- this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
41
- this.$allowlistTags = [];
42
- this.$skipAcWhenSaving = false;
43
- if (this.guid == null) {
44
- this.$data.enabled = true;
45
- this.$data.abilities = [];
46
- this.$data.groups = [];
47
- this.$data.inheritAbilities = true;
48
- this.$updateDataProtection();
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] = _;
49
30
  }
50
31
  }
51
- static current(returnObjectIfNotExist) {
52
- if (this.tilmeld.currentUser == null) {
53
- return returnObjectIfNotExist ? this.factorySync() : null;
32
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
+ done = true;
34
+ };
35
+ import crypto from 'node:crypto';
36
+ import { transaction, transactional, } from '@nymphjs/nymph';
37
+ import { humanSecret, nanoid } from '@nymphjs/guid';
38
+ import { strtotime } from 'locutus/php/datetime/index';
39
+ import { difference, xor } from 'lodash-es';
40
+ import { TOTP, Secret } from 'otpauth';
41
+ import { toDataURL } from 'qrcode';
42
+ import { splitn } from '@sciactive/splitn';
43
+ import { enforceTilmeld } from './enforceTilmeld.js';
44
+ import AbleObject from './AbleObject.js';
45
+ import GroupClass from './Group.js';
46
+ import { AccessControlError, BadDataError, BadEmailError, BadUsernameError, EmailChangeRateLimitExceededError, } from './errors/index.js';
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.
83
+ 'abilities',
84
+ // 'username',
85
+ // 'nameFirst',
86
+ // 'nameMiddle',
87
+ // 'nameLast',
88
+ // 'name',
89
+ 'email',
90
+ // 'avatar',
91
+ 'phone',
92
+ 'group',
93
+ 'groups',
94
+ 'inheritAbilities',
95
+ 'enabled',
96
+ 'secret',
97
+ 'emailChangeDate',
98
+ 'newEmailSecret',
99
+ 'newEmailAddress',
100
+ 'cancelEmailSecret',
101
+ 'cancelEmailAddress',
102
+ 'recoverSecret',
103
+ 'recoverSecretDate',
104
+ 'salt',
105
+ 'password',
106
+ 'passwordTemp',
107
+ 'revokeTokenDate',
108
+ 'totpSecret',
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;
177
+ }
178
+ return super.getFTSText(name, value);
54
179
  }
55
- return this.tilmeld.currentUser;
56
- }
57
- static async sendRecovery(data) {
58
- if (!this.tilmeld.config.pwRecovery) {
59
- return {
60
- result: false,
61
- message: 'Account recovery is not allowed.',
62
- };
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;
63
192
  }
64
- let user;
65
- const options = {};
66
- if (!this.tilmeld.config.emailUsernames &&
67
- data.recoveryType === 'username') {
68
- const getUser = await this.nymph.getEntity({ class: this.tilmeld.User, skipAc: true }, {
69
- type: '&',
70
- ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
71
- });
72
- if (getUser == null) {
193
+ static current(returnObjectIfNotExist) {
194
+ const tilmeld = enforceTilmeld(this);
195
+ if (tilmeld.currentUser == null) {
196
+ return returnObjectIfNotExist ? this.factorySync() : null;
197
+ }
198
+ return tilmeld.currentUser;
199
+ }
200
+ /**
201
+ * Send an account recovery link.
202
+ *
203
+ * @param data The input data from the client.
204
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
205
+ */
206
+ static async sendRecovery(data) {
207
+ const tilmeld = enforceTilmeld(this);
208
+ if (!tilmeld.config.pwRecovery) {
209
+ return { result: false, message: 'Account recovery is not allowed.' };
210
+ }
211
+ let user;
212
+ const options = {};
213
+ if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
214
+ // Create a username recovery email.
215
+ const getUser = await this.nymph.getEntity({ class: tilmeld.User, skipAc: true }, {
216
+ type: '&',
217
+ ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
218
+ });
219
+ if (getUser == null) {
220
+ return {
221
+ result: false,
222
+ message: 'Requested account is not accessible.',
223
+ };
224
+ }
225
+ // Send the recovery email.
226
+ user = getUser;
227
+ options.template = 'RecoverUsername';
228
+ options.message = {
229
+ to: { name: user.name ?? '', address: user.email ?? '' },
230
+ };
231
+ options.locals = {};
232
+ }
233
+ else if (data.recoveryType === 'password') {
234
+ // Create a password recovery email.
235
+ const getUser = await tilmeld.User.factoryUsername(data.account);
236
+ if (getUser.guid == null) {
237
+ return {
238
+ result: false,
239
+ message: 'Requested account is not accessible.',
240
+ };
241
+ }
242
+ // Create a unique secret.
243
+ getUser.recoverSecret = humanSecret();
244
+ getUser.recoverSecretDate = Date.now();
245
+ try {
246
+ await getUser.$saveSkipAC();
247
+ }
248
+ catch (e) {
249
+ return { result: false, message: "Couldn't save recovery secret." };
250
+ }
251
+ // Send the recovery email.
252
+ user = getUser;
253
+ options.template = 'RecoverPassword';
254
+ options.message = {
255
+ to: { name: user.name ?? '', address: user.email ?? '' },
256
+ };
257
+ options.locals = {
258
+ recoverCode: getUser.recoverSecret,
259
+ timeLimit: tilmeld.config.pwRecoveryTimeLimit,
260
+ };
261
+ }
262
+ else {
263
+ return { result: false, message: 'Invalid recovery type.' };
264
+ }
265
+ this.nymph.config.debugLog('tilmeld', `User account recovery (${options.template}) request for "${user.username}".`);
266
+ // Send the email.
267
+ if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
73
268
  return {
74
- result: false,
75
- 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.',
76
272
  };
77
273
  }
78
- user = getUser;
79
- options.template = 'RecoverUsername';
80
- options.message = {
81
- to: {
82
- name: user.name ?? '',
83
- address: user.email ?? '',
84
- },
85
- };
86
- options.locals = {};
274
+ else {
275
+ return { result: false, message: "Couldn't send recovery email." };
276
+ }
87
277
  }
88
- else if (data.recoveryType === 'password') {
89
- const getUser = await this.tilmeld.User.factoryUsername(data.account);
90
- if (getUser.guid == null) {
278
+ /**
279
+ * Recover account details.
280
+ *
281
+ * @param data The input data from the client.
282
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
283
+ */
284
+ static async recover(data) {
285
+ const tilmeld = enforceTilmeld(this);
286
+ if (!tilmeld.config.pwRecovery) {
287
+ return { result: false, message: 'Account recovery is not allowed.' };
288
+ }
289
+ const user = await tilmeld.User.factoryUsername(data.username);
290
+ if (user.guid == null ||
291
+ user.recoverSecret == null ||
292
+ data.secret !== user.recoverSecret ||
293
+ (strtotime('+' + tilmeld.config.pwRecoveryTimeLimit, Math.floor((user.recoverSecretDate ?? 0) / 1000)) || 0) *
294
+ 1000 <
295
+ Date.now()) {
296
+ return { result: false, message: 'The secret code does not match.' };
297
+ }
298
+ if (!('password' in data) || !data.password.length) {
299
+ return { result: false, message: 'Password cannot be empty.' };
300
+ }
301
+ this.nymph.config.debugLog('tilmeld', `User account recovery for "${user.username}".`);
302
+ user.$password(data.password);
303
+ delete user.recoverSecret;
304
+ delete user.recoverSecretDate;
305
+ try {
306
+ await user.$saveSkipAC();
91
307
  return {
92
- result: false,
93
- message: 'Requested account is not accessible.',
308
+ result: true,
309
+ message: 'Your password has been reset. You can now log in using your new password.',
94
310
  };
95
311
  }
96
- getUser.recoverSecret = (0, guid_1.humanSecret)();
97
- getUser.recoverSecretDate = Date.now();
98
- if (!(await getUser.$saveSkipAC())) {
99
- return { result: false, message: "Couldn't save recovery secret." };
100
- }
101
- user = getUser;
102
- options.template = 'RecoverPassword';
103
- options.message = {
104
- to: {
105
- name: user.name ?? '',
106
- address: user.email ?? '',
107
- },
108
- };
109
- options.locals = {
110
- recoverCode: getUser.recoverSecret,
111
- timeLimit: this.tilmeld.config.pwRecoveryTimeLimit,
112
- };
113
- }
114
- else {
115
- return { result: false, message: 'Invalid recovery type.' };
116
- }
117
- if (await this.tilmeld.config.sendEmail(this.tilmeld, options, user)) {
118
- return {
119
- result: true,
120
- message: "We've sent an email to your registered address. Please " +
121
- 'check your email to continue with account recovery.',
122
- };
123
- }
124
- else {
125
- return { result: false, message: "Couldn't send recovery email." };
126
- }
127
- }
128
- static async recover(data) {
129
- if (!this.tilmeld.config.pwRecovery) {
130
- return {
131
- result: false,
132
- message: 'Account recovery is not allowed.',
133
- };
134
- }
135
- const user = await this.tilmeld.User.factoryUsername(data.username);
136
- if (user.guid == null ||
137
- user.recoverSecret == null ||
138
- data.secret !== user.recoverSecret ||
139
- (0, strtotime_1.default)('+' + this.tilmeld.config.pwRecoveryTimeLimit, Math.floor((user.recoverSecretDate ?? 0) / 1000)) *
140
- 1000 <
141
- Date.now()) {
142
- return {
143
- result: false,
144
- message: 'The secret code does not match.',
145
- };
146
- }
147
- if (!('password' in data) || !data.password.length) {
148
- return { result: false, message: 'Password cannot be empty.' };
312
+ catch (e) {
313
+ return { result: false, message: 'Error saving new password.' };
314
+ }
149
315
  }
150
- user.$password(data.password);
151
- delete user.recoverSecret;
152
- delete user.recoverSecretDate;
153
- if (await user.$saveSkipAC()) {
316
+ static getClientConfig() {
317
+ const tilmeld = enforceTilmeld(this);
154
318
  return {
155
- result: true,
156
- 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,
157
328
  };
158
329
  }
159
- else {
160
- return { result: false, message: 'Error saving new password.' };
161
- }
162
- }
163
- static getClientConfig() {
164
- return {
165
- regFields: this.tilmeld.config.regFields,
166
- userFields: this.tilmeld.config.userFields,
167
- emailUsernames: this.tilmeld.config.emailUsernames,
168
- allowRegistration: this.tilmeld.config.allowRegistration,
169
- allowUsernameChange: this.tilmeld.config.allowUsernameChange,
170
- pwRecovery: this.tilmeld.config.pwRecovery,
171
- verifyEmail: this.tilmeld.config.verifyEmail,
172
- unverifiedAccess: this.tilmeld.config.unverifiedAccess,
173
- };
174
- }
175
- static async loginUser(data) {
176
- if (!('username' in data) || !data.username.length) {
177
- return { result: false, message: 'Incorrect login/password.' };
178
- }
179
- const user = await this.tilmeld.User.factoryUsername(data.username);
180
- const result = await user.$login(data);
181
- if (result.result) {
182
- user.$updateDataProtection();
183
- result.user = user;
184
- }
185
- return result;
186
- }
187
- async $login(data) {
188
- const tilmeld = this.$nymph.tilmeld;
189
- if (this.guid == null) {
190
- return { result: false, message: 'Incorrect login/password.' };
191
- }
192
- if (!this.$data.enabled) {
193
- return { result: false, message: 'This user is disabled.' };
330
+ static async loginUser(data) {
331
+ const tilmeld = enforceTilmeld(this);
332
+ if (!('username' in data) || !data.username.length) {
333
+ return { result: false, message: 'Incorrect login/password.' };
334
+ }
335
+ const user = await tilmeld.User.factoryUsername(data.username);
336
+ const result = await user.$login(data);
337
+ if (result.result) {
338
+ user.$updateDataProtection();
339
+ result.user = user;
340
+ }
341
+ return result;
342
+ }
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
+ });
194
371
  }
195
- if (this.$gatekeeper()) {
196
- return { result: true, message: 'You are already logged in.' };
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();
197
380
  }
198
- if (!this.$checkPassword(data.password)) {
199
- return { result: false, message: 'Incorrect login/password.' };
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;
200
392
  }
201
- try {
393
+ async $login(data) {
394
+ const tilmeld = enforceTilmeld(this);
395
+ if (this.guid == null) {
396
+ await tilmeld.config.failedLoginAttempt(null, data);
397
+ return { result: false, message: 'Incorrect login/password.' };
398
+ }
399
+ if (!this.$checkPassword(data.password)) {
400
+ await tilmeld.config.failedLoginAttempt(this, data);
401
+ return { result: false, message: 'Incorrect login/password.' };
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.' };
439
+ }
202
440
  for (let callback of this.constructor
203
- .beforeLoginCallbacks) {
441
+ .afterLoginCallbacks) {
204
442
  if (callback) {
205
- await callback(this, data);
443
+ await callback(this);
206
444
  }
207
445
  }
446
+ // Login was successful.
447
+ return { result: true, message: 'You are logged in.' };
208
448
  }
209
- catch (e) {
210
- return {
211
- result: false,
212
- message: e.message,
213
- };
214
- }
215
- if (!tilmeld.login(this, true)) {
216
- return { result: false, message: 'Incorrect login/password.' };
217
- }
218
- for (let callback of this.constructor
219
- .afterLoginCallbacks) {
220
- if (callback) {
221
- 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.' };
222
494
  }
223
- }
224
- return { result: true, message: 'You are logged in.' };
225
- }
226
- async $logout() {
227
- const tilmeld = this.$nymph.tilmeld;
228
- try {
229
495
  for (let callback of this.constructor
230
- .beforeLogoutCallbacks) {
496
+ .afterLoginCallbacks) {
231
497
  if (callback) {
232
498
  await callback(this);
233
499
  }
234
500
  }
235
- }
236
- catch (e) {
237
- return {
238
- result: false,
239
- message: e.message,
240
- };
241
- }
242
- tilmeld.logout();
243
- for (let callback of this.constructor
244
- .afterLogoutCallbacks) {
245
- if (callback) {
246
- await callback(this);
501
+ // Login was successful.
502
+ return { result: true, message: 'You are logged in.' };
503
+ }
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
+ }
247
517
  }
248
- }
249
- return { result: true, message: 'You have been logged out.' };
250
- }
251
- $getAvatar() {
252
- if (this.$data.avatar != null && this.$data.avatar !== '') {
253
- return this.$data.avatar;
254
- }
255
- if (this.$data.email == null || this.$data.email === '') {
256
- return 'https://secure.gravatar.com/avatar/?d=mm&s=40';
257
- }
258
- return ('https://secure.gravatar.com/avatar/' +
259
- (0, md5_1.default)(this.$data.email.trim().toLowerCase()).toString() +
260
- '?d=identicon&s=40');
261
- }
262
- async $getDescendantGroups() {
263
- if (this.$descendantGroups == null) {
264
- this.$descendantGroups = [];
265
- if (this.$data.group != null) {
266
- this.$descendantGroups = await this.$data.group.$getDescendants();
518
+ catch (e) {
519
+ return { result: false, message: e.message };
267
520
  }
268
- for (let curGroup of this.$data.groups ?? []) {
269
- this.$descendantGroups = this.$descendantGroups?.concat(await curGroup.$getDescendants());
521
+ this.$nymph.config.debugLog('tilmeld', `User logout for "${this.$data.username}".`);
522
+ await tilmeld.logout();
523
+ for (let callback of this.constructor
524
+ .afterLogoutCallbacks) {
525
+ if (callback) {
526
+ await callback(this);
527
+ }
270
528
  }
529
+ return { result: true, message: 'You have been logged out.' };
271
530
  }
272
- return this.$descendantGroups;
273
- }
274
- $getDescendantGroupsSync() {
275
- if (this.$descendantGroups == null) {
276
- this.$descendantGroups = [];
277
- if (this.$data.group != null) {
278
- this.$descendantGroups = this.$data.group.$getDescendantsSync();
531
+ $getAvatar() {
532
+ if (this.$data.avatar != null && this.$data.avatar !== '') {
533
+ return this.$data.avatar;
279
534
  }
280
- for (let curGroup of this.$data.groups ?? []) {
281
- this.$descendantGroups = this.$descendantGroups?.concat(curGroup.$getDescendantsSync());
535
+ if (this.$data.email == null || this.$data.email === '') {
536
+ return 'https://secure.gravatar.com/avatar/?d=mm&s=40';
282
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');
545
+ }
546
+ $getGid() {
547
+ if (this.$data.group) {
548
+ return this.$data.group.guid;
549
+ }
550
+ return null;
283
551
  }
284
- return this.$descendantGroups;
285
- }
286
- $jsonAcceptData(input, allowConflict = false) {
287
- const tilmeld = this.$nymph.tilmeld;
288
- this.$referenceWake();
289
- if ('abilities' in input.data &&
290
- input.data.abilities.indexOf('system/admin') !== -1 &&
291
- this.$data.abilities?.indexOf('system/admin') === -1 &&
292
- tilmeld.gatekeeper('tilmeld/admin') &&
293
- !tilmeld.gatekeeper('system/admin')) {
294
- throw new errors_1.BadDataError("You don't have the authority to make this user a system admin.");
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;
559
+ }
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;
295
576
  }
296
- this.$originalEmail = this.$data.email;
297
- super.$jsonAcceptData(input, allowConflict);
298
- }
299
- $jsonAcceptPatch(patch, allowConflict = false) {
300
- const tilmeld = this.$nymph.tilmeld;
301
- this.$referenceWake();
302
- if ('abilities' in patch.set &&
303
- patch.set.abilities.indexOf('system/admin') !== -1 &&
304
- this.$data.abilities?.indexOf('system/admin') === -1 &&
305
- tilmeld.gatekeeper('tilmeld/admin') &&
306
- !tilmeld.gatekeeper('system/admin')) {
307
- throw new errors_1.BadDataError("You don't have the authority to make this user a system admin.");
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);
606
+ }
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);
612
+ }
308
613
  }
309
- this.$originalEmail = this.$data.email;
310
- super.$jsonAcceptPatch(patch, allowConflict);
311
- }
312
- $putData(data, sdata) {
313
- super.$putData(data, sdata);
314
- this.$updateDataProtection();
315
- }
316
- $updateDataProtection(givenUser) {
317
- const tilmeld = this.$nymph.tilmeld;
318
- let user = givenUser ?? tilmeld.User.current();
319
- this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
320
- this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
321
- this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
322
- if (tilmeld.config.emailUsernames) {
323
- this.$privateData.push('username');
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);
324
620
  }
325
- const isCurrentUser = user != null && user.$is(this);
326
- const abilities = (isCurrentUser || user == null
327
- ? this.$data.abilities
328
- : user?.abilities) ?? [];
329
- const isNewUser = this.guid == null;
330
- if (isCurrentUser) {
331
- this.$clientEnabledMethods.push('$gatekeeper');
332
- this.$clientEnabledMethods.push('$changePassword');
333
- this.$clientEnabledMethods.push('$logout');
334
- this.$clientEnabledMethods.push('$sendEmailVerification');
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
+ }
335
651
  }
336
- if (user != null &&
337
- (abilities.indexOf('tilmeld/admin') !== -1 ||
338
- abilities.indexOf('system/admin') !== -1)) {
339
- this.$privateData = ['password', 'salt'];
340
- this.$allowlistData = undefined;
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();
341
666
  }
342
- else if (isCurrentUser || isNewUser) {
343
- if (tilmeld.config.allowUsernameChange || isNewUser) {
344
- this.$allowlistData.push('username');
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];
678
+ // Lowercase the domain part.
679
+ if (domain) {
680
+ domain = domain.toLowerCase();
345
681
  }
346
- this.$allowlistData.push('avatar');
347
- if (tilmeld.config.userFields.indexOf('name') !== -1) {
348
- this.$allowlistData.push('nameFirst');
349
- this.$allowlistData.push('nameMiddle');
350
- this.$allowlistData.push('nameLast');
351
- this.$allowlistData.push('name');
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');
352
687
  }
353
- if (tilmeld.config.userFields.indexOf('email') !== -1) {
354
- this.$allowlistData.push('email');
688
+ const isCurrentUser = user != null && user.$is(this);
689
+ // While loading, accessing abilities on the current user will infinitely
690
+ // loop, but if this is the current user, we can use its abilities.
691
+ const abilities = (isCurrentUser || user == null
692
+ ? this.$data.abilities
693
+ : user?.abilities) ?? [];
694
+ const isNewUser = this.guid == null;
695
+ const isDomainUser = tilmeld.config.domainSupport &&
696
+ !isCurrentUser &&
697
+ user != null &&
698
+ domain &&
699
+ abilities.includes(`tilmeld/domain/${domain}/admin`);
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');
355
710
  }
356
- if (tilmeld.config.userFields.indexOf('phone') !== -1) {
357
- this.$allowlistData.push('phone');
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');
358
834
  }
359
- this.$privateData = [
360
- 'secret',
361
- 'newEmailSecret',
362
- 'cancelEmailAddress',
363
- 'cancelEmailSecret',
364
- 'emailChangeDate',
365
- 'recoverSecret',
366
- 'recoverSecretDate',
367
- 'password',
368
- 'salt',
369
- ];
370
835
  }
371
- }
372
- $gatekeeper(ability) {
373
- const tilmeld = this.$nymph.tilmeld;
374
- if (ability == null) {
375
- return tilmeld.User.current()?.$is(this) ?? false;
836
+ $updateGroupDataProtection() {
837
+ if (this.$data.group != null && !this.$data.group.$asleep()) {
838
+ this.$data.group.$updateDataProtection();
839
+ }
840
+ for (let group of this.$data.groups ?? []) {
841
+ if (!group.$asleep()) {
842
+ group.$updateDataProtection();
843
+ }
844
+ }
376
845
  }
377
- if (this.$gatekeeperCache == null) {
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();
378
878
  let abilities = this.$data.abilities ?? [];
379
879
  if (this.$data.inheritAbilities) {
880
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
380
881
  for (let curGroup of this.$data.groups ?? []) {
381
882
  if (curGroup.enabled) {
382
883
  abilities = abilities.concat(curGroup.abilities ?? []);
383
884
  }
384
885
  }
385
- if (this.$data.group != null &&
386
- this.$data.group.cdate != null &&
387
- this.$data.group.enabled) {
388
- abilities = abilities.concat(this.$data.group.abilities ?? []);
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 ?? []);
389
890
  }
390
891
  }
391
- this.$gatekeeperCache = Object.fromEntries(abilities.map((ability) => [ability, true]));
392
- }
393
- return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
394
- !!this.$gatekeeperCache['system/admin']);
395
- }
396
- $clearCache() {
397
- const ret = super.$clearCache();
398
- this.$descendantGroups = undefined;
399
- this.$gatekeeperCache = undefined;
400
- return ret;
401
- }
402
- async $sendEmailVerification() {
403
- const tilmeld = this.$nymph.tilmeld;
404
- if (this.guid == null) {
405
- return false;
892
+ return Object.fromEntries(abilities.map((ability) => [ability, true]));
406
893
  }
407
- let success = true;
408
- const verifyUrl = `${tilmeld.config.appUrl.replace(/\/$/, () => '')}${tilmeld.config.setupPath}/verify`;
409
- if (this.$data.secret != null) {
410
- const link = `${verifyUrl}?action=verify&id=${encodeURIComponent(this.guid)}&secret=${encodeURIComponent(this.$data.secret)}`;
411
- success =
412
- success &&
413
- (await tilmeld.config.sendEmail(tilmeld, {
414
- template: 'VerifyEmail',
415
- message: {
416
- to: {
417
- name: this.$data.name ?? '',
418
- address: this.$data.email ?? '',
894
+ $clearCache() {
895
+ const ret = super.$clearCache();
896
+ this.$descendantGroups = undefined;
897
+ this.$gatekeeperCache = undefined;
898
+ return ret;
899
+ }
900
+ /**
901
+ * Send the user email verification/change/cancellation links.
902
+ *
903
+ * @returns True on success, false on failure.
904
+ */
905
+ async $sendEmailVerification() {
906
+ const tilmeld = enforceTilmeld(this);
907
+ if (this.guid == null) {
908
+ return false;
909
+ }
910
+ let success = true;
911
+ const verifyUrl = `${tilmeld.config.appUrl.replace(/\/$/, () => '')}${tilmeld.config.setupPath}/verify`;
912
+ if (this.$data.secret != null) {
913
+ const link = `${verifyUrl}?action=verify&id=${encodeURIComponent(this.guid)}&secret=${encodeURIComponent(this.$data.secret)}`;
914
+ success =
915
+ success &&
916
+ (await tilmeld.config.sendEmail(tilmeld, {
917
+ template: 'VerifyEmail',
918
+ message: {
919
+ to: {
920
+ name: this.$data.name ?? '',
921
+ address: this.$data.email ?? '',
922
+ },
419
923
  },
420
- },
421
- locals: {
422
- verifyLink: link,
423
- },
424
- }, this));
425
- }
426
- if (this.$data.newEmailSecret != null) {
427
- const link = `${verifyUrl}?action=verifychange&id=${encodeURIComponent(this.guid)}&secret=${encodeURIComponent(this.$data.newEmailSecret)}`;
428
- success =
429
- success &&
430
- (await tilmeld.config.sendEmail(tilmeld, {
431
- template: 'VerifyEmailChange',
432
- message: {
433
- to: {
434
- name: this.$data.name ?? '',
435
- 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
+ },
436
938
  },
437
- },
438
- locals: {
439
- verifyLink: link,
440
- oldEmail: this.$data.cancelEmailAddress,
441
- newEmail: this.$data.newEmailAddress,
442
- },
443
- }, this));
444
- }
445
- if (this.$data.cancelEmailSecret != null) {
446
- const link = `${verifyUrl}?action=cancelchange&id=${encodeURIComponent(this.guid)}&secret=${encodeURIComponent(this.$data.cancelEmailSecret)}`;
447
- success =
448
- success &&
449
- (await tilmeld.config.sendEmail(tilmeld, {
450
- template: 'CancelEmailChange',
451
- message: {
452
- to: {
453
- name: this.$data.name ?? '',
454
- address: this.$data.cancelEmailAddress ?? '',
939
+ locals: {
940
+ verifyLink: link,
941
+ oldEmail: this.$data.cancelEmailAddress,
942
+ newEmail: this.$data.newEmailAddress,
455
943
  },
456
- },
457
- locals: {
458
- cancelLink: link,
459
- oldEmail: this.$data.cancelEmailAddress,
460
- newEmail: this.$data.newEmailAddress ?? this.$data.email,
461
- },
462
- }, this));
463
- }
464
- return success;
465
- }
466
- $addGroup(group) {
467
- if (this.$data.groups == null) {
468
- this.$data.groups = [];
469
- }
470
- if (!group.$inArray(this.$data.groups)) {
471
- this.$data.groups.push(group);
472
- return this.$data.groups;
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;
966
+ }
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;
473
985
  }
474
- return true;
475
- }
476
- $checkPassword(password) {
477
- const tilmeld = this.$nymph.tilmeld;
478
- switch (tilmeld.config.pwMethod) {
479
- case 'plain':
480
- return this.$data.password === password;
481
- case 'digest':
482
- return this.$data.password === (0, sha256_1.default)(password).toString(enc_base64_1.default);
483
- case 'salt':
484
- default:
485
- return (this.$data.password ===
486
- (0, sha256_1.default)(password + this.$data.salt).toString(enc_base64_1.default));
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
+ }
487
1008
  }
488
- }
489
- $delGroup(group) {
490
- if (this.$data.groups != null && group.$inArray(this.$data.groups)) {
491
- const newGroups = [];
492
- for (let curGroup of this.$data.groups) {
493
- if (!group.$is(curGroup)) {
494
- newGroups.push(curGroup);
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
+ }
495
1024
  }
1025
+ this.$data.groups = newGroups;
1026
+ return this.$data.groups;
496
1027
  }
497
- this.$data.groups = newGroups;
498
- return this.$data.groups;
1028
+ return true;
499
1029
  }
500
- return true;
501
- }
502
- $inGroup(group) {
503
- if (typeof group === 'string') {
504
- if (this.$data.group?.guid === group) {
505
- return true;
506
- }
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))) {
1062
+ return true;
1063
+ }
1064
+ await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
507
1065
  for (let curGroup of this.$data.groups ?? []) {
508
- if (curGroup.guid === group) {
1066
+ if (await curGroup.$isDescendant(group)) {
509
1067
  return true;
510
1068
  }
511
1069
  }
512
1070
  return false;
513
1071
  }
514
- return (group.$is(this.$data.group) || group.$inArray(this.$data.groups ?? []));
515
- }
516
- $isDescendant(group) {
517
- if (this.$data.group?.cdate != null &&
518
- this.$data.group.$isDescendant(group)) {
519
- return true;
520
- }
521
- for (let curGroup of this.$data.groups ?? []) {
522
- if (curGroup.$isDescendant(group)) {
523
- 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." };
524
1101
  }
525
1102
  }
526
- return false;
527
- }
528
- async $changePassword(data) {
529
- if (!('newPassword' in data) || !data.newPassword.length) {
530
- return { result: false, message: 'Please specify a password.' };
531
- }
532
- if (!this.$checkPassword(data.currentPassword ?? '')) {
533
- return { result: false, message: 'Incorrect password.' };
534
- }
535
- this.$data.passwordTemp = data.newPassword;
536
- if (await this.$save()) {
537
- return { result: true, message: 'Your password has been changed.' };
538
- }
539
- else {
540
- return { result: false, message: "Couldn't save new password." };
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
+ }
1144
+ this.$data.revokeTokenDate = Date.now();
1145
+ try {
1146
+ await this.$save();
1147
+ const tilmeld = enforceTilmeld(this);
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." };
1156
+ }
541
1157
  }
542
- }
543
- $password(password) {
544
- const tilmeld = this.$nymph.tilmeld;
545
- switch (tilmeld.config.pwMethod) {
546
- case 'plain':
547
- delete this.$data.salt;
548
- this.$data.password = password;
549
- break;
550
- case 'digest':
551
- delete this.$data.salt;
552
- this.$data.password = (0, sha256_1.default)(password).toString(enc_base64_1.default);
553
- break;
554
- case 'salt':
555
- default:
556
- this.$data.salt = (0, guid_1.nanoid)();
557
- this.$data.password = (0, sha256_1.default)(password + this.$data.salt).toString(enc_base64_1.default);
558
- break;
1158
+ $hasTOTPSecret() {
1159
+ return this.$data.totpSecret != null;
1160
+ }
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
+ }
1171
+ const tilmeld = enforceTilmeld(this);
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) {
1212
+ if (!this.$checkPassword(data.password ?? '')) {
1213
+ return { result: false, message: 'Incorrect password.' };
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
+ }
1223
+ if (data.code == null ||
1224
+ typeof data.code !== 'string' ||
1225
+ data.code.length !== 6) {
1226
+ return { result: false, message: '2FA code is invalid.' };
1227
+ }
1228
+ const tilmeld = enforceTilmeld(this);
1229
+ const totp = new TOTP({
1230
+ issuer: tilmeld.config.appName,
1231
+ label: this.$data.username,
1232
+ algorithm: 'SHA1',
1233
+ digits: 6,
1234
+ period: 30,
1235
+ secret: data.secret,
1236
+ });
1237
+ if (totp.validate({ token: data.code, window: 2 }) == null) {
1238
+ return { result: false, message: '2FA code is incorrect.' };
1239
+ }
1240
+ this.$data.totpSecret = data.secret;
1241
+ try {
1242
+ await this.$save();
1243
+ return { result: true, message: 'Your two factor secret is now set.' };
1244
+ }
1245
+ catch (e) {
1246
+ return { result: false, message: "Couldn't save two factor secret." };
1247
+ }
559
1248
  }
560
- return this.$data.password;
561
- }
562
- async $checkUsername() {
563
- const tilmeld = this.$nymph.tilmeld;
564
- if (!tilmeld.config.emailUsernames) {
565
- if (this.$data.username == null || !this.$data.username.length) {
566
- return { result: false, message: 'Please specify a username.' };
567
- }
568
- if (tilmeld.config.maxUsernameLength > 0 &&
569
- this.$data.username.length > tilmeld.config.maxUsernameLength) {
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
+ }
1281
+ }
1282
+ else if (!tilmeld.gatekeeper('tilmeld/admin')) {
570
1283
  return {
571
1284
  result: false,
572
- message: 'Usernames must not exceed ' +
573
- tilmeld.config.maxUsernameLength +
574
- ' characters.',
1285
+ message: 'You must provide your password and 2FA code.',
575
1286
  };
576
1287
  }
577
- if ((0, lodash_1.difference)(this.$data.username.split(''), tilmeld.config.validChars.split('')).length) {
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." };
1295
+ }
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
+ }
1384
+ return {
1385
+ result: true,
1386
+ message: this.guid != null ? 'Username is valid.' : 'Username is available!',
1387
+ };
1388
+ }
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) {
1395
+ return {
1396
+ result: false,
1397
+ message: 'Emails must be at least ' +
1398
+ tilmeld.config.minUsernameLength +
1399
+ ' characters.',
1400
+ };
1401
+ }
1402
+ if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
1403
+ return {
1404
+ result: false,
1405
+ message: 'Emails must not exceed ' +
1406
+ tilmeld.config.maxUsernameLength +
1407
+ ' characters.',
1408
+ };
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) {
578
1441
  return {
579
1442
  result: false,
580
- message: tilmeld.config.validCharsNotice,
1443
+ message: 'That email address is already registered.',
581
1444
  };
582
1445
  }
583
- if (!tilmeld.config.validRegex.test(this.$data.username)) {
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)) {
584
1464
  return {
585
1465
  result: false,
586
- message: tilmeld.config.validRegexNotice,
1466
+ message: 'Phone must contain at least 10 digits, but formatting does not matter.',
587
1467
  };
588
1468
  }
589
1469
  const selector = {
590
1470
  type: '&',
591
- ilike: [
592
- 'username',
593
- this.$data.username.replace(/([\\%_])/g, (s) => `\\${s}`),
594
- ],
1471
+ equal: ['phone', stripToDigits],
595
1472
  };
596
1473
  if (this.guid != null) {
597
1474
  selector['!guid'] = this.guid;
598
1475
  }
599
1476
  const test = await this.$nymph.getEntity({ class: tilmeld.User, skipAc: true }, selector);
600
1477
  if (test != null) {
601
- return { result: false, message: 'That username is taken.' };
1478
+ return { result: false, message: 'Phone number is in use.' };
602
1479
  }
603
1480
  return {
604
1481
  result: true,
605
- message: this.guid != null ? 'Username is valid.' : 'Username is available!',
1482
+ message: this.guid != null ? 'Phone number is valid.' : 'Phone number is valid!',
606
1483
  };
607
1484
  }
608
- else {
609
- this.$data.email = this.$data.username;
610
- if (this.$data.username == null || !this.$data.username.length) {
611
- return { result: false, message: 'Please specify an email.' };
1485
+ async $register(data) {
1486
+ let tilmeld = enforceTilmeld(this);
1487
+ if (!tilmeld.config.allowRegistration) {
1488
+ return {
1489
+ result: false,
1490
+ loggedin: false,
1491
+ message: 'Registration is not allowed.',
1492
+ };
612
1493
  }
613
- if (tilmeld.config.maxUsernameLength > 0 &&
614
- this.$data.username.length > tilmeld.config.maxUsernameLength) {
1494
+ if (this.guid != null) {
615
1495
  return {
616
1496
  result: false,
617
- message: 'Emails must not exceed ' +
618
- tilmeld.config.maxUsernameLength +
619
- ' characters.',
1497
+ loggedin: false,
1498
+ message: 'This is already a registered user.',
620
1499
  };
621
1500
  }
622
- return await this.$checkEmail();
623
- }
624
- }
625
- async $checkEmail() {
626
- const tilmeld = this.$nymph.tilmeld;
627
- if (this.$data.email == null || !this.$data.email.length) {
628
- if (tilmeld.config.verifyEmail) {
629
- return { result: false, message: 'Please specify an email.' };
1501
+ if (!('password' in data) || !data.password.length) {
1502
+ return {
1503
+ result: false,
1504
+ loggedin: false,
1505
+ message: 'Password is a required field.',
1506
+ };
630
1507
  }
631
- else {
632
- return { result: true, message: '' };
1508
+ const unCheck = await this.$checkUsername();
1509
+ if (!unCheck.result) {
1510
+ return { ...unCheck, loggedin: false };
633
1511
  }
634
- }
635
- if (!tilmeld.config.validEmailRegex.test(this.$data.email)) {
636
- return {
637
- result: false,
638
- message: tilmeld.config.validEmailRegexNotice,
639
- };
640
- }
641
- const selector = {
642
- type: '&',
643
- ilike: ['email', this.$data.email.replace(/([\\%_])/g, (s) => `\\${s}`)],
644
- };
645
- if (this.guid != null) {
646
- selector['!guid'] = this.guid;
647
- }
648
- const test = await this.$nymph.getEntity({ class: tilmeld.User, skipAc: true }, selector);
649
- if (test != null) {
650
- return {
651
- result: false,
652
- message: 'That email address is already registered.',
653
- };
654
- }
655
- return {
656
- result: true,
657
- message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
658
- };
659
- }
660
- async $checkPhone() {
661
- const tilmeld = this.$nymph.tilmeld;
662
- if (this.$data.phone == null || !this.$data.phone.length) {
663
- return { result: false, message: 'Please specify a phone number.' };
664
- }
665
- const stripToDigits = this.$data.phone.replace(/\D/g, () => '');
666
- if (!/\d{10}/.test(stripToDigits)) {
667
- return {
668
- result: false,
669
- message: 'Phone must contain at least 10 digits, but formatting does not matter.',
670
- };
671
- }
672
- const selector = {
673
- type: '&',
674
- equal: ['phone', stripToDigits],
675
- };
676
- if (this.guid != null) {
677
- selector['!guid'] = this.guid;
678
- }
679
- const test = await this.$nymph.getEntity({ class: tilmeld.User, skipAc: true }, selector);
680
- if (test != null) {
681
- return { result: false, message: 'Phone number is in use.' };
682
- }
683
- return {
684
- result: true,
685
- message: this.guid != null ? 'Phone number is valid.' : 'Phone number is valid!',
686
- };
687
- }
688
- async $register(data) {
689
- let tilmeld = this.$nymph.tilmeld;
690
- if (!tilmeld.config.allowRegistration) {
691
- return {
692
- result: false,
693
- loggedin: false,
694
- message: 'Registration is not allowed.',
695
- };
696
- }
697
- if (this.guid != null) {
698
- return {
699
- result: false,
700
- loggedin: false,
701
- message: 'This is already a registered user.',
702
- };
703
- }
704
- try {
705
- for (let callback of this.constructor
706
- .beforeRegisterCallbacks) {
707
- if (callback) {
708
- await callback(this, data);
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;
709
1520
  }
710
1521
  }
711
- }
712
- catch (e) {
713
- return {
714
- result: false,
715
- loggedin: false,
716
- message: e.message,
717
- };
718
- }
719
- if (!('password' in data) || !data.password.length) {
720
- return {
721
- result: false,
722
- loggedin: false,
723
- message: 'Password is a required field.',
724
- };
725
- }
726
- const unCheck = await this.$checkUsername();
727
- if (!unCheck.result) {
728
- return { ...unCheck, loggedin: false };
729
- }
730
- this.$password(data.password);
731
- if (tilmeld.config.regFields.indexOf('name') !== -1) {
732
- this.$data.name =
733
- this.$data.nameFirst +
734
- (this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
735
- (this.$data.nameLast == null ? '' : ' ' + this.$data.nameLast);
736
- if (this.$data.name === '') {
737
- this.$data.name = this.$data.username;
1522
+ if (tilmeld.config.emailUsernames) {
1523
+ this.$data.email = this.$data.username;
738
1524
  }
739
- }
740
- if (tilmeld.config.emailUsernames) {
741
- this.$data.email = this.$data.username;
742
- }
743
- const transaction = 'tilmeld-register-' + (0, guid_1.nanoid)();
744
- const nymph = this.$nymph;
745
- const tnymph = await this.$nymph.startTransaction(transaction);
746
- this.$nymph = tnymph;
747
- tilmeld = this.$nymph.tilmeld;
748
- try {
749
- let generatedPrimaryGroup = null;
750
- if (tilmeld.config.generatePrimary) {
751
- generatedPrimaryGroup = await tilmeld.Group.factory();
752
- generatedPrimaryGroup.groupname = this.$data.username;
753
- generatedPrimaryGroup.avatar = this.$data.avatar;
754
- generatedPrimaryGroup.name = this.$data.name;
755
- generatedPrimaryGroup.email = this.$data.email;
756
- const parent = await tnymph.getEntity({ class: tilmeld.Group }, {
757
- type: '&',
758
- equal: ['defaultPrimary', true],
1525
+ try {
1526
+ const result = await transaction(this.$nymph, 'tilmeld-register-' + nanoid(), async (nymph) => {
1527
+ this.$setNymph(nymph);
1528
+ const tilmeld = enforceTilmeld(this);
1529
+ let message = '';
1530
+ let loggedin = false;
1531
+ for (let callback of this.constructor
1532
+ .beforeRegisterCallbacks) {
1533
+ if (callback) {
1534
+ await callback(this, data);
1535
+ }
1536
+ }
1537
+ if (tilmeld.config.userFields.includes('email') &&
1538
+ tilmeld.config.verifyEmail) {
1539
+ // The user will be enabled after verifying their e-mail address.
1540
+ if (!tilmeld.config.unverifiedAccess) {
1541
+ this.$data.enabled = false;
1542
+ }
1543
+ }
1544
+ else {
1545
+ this.$data.enabled = true;
1546
+ }
1547
+ // If createAdmin is true and there are no other users, grant
1548
+ // "system/admin".
1549
+ let madeAdmin = false;
1550
+ if (tilmeld.config.createAdmin) {
1551
+ const otherUsers = await this.$nymph.getEntities({
1552
+ class: tilmeld.User,
1553
+ skipAc: true,
1554
+ limit: 1,
1555
+ return: 'guid',
1556
+ });
1557
+ // Make sure it's not just null, cause that means an error.
1558
+ if (otherUsers == null) {
1559
+ throw new Error('An error occurred.');
1560
+ }
1561
+ if (!otherUsers.length) {
1562
+ this.$grant('system/admin');
1563
+ this.$data.enabled = true;
1564
+ madeAdmin = true;
1565
+ }
1566
+ }
1567
+ this.$nymph.config.debugLog('tilmeld', `Registering new user "${this.$data.username}".`);
1568
+ await this.$saveSkipAC();
1569
+ // Finish up.
1570
+ if (tilmeld.config.verifyEmail &&
1571
+ !tilmeld.config.unverifiedAccess &&
1572
+ !madeAdmin &&
1573
+ tilmeld.config.userFields.includes('email')) {
1574
+ message +=
1575
+ `Almost there. An email has been sent to ${this.$data.email} ` +
1576
+ 'with a verification link for you to finish registration.';
1577
+ }
1578
+ else if (tilmeld.config.verifyEmail &&
1579
+ tilmeld.config.unverifiedAccess &&
1580
+ !madeAdmin &&
1581
+ tilmeld.config.userFields.includes('email')) {
1582
+ if (!(await tilmeld.login(this, true))) {
1583
+ throw new Error('An error occurred trying to log you in.');
1584
+ }
1585
+ message +=
1586
+ "You're now logged in! An email has been sent to " +
1587
+ `${this.$data.email} with a verification link for you to finish ` +
1588
+ 'registration.';
1589
+ loggedin = true;
1590
+ }
1591
+ else {
1592
+ if (!(await tilmeld.login(this, true))) {
1593
+ throw new Error('An error occurred trying to log you in.');
1594
+ }
1595
+ message += "You're now registered and logged in!";
1596
+ loggedin = true;
1597
+ }
1598
+ for (let callback of this.constructor
1599
+ .afterRegisterCallbacks) {
1600
+ if (callback) {
1601
+ await callback(this, { loggedin, message });
1602
+ }
1603
+ }
1604
+ return { result: true, loggedin, message };
1605
+ }, async (nymph) => {
1606
+ this.$setNymph(nymph);
759
1607
  });
760
- if (parent != null) {
761
- generatedPrimaryGroup.parent = parent;
1608
+ // Send the new user registered email.
1609
+ if (tilmeld.config.userRegisteredRecipient != null) {
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
+ }
1625
+ }
1626
+ catch (e) {
1627
+ this.$nymph.config.debugError('tilmeld', `Couldn't send new user registered email for "${this.$data.username}": ${e.message}`);
1628
+ }
762
1629
  }
763
- if (!(await generatedPrimaryGroup.$saveSkipAC())) {
764
- await tnymph.rollback(transaction);
765
- this.$nymph = nymph;
766
- return {
767
- result: false,
768
- loggedin: false,
769
- message: 'Error creating primary group for user.',
770
- };
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
+ };
1640
+ }
771
1641
  }
772
- this.$data.group = generatedPrimaryGroup;
1642
+ return result;
773
1643
  }
774
- else {
775
- const group = await tnymph.getEntity({ class: tilmeld.Group }, {
776
- type: '&',
777
- equal: ['defaultPrimary', true],
778
- });
779
- if (group != null) {
780
- this.$data.group = group;
1644
+ catch (e) {
1645
+ return { result: false, loggedin: false, message: e.message };
1646
+ }
1647
+ }
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.');
1652
+ }
1653
+ if (!this.$skipAcWhenSaving &&
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()}`;
781
1664
  }
782
1665
  }
783
- if (tilmeld.config.verifyEmail && tilmeld.config.unverifiedAccess) {
784
- this.$data.groups = await tnymph.getEntities({ class: tilmeld.Group }, {
785
- type: '&',
786
- equal: ['unverifiedSecondary', true],
787
- });
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.");
1673
+ }
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
+ }
1688
+ }
788
1689
  }
789
- else {
790
- this.$data.groups = await tnymph.getEntities({ class: tilmeld.Group }, {
791
- type: '&',
792
- equal: ['defaultSecondary', true],
793
- });
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;
1695
+ }
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;
794
1739
  }
795
- if (tilmeld.config.verifyEmail) {
796
- if (!tilmeld.config.unverifiedAccess) {
797
- this.$data.enabled = false;
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);
798
1783
  }
799
1784
  }
800
- else {
801
- this.$data.enabled = true;
802
- }
803
- let madeAdmin = false;
804
- if (tilmeld.config.createAdmin) {
805
- const otherUsers = await tnymph.getEntities({
806
- class: tilmeld.User,
807
- skipAc: true,
808
- limit: 1,
809
- return: 'guid',
810
- });
811
- if (!otherUsers.length) {
812
- this.$grant('system/admin');
813
- this.$data.enabled = true;
814
- madeAdmin = true;
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
+ }
1799
+ }
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.');
1810
+ }
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;
1833
+ }
1834
+ }
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;
815
1849
  }
816
1850
  }
817
- if (await this.$saveSkipAC()) {
818
- if (tilmeld.config.userRegisteredRecipient != null) {
819
- await tilmeld.config.sendEmail(tilmeld, {
820
- template: 'UserRegistered',
821
- message: {
822
- to: tilmeld.config.userRegisteredRecipient,
823
- },
824
- locals: {
825
- userUsername: this.$data.username,
826
- userName: this.$data.name,
827
- userFirstName: this.$data.nameFirst,
828
- userLastName: this.$data.nameLast,
829
- userEmail: this.$data.email,
830
- userPhone: this.$data.phone,
831
- },
832
- }, this);
833
- }
834
- let message = '';
835
- let loggedin = false;
836
- if (generatedPrimaryGroup != null) {
837
- generatedPrimaryGroup.user = this;
838
- if (!(await generatedPrimaryGroup.$saveSkipAC())) {
839
- await tnymph.rollback(transaction);
840
- this.$nymph = nymph;
841
- return {
842
- result: false,
843
- loggedin: false,
844
- message: 'Your primary group could not be assigned. Please try again later.',
845
- };
1851
+ if (this.$data.passwordTemp != null) {
1852
+ this.$password(this.$data.passwordTemp);
1853
+ }
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
+ }
846
1875
  }
847
1876
  }
848
- if (tilmeld.config.verifyEmail &&
849
- !tilmeld.config.unverifiedAccess &&
850
- !madeAdmin) {
851
- message +=
852
- `Almost there. An email has been sent to ${this.$data.email} ` +
853
- 'with a verification link for you to finish registration.';
854
- }
855
- else if (tilmeld.config.verifyEmail &&
856
- tilmeld.config.unverifiedAccess &&
857
- !madeAdmin) {
858
- if (!tilmeld.login(this, true)) {
859
- throw new Error('An error occurred trying to log you in.');
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
+ }
860
1893
  }
861
- message +=
862
- "You're now logged in! An email has been sent to " +
863
- `${this.$data.email} with a verification link for you to finish ` +
864
- 'registration.';
865
- loggedin = true;
866
1894
  }
867
- else {
868
- if (!tilmeld.login(this, true)) {
869
- throw new Error('An error occurred trying to log you in.');
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;
870
1910
  }
871
- message += "You're now registered and logged in!";
872
- loggedin = true;
1911
+ if (tilmeld.config.userFields.includes('name') &&
1912
+ group.name !== this.$data.name) {
1913
+ group.name = this.$data.name;
1914
+ changed = true;
1915
+ }
1916
+ if (tilmeld.config.userFields.includes('phone') &&
1917
+ group.phone !== this.$data.phone) {
1918
+ group.phone = this.$data.phone;
1919
+ changed = true;
1920
+ }
1921
+ if (changed || group.guid == null) {
1922
+ await group.$saveSkipAC();
1923
+ }
1924
+ this.$data.group = group;
873
1925
  }
874
- for (let callback of this.constructor
875
- .afterRegisterCallbacks) {
876
- if (callback) {
877
- await callback(this, {
878
- loggedin,
879
- message,
880
- });
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] });
881
1938
  }
882
1939
  }
883
1940
  try {
884
- await tnymph.commit(transaction);
885
- this.$nymph = nymph;
886
- this.$nymph.tilmeld?.fillSession(this);
1941
+ tilmeld.config.validatorUser(tilmeld, this);
887
1942
  }
888
1943
  catch (e) {
889
- throw e;
1944
+ throw new BadDataError(e?.message);
890
1945
  }
891
- return {
892
- result: true,
893
- loggedin,
894
- message,
895
- };
896
- }
897
- else {
898
- await tnymph.rollback(transaction);
899
- this.$nymph = nymph;
900
- return {
901
- result: false,
902
- loggedin: false,
903
- message: 'Error registering user.',
904
- };
905
- }
906
- }
907
- catch (e) {
908
- await tnymph.rollback(transaction);
909
- this.$nymph = nymph;
910
- throw e;
911
- }
912
- }
913
- async $save() {
914
- let tilmeld = this.$nymph.tilmeld;
915
- if (this.$data.username == null || !this.$data.username.trim().length) {
916
- return false;
917
- }
918
- if (tilmeld.gatekeeper('tilmeld/admin') &&
919
- !tilmeld.gatekeeper('system/admin') &&
920
- this.$gatekeeper('system/admin')) {
921
- throw new errors_1.BadDataError("You don't have the authority to modify system admins.");
922
- }
923
- let sendVerification = false;
924
- this.$data.username = this.$data.username.trim();
925
- if (tilmeld.config.emailUsernames) {
926
- this.$data.email = this.$data.username;
927
- }
928
- this.$data.nameFirst = (this.$data.nameFirst ?? '').trim();
929
- this.$data.nameMiddle = (this.$data.nameMiddle ?? '').trim();
930
- this.$data.nameLast = (this.$data.nameLast ?? '').trim();
931
- this.$data.phone = (this.$data.phone ?? '').trim();
932
- this.$data.name =
933
- this.$data.nameFirst +
934
- (this.$data.nameMiddle ? ' ' + this.$data.nameMiddle : '') +
935
- (this.$data.nameLast ? ' ' + this.$data.nameLast : '');
936
- this.$data.enabled = !!this.$data.enabled;
937
- if (this.$data.nameFirst === '') {
938
- delete this.$data.nameFirst;
939
- }
940
- if (this.$data.nameMiddle === '') {
941
- delete this.$data.nameMiddle;
942
- }
943
- if (this.$data.nameLast === '') {
944
- delete this.$data.nameLast;
945
- }
946
- if (this.$data.name === '') {
947
- delete this.$data.name;
948
- }
949
- if (this.$data.email === '') {
950
- delete this.$data.email;
951
- }
952
- if (this.$data.avatar === '') {
953
- delete this.$data.avatar;
954
- }
955
- if (this.$data.phone === '') {
956
- delete this.$data.phone;
957
- }
958
- if (this.$data.secret === '') {
959
- delete this.$data.secret;
960
- }
961
- if (this.$data.emailChangeDate === 0) {
962
- delete this.$data.emailChangeDate;
963
- }
964
- if (this.$data.newEmailSecret === '') {
965
- delete this.$data.newEmailSecret;
966
- }
967
- if (this.$data.newEmailAddress === '') {
968
- delete this.$data.newEmailAddress;
969
- }
970
- if (this.$data.cancelEmailSecret === '') {
971
- delete this.$data.cancelEmailSecret;
972
- }
973
- if (this.$data.cancelEmailAddress === '') {
974
- delete this.$data.cancelEmailAddress;
975
- }
976
- if (this.$data.recoverSecret === '') {
977
- delete this.$data.recoverSecret;
978
- }
979
- if (this.$data.recoverSecretDate === 0) {
980
- delete this.$data.recoverSecretDate;
981
- }
982
- if (this.$data.password === '') {
983
- delete this.$data.password;
984
- }
985
- if (this.$data.passwordTemp === '') {
986
- delete this.$data.passwordTemp;
987
- }
988
- if (this.$data.inheritAbilities === false) {
989
- delete this.$data.inheritAbilities;
990
- }
991
- const unCheck = await this.$checkUsername();
992
- if (!unCheck.result) {
993
- throw new errors_1.BadUsernameError(unCheck.message);
994
- }
995
- if (!tilmeld.config.emailUsernames) {
996
- const emCheck = await this.$checkEmail();
997
- if (!emCheck.result) {
998
- throw new errors_1.BadEmailError(emCheck.message);
999
- }
1000
- }
1001
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
1002
- if (tilmeld.config.verifyEmail) {
1003
- if (this.guid == null) {
1004
- if ((this.$data.abilities?.indexOf('system/admin') ?? -1) === -1) {
1005
- this.$data.secret = (0, guid_1.nanoid)();
1006
- sendVerification = true;
1946
+ for (let callback of this.constructor
1947
+ .beforeSaveCallbacks) {
1948
+ if (callback) {
1949
+ await callback(this);
1007
1950
  }
1008
1951
  }
1009
- else if (this.$originalEmail != null &&
1010
- this.$originalEmail !== this.$data.email) {
1011
- if (tilmeld.config.emailRateLimit !== '' &&
1012
- this.$data.emailChangeDate != null &&
1013
- this.$data.emailChangeDate >=
1014
- (0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000) {
1015
- throw new errors_1.EmailChangeRateLimitExceededError('You already changed your email address recently. Please wait until ' +
1016
- new Date((0, strtotime_1.default)('+' + tilmeld.config.emailRateLimit, Math.floor(this.$data.emailChangeDate / 1000)) * 1000).toString() +
1017
- ' to change your email address again.');
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.");
1018
1957
  }
1019
- else {
1020
- if (this.$data.newEmailSecret == null &&
1021
- tilmeld.config.emailRateLimit !== '' &&
1022
- (this.$data.emailChangeDate == null ||
1023
- this.$data.emailChangeDate <
1024
- (0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
1025
- this.$data.cancelEmailAddress = this.$originalEmail;
1026
- this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
1027
- this.$data.emailChangeDate = Date.now();
1028
- }
1029
- this.$data.newEmailAddress = this.$data.email;
1030
- this.$data.email = this.$originalEmail;
1031
- if (tilmeld.config.emailUsernames) {
1032
- this.$data.username = this.$data.email;
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);
1033
1977
  }
1034
- this.$data.newEmailSecret = (0, guid_1.nanoid)();
1035
- sendVerification = true;
1978
+ }
1979
+ catch (e) {
1980
+ // Ignore.
1036
1981
  }
1037
1982
  }
1038
- }
1039
- else if (this.guid != null &&
1040
- this.$originalEmail != null &&
1041
- this.$originalEmail !== this.$data.email &&
1042
- (this.$data.emailChangeDate == null ||
1043
- this.$data.emailChangeDate <
1044
- (0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
1045
- this.$data.cancelEmailAddress = this.$originalEmail;
1046
- this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
1047
- sendVerification = true;
1048
- }
1049
- }
1050
- if (this.$data.passwordTemp != null) {
1051
- this.$password(this.$data.passwordTemp);
1052
- }
1053
- delete this.$data.passwordTemp;
1054
- if (this.$data.password == null) {
1055
- throw new errors_1.BadDataError('A password is required.');
1983
+ else {
1984
+ this.guid = preGuid;
1985
+ this.cdate = preCdate;
1986
+ this.mdate = preMdate;
1987
+ }
1988
+ });
1056
1989
  }
1057
- try {
1058
- tilmeld.config.validatorUser(this);
1990
+ /**
1991
+ * This should *never* be accessible on the client.
1992
+ */
1993
+ async $saveSkipAC() {
1994
+ this.$skipAcWhenSaving = true;
1995
+ await this.$save();
1059
1996
  }
1060
- catch (e) {
1061
- throw new errors_1.BadDataError(e?.message);
1997
+ $tilmeldSaveSkipAC() {
1998
+ if (this.$skipAcWhenSaving) {
1999
+ this.$skipAcWhenSaving = false;
2000
+ return true;
2001
+ }
2002
+ return false;
1062
2003
  }
1063
- const transaction = 'tilmeld-save-' + (0, guid_1.nanoid)();
1064
- const nymph = this.$nymph;
1065
- const tnymph = await this.$nymph.startTransaction(transaction);
1066
- this.$nymph = tnymph;
1067
- tilmeld = this.$nymph.tilmeld;
1068
- if (this.$data.group != null &&
1069
- this.$data.group.user != null &&
1070
- this.$is(this.$data.group.user)) {
1071
- try {
1072
- this.$data.group.groupname = this.$data.username;
1073
- this.$data.group.avatar = this.$data.avatar;
1074
- this.$data.group.email = this.$data.email;
1075
- this.$data.group.name = this.$data.name;
1076
- this.$data.group.phone = this.$data.phone;
1077
- await this.$data.group.$saveSkipAC();
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.");
1078
2008
  }
1079
- catch (e) {
1080
- await tnymph.rollback(transaction);
1081
- this.$nymph = nymph;
1082
- throw e;
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.");
1083
2013
  }
1084
- }
1085
- let ret;
1086
- let preGuid = this.guid;
1087
- let preCdate = this.cdate;
1088
- let preMdate = this.mdate;
1089
- try {
1090
- ret = await super.$save();
1091
- }
1092
- catch (e) {
1093
- await tnymph.rollback(transaction);
1094
- this.$nymph = nymph;
1095
- throw e;
1096
- }
1097
- if (ret) {
1098
- if (sendVerification) {
1099
- if (!(await this.$sendEmailVerification())) {
1100
- await tnymph.rollback(transaction);
1101
- this.guid = preGuid;
1102
- this.cdate = preCdate;
1103
- this.mdate = preMdate;
1104
- this.$nymph = nymph;
1105
- throw new Error("Couldn't send verification email.");
2014
+ if (this.$data.username !== this.$originalUsername) {
2015
+ throw new AccessControlError("You can't delete a user while switching their username.");
2016
+ }
2017
+ if (!(await this.$refresh())) {
2018
+ throw new BadDataError('User could not be refreshed.');
2019
+ }
2020
+ if (this.$data.group != null) {
2021
+ if (this.$data.group.$asleep()) {
2022
+ await this.$data.group.$wake();
2023
+ }
2024
+ else {
2025
+ if (!(await this.$data.group.$refresh())) {
2026
+ throw new BadDataError('Group could not be refreshed.');
2027
+ }
1106
2028
  }
1107
2029
  }
1108
- this.$descendantGroups = undefined;
1109
- this.$gatekeeperCache = undefined;
1110
- await tnymph.commit(transaction);
1111
- if (nymph.tilmeld.User.current(true).$is(this)) {
1112
- nymph.tilmeld.fillSession(this);
2030
+ for (let callback of this.constructor
2031
+ .beforeDeleteCallbacks) {
2032
+ if (callback) {
2033
+ await callback(this);
2034
+ }
2035
+ }
2036
+ if (tilmeld.User.current(true).$is(this)) {
2037
+ await this.$logout();
2038
+ }
2039
+ if (this.$data.group != null && this.$is(this.$data.group.user)) {
2040
+ // Read the skip ac value here, because super.$delete changes it.
2041
+ const $skipAcWhenDeleting = this.$skipAcWhenDeleting;
2042
+ // Delete the user.
2043
+ await super.$delete();
2044
+ // Delete the generated group.
2045
+ if ($skipAcWhenDeleting) {
2046
+ await this.$data.group.$deleteSkipAC();
2047
+ }
2048
+ else {
2049
+ await this.$data.group.$delete();
2050
+ }
2051
+ }
2052
+ else {
2053
+ await super.$delete();
2054
+ }
2055
+ for (let callback of this.constructor
2056
+ .afterDeleteCallbacks) {
2057
+ if (callback) {
2058
+ await callback(this);
2059
+ }
1113
2060
  }
1114
2061
  }
1115
- else {
1116
- await tnymph.rollback(transaction);
1117
- }
1118
- this.$nymph = nymph;
1119
- return ret;
1120
- }
1121
- async $saveSkipAC() {
1122
- this.$skipAcWhenSaving = true;
1123
- return await this.$save();
1124
- }
1125
- $tilmeldSaveSkipAC() {
1126
- if (this.$skipAcWhenSaving) {
1127
- this.$skipAcWhenSaving = false;
1128
- return true;
1129
- }
1130
- return false;
1131
- }
1132
- async $delete() {
1133
- const tilmeld = this.$nymph.tilmeld;
1134
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
1135
- throw new errors_1.BadDataError("You don't have the authority to delete users.");
1136
- }
1137
- if (!tilmeld.gatekeeper('system/admin') &&
1138
- this.$gatekeeper('system/admin')) {
1139
- throw new errors_1.BadDataError("You don't have the authority to delete system admins.");
1140
- }
1141
- if (tilmeld.User.current(true).$is(this)) {
1142
- await this.$logout();
1143
- }
1144
- return await super.$delete();
1145
- }
1146
- static on(event, callback) {
1147
- const prop = (event + 'Callbacks');
1148
- if (!(prop in this)) {
1149
- 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();
1150
2068
  }
1151
- this[prop].push(callback);
1152
- return () => this.off(event, callback);
1153
- }
1154
- static off(event, callback) {
1155
- const prop = (event + 'Callbacks');
1156
- if (!(prop in this)) {
2069
+ $tilmeldDeleteSkipAC() {
2070
+ if (this.$skipAcWhenDeleting) {
2071
+ this.$skipAcWhenDeleting = false;
2072
+ return true;
2073
+ }
1157
2074
  return false;
1158
2075
  }
1159
- const i = this[prop].indexOf(callback);
1160
- if (i > -1) {
1161
- this[prop].splice(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
+ }
2081
+ // @ts-ignore: The callback should always be the right type here.
2082
+ this[prop].push(callback);
2083
+ return () => this.off(event, callback);
2084
+ }
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;
1162
2097
  }
1163
- return true;
1164
- }
1165
- }
1166
- exports.default = User;
1167
- User.ETYPE = 'tilmeld_user';
1168
- User.class = 'User';
1169
- User.beforeRegisterCallbacks = [];
1170
- User.afterRegisterCallbacks = [];
1171
- User.beforeLoginCallbacks = [];
1172
- User.afterLoginCallbacks = [];
1173
- User.beforeLogoutCallbacks = [];
1174
- User.afterLogoutCallbacks = [];
1175
- User.DEFAULT_CLIENT_ENABLED_METHODS = [
1176
- '$checkUsername',
1177
- '$checkEmail',
1178
- '$checkPhone',
1179
- '$getAvatar',
1180
- '$register',
1181
- ];
1182
- User.DEFAULT_PRIVATE_DATA = [
1183
- 'email',
1184
- 'originalEmail',
1185
- 'phone',
1186
- 'group',
1187
- 'groups',
1188
- 'abilities',
1189
- 'inheritAbilities',
1190
- 'recoverSecret',
1191
- 'recoverSecretDate',
1192
- 'password',
1193
- 'salt',
1194
- 'secret',
1195
- 'newEmailAddress',
1196
- 'newEmailSecret',
1197
- 'cancelEmailAddress',
1198
- 'cancelEmailSecret',
1199
- 'emailChangeDate',
1200
- ];
1201
- User.DEFAULT_ALLOWLIST_DATA = [];
1202
- User.clientEnabledStaticMethods = [
1203
- 'current',
1204
- 'loginUser',
1205
- 'sendRecovery',
1206
- 'recover',
1207
- 'getClientConfig',
1208
- ];
1209
- User.searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
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;
1210
2110
  //# sourceMappingURL=User.js.map