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

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