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