@nymphjs/tilmeld 1.0.0-beta.12 → 1.0.0-beta.121
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 +550 -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 +571 -191
- package/dist/Group.js.map +1 -1
- package/dist/Group.testArtifacts.d.ts +3 -0
- package/dist/Group.testArtifacts.js +120 -0
- package/dist/Group.testArtifacts.js.map +1 -0
- package/dist/Tilmeld.d.ts +241 -12
- package/dist/Tilmeld.js +721 -273
- 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 +1386 -367
- package/dist/User.js.map +1 -1
- package/dist/User.testArtifacts.d.ts +3 -0
- package/dist/User.testArtifacts.js +175 -0
- package/dist/User.testArtifacts.js.map +1 -0
- 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 +355 -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 +11 -10
- package/dist/index.js +16 -35
- package/dist/index.js.map +1 -1
- package/dist/testArtifacts.d.ts +19 -0
- package/dist/testArtifacts.js +1173 -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 +31 -5
- package/package.json +41 -34
- package/src/AbleObject.ts +2 -2
- package/src/Group.testArtifacts.ts +159 -0
- package/src/Group.ts +684 -225
- package/src/Tilmeld.ts +547 -283
- package/src/Tilmeld.types.ts +5 -5
- package/src/User.testArtifacts.ts +226 -0
- package/src/User.ts +1635 -461
- package/src/conf/defaults.ts +172 -109
- package/src/conf/index.ts +370 -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 +12 -10
- package/src/testArtifacts.ts +1979 -0
- package/src/tilmeldJoiProps.ts +8 -23
- package/tsconfig.json +6 -3
- package/typedoc.json +4 -0
- package/dist/Group.test.d.ts +0 -1
- package/dist/Group.test.js +0 -48
- package/dist/Group.test.js.map +0 -1
- package/dist/User.test.d.ts +0 -1
- package/dist/User.test.js +0 -51
- package/dist/User.test.js.map +0 -1
- package/dist/conf/d.d.ts +0 -55
- package/dist/conf/d.js +0 -3
- package/dist/conf/d.js.map +0 -1
- package/src/Group.test.ts +0 -58
- package/src/User.test.ts +0 -61
- package/src/conf/d.ts +0 -303
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/index';
|
|
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@port87.com>
|
|
18
|
+
* @copyright SciActive Inc
|
|
19
|
+
* @see http://nymph.io/
|
|
20
|
+
*/
|
|
21
|
+
export default class User extends AbleObject {
|
|
22
|
+
/**
|
|
23
|
+
* The instance of Tilmeld to use for queries.
|
|
24
|
+
*/
|
|
25
|
+
static ETYPE = 'tilmeld_user';
|
|
26
|
+
static class = 'User';
|
|
27
|
+
static checkUsernameCallbacks = [];
|
|
28
|
+
static beforeRegisterCallbacks = [];
|
|
29
|
+
static afterRegisterCallbacks = [];
|
|
30
|
+
static beforeSaveCallbacks = [];
|
|
31
|
+
static afterSaveCallbacks = [];
|
|
32
|
+
static beforeLoginCallbacks = [];
|
|
33
|
+
static afterLoginCallbacks = [];
|
|
34
|
+
static beforeLogoutCallbacks = [];
|
|
35
|
+
static afterLogoutCallbacks = [];
|
|
36
|
+
static beforeDeleteCallbacks = [];
|
|
37
|
+
static afterDeleteCallbacks = [];
|
|
38
|
+
static DEFAULT_CLIENT_ENABLED_METHODS = [
|
|
39
|
+
'$checkUsername',
|
|
40
|
+
'$checkEmail',
|
|
41
|
+
'$checkPhone',
|
|
42
|
+
'$getAvatar',
|
|
43
|
+
'$register',
|
|
44
|
+
];
|
|
45
|
+
static DEFAULT_PRIVATE_DATA = [
|
|
46
|
+
// The things that are commented are there to show what is _not_ private.
|
|
47
|
+
'abilities',
|
|
48
|
+
// 'username',
|
|
49
|
+
// 'nameFirst',
|
|
50
|
+
// 'nameMiddle',
|
|
51
|
+
// 'nameLast',
|
|
52
|
+
// 'name',
|
|
53
|
+
'email',
|
|
54
|
+
// 'avatar',
|
|
55
|
+
'phone',
|
|
56
|
+
'group',
|
|
57
|
+
'groups',
|
|
58
|
+
'inheritAbilities',
|
|
59
|
+
'enabled',
|
|
60
|
+
'secret',
|
|
61
|
+
'emailChangeDate',
|
|
62
|
+
'newEmailSecret',
|
|
63
|
+
'newEmailAddress',
|
|
64
|
+
'cancelEmailSecret',
|
|
65
|
+
'cancelEmailAddress',
|
|
66
|
+
'recoverSecret',
|
|
67
|
+
'recoverSecretDate',
|
|
68
|
+
'salt',
|
|
69
|
+
'password',
|
|
70
|
+
'passwordTemp',
|
|
71
|
+
'revokeTokenDate',
|
|
72
|
+
'totpSecret',
|
|
73
|
+
];
|
|
74
|
+
static DEFAULT_ALLOWLIST_DATA = [];
|
|
75
|
+
$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
76
|
+
static clientEnabledStaticMethods = [
|
|
77
|
+
'current',
|
|
78
|
+
'loginUser',
|
|
79
|
+
'sendRecovery',
|
|
80
|
+
'recover',
|
|
81
|
+
'getClientConfig',
|
|
82
|
+
'getDomainUsers',
|
|
83
|
+
];
|
|
84
|
+
$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
85
|
+
static searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
|
|
86
|
+
$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
|
|
87
|
+
$allowlistTags = [];
|
|
88
|
+
/**
|
|
89
|
+
* Gatekeeper ability cache.
|
|
90
|
+
*
|
|
91
|
+
* Gatekeeper will cache the user's abilities that it calculates, so it can
|
|
92
|
+
* check faster if that user has been checked before.
|
|
93
|
+
*/
|
|
94
|
+
$gatekeeperCache;
|
|
95
|
+
/**
|
|
96
|
+
* This should only be used by the backend.
|
|
97
|
+
*/
|
|
98
|
+
$skipAcWhenSaving = false;
|
|
99
|
+
/**
|
|
100
|
+
* This should only be used by the backend.
|
|
101
|
+
*/
|
|
102
|
+
$skipAcWhenDeleting = false;
|
|
103
|
+
/**
|
|
104
|
+
* The user's group descendants.
|
|
105
|
+
*/
|
|
106
|
+
$descendantGroups;
|
|
107
|
+
/**
|
|
108
|
+
* Used to save the current email address to send verification if it changes
|
|
109
|
+
* from the frontend.
|
|
110
|
+
*
|
|
111
|
+
* If you are changing a user's email address and want to bypass email
|
|
112
|
+
* verification, don't set this.
|
|
113
|
+
*/
|
|
114
|
+
$originalEmail;
|
|
115
|
+
/**
|
|
116
|
+
* Used to save the current username for domain admin permissions.
|
|
117
|
+
*/
|
|
118
|
+
$originalUsername;
|
|
119
|
+
static getFTSText(name, value) {
|
|
120
|
+
if ([
|
|
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)) || 0) *
|
|
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,149 @@ 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
|
+
// TODO: Fix these being added by the drivers.
|
|
670
|
+
// @ts-ignore
|
|
671
|
+
delete this.$data.acRead;
|
|
672
|
+
// @ts-ignore
|
|
673
|
+
delete this.$data.acWrite;
|
|
674
|
+
// @ts-ignore
|
|
675
|
+
delete this.$data.acFull;
|
|
676
|
+
if (source === 'server') {
|
|
677
|
+
this.$originalEmail = this.$data.email;
|
|
678
|
+
this.$originalUsername = this.$data.username;
|
|
679
|
+
}
|
|
314
680
|
this.$updateDataProtection();
|
|
315
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* Update the data protection arrays for a user.
|
|
684
|
+
*
|
|
685
|
+
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
686
|
+
*/
|
|
316
687
|
$updateDataProtection(givenUser) {
|
|
317
|
-
const tilmeld = this
|
|
688
|
+
const tilmeld = enforceTilmeld(this);
|
|
318
689
|
let user = givenUser ?? tilmeld.User.current();
|
|
690
|
+
let [_username, domain] = tilmeld.config.domainSupport
|
|
691
|
+
? splitn(this.$originalUsername ?? this.$data.username ?? '', '@', 2)
|
|
692
|
+
: [this.$originalUsername ?? this.$data.username, null];
|
|
693
|
+
// Lowercase the domain part.
|
|
694
|
+
if (domain) {
|
|
695
|
+
domain = domain.toLowerCase();
|
|
696
|
+
}
|
|
319
697
|
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
320
698
|
this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
321
699
|
this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
|
|
@@ -323,75 +701,211 @@ class User extends AbleObject_1.default {
|
|
|
323
701
|
this.$privateData.push('username');
|
|
324
702
|
}
|
|
325
703
|
const isCurrentUser = user != null && user.$is(this);
|
|
704
|
+
// While loading, accessing abilities on the current user will infinitely
|
|
705
|
+
// loop, but if this is the current user, we can use its abilities.
|
|
326
706
|
const abilities = (isCurrentUser || user == null
|
|
327
707
|
? this.$data.abilities
|
|
328
708
|
: user?.abilities) ?? [];
|
|
329
709
|
const isNewUser = this.guid == null;
|
|
710
|
+
const isDomainUser = tilmeld.config.domainSupport &&
|
|
711
|
+
!isCurrentUser &&
|
|
712
|
+
user != null &&
|
|
713
|
+
domain &&
|
|
714
|
+
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
330
715
|
if (isCurrentUser) {
|
|
716
|
+
// Users can check to see what abilities they have.
|
|
331
717
|
this.$clientEnabledMethods.push('$gatekeeper');
|
|
332
718
|
this.$clientEnabledMethods.push('$changePassword');
|
|
719
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
333
720
|
this.$clientEnabledMethods.push('$logout');
|
|
334
721
|
this.$clientEnabledMethods.push('$sendEmailVerification');
|
|
722
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
723
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
724
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
725
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
726
|
+
}
|
|
727
|
+
if (isDomainUser) {
|
|
728
|
+
// Domain admins can check to see what abilities their users have.
|
|
729
|
+
this.$clientEnabledMethods.push('$gatekeeper');
|
|
730
|
+
this.$clientEnabledMethods.push('$changePassword');
|
|
731
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
732
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
733
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
734
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
735
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
335
736
|
}
|
|
336
737
|
if (user != null &&
|
|
337
|
-
(abilities.
|
|
338
|
-
abilities.
|
|
339
|
-
|
|
738
|
+
(abilities.includes('tilmeld/admin') ||
|
|
739
|
+
abilities.includes('system/admin'))) {
|
|
740
|
+
// Users who can edit other users can see most of their data.
|
|
741
|
+
this.$privateData = ['password', 'salt', 'totpSecret'];
|
|
742
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
743
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
340
744
|
this.$allowlistData = undefined;
|
|
341
745
|
}
|
|
342
|
-
else if (
|
|
746
|
+
else if (isDomainUser) {
|
|
747
|
+
// Domain admins can see their users' data, and edit some of it.
|
|
343
748
|
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
344
749
|
this.$allowlistData.push('username');
|
|
345
750
|
}
|
|
346
751
|
this.$allowlistData.push('avatar');
|
|
347
|
-
if (tilmeld.config.userFields.
|
|
752
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
348
753
|
this.$allowlistData.push('nameFirst');
|
|
349
754
|
this.$allowlistData.push('nameMiddle');
|
|
350
755
|
this.$allowlistData.push('nameLast');
|
|
351
756
|
this.$allowlistData.push('name');
|
|
352
757
|
}
|
|
353
|
-
if (tilmeld.config.userFields.
|
|
758
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
354
759
|
this.$allowlistData.push('email');
|
|
355
760
|
}
|
|
356
|
-
if (tilmeld.config.userFields.
|
|
761
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
357
762
|
this.$allowlistData.push('phone');
|
|
358
763
|
}
|
|
764
|
+
this.$allowlistData.push('enabled');
|
|
765
|
+
this.$allowlistData.push('passwordTemp');
|
|
359
766
|
this.$privateData = [
|
|
767
|
+
// The things commented are there to show what is _not_ private.
|
|
768
|
+
// 'abilities',
|
|
769
|
+
// 'username',
|
|
770
|
+
// 'nameFirst',
|
|
771
|
+
// 'nameMiddle',
|
|
772
|
+
// 'nameLast',
|
|
773
|
+
// 'name',
|
|
774
|
+
// 'email',
|
|
775
|
+
// 'avatar',
|
|
776
|
+
// 'phone',
|
|
777
|
+
// 'group',
|
|
778
|
+
// 'groups',
|
|
779
|
+
// 'inheritAbilities',
|
|
780
|
+
// 'enabled',
|
|
360
781
|
'secret',
|
|
782
|
+
'emailChangeDate',
|
|
361
783
|
'newEmailSecret',
|
|
362
|
-
'
|
|
784
|
+
// 'newEmailAddress',
|
|
363
785
|
'cancelEmailSecret',
|
|
364
|
-
'
|
|
786
|
+
'cancelEmailAddress',
|
|
365
787
|
'recoverSecret',
|
|
366
788
|
'recoverSecretDate',
|
|
789
|
+
'salt',
|
|
367
790
|
'password',
|
|
791
|
+
// 'passwordTemp',
|
|
792
|
+
// 'revokeTokenDate',
|
|
793
|
+
'totpSecret',
|
|
794
|
+
];
|
|
795
|
+
}
|
|
796
|
+
else if (isCurrentUser || isNewUser) {
|
|
797
|
+
// Users can see their own data, and edit some of it.
|
|
798
|
+
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
799
|
+
this.$allowlistData.push('username');
|
|
800
|
+
}
|
|
801
|
+
this.$allowlistData.push('avatar');
|
|
802
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
803
|
+
this.$allowlistData.push('nameFirst');
|
|
804
|
+
this.$allowlistData.push('nameMiddle');
|
|
805
|
+
this.$allowlistData.push('nameLast');
|
|
806
|
+
this.$allowlistData.push('name');
|
|
807
|
+
}
|
|
808
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
809
|
+
this.$allowlistData.push('email');
|
|
810
|
+
}
|
|
811
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
812
|
+
this.$allowlistData.push('phone');
|
|
813
|
+
}
|
|
814
|
+
this.$privateData = [
|
|
815
|
+
// The things commented are there to show what is _not_ private.
|
|
816
|
+
// 'abilities',
|
|
817
|
+
// 'username',
|
|
818
|
+
// 'nameFirst',
|
|
819
|
+
// 'nameMiddle',
|
|
820
|
+
// 'nameLast',
|
|
821
|
+
// 'name',
|
|
822
|
+
// 'email',
|
|
823
|
+
// 'avatar',
|
|
824
|
+
// 'phone',
|
|
825
|
+
// 'group',
|
|
826
|
+
// 'groups',
|
|
827
|
+
// 'inheritAbilities',
|
|
828
|
+
'enabled',
|
|
829
|
+
'secret',
|
|
830
|
+
'emailChangeDate',
|
|
831
|
+
'newEmailSecret',
|
|
832
|
+
// 'newEmailAddress',
|
|
833
|
+
'cancelEmailSecret',
|
|
834
|
+
'cancelEmailAddress',
|
|
835
|
+
'recoverSecret',
|
|
836
|
+
'recoverSecretDate',
|
|
368
837
|
'salt',
|
|
838
|
+
'password',
|
|
839
|
+
'passwordTemp',
|
|
840
|
+
// 'revokeTokenDate',
|
|
841
|
+
'totpSecret',
|
|
369
842
|
];
|
|
370
843
|
}
|
|
844
|
+
if (user != null &&
|
|
845
|
+
!isCurrentUser &&
|
|
846
|
+
(abilities.includes('tilmeld/switch') ||
|
|
847
|
+
abilities.includes('system/admin'))) {
|
|
848
|
+
// Users with this ability can switch to other users.
|
|
849
|
+
this.$clientEnabledMethods.push('$switchUser');
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
$updateGroupDataProtection() {
|
|
853
|
+
if (this.$data.group != null && !this.$data.group.$asleep()) {
|
|
854
|
+
this.$data.group.$updateDataProtection();
|
|
855
|
+
}
|
|
856
|
+
for (let group of this.$data.groups ?? []) {
|
|
857
|
+
if (!group.$asleep()) {
|
|
858
|
+
group.$updateDataProtection();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
371
861
|
}
|
|
372
|
-
|
|
373
|
-
|
|
862
|
+
/**
|
|
863
|
+
* Check to see if a user has an ability.
|
|
864
|
+
*
|
|
865
|
+
* This function will check both user and group abilities, if the user is
|
|
866
|
+
* marked to inherit the abilities of its group.
|
|
867
|
+
*
|
|
868
|
+
* If `ability` is undefined, it will check to see if the user is currently
|
|
869
|
+
* logged in.
|
|
870
|
+
*
|
|
871
|
+
* If the user has the "system/admin" ability, this function will return true.
|
|
872
|
+
*
|
|
873
|
+
* @param ability The ability.
|
|
874
|
+
* @returns True or false.
|
|
875
|
+
*/
|
|
876
|
+
async $gatekeeper(ability) {
|
|
877
|
+
await this.$wake();
|
|
878
|
+
const tilmeld = enforceTilmeld(this);
|
|
374
879
|
if (ability == null) {
|
|
375
880
|
return tilmeld.User.current()?.$is(this) ?? false;
|
|
376
881
|
}
|
|
882
|
+
// Check the cache to see if we've already checked this user.
|
|
377
883
|
if (this.$gatekeeperCache == null) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
884
|
+
this.$gatekeeperCache = await this.$getGatekeeperCache();
|
|
885
|
+
}
|
|
886
|
+
return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
|
|
887
|
+
!!this.$data.abilities?.includes('system/admin'));
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Build a gatekeeper cache object.
|
|
891
|
+
*/
|
|
892
|
+
async $getGatekeeperCache() {
|
|
893
|
+
await this.$wake();
|
|
894
|
+
let abilities = this.$data.abilities ?? [];
|
|
895
|
+
if (this.$data.inheritAbilities) {
|
|
896
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
897
|
+
for (let curGroup of this.$data.groups ?? []) {
|
|
898
|
+
if (curGroup.enabled) {
|
|
899
|
+
abilities = abilities.concat(curGroup.abilities ?? []);
|
|
389
900
|
}
|
|
390
901
|
}
|
|
391
|
-
this.$
|
|
902
|
+
await this.$data.group?.$wake();
|
|
903
|
+
const group = this.$data.group;
|
|
904
|
+
if (group != null && group.cdate != null && group.enabled) {
|
|
905
|
+
abilities = abilities.concat(group.abilities ?? []);
|
|
906
|
+
}
|
|
392
907
|
}
|
|
393
|
-
return ((ability
|
|
394
|
-
!!this.$gatekeeperCache['system/admin']);
|
|
908
|
+
return Object.fromEntries(abilities.map((ability) => [ability, true]));
|
|
395
909
|
}
|
|
396
910
|
$clearCache() {
|
|
397
911
|
const ret = super.$clearCache();
|
|
@@ -399,8 +913,13 @@ class User extends AbleObject_1.default {
|
|
|
399
913
|
this.$gatekeeperCache = undefined;
|
|
400
914
|
return ret;
|
|
401
915
|
}
|
|
916
|
+
/**
|
|
917
|
+
* Send the user email verification/change/cancellation links.
|
|
918
|
+
*
|
|
919
|
+
* @returns True on success, false on failure.
|
|
920
|
+
*/
|
|
402
921
|
async $sendEmailVerification() {
|
|
403
|
-
const tilmeld = this
|
|
922
|
+
const tilmeld = enforceTilmeld(this);
|
|
404
923
|
if (this.guid == null) {
|
|
405
924
|
return false;
|
|
406
925
|
}
|
|
@@ -463,33 +982,60 @@ class User extends AbleObject_1.default {
|
|
|
463
982
|
}
|
|
464
983
|
return success;
|
|
465
984
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
985
|
+
/**
|
|
986
|
+
* Add the user to a (secondary) group.
|
|
987
|
+
*
|
|
988
|
+
* @param group The group.
|
|
989
|
+
* @returns True if the user is already in the group. The resulting array of groups if the user was not.
|
|
990
|
+
*/
|
|
991
|
+
async $addGroup(group) {
|
|
992
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
993
|
+
let groups = this.$data.groups;
|
|
994
|
+
if (groups == null) {
|
|
995
|
+
groups = [];
|
|
996
|
+
}
|
|
997
|
+
if (!group.$inArray(groups)) {
|
|
998
|
+
groups.push(group);
|
|
999
|
+
this.$data.groups = groups;
|
|
1000
|
+
return groups;
|
|
473
1001
|
}
|
|
474
1002
|
return true;
|
|
475
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Check the given password against the user's.
|
|
1006
|
+
*
|
|
1007
|
+
* @param password The password in question.
|
|
1008
|
+
* @returns True if the passwords match, otherwise false.
|
|
1009
|
+
*/
|
|
476
1010
|
$checkPassword(password) {
|
|
477
|
-
const tilmeld = this
|
|
1011
|
+
const tilmeld = enforceTilmeld(this);
|
|
478
1012
|
switch (tilmeld.config.pwMethod) {
|
|
479
1013
|
case 'plain':
|
|
480
1014
|
return this.$data.password === password;
|
|
481
1015
|
case 'digest':
|
|
482
|
-
return this.$data.password ===
|
|
1016
|
+
return (this.$data.password ===
|
|
1017
|
+
crypto.createHash('sha256').update(password).digest('base64'));
|
|
483
1018
|
case 'salt':
|
|
484
1019
|
default:
|
|
485
1020
|
return (this.$data.password ===
|
|
486
|
-
|
|
1021
|
+
crypto
|
|
1022
|
+
.createHash('sha256')
|
|
1023
|
+
.update(password + this.$data.salt)
|
|
1024
|
+
.digest('base64'));
|
|
487
1025
|
}
|
|
488
1026
|
}
|
|
489
|
-
|
|
490
|
-
|
|
1027
|
+
/**
|
|
1028
|
+
* Remove the user from a (secondary) group.
|
|
1029
|
+
*
|
|
1030
|
+
* @param group The group.
|
|
1031
|
+
* @returns True if the user wasn't in the group. The resulting array of groups if the user was.
|
|
1032
|
+
*/
|
|
1033
|
+
async $delGroup(group) {
|
|
1034
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1035
|
+
let groups = this.$data.groups || [];
|
|
1036
|
+
if (this.$data.groups != null && group.$inArray(groups)) {
|
|
491
1037
|
const newGroups = [];
|
|
492
|
-
for (let curGroup of
|
|
1038
|
+
for (let curGroup of groups) {
|
|
493
1039
|
if (!group.$is(curGroup)) {
|
|
494
1040
|
newGroups.push(curGroup);
|
|
495
1041
|
}
|
|
@@ -499,7 +1045,13 @@ class User extends AbleObject_1.default {
|
|
|
499
1045
|
}
|
|
500
1046
|
return true;
|
|
501
1047
|
}
|
|
502
|
-
|
|
1048
|
+
/**
|
|
1049
|
+
* Check whether the user is in a (primary or secondary) group.
|
|
1050
|
+
*
|
|
1051
|
+
* @param group The group, or the group's GUID.
|
|
1052
|
+
* @returns True or false.
|
|
1053
|
+
*/
|
|
1054
|
+
async $inGroup(group) {
|
|
503
1055
|
if (typeof group === 'string') {
|
|
504
1056
|
if (this.$data.group?.guid === group) {
|
|
505
1057
|
return true;
|
|
@@ -513,35 +1065,66 @@ class User extends AbleObject_1.default {
|
|
|
513
1065
|
}
|
|
514
1066
|
return (group.$is(this.$data.group) || group.$inArray(this.$data.groups ?? []));
|
|
515
1067
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1068
|
+
/**
|
|
1069
|
+
* Check whether the user is a descendant of a group.
|
|
1070
|
+
*
|
|
1071
|
+
* @param group The group, or the group's GUID.
|
|
1072
|
+
* @returns True or false.
|
|
1073
|
+
*/
|
|
1074
|
+
async $isDescendant(group) {
|
|
1075
|
+
// Check to see if the user is in a descendant group of the given group.
|
|
1076
|
+
await this.$data.group?.$wake();
|
|
1077
|
+
if (this.$data.group != null &&
|
|
1078
|
+
this.$data.group?.cdate != null &&
|
|
1079
|
+
(await this.$data.group?.$isDescendant(group))) {
|
|
519
1080
|
return true;
|
|
520
1081
|
}
|
|
1082
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
521
1083
|
for (let curGroup of this.$data.groups ?? []) {
|
|
522
|
-
if (curGroup.$isDescendant(group)) {
|
|
1084
|
+
if (await curGroup.$isDescendant(group)) {
|
|
523
1085
|
return true;
|
|
524
1086
|
}
|
|
525
1087
|
}
|
|
526
1088
|
return false;
|
|
527
1089
|
}
|
|
1090
|
+
/**
|
|
1091
|
+
* A frontend accessible method to change the user's password.
|
|
1092
|
+
*
|
|
1093
|
+
* @param data The input data from the client.
|
|
1094
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1095
|
+
*/
|
|
528
1096
|
async $changePassword(data) {
|
|
1097
|
+
this.$nymph.config.debugLog('tilmeld', `Password change request for "${this.$data.username}".`);
|
|
529
1098
|
if (!('newPassword' in data) || !data.newPassword.length) {
|
|
530
1099
|
return { result: false, message: 'Please specify a password.' };
|
|
531
1100
|
}
|
|
532
1101
|
if (!this.$checkPassword(data.currentPassword ?? '')) {
|
|
1102
|
+
this.$nymph.config.debugLog('tilmeld', `Incorrect password for change request for "${this.$data.username}".`);
|
|
533
1103
|
return { result: false, message: 'Incorrect password.' };
|
|
534
1104
|
}
|
|
535
1105
|
this.$data.passwordTemp = data.newPassword;
|
|
1106
|
+
if (data.revokeCurrentTokens) {
|
|
1107
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1108
|
+
}
|
|
536
1109
|
if (await this.$save()) {
|
|
1110
|
+
if (data.revokeCurrentTokens) {
|
|
1111
|
+
const tilmeld = enforceTilmeld(this);
|
|
1112
|
+
await tilmeld.login(this, true);
|
|
1113
|
+
}
|
|
537
1114
|
return { result: true, message: 'Your password has been changed.' };
|
|
538
1115
|
}
|
|
539
1116
|
else {
|
|
540
1117
|
return { result: false, message: "Couldn't save new password." };
|
|
541
1118
|
}
|
|
542
1119
|
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Change the user's password.
|
|
1122
|
+
*
|
|
1123
|
+
* @param password The new password.
|
|
1124
|
+
* @returns The resulting password or hash which is stored in the entity.
|
|
1125
|
+
*/
|
|
543
1126
|
$password(password) {
|
|
544
|
-
const tilmeld = this
|
|
1127
|
+
const tilmeld = enforceTilmeld(this);
|
|
545
1128
|
switch (tilmeld.config.pwMethod) {
|
|
546
1129
|
case 'plain':
|
|
547
1130
|
delete this.$data.salt;
|
|
@@ -549,24 +1132,221 @@ class User extends AbleObject_1.default {
|
|
|
549
1132
|
break;
|
|
550
1133
|
case 'digest':
|
|
551
1134
|
delete this.$data.salt;
|
|
552
|
-
this.$data.password =
|
|
1135
|
+
this.$data.password = crypto
|
|
1136
|
+
.createHash('sha256')
|
|
1137
|
+
.update(password)
|
|
1138
|
+
.digest('base64');
|
|
553
1139
|
break;
|
|
554
1140
|
case 'salt':
|
|
555
1141
|
default:
|
|
556
|
-
this.$data.salt =
|
|
557
|
-
this.$data.password =
|
|
1142
|
+
this.$data.salt = nanoid();
|
|
1143
|
+
this.$data.password = crypto
|
|
1144
|
+
.createHash('sha256')
|
|
1145
|
+
.update(password + this.$data.salt)
|
|
1146
|
+
.digest('base64');
|
|
558
1147
|
break;
|
|
559
1148
|
}
|
|
560
1149
|
return this.$data.password;
|
|
561
1150
|
}
|
|
1151
|
+
/**
|
|
1152
|
+
* A frontend accessible method to revoke all currently issued tokens.
|
|
1153
|
+
*
|
|
1154
|
+
* @param data The input data from the client.
|
|
1155
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1156
|
+
*/
|
|
1157
|
+
async $revokeCurrentTokens(data) {
|
|
1158
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1159
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1160
|
+
}
|
|
1161
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1162
|
+
if (await this.$save()) {
|
|
1163
|
+
const tilmeld = enforceTilmeld(this);
|
|
1164
|
+
await tilmeld.login(this, true);
|
|
1165
|
+
return {
|
|
1166
|
+
result: true,
|
|
1167
|
+
message: 'You have logged out of all other sessions.',
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
else {
|
|
1171
|
+
return { result: false, message: "Couldn't save revocation date." };
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
async $hasTOTPSecret() {
|
|
1175
|
+
return this.$data.totpSecret != null;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Check the given code against the user's TOTP secret.
|
|
1179
|
+
*
|
|
1180
|
+
* @param code The code in question.
|
|
1181
|
+
* @returns True if the code is valid, otherwise false.
|
|
1182
|
+
*/
|
|
1183
|
+
$checkTOTPCode(code) {
|
|
1184
|
+
if (this.$data.totpSecret == null) {
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
const tilmeld = enforceTilmeld(this);
|
|
1188
|
+
const totp = new TOTP({
|
|
1189
|
+
issuer: tilmeld.config.appName,
|
|
1190
|
+
label: this.$data.username,
|
|
1191
|
+
algorithm: 'SHA1',
|
|
1192
|
+
digits: 6,
|
|
1193
|
+
period: 30,
|
|
1194
|
+
secret: this.$data.totpSecret,
|
|
1195
|
+
});
|
|
1196
|
+
return totp.validate({ token: code, window: 2 }) != null;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* A frontend accessible method to generate a new TOTP secret.
|
|
1200
|
+
*
|
|
1201
|
+
* @returns An object with 'uri', 'qrcode', and 'secret'.
|
|
1202
|
+
*/
|
|
1203
|
+
async $getNewTOTPSecret() {
|
|
1204
|
+
const tilmeld = enforceTilmeld(this);
|
|
1205
|
+
if (this.$data.totpSecret != null) {
|
|
1206
|
+
throw new BadDataError('You already have a 2FA secret.');
|
|
1207
|
+
}
|
|
1208
|
+
const secret = new Secret();
|
|
1209
|
+
const totp = new TOTP({
|
|
1210
|
+
issuer: tilmeld.config.appName,
|
|
1211
|
+
label: this.$data.username,
|
|
1212
|
+
algorithm: 'SHA1',
|
|
1213
|
+
digits: 6,
|
|
1214
|
+
period: 30,
|
|
1215
|
+
secret,
|
|
1216
|
+
});
|
|
1217
|
+
const uri = totp.toString();
|
|
1218
|
+
const qrcode = await toDataURL(uri);
|
|
1219
|
+
return {
|
|
1220
|
+
uri,
|
|
1221
|
+
qrcode,
|
|
1222
|
+
secret: secret.base32,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* A frontend accessible method to save a TOTP secret into the user's account.
|
|
1227
|
+
*
|
|
1228
|
+
* @param data The input data from the client.
|
|
1229
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1230
|
+
*/
|
|
1231
|
+
async $saveTOTPSecret(data) {
|
|
1232
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1233
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1234
|
+
}
|
|
1235
|
+
if (this.$data.totpSecret != null) {
|
|
1236
|
+
return { result: false, message: 'You already have a 2FA secret.' };
|
|
1237
|
+
}
|
|
1238
|
+
if (data.secret == null ||
|
|
1239
|
+
typeof data.secret !== 'string' ||
|
|
1240
|
+
data.secret === '') {
|
|
1241
|
+
return { result: false, message: '2FA secret is invalid.' };
|
|
1242
|
+
}
|
|
1243
|
+
if (data.code == null ||
|
|
1244
|
+
typeof data.code !== 'string' ||
|
|
1245
|
+
data.code.length !== 6) {
|
|
1246
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1247
|
+
}
|
|
1248
|
+
const tilmeld = enforceTilmeld(this);
|
|
1249
|
+
const totp = new TOTP({
|
|
1250
|
+
issuer: tilmeld.config.appName,
|
|
1251
|
+
label: this.$data.username,
|
|
1252
|
+
algorithm: 'SHA1',
|
|
1253
|
+
digits: 6,
|
|
1254
|
+
period: 30,
|
|
1255
|
+
secret: data.secret,
|
|
1256
|
+
});
|
|
1257
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1258
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1259
|
+
}
|
|
1260
|
+
this.$data.totpSecret = data.secret;
|
|
1261
|
+
if (await this.$save()) {
|
|
1262
|
+
return { result: true, message: 'Your two factor secret is now set.' };
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
return { result: false, message: "Couldn't save two factor secret." };
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* A frontend accessible method to remove the TOTP secret from the user's
|
|
1270
|
+
* account.
|
|
1271
|
+
*
|
|
1272
|
+
* @param data The input data from the client.
|
|
1273
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1274
|
+
*/
|
|
1275
|
+
async $removeTOTPSecret(data) {
|
|
1276
|
+
const tilmeld = enforceTilmeld(this);
|
|
1277
|
+
if (data) {
|
|
1278
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1279
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1280
|
+
}
|
|
1281
|
+
if (data.code == null ||
|
|
1282
|
+
typeof data.code !== 'string' ||
|
|
1283
|
+
data.code.length !== 6) {
|
|
1284
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1285
|
+
}
|
|
1286
|
+
if (this.$data.totpSecret == null) {
|
|
1287
|
+
return { result: false, message: "You don't have a 2FA secret." };
|
|
1288
|
+
}
|
|
1289
|
+
const totp = new TOTP({
|
|
1290
|
+
issuer: tilmeld.config.appName,
|
|
1291
|
+
label: this.$data.username,
|
|
1292
|
+
algorithm: 'SHA1',
|
|
1293
|
+
digits: 6,
|
|
1294
|
+
period: 30,
|
|
1295
|
+
secret: this.$data.totpSecret,
|
|
1296
|
+
});
|
|
1297
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1298
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
else if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1302
|
+
return {
|
|
1303
|
+
result: false,
|
|
1304
|
+
message: 'You must provide your password and 2FA code.',
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
delete this.$data.totpSecret;
|
|
1308
|
+
if (await this.$save()) {
|
|
1309
|
+
return {
|
|
1310
|
+
result: true,
|
|
1311
|
+
message: 'Two factor secret has been removed.',
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
else {
|
|
1315
|
+
return { result: false, message: "Couldn't remove two factor secret." };
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Check that a username is valid.
|
|
1320
|
+
*
|
|
1321
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1322
|
+
*/
|
|
562
1323
|
async $checkUsername() {
|
|
563
|
-
const tilmeld = this
|
|
1324
|
+
const tilmeld = enforceTilmeld(this);
|
|
1325
|
+
this.$nymph.config.debugInfo('tilmeld', `Username check "${this.$data.username}".`);
|
|
564
1326
|
if (!tilmeld.config.emailUsernames) {
|
|
565
|
-
|
|
1327
|
+
let [username, domain] = tilmeld.config.domainSupport
|
|
1328
|
+
? splitn(this.$data.username ?? '', '@', 2)
|
|
1329
|
+
: [this.$data.username, null];
|
|
1330
|
+
// Lowercase the domain part.
|
|
1331
|
+
if (domain) {
|
|
1332
|
+
domain = domain.toLowerCase();
|
|
1333
|
+
this.$data.username = `${username}@${domain}`;
|
|
1334
|
+
}
|
|
1335
|
+
if (this.$data.username == null ||
|
|
1336
|
+
!this.$data.username.length ||
|
|
1337
|
+
username == null ||
|
|
1338
|
+
!username.length) {
|
|
566
1339
|
return { result: false, message: 'Please specify a username.' };
|
|
567
1340
|
}
|
|
568
|
-
if (tilmeld.config.
|
|
569
|
-
|
|
1341
|
+
if (username.length < tilmeld.config.minUsernameLength) {
|
|
1342
|
+
return {
|
|
1343
|
+
result: false,
|
|
1344
|
+
message: 'Usernames must be at least ' +
|
|
1345
|
+
tilmeld.config.minUsernameLength +
|
|
1346
|
+
' characters.',
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
if (username.length > tilmeld.config.maxUsernameLength) {
|
|
570
1350
|
return {
|
|
571
1351
|
result: false,
|
|
572
1352
|
message: 'Usernames must not exceed ' +
|
|
@@ -574,18 +1354,35 @@ class User extends AbleObject_1.default {
|
|
|
574
1354
|
' characters.',
|
|
575
1355
|
};
|
|
576
1356
|
}
|
|
577
|
-
if (
|
|
1357
|
+
if (difference(username.split(''), tilmeld.config.validChars.split(''))
|
|
1358
|
+
.length) {
|
|
578
1359
|
return {
|
|
579
1360
|
result: false,
|
|
580
1361
|
message: tilmeld.config.validCharsNotice,
|
|
581
1362
|
};
|
|
582
1363
|
}
|
|
583
|
-
if (!tilmeld.config.validRegex.test(
|
|
1364
|
+
if (!tilmeld.config.validRegex.test(username)) {
|
|
584
1365
|
return {
|
|
585
1366
|
result: false,
|
|
586
1367
|
message: tilmeld.config.validRegexNotice,
|
|
587
1368
|
};
|
|
588
1369
|
}
|
|
1370
|
+
if (domain != null) {
|
|
1371
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
1372
|
+
return {
|
|
1373
|
+
result: false,
|
|
1374
|
+
message: tilmeld.config.validDomainRegexNotice,
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
if (!this.$skipAcWhenSaving &&
|
|
1378
|
+
!this.$is(tilmeld.currentUser) &&
|
|
1379
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
1380
|
+
return {
|
|
1381
|
+
result: false,
|
|
1382
|
+
message: "You don't have the authority to manage users on this domain.",
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
589
1386
|
const selector = {
|
|
590
1387
|
type: '&',
|
|
591
1388
|
ilike: [
|
|
@@ -600,6 +1397,17 @@ class User extends AbleObject_1.default {
|
|
|
600
1397
|
if (test != null) {
|
|
601
1398
|
return { result: false, message: 'That username is taken.' };
|
|
602
1399
|
}
|
|
1400
|
+
for (let callback of this.constructor
|
|
1401
|
+
.checkUsernameCallbacks) {
|
|
1402
|
+
if (callback) {
|
|
1403
|
+
const result = await callback(this, {
|
|
1404
|
+
username: this.$data.username,
|
|
1405
|
+
});
|
|
1406
|
+
if (!result.result) {
|
|
1407
|
+
return { message: 'Username is not available.', ...result };
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
603
1411
|
return {
|
|
604
1412
|
result: true,
|
|
605
1413
|
message: this.guid != null ? 'Username is valid.' : 'Username is available!',
|
|
@@ -610,8 +1418,15 @@ class User extends AbleObject_1.default {
|
|
|
610
1418
|
if (this.$data.username == null || !this.$data.username.length) {
|
|
611
1419
|
return { result: false, message: 'Please specify an email.' };
|
|
612
1420
|
}
|
|
613
|
-
if (tilmeld.config.
|
|
614
|
-
|
|
1421
|
+
if (this.$data.username.length < tilmeld.config.minUsernameLength) {
|
|
1422
|
+
return {
|
|
1423
|
+
result: false,
|
|
1424
|
+
message: 'Emails must be at least ' +
|
|
1425
|
+
tilmeld.config.minUsernameLength +
|
|
1426
|
+
' characters.',
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
|
|
615
1430
|
return {
|
|
616
1431
|
result: false,
|
|
617
1432
|
message: 'Emails must not exceed ' +
|
|
@@ -622,8 +1437,14 @@ class User extends AbleObject_1.default {
|
|
|
622
1437
|
return await this.$checkEmail();
|
|
623
1438
|
}
|
|
624
1439
|
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Check that an email is unique.
|
|
1442
|
+
*
|
|
1443
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1444
|
+
*/
|
|
625
1445
|
async $checkEmail() {
|
|
626
|
-
const tilmeld = this
|
|
1446
|
+
const tilmeld = enforceTilmeld(this);
|
|
1447
|
+
this.$nymph.config.debugInfo('tilmeld', `Email check "${this.$data.email}".`);
|
|
627
1448
|
if (this.$data.email == null || !this.$data.email.length) {
|
|
628
1449
|
if (tilmeld.config.verifyEmail) {
|
|
629
1450
|
return { result: false, message: 'Please specify an email.' };
|
|
@@ -657,8 +1478,14 @@ class User extends AbleObject_1.default {
|
|
|
657
1478
|
message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
|
|
658
1479
|
};
|
|
659
1480
|
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Check that a phone number is unique.
|
|
1483
|
+
*
|
|
1484
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1485
|
+
*/
|
|
660
1486
|
async $checkPhone() {
|
|
661
|
-
const tilmeld = this
|
|
1487
|
+
const tilmeld = enforceTilmeld(this);
|
|
1488
|
+
this.$nymph.config.debugInfo('tilmeld', `Phone check "${this.$data.phone}".`);
|
|
662
1489
|
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
663
1490
|
return { result: false, message: 'Please specify a phone number.' };
|
|
664
1491
|
}
|
|
@@ -686,7 +1513,7 @@ class User extends AbleObject_1.default {
|
|
|
686
1513
|
};
|
|
687
1514
|
}
|
|
688
1515
|
async $register(data) {
|
|
689
|
-
let tilmeld = this
|
|
1516
|
+
let tilmeld = enforceTilmeld(this);
|
|
690
1517
|
if (!tilmeld.config.allowRegistration) {
|
|
691
1518
|
return {
|
|
692
1519
|
result: false,
|
|
@@ -701,21 +1528,6 @@ class User extends AbleObject_1.default {
|
|
|
701
1528
|
message: 'This is already a registered user.',
|
|
702
1529
|
};
|
|
703
1530
|
}
|
|
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
1531
|
if (!('password' in data) || !data.password.length) {
|
|
720
1532
|
return {
|
|
721
1533
|
result: false,
|
|
@@ -728,7 +1540,7 @@ class User extends AbleObject_1.default {
|
|
|
728
1540
|
return { ...unCheck, loggedin: false };
|
|
729
1541
|
}
|
|
730
1542
|
this.$password(data.password);
|
|
731
|
-
if (tilmeld.config.regFields.
|
|
1543
|
+
if (tilmeld.config.regFields.includes('name')) {
|
|
732
1544
|
this.$data.name =
|
|
733
1545
|
this.$data.nameFirst +
|
|
734
1546
|
(this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
|
|
@@ -740,59 +1552,35 @@ class User extends AbleObject_1.default {
|
|
|
740
1552
|
if (tilmeld.config.emailUsernames) {
|
|
741
1553
|
this.$data.email = this.$data.username;
|
|
742
1554
|
}
|
|
743
|
-
|
|
1555
|
+
// Start transaction.
|
|
1556
|
+
const transaction = 'tilmeld-register-' + nanoid();
|
|
744
1557
|
const nymph = this.$nymph;
|
|
745
1558
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
746
|
-
this.$
|
|
747
|
-
tilmeld = this
|
|
1559
|
+
this.$setNymph(tnymph);
|
|
1560
|
+
tilmeld = enforceTilmeld(this);
|
|
1561
|
+
let message = '';
|
|
1562
|
+
let loggedin = false;
|
|
748
1563
|
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;
|
|
1564
|
+
try {
|
|
1565
|
+
for (let callback of this.constructor
|
|
1566
|
+
.beforeRegisterCallbacks) {
|
|
1567
|
+
if (callback) {
|
|
1568
|
+
await callback(this, data);
|
|
1569
|
+
}
|
|
781
1570
|
}
|
|
782
1571
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
type: '&',
|
|
792
|
-
equal: ['defaultSecondary', true],
|
|
793
|
-
});
|
|
1572
|
+
catch (e) {
|
|
1573
|
+
await tnymph.rollback(transaction);
|
|
1574
|
+
this.$setNymph(nymph);
|
|
1575
|
+
return {
|
|
1576
|
+
result: false,
|
|
1577
|
+
loggedin: false,
|
|
1578
|
+
message: e.message,
|
|
1579
|
+
};
|
|
794
1580
|
}
|
|
795
|
-
if (tilmeld.config.
|
|
1581
|
+
if (tilmeld.config.userFields.includes('email') &&
|
|
1582
|
+
tilmeld.config.verifyEmail) {
|
|
1583
|
+
// The user will be enabled after verifying their e-mail address.
|
|
796
1584
|
if (!tilmeld.config.unverifiedAccess) {
|
|
797
1585
|
this.$data.enabled = false;
|
|
798
1586
|
}
|
|
@@ -800,6 +1588,8 @@ class User extends AbleObject_1.default {
|
|
|
800
1588
|
else {
|
|
801
1589
|
this.$data.enabled = true;
|
|
802
1590
|
}
|
|
1591
|
+
// If createAdmin is true and there are no other users, grant
|
|
1592
|
+
// "system/admin".
|
|
803
1593
|
let madeAdmin = false;
|
|
804
1594
|
if (tilmeld.config.createAdmin) {
|
|
805
1595
|
const otherUsers = await tnymph.getEntities({
|
|
@@ -808,13 +1598,26 @@ class User extends AbleObject_1.default {
|
|
|
808
1598
|
limit: 1,
|
|
809
1599
|
return: 'guid',
|
|
810
1600
|
});
|
|
1601
|
+
// Make sure it's not just null, cause that means an error.
|
|
1602
|
+
if (otherUsers == null) {
|
|
1603
|
+
await tnymph.rollback(transaction);
|
|
1604
|
+
this.$setNymph(nymph);
|
|
1605
|
+
return {
|
|
1606
|
+
result: false,
|
|
1607
|
+
loggedin: false,
|
|
1608
|
+
message: 'An error occurred.',
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
811
1611
|
if (!otherUsers.length) {
|
|
812
1612
|
this.$grant('system/admin');
|
|
813
1613
|
this.$data.enabled = true;
|
|
814
1614
|
madeAdmin = true;
|
|
815
1615
|
}
|
|
816
1616
|
}
|
|
1617
|
+
this.$nymph.config.debugLog('tilmeld', `Registering new user "${this.$data.username}".`);
|
|
817
1618
|
if (await this.$saveSkipAC()) {
|
|
1619
|
+
this.$nymph.config.debugLog('tilmeld', `New user registered "${this.$data.username}".`);
|
|
1620
|
+
// Send the new user registered email.
|
|
818
1621
|
if (tilmeld.config.userRegisteredRecipient != null) {
|
|
819
1622
|
await tilmeld.config.sendEmail(tilmeld, {
|
|
820
1623
|
template: 'UserRegistered',
|
|
@@ -831,31 +1634,20 @@ class User extends AbleObject_1.default {
|
|
|
831
1634
|
},
|
|
832
1635
|
}, this);
|
|
833
1636
|
}
|
|
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
|
-
}
|
|
1637
|
+
// Finish up.
|
|
848
1638
|
if (tilmeld.config.verifyEmail &&
|
|
849
1639
|
!tilmeld.config.unverifiedAccess &&
|
|
850
|
-
!madeAdmin
|
|
1640
|
+
!madeAdmin &&
|
|
1641
|
+
tilmeld.config.userFields.includes('email')) {
|
|
851
1642
|
message +=
|
|
852
1643
|
`Almost there. An email has been sent to ${this.$data.email} ` +
|
|
853
1644
|
'with a verification link for you to finish registration.';
|
|
854
1645
|
}
|
|
855
1646
|
else if (tilmeld.config.verifyEmail &&
|
|
856
1647
|
tilmeld.config.unverifiedAccess &&
|
|
857
|
-
!madeAdmin
|
|
858
|
-
|
|
1648
|
+
!madeAdmin &&
|
|
1649
|
+
tilmeld.config.userFields.includes('email')) {
|
|
1650
|
+
if (!(await tilmeld.login(this, true))) {
|
|
859
1651
|
throw new Error('An error occurred trying to log you in.');
|
|
860
1652
|
}
|
|
861
1653
|
message +=
|
|
@@ -865,7 +1657,7 @@ class User extends AbleObject_1.default {
|
|
|
865
1657
|
loggedin = true;
|
|
866
1658
|
}
|
|
867
1659
|
else {
|
|
868
|
-
if (!tilmeld.login(this, true)) {
|
|
1660
|
+
if (!(await tilmeld.login(this, true))) {
|
|
869
1661
|
throw new Error('An error occurred trying to log you in.');
|
|
870
1662
|
}
|
|
871
1663
|
message += "You're now registered and logged in!";
|
|
@@ -880,23 +1672,11 @@ class User extends AbleObject_1.default {
|
|
|
880
1672
|
});
|
|
881
1673
|
}
|
|
882
1674
|
}
|
|
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
1675
|
}
|
|
897
1676
|
else {
|
|
1677
|
+
this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}".`);
|
|
898
1678
|
await tnymph.rollback(transaction);
|
|
899
|
-
this.$nymph
|
|
1679
|
+
this.$setNymph(nymph);
|
|
900
1680
|
return {
|
|
901
1681
|
result: false,
|
|
902
1682
|
loggedin: false,
|
|
@@ -905,22 +1685,69 @@ class User extends AbleObject_1.default {
|
|
|
905
1685
|
}
|
|
906
1686
|
}
|
|
907
1687
|
catch (e) {
|
|
1688
|
+
this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}": ${e}`);
|
|
908
1689
|
await tnymph.rollback(transaction);
|
|
909
|
-
this.$nymph
|
|
1690
|
+
this.$setNymph(nymph);
|
|
1691
|
+
throw e;
|
|
1692
|
+
}
|
|
1693
|
+
try {
|
|
1694
|
+
await tnymph.commit(transaction);
|
|
1695
|
+
this.$setNymph(nymph);
|
|
1696
|
+
await this.$nymph.tilmeld?.fillSession(this);
|
|
1697
|
+
}
|
|
1698
|
+
catch (e) {
|
|
910
1699
|
throw e;
|
|
911
1700
|
}
|
|
1701
|
+
return {
|
|
1702
|
+
result: true,
|
|
1703
|
+
loggedin,
|
|
1704
|
+
message,
|
|
1705
|
+
};
|
|
912
1706
|
}
|
|
913
1707
|
async $save() {
|
|
914
|
-
let tilmeld = this
|
|
1708
|
+
let tilmeld = enforceTilmeld(this);
|
|
915
1709
|
if (this.$data.username == null || !this.$data.username.trim().length) {
|
|
916
1710
|
return false;
|
|
917
1711
|
}
|
|
918
|
-
if (
|
|
1712
|
+
if (!this.$skipAcWhenSaving &&
|
|
1713
|
+
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
919
1714
|
!tilmeld.gatekeeper('system/admin') &&
|
|
920
|
-
this.$
|
|
921
|
-
throw new
|
|
1715
|
+
this.$data.abilities?.includes('system/admin')) {
|
|
1716
|
+
throw new AccessControlError("You don't have the authority to modify system admins.");
|
|
1717
|
+
}
|
|
1718
|
+
// Lowercase the domain part.
|
|
1719
|
+
if (tilmeld.config.domainSupport) {
|
|
1720
|
+
const [username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
1721
|
+
if (domain) {
|
|
1722
|
+
this.$data.username = `${username}@${domain.toLowerCase()}`;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (tilmeld.config.domainSupport) {
|
|
1726
|
+
const [_username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
1727
|
+
const [_originalUsername, originalDomain] = splitn(this.$originalUsername ?? '', '@', 2);
|
|
1728
|
+
if (this.guid != null &&
|
|
1729
|
+
((originalDomain == null && domain != null) ||
|
|
1730
|
+
(domain == null && originalDomain != null))) {
|
|
1731
|
+
throw new AccessControlError("Users can't switch to or from having a domain once created.");
|
|
1732
|
+
}
|
|
1733
|
+
if (!this.$skipAcWhenSaving &&
|
|
1734
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1735
|
+
!tilmeld.gatekeeper('system/admin')) {
|
|
1736
|
+
if (domain != null &&
|
|
1737
|
+
!this.$is(tilmeld.currentUser) &&
|
|
1738
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
1739
|
+
throw new AccessControlError("You don't have the authority to modify users on this domain.");
|
|
1740
|
+
}
|
|
1741
|
+
if (originalDomain != null &&
|
|
1742
|
+
originalDomain !== domain &&
|
|
1743
|
+
(!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
|
|
1744
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))) {
|
|
1745
|
+
throw new AccessControlError("You don't have the authority to modify users on this domain.");
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
922
1748
|
}
|
|
923
1749
|
let sendVerification = false;
|
|
1750
|
+
// Formatting.
|
|
924
1751
|
this.$data.username = this.$data.username.trim();
|
|
925
1752
|
if (tilmeld.config.emailUsernames) {
|
|
926
1753
|
this.$data.email = this.$data.username;
|
|
@@ -934,49 +1761,63 @@ class User extends AbleObject_1.default {
|
|
|
934
1761
|
(this.$data.nameMiddle ? ' ' + this.$data.nameMiddle : '') +
|
|
935
1762
|
(this.$data.nameLast ? ' ' + this.$data.nameLast : '');
|
|
936
1763
|
this.$data.enabled = !!this.$data.enabled;
|
|
937
|
-
|
|
1764
|
+
// Clear empty values.
|
|
1765
|
+
if (this.$data.nameFirst === '' ||
|
|
1766
|
+
!tilmeld.config.userFields.includes('name')) {
|
|
938
1767
|
delete this.$data.nameFirst;
|
|
939
1768
|
}
|
|
940
|
-
if (this.$data.nameMiddle === ''
|
|
1769
|
+
if (this.$data.nameMiddle === '' ||
|
|
1770
|
+
!tilmeld.config.userFields.includes('name')) {
|
|
941
1771
|
delete this.$data.nameMiddle;
|
|
942
1772
|
}
|
|
943
|
-
if (this.$data.nameLast === ''
|
|
1773
|
+
if (this.$data.nameLast === '' ||
|
|
1774
|
+
!tilmeld.config.userFields.includes('name')) {
|
|
944
1775
|
delete this.$data.nameLast;
|
|
945
1776
|
}
|
|
946
|
-
if (this.$data.name === '') {
|
|
1777
|
+
if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
947
1778
|
delete this.$data.name;
|
|
948
1779
|
}
|
|
949
|
-
if (this.$data.email === ''
|
|
1780
|
+
if (this.$data.email === '' ||
|
|
1781
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
950
1782
|
delete this.$data.email;
|
|
951
1783
|
}
|
|
952
1784
|
if (this.$data.avatar === '') {
|
|
953
1785
|
delete this.$data.avatar;
|
|
954
1786
|
}
|
|
955
|
-
if (this.$data.phone === ''
|
|
1787
|
+
if (this.$data.phone === '' ||
|
|
1788
|
+
!tilmeld.config.userFields.includes('phone')) {
|
|
956
1789
|
delete this.$data.phone;
|
|
957
1790
|
}
|
|
958
|
-
if (this.$data.secret === ''
|
|
1791
|
+
if (this.$data.secret === '' ||
|
|
1792
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
959
1793
|
delete this.$data.secret;
|
|
960
1794
|
}
|
|
961
|
-
if (this.$data.emailChangeDate === 0
|
|
1795
|
+
if (this.$data.emailChangeDate === 0 ||
|
|
1796
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
962
1797
|
delete this.$data.emailChangeDate;
|
|
963
1798
|
}
|
|
964
|
-
if (this.$data.newEmailSecret === ''
|
|
1799
|
+
if (this.$data.newEmailSecret === '' ||
|
|
1800
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
965
1801
|
delete this.$data.newEmailSecret;
|
|
966
1802
|
}
|
|
967
|
-
if (this.$data.newEmailAddress === ''
|
|
1803
|
+
if (this.$data.newEmailAddress === '' ||
|
|
1804
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
968
1805
|
delete this.$data.newEmailAddress;
|
|
969
1806
|
}
|
|
970
|
-
if (this.$data.cancelEmailSecret === ''
|
|
1807
|
+
if (this.$data.cancelEmailSecret === '' ||
|
|
1808
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
971
1809
|
delete this.$data.cancelEmailSecret;
|
|
972
1810
|
}
|
|
973
|
-
if (this.$data.cancelEmailAddress === ''
|
|
1811
|
+
if (this.$data.cancelEmailAddress === '' ||
|
|
1812
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
974
1813
|
delete this.$data.cancelEmailAddress;
|
|
975
1814
|
}
|
|
976
|
-
if (this.$data.recoverSecret === ''
|
|
1815
|
+
if (this.$data.recoverSecret === '' ||
|
|
1816
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
977
1817
|
delete this.$data.recoverSecret;
|
|
978
1818
|
}
|
|
979
|
-
if (this.$data.recoverSecretDate === 0
|
|
1819
|
+
if (this.$data.recoverSecretDate === 0 ||
|
|
1820
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
980
1821
|
delete this.$data.recoverSecretDate;
|
|
981
1822
|
}
|
|
982
1823
|
if (this.$data.password === '') {
|
|
@@ -988,50 +1829,65 @@ class User extends AbleObject_1.default {
|
|
|
988
1829
|
if (this.$data.inheritAbilities === false) {
|
|
989
1830
|
delete this.$data.inheritAbilities;
|
|
990
1831
|
}
|
|
1832
|
+
// Verification.
|
|
991
1833
|
const unCheck = await this.$checkUsername();
|
|
992
1834
|
if (!unCheck.result) {
|
|
993
|
-
throw new
|
|
1835
|
+
throw new BadUsernameError(unCheck.message);
|
|
994
1836
|
}
|
|
995
|
-
if (!tilmeld.config.emailUsernames
|
|
1837
|
+
if (!tilmeld.config.emailUsernames &&
|
|
1838
|
+
tilmeld.config.userFields.includes('email')) {
|
|
996
1839
|
const emCheck = await this.$checkEmail();
|
|
997
1840
|
if (!emCheck.result) {
|
|
998
|
-
throw new
|
|
1841
|
+
throw new BadEmailError(emCheck.message);
|
|
999
1842
|
}
|
|
1000
1843
|
}
|
|
1001
|
-
|
|
1844
|
+
// Email changes.
|
|
1845
|
+
if (!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1846
|
+
tilmeld.config.userFields.includes('email')) {
|
|
1847
|
+
// The user isn't an admin, so email address changes should contain some
|
|
1848
|
+
// security measures.
|
|
1002
1849
|
if (tilmeld.config.verifyEmail) {
|
|
1850
|
+
// The user needs to verify this new email address.
|
|
1003
1851
|
if (this.guid == null) {
|
|
1004
|
-
|
|
1005
|
-
|
|
1852
|
+
// If this is the first user, they'll be made an admin, so they don't
|
|
1853
|
+
// need to verify.
|
|
1854
|
+
if (!this.$data.abilities?.includes('system/admin')) {
|
|
1855
|
+
this.$data.secret = nanoid();
|
|
1006
1856
|
sendVerification = true;
|
|
1007
1857
|
}
|
|
1008
1858
|
}
|
|
1009
1859
|
else if (this.$originalEmail != null &&
|
|
1010
1860
|
this.$originalEmail !== this.$data.email) {
|
|
1861
|
+
// The user already has an old email address.
|
|
1011
1862
|
if (tilmeld.config.emailRateLimit !== '' &&
|
|
1012
1863
|
this.$data.emailChangeDate != null &&
|
|
1013
1864
|
this.$data.emailChangeDate >=
|
|
1014
|
-
(
|
|
1015
|
-
throw new
|
|
1016
|
-
new Date((
|
|
1865
|
+
(strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000) {
|
|
1866
|
+
throw new EmailChangeRateLimitExceededError('You already changed your email address recently. Please wait until ' +
|
|
1867
|
+
new Date((strtotime('+' + tilmeld.config.emailRateLimit, Math.floor(this.$data.emailChangeDate / 1000)) || 0) * 1000).toString() +
|
|
1017
1868
|
' to change your email address again.');
|
|
1018
1869
|
}
|
|
1019
1870
|
else {
|
|
1020
1871
|
if (this.$data.newEmailSecret == null &&
|
|
1021
1872
|
tilmeld.config.emailRateLimit !== '' &&
|
|
1873
|
+
// Make sure the user has at least the rate limit time to cancel
|
|
1874
|
+
// an email change.
|
|
1022
1875
|
(this.$data.emailChangeDate == null ||
|
|
1023
1876
|
this.$data.emailChangeDate <
|
|
1024
|
-
(
|
|
1877
|
+
(strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000)) {
|
|
1878
|
+
// Save the old email in case the cancel change link is clicked.
|
|
1025
1879
|
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
1026
|
-
this.$data.cancelEmailSecret =
|
|
1880
|
+
this.$data.cancelEmailSecret = nanoid();
|
|
1027
1881
|
this.$data.emailChangeDate = Date.now();
|
|
1028
1882
|
}
|
|
1883
|
+
// Save the new address and reset to the old one (until they verify
|
|
1884
|
+
// it).
|
|
1029
1885
|
this.$data.newEmailAddress = this.$data.email;
|
|
1030
1886
|
this.$data.email = this.$originalEmail;
|
|
1031
1887
|
if (tilmeld.config.emailUsernames) {
|
|
1032
1888
|
this.$data.username = this.$data.email;
|
|
1033
1889
|
}
|
|
1034
|
-
this.$data.newEmailSecret =
|
|
1890
|
+
this.$data.newEmailSecret = nanoid();
|
|
1035
1891
|
sendVerification = true;
|
|
1036
1892
|
}
|
|
1037
1893
|
}
|
|
@@ -1039,11 +1895,15 @@ class User extends AbleObject_1.default {
|
|
|
1039
1895
|
else if (this.guid != null &&
|
|
1040
1896
|
this.$originalEmail != null &&
|
|
1041
1897
|
this.$originalEmail !== this.$data.email &&
|
|
1898
|
+
// Make sure the user has at least the rate limit time to cancel an
|
|
1899
|
+
// email change.
|
|
1042
1900
|
(this.$data.emailChangeDate == null ||
|
|
1043
1901
|
this.$data.emailChangeDate <
|
|
1044
|
-
(
|
|
1902
|
+
(strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000)) {
|
|
1903
|
+
// The user doesn't need to verify their new email address, but should
|
|
1904
|
+
// be able to cancel the email change from their old address.
|
|
1045
1905
|
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
1046
|
-
this.$data.cancelEmailSecret =
|
|
1906
|
+
this.$data.cancelEmailSecret = nanoid();
|
|
1047
1907
|
sendVerification = true;
|
|
1048
1908
|
}
|
|
1049
1909
|
}
|
|
@@ -1052,72 +1912,188 @@ class User extends AbleObject_1.default {
|
|
|
1052
1912
|
}
|
|
1053
1913
|
delete this.$data.passwordTemp;
|
|
1054
1914
|
if (this.$data.password == null) {
|
|
1055
|
-
throw new
|
|
1056
|
-
}
|
|
1057
|
-
try {
|
|
1058
|
-
tilmeld.config.validatorUser(this);
|
|
1915
|
+
throw new BadDataError('A password is required.');
|
|
1059
1916
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
}
|
|
1063
|
-
const transaction = 'tilmeld-save-' + (0, guid_1.nanoid)();
|
|
1917
|
+
// Start transaction.
|
|
1918
|
+
const transaction = 'tilmeld-save-user-' + (this.guid || nanoid());
|
|
1064
1919
|
const nymph = this.$nymph;
|
|
1065
1920
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1066
|
-
this.$
|
|
1067
|
-
tilmeld = this
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
this.$
|
|
1921
|
+
this.$setNymph(tnymph);
|
|
1922
|
+
tilmeld = enforceTilmeld(this);
|
|
1923
|
+
const Group = tnymph.getEntityClass(GroupClass);
|
|
1924
|
+
try {
|
|
1925
|
+
let group = this.$data.group;
|
|
1926
|
+
if (group != null) {
|
|
1927
|
+
// Make sure to get fresh group data.
|
|
1928
|
+
if (group.$asleep()) {
|
|
1929
|
+
await group.$wake();
|
|
1930
|
+
}
|
|
1931
|
+
else {
|
|
1932
|
+
if (!(await group.$refresh())) {
|
|
1933
|
+
throw new Error("Couldn't retrieve primary group info.");
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
if (group == null && this.guid == null) {
|
|
1938
|
+
if (tilmeld.config.generatePrimary) {
|
|
1939
|
+
// Generate a new primary group for the user.
|
|
1940
|
+
group = await Group.factory();
|
|
1941
|
+
group.user = this;
|
|
1942
|
+
const parent = await tnymph.getEntity({ class: Group }, {
|
|
1943
|
+
type: '&',
|
|
1944
|
+
equal: ['defaultPrimary', true],
|
|
1945
|
+
});
|
|
1946
|
+
if (parent != null) {
|
|
1947
|
+
group.parent = parent;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
else {
|
|
1951
|
+
// Add the default primary.
|
|
1952
|
+
const group = await tnymph.getEntity({ class: Group }, {
|
|
1953
|
+
type: '&',
|
|
1954
|
+
equal: ['defaultPrimary', true],
|
|
1955
|
+
});
|
|
1956
|
+
if (group != null) {
|
|
1957
|
+
this.$data.group = group;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if (group != null && group.user != null && this.$is(group.user)) {
|
|
1962
|
+
// Update the user's generated primary group.
|
|
1963
|
+
let changed = false;
|
|
1964
|
+
if (group.groupname !== this.$data.username) {
|
|
1965
|
+
group.groupname = this.$data.username;
|
|
1966
|
+
changed = true;
|
|
1967
|
+
}
|
|
1968
|
+
if (group.avatar !== this.$data.avatar) {
|
|
1969
|
+
group.avatar = this.$data.avatar;
|
|
1970
|
+
changed = true;
|
|
1971
|
+
}
|
|
1972
|
+
if (tilmeld.config.userFields.includes('email') &&
|
|
1973
|
+
group.email !== this.$data.email) {
|
|
1974
|
+
group.email = this.$data.email;
|
|
1975
|
+
changed = true;
|
|
1976
|
+
}
|
|
1977
|
+
if (tilmeld.config.userFields.includes('name') &&
|
|
1978
|
+
group.name !== this.$data.name) {
|
|
1979
|
+
group.name = this.$data.name;
|
|
1980
|
+
changed = true;
|
|
1981
|
+
}
|
|
1982
|
+
if (tilmeld.config.userFields.includes('phone') &&
|
|
1983
|
+
group.phone !== this.$data.phone) {
|
|
1984
|
+
group.phone = this.$data.phone;
|
|
1985
|
+
changed = true;
|
|
1986
|
+
}
|
|
1987
|
+
if (changed || group.guid == null) {
|
|
1988
|
+
if (!(await group.$saveSkipAC())) {
|
|
1989
|
+
throw Error('Error updating primary group for user.');
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
this.$data.group = group;
|
|
1993
|
+
}
|
|
1994
|
+
if (this.$data.groups == null || this.$data.groups.length === 0) {
|
|
1995
|
+
// Add secondary groups.
|
|
1996
|
+
if (tilmeld.config.userFields.includes('email') &&
|
|
1997
|
+
tilmeld.config.verifyEmail &&
|
|
1998
|
+
tilmeld.config.unverifiedAccess &&
|
|
1999
|
+
this.$data.secret != null) {
|
|
2000
|
+
// Add the default secondaries for unverified users.
|
|
2001
|
+
this.$data.groups = await tnymph.getEntities({ class: Group }, {
|
|
2002
|
+
type: '&',
|
|
2003
|
+
equal: ['unverifiedSecondary', true],
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
else {
|
|
2007
|
+
// Add the default secondaries.
|
|
2008
|
+
this.$data.groups = await tnymph.getEntities({ class: Group }, {
|
|
2009
|
+
type: '&',
|
|
2010
|
+
equal: ['defaultSecondary', true],
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
1071
2014
|
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();
|
|
2015
|
+
tilmeld.config.validatorUser(tilmeld, this);
|
|
1078
2016
|
}
|
|
1079
2017
|
catch (e) {
|
|
1080
|
-
|
|
1081
|
-
this.$nymph = nymph;
|
|
1082
|
-
throw e;
|
|
2018
|
+
throw new BadDataError(e?.message);
|
|
1083
2019
|
}
|
|
1084
2020
|
}
|
|
1085
|
-
|
|
2021
|
+
catch (e) {
|
|
2022
|
+
await tnymph.rollback(transaction);
|
|
2023
|
+
this.$setNymph(nymph);
|
|
2024
|
+
throw e;
|
|
2025
|
+
}
|
|
2026
|
+
let ret = false;
|
|
1086
2027
|
let preGuid = this.guid;
|
|
1087
2028
|
let preCdate = this.cdate;
|
|
1088
2029
|
let preMdate = this.mdate;
|
|
1089
2030
|
try {
|
|
2031
|
+
for (let callback of this.constructor
|
|
2032
|
+
.beforeSaveCallbacks) {
|
|
2033
|
+
if (callback) {
|
|
2034
|
+
await callback(this);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
1090
2037
|
ret = await super.$save();
|
|
1091
2038
|
}
|
|
1092
2039
|
catch (e) {
|
|
1093
2040
|
await tnymph.rollback(transaction);
|
|
1094
|
-
this
|
|
2041
|
+
this.guid = preGuid;
|
|
2042
|
+
this.cdate = preCdate;
|
|
2043
|
+
this.mdate = preMdate;
|
|
2044
|
+
this.$setNymph(nymph);
|
|
1095
2045
|
throw e;
|
|
1096
2046
|
}
|
|
1097
2047
|
if (ret) {
|
|
1098
2048
|
if (sendVerification) {
|
|
2049
|
+
// The email has changed, so send a new verification email.
|
|
1099
2050
|
if (!(await this.$sendEmailVerification())) {
|
|
1100
2051
|
await tnymph.rollback(transaction);
|
|
1101
2052
|
this.guid = preGuid;
|
|
1102
2053
|
this.cdate = preCdate;
|
|
1103
2054
|
this.mdate = preMdate;
|
|
1104
|
-
this.$nymph
|
|
2055
|
+
this.$setNymph(nymph);
|
|
1105
2056
|
throw new Error("Couldn't send verification email.");
|
|
1106
2057
|
}
|
|
1107
2058
|
}
|
|
1108
2059
|
this.$descendantGroups = undefined;
|
|
1109
2060
|
this.$gatekeeperCache = undefined;
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
2061
|
+
try {
|
|
2062
|
+
for (let callback of this.constructor
|
|
2063
|
+
.afterSaveCallbacks) {
|
|
2064
|
+
if (callback) {
|
|
2065
|
+
await callback(this);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
1113
2068
|
}
|
|
2069
|
+
catch (e) {
|
|
2070
|
+
await tnymph.rollback(transaction);
|
|
2071
|
+
this.guid = preGuid;
|
|
2072
|
+
this.cdate = preCdate;
|
|
2073
|
+
this.mdate = preMdate;
|
|
2074
|
+
this.$setNymph(nymph);
|
|
2075
|
+
throw e;
|
|
2076
|
+
}
|
|
2077
|
+
ret = await tnymph.commit(transaction);
|
|
1114
2078
|
}
|
|
1115
2079
|
else {
|
|
1116
2080
|
await tnymph.rollback(transaction);
|
|
1117
2081
|
}
|
|
1118
|
-
this.$nymph
|
|
2082
|
+
this.$setNymph(nymph);
|
|
2083
|
+
if (ret) {
|
|
2084
|
+
this.$originalEmail = this.$data.email;
|
|
2085
|
+
this.$originalUsername = this.$data.username;
|
|
2086
|
+
const tilmeld = enforceTilmeld(nymph);
|
|
2087
|
+
if (tilmeld.User.current(true).$is(this)) {
|
|
2088
|
+
// Update the user in the session cache.
|
|
2089
|
+
await tilmeld.fillSession(this);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
1119
2092
|
return ret;
|
|
1120
2093
|
}
|
|
2094
|
+
/**
|
|
2095
|
+
* This should *never* be accessible on the client.
|
|
2096
|
+
*/
|
|
1121
2097
|
async $saveSkipAC() {
|
|
1122
2098
|
this.$skipAcWhenSaving = true;
|
|
1123
2099
|
return await this.$save();
|
|
@@ -1130,24 +2106,109 @@ class User extends AbleObject_1.default {
|
|
|
1130
2106
|
return false;
|
|
1131
2107
|
}
|
|
1132
2108
|
async $delete() {
|
|
1133
|
-
const tilmeld = this
|
|
1134
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1135
|
-
throw new
|
|
2109
|
+
const tilmeld = enforceTilmeld(this);
|
|
2110
|
+
if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
|
|
2111
|
+
throw new AccessControlError("You don't have the authority to delete users.");
|
|
2112
|
+
}
|
|
2113
|
+
if (!this.$skipAcWhenDeleting &&
|
|
2114
|
+
!tilmeld.gatekeeper('system/admin') &&
|
|
2115
|
+
this.$data.abilities?.includes('system/admin')) {
|
|
2116
|
+
throw new AccessControlError("You don't have the authority to delete system admins.");
|
|
1136
2117
|
}
|
|
1137
|
-
if (
|
|
1138
|
-
|
|
1139
|
-
throw new errors_1.BadDataError("You don't have the authority to delete system admins.");
|
|
2118
|
+
if (this.$data.username !== this.$originalUsername) {
|
|
2119
|
+
throw new AccessControlError("You can't delete a user while switching their username.");
|
|
1140
2120
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
2121
|
+
const transaction = 'tilmeld-delete-user-' + this.guid;
|
|
2122
|
+
const nymph = this.$nymph;
|
|
2123
|
+
const tnymph = await nymph.startTransaction(transaction);
|
|
2124
|
+
this.$setNymph(tnymph);
|
|
2125
|
+
let success = false;
|
|
2126
|
+
try {
|
|
2127
|
+
if (!(await this.$refresh())) {
|
|
2128
|
+
throw new BadDataError('User could not be refreshed.');
|
|
2129
|
+
}
|
|
2130
|
+
if (this.$data.group != null) {
|
|
2131
|
+
if (this.$data.group.$asleep()) {
|
|
2132
|
+
await this.$data.group.$wake();
|
|
2133
|
+
}
|
|
2134
|
+
else {
|
|
2135
|
+
if (!(await this.$data.group.$refresh())) {
|
|
2136
|
+
throw new BadDataError('Group could not be refreshed.');
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
for (let callback of this.constructor
|
|
2141
|
+
.beforeDeleteCallbacks) {
|
|
2142
|
+
if (callback) {
|
|
2143
|
+
await callback(this);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
if (tilmeld.User.current(true).$is(this)) {
|
|
2147
|
+
await this.$logout();
|
|
2148
|
+
}
|
|
2149
|
+
if (this.$data.group != null && this.$is(this.$data.group.user)) {
|
|
2150
|
+
// Read the skip ac value here, because super.$delete changes it.
|
|
2151
|
+
const $skipAcWhenDeleting = this.$skipAcWhenDeleting;
|
|
2152
|
+
// Delete the user.
|
|
2153
|
+
success = await super.$delete();
|
|
2154
|
+
if (success) {
|
|
2155
|
+
// Delete the generated group.
|
|
2156
|
+
success = $skipAcWhenDeleting
|
|
2157
|
+
? await this.$data.group.$deleteSkipAC()
|
|
2158
|
+
: await this.$data.group.$delete();
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
else {
|
|
2162
|
+
success = await super.$delete();
|
|
2163
|
+
}
|
|
1143
2164
|
}
|
|
1144
|
-
|
|
2165
|
+
catch (e) {
|
|
2166
|
+
await tnymph.rollback(transaction);
|
|
2167
|
+
this.$setNymph(nymph);
|
|
2168
|
+
throw e;
|
|
2169
|
+
}
|
|
2170
|
+
if (success) {
|
|
2171
|
+
try {
|
|
2172
|
+
for (let callback of this.constructor
|
|
2173
|
+
.afterDeleteCallbacks) {
|
|
2174
|
+
if (callback) {
|
|
2175
|
+
await callback(this);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
catch (e) {
|
|
2180
|
+
await tnymph.rollback(transaction);
|
|
2181
|
+
this.$setNymph(nymph);
|
|
2182
|
+
throw e;
|
|
2183
|
+
}
|
|
2184
|
+
success = await tnymph.commit(transaction);
|
|
2185
|
+
}
|
|
2186
|
+
else {
|
|
2187
|
+
await tnymph.rollback(transaction);
|
|
2188
|
+
}
|
|
2189
|
+
this.$setNymph(nymph);
|
|
2190
|
+
return success;
|
|
2191
|
+
}
|
|
2192
|
+
/*
|
|
2193
|
+
* This should *never* be accessible on the client.
|
|
2194
|
+
*/
|
|
2195
|
+
async $deleteSkipAC() {
|
|
2196
|
+
this.$skipAcWhenDeleting = true;
|
|
2197
|
+
return await this.$delete();
|
|
2198
|
+
}
|
|
2199
|
+
$tilmeldDeleteSkipAC() {
|
|
2200
|
+
if (this.$skipAcWhenDeleting) {
|
|
2201
|
+
this.$skipAcWhenDeleting = false;
|
|
2202
|
+
return true;
|
|
2203
|
+
}
|
|
2204
|
+
return false;
|
|
1145
2205
|
}
|
|
1146
2206
|
static on(event, callback) {
|
|
1147
2207
|
const prop = (event + 'Callbacks');
|
|
1148
2208
|
if (!(prop in this)) {
|
|
1149
2209
|
throw new Error('Invalid event type.');
|
|
1150
2210
|
}
|
|
2211
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1151
2212
|
this[prop].push(callback);
|
|
1152
2213
|
return () => this.off(event, callback);
|
|
1153
2214
|
}
|
|
@@ -1156,55 +2217,13 @@ class User extends AbleObject_1.default {
|
|
|
1156
2217
|
if (!(prop in this)) {
|
|
1157
2218
|
return false;
|
|
1158
2219
|
}
|
|
2220
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1159
2221
|
const i = this[prop].indexOf(callback);
|
|
1160
2222
|
if (i > -1) {
|
|
2223
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1161
2224
|
this[prop].splice(i, 1);
|
|
1162
2225
|
}
|
|
1163
2226
|
return true;
|
|
1164
2227
|
}
|
|
1165
2228
|
}
|
|
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
2229
|
//# sourceMappingURL=User.js.map
|