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