@nymphjs/tilmeld 1.0.0-beta.1 → 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 +505 -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 +657 -259
- 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 +405 -32
- package/dist/User.js +1361 -372
- 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 +449 -260
- package/src/Tilmeld.types.ts +2 -2
- package/src/User.test.ts +160 -7
- package/src/User.ts +1615 -468
- 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,48 +266,205 @@ 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
|
|
180
|
-
const result = user.$login(data);
|
|
287
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
288
|
+
const result = await user.$login(data);
|
|
181
289
|
if (result.result) {
|
|
182
290
|
user.$updateDataProtection();
|
|
183
291
|
result.user = user;
|
|
184
292
|
}
|
|
185
293
|
return result;
|
|
186
294
|
}
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
}
|
|
374
|
+
async $login(data) {
|
|
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 (
|
|
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
|
+
}
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
for (let callback of this.constructor
|
|
407
|
+
.beforeLoginCallbacks) {
|
|
408
|
+
if (callback) {
|
|
409
|
+
await callback(this, data);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch (e) {
|
|
414
|
+
return {
|
|
415
|
+
result: false,
|
|
416
|
+
message: e.message,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
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) {
|
|
199
436
|
return { result: false, message: 'Incorrect login/password.' };
|
|
200
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
|
+
}
|
|
201
459
|
try {
|
|
202
460
|
for (let callback of this.constructor
|
|
203
461
|
.beforeLoginCallbacks) {
|
|
204
462
|
if (callback) {
|
|
205
|
-
callback(this,
|
|
463
|
+
await callback(this, {
|
|
464
|
+
username: this.$data.username || '',
|
|
465
|
+
password: '',
|
|
466
|
+
...(data || {}),
|
|
467
|
+
});
|
|
206
468
|
}
|
|
207
469
|
}
|
|
208
470
|
}
|
|
@@ -212,24 +474,31 @@ class User extends AbleObject_1.default {
|
|
|
212
474
|
message: e.message,
|
|
213
475
|
};
|
|
214
476
|
}
|
|
215
|
-
|
|
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
|
|
219
483
|
.afterLoginCallbacks) {
|
|
220
484
|
if (callback) {
|
|
221
|
-
callback(this);
|
|
485
|
+
await callback(this);
|
|
222
486
|
}
|
|
223
487
|
}
|
|
488
|
+
// Login was successful.
|
|
224
489
|
return { result: true, message: 'You are logged in.' };
|
|
225
490
|
}
|
|
226
|
-
|
|
227
|
-
|
|
491
|
+
/**
|
|
492
|
+
* Log a user out of the system.
|
|
493
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
494
|
+
*/
|
|
495
|
+
async $logout() {
|
|
496
|
+
const tilmeld = enforceTilmeld(this);
|
|
228
497
|
try {
|
|
229
498
|
for (let callback of this.constructor
|
|
230
499
|
.beforeLogoutCallbacks) {
|
|
231
500
|
if (callback) {
|
|
232
|
-
callback(this);
|
|
501
|
+
await callback(this);
|
|
233
502
|
}
|
|
234
503
|
}
|
|
235
504
|
}
|
|
@@ -239,11 +508,12 @@ 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) {
|
|
246
|
-
callback(this);
|
|
516
|
+
await callback(this);
|
|
247
517
|
}
|
|
248
518
|
}
|
|
249
519
|
return { result: true, message: 'You have been logged out.' };
|
|
@@ -256,64 +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 (input.data
|
|
290
|
-
this.$data.abilities
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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);
|
|
294
596
|
}
|
|
295
|
-
this.$originalEmail = this.$data.email;
|
|
296
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
|
+
}
|
|
297
603
|
}
|
|
298
604
|
$jsonAcceptPatch(patch, allowConflict = false) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
this.$data.abilities?.indexOf('system/admin') === -1 &&
|
|
303
|
-
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
304
|
-
!tilmeld.gatekeeper('system/admin')) {
|
|
305
|
-
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);
|
|
306
608
|
}
|
|
307
|
-
this.$originalEmail = this.$data.email;
|
|
308
609
|
super.$jsonAcceptPatch(patch, allowConflict);
|
|
309
610
|
}
|
|
310
|
-
$
|
|
311
|
-
|
|
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
|
+
}
|
|
312
648
|
this.$updateDataProtection();
|
|
313
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
|
+
*/
|
|
314
655
|
$updateDataProtection(givenUser) {
|
|
315
|
-
const tilmeld = this
|
|
656
|
+
const tilmeld = enforceTilmeld(this);
|
|
316
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
|
+
}
|
|
317
665
|
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
318
666
|
this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
319
667
|
this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
|
|
@@ -321,75 +669,211 @@ class User extends AbleObject_1.default {
|
|
|
321
669
|
this.$privateData.push('username');
|
|
322
670
|
}
|
|
323
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.
|
|
324
674
|
const abilities = (isCurrentUser || user == null
|
|
325
675
|
? this.$data.abilities
|
|
326
676
|
: user?.abilities) ?? [];
|
|
327
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`);
|
|
328
683
|
if (isCurrentUser) {
|
|
684
|
+
// Users can check to see what abilities they have.
|
|
329
685
|
this.$clientEnabledMethods.push('$gatekeeper');
|
|
330
686
|
this.$clientEnabledMethods.push('$changePassword');
|
|
687
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
331
688
|
this.$clientEnabledMethods.push('$logout');
|
|
332
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');
|
|
333
704
|
}
|
|
334
705
|
if (user != null &&
|
|
335
|
-
(abilities.
|
|
336
|
-
abilities.
|
|
337
|
-
|
|
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');
|
|
338
712
|
this.$allowlistData = undefined;
|
|
339
713
|
}
|
|
340
|
-
else if (
|
|
714
|
+
else if (isDomainUser) {
|
|
715
|
+
// Domain admins can see their users' data, and edit some of it.
|
|
341
716
|
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
342
717
|
this.$allowlistData.push('username');
|
|
343
718
|
}
|
|
344
719
|
this.$allowlistData.push('avatar');
|
|
345
|
-
if (tilmeld.config.userFields.
|
|
720
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
346
721
|
this.$allowlistData.push('nameFirst');
|
|
347
722
|
this.$allowlistData.push('nameMiddle');
|
|
348
723
|
this.$allowlistData.push('nameLast');
|
|
349
724
|
this.$allowlistData.push('name');
|
|
350
725
|
}
|
|
351
|
-
if (tilmeld.config.userFields.
|
|
726
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
352
727
|
this.$allowlistData.push('email');
|
|
353
728
|
}
|
|
354
|
-
if (tilmeld.config.userFields.
|
|
729
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
355
730
|
this.$allowlistData.push('phone');
|
|
356
731
|
}
|
|
732
|
+
this.$allowlistData.push('enabled');
|
|
733
|
+
this.$allowlistData.push('passwordTemp');
|
|
357
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',
|
|
358
749
|
'secret',
|
|
750
|
+
'emailChangeDate',
|
|
359
751
|
'newEmailSecret',
|
|
360
|
-
'
|
|
752
|
+
// 'newEmailAddress',
|
|
361
753
|
'cancelEmailSecret',
|
|
362
|
-
'
|
|
754
|
+
'cancelEmailAddress',
|
|
363
755
|
'recoverSecret',
|
|
364
756
|
'recoverSecretDate',
|
|
757
|
+
'salt',
|
|
365
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',
|
|
366
805
|
'salt',
|
|
806
|
+
'password',
|
|
807
|
+
'passwordTemp',
|
|
808
|
+
// 'revokeTokenDate',
|
|
809
|
+
'totpSecret',
|
|
367
810
|
];
|
|
368
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
|
+
}
|
|
369
829
|
}
|
|
370
|
-
|
|
371
|
-
|
|
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);
|
|
372
847
|
if (ability == null) {
|
|
373
848
|
return tilmeld.User.current()?.$is(this) ?? false;
|
|
374
849
|
}
|
|
850
|
+
// Check the cache to see if we've already checked this user.
|
|
375
851
|
if (this.$gatekeeperCache == null) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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 ?? []);
|
|
387
868
|
}
|
|
388
869
|
}
|
|
389
|
-
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
|
+
}
|
|
390
875
|
}
|
|
391
|
-
return ((ability
|
|
392
|
-
!!this.$gatekeeperCache['system/admin']);
|
|
876
|
+
return Object.fromEntries(abilities.map((ability) => [ability, true]));
|
|
393
877
|
}
|
|
394
878
|
$clearCache() {
|
|
395
879
|
const ret = super.$clearCache();
|
|
@@ -397,8 +881,13 @@ class User extends AbleObject_1.default {
|
|
|
397
881
|
this.$gatekeeperCache = undefined;
|
|
398
882
|
return ret;
|
|
399
883
|
}
|
|
884
|
+
/**
|
|
885
|
+
* Send the user email verification/change/cancellation links.
|
|
886
|
+
*
|
|
887
|
+
* @returns True on success, false on failure.
|
|
888
|
+
*/
|
|
400
889
|
async $sendEmailVerification() {
|
|
401
|
-
const tilmeld = this
|
|
890
|
+
const tilmeld = enforceTilmeld(this);
|
|
402
891
|
if (this.guid == null) {
|
|
403
892
|
return false;
|
|
404
893
|
}
|
|
@@ -461,33 +950,60 @@ class User extends AbleObject_1.default {
|
|
|
461
950
|
}
|
|
462
951
|
return success;
|
|
463
952
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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;
|
|
471
969
|
}
|
|
472
970
|
return true;
|
|
473
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
|
+
*/
|
|
474
978
|
$checkPassword(password) {
|
|
475
|
-
const tilmeld = this
|
|
979
|
+
const tilmeld = enforceTilmeld(this);
|
|
476
980
|
switch (tilmeld.config.pwMethod) {
|
|
477
981
|
case 'plain':
|
|
478
982
|
return this.$data.password === password;
|
|
479
983
|
case 'digest':
|
|
480
|
-
return this.$data.password ===
|
|
984
|
+
return (this.$data.password ===
|
|
985
|
+
crypto.createHash('sha256').update(password).digest('base64'));
|
|
481
986
|
case 'salt':
|
|
482
987
|
default:
|
|
483
988
|
return (this.$data.password ===
|
|
484
|
-
|
|
989
|
+
crypto
|
|
990
|
+
.createHash('sha256')
|
|
991
|
+
.update(password + this.$data.salt)
|
|
992
|
+
.digest('base64'));
|
|
485
993
|
}
|
|
486
994
|
}
|
|
487
|
-
|
|
488
|
-
|
|
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)) {
|
|
489
1005
|
const newGroups = [];
|
|
490
|
-
for (let curGroup of
|
|
1006
|
+
for (let curGroup of groups) {
|
|
491
1007
|
if (!group.$is(curGroup)) {
|
|
492
1008
|
newGroups.push(curGroup);
|
|
493
1009
|
}
|
|
@@ -497,7 +1013,13 @@ class User extends AbleObject_1.default {
|
|
|
497
1013
|
}
|
|
498
1014
|
return true;
|
|
499
1015
|
}
|
|
500
|
-
|
|
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) {
|
|
501
1023
|
if (typeof group === 'string') {
|
|
502
1024
|
if (this.$data.group?.guid === group) {
|
|
503
1025
|
return true;
|
|
@@ -511,35 +1033,66 @@ class User extends AbleObject_1.default {
|
|
|
511
1033
|
}
|
|
512
1034
|
return (group.$is(this.$data.group) || group.$inArray(this.$data.groups ?? []));
|
|
513
1035
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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))) {
|
|
517
1048
|
return true;
|
|
518
1049
|
}
|
|
1050
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
519
1051
|
for (let curGroup of this.$data.groups ?? []) {
|
|
520
|
-
if (curGroup.$isDescendant(group)) {
|
|
1052
|
+
if (await curGroup.$isDescendant(group)) {
|
|
521
1053
|
return true;
|
|
522
1054
|
}
|
|
523
1055
|
}
|
|
524
1056
|
return false;
|
|
525
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
|
+
*/
|
|
526
1064
|
async $changePassword(data) {
|
|
1065
|
+
this.$nymph.config.debugLog('tilmeld', `Password change request for "${this.$data.username}".`);
|
|
527
1066
|
if (!('newPassword' in data) || !data.newPassword.length) {
|
|
528
1067
|
return { result: false, message: 'Please specify a password.' };
|
|
529
1068
|
}
|
|
530
1069
|
if (!this.$checkPassword(data.currentPassword ?? '')) {
|
|
1070
|
+
this.$nymph.config.debugLog('tilmeld', `Incorrect password for change request for "${this.$data.username}".`);
|
|
531
1071
|
return { result: false, message: 'Incorrect password.' };
|
|
532
1072
|
}
|
|
533
1073
|
this.$data.passwordTemp = data.newPassword;
|
|
1074
|
+
if (data.revokeCurrentTokens) {
|
|
1075
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1076
|
+
}
|
|
534
1077
|
if (await this.$save()) {
|
|
1078
|
+
if (data.revokeCurrentTokens) {
|
|
1079
|
+
const tilmeld = enforceTilmeld(this);
|
|
1080
|
+
await tilmeld.login(this, true);
|
|
1081
|
+
}
|
|
535
1082
|
return { result: true, message: 'Your password has been changed.' };
|
|
536
1083
|
}
|
|
537
1084
|
else {
|
|
538
1085
|
return { result: false, message: "Couldn't save new password." };
|
|
539
1086
|
}
|
|
540
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
|
+
*/
|
|
541
1094
|
$password(password) {
|
|
542
|
-
const tilmeld = this
|
|
1095
|
+
const tilmeld = enforceTilmeld(this);
|
|
543
1096
|
switch (tilmeld.config.pwMethod) {
|
|
544
1097
|
case 'plain':
|
|
545
1098
|
delete this.$data.salt;
|
|
@@ -547,24 +1100,221 @@ class User extends AbleObject_1.default {
|
|
|
547
1100
|
break;
|
|
548
1101
|
case 'digest':
|
|
549
1102
|
delete this.$data.salt;
|
|
550
|
-
this.$data.password =
|
|
1103
|
+
this.$data.password = crypto
|
|
1104
|
+
.createHash('sha256')
|
|
1105
|
+
.update(password)
|
|
1106
|
+
.digest('base64');
|
|
551
1107
|
break;
|
|
552
1108
|
case 'salt':
|
|
553
1109
|
default:
|
|
554
|
-
this.$data.salt =
|
|
555
|
-
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');
|
|
556
1115
|
break;
|
|
557
1116
|
}
|
|
558
1117
|
return this.$data.password;
|
|
559
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
|
+
*/
|
|
560
1291
|
async $checkUsername() {
|
|
561
|
-
const tilmeld = this
|
|
1292
|
+
const tilmeld = enforceTilmeld(this);
|
|
1293
|
+
this.$nymph.config.debugInfo('tilmeld', `Username check "${this.$data.username}".`);
|
|
562
1294
|
if (!tilmeld.config.emailUsernames) {
|
|
563
|
-
|
|
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) {
|
|
564
1307
|
return { result: false, message: 'Please specify a username.' };
|
|
565
1308
|
}
|
|
566
|
-
if (tilmeld.config.
|
|
567
|
-
|
|
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) {
|
|
568
1318
|
return {
|
|
569
1319
|
result: false,
|
|
570
1320
|
message: 'Usernames must not exceed ' +
|
|
@@ -572,18 +1322,35 @@ class User extends AbleObject_1.default {
|
|
|
572
1322
|
' characters.',
|
|
573
1323
|
};
|
|
574
1324
|
}
|
|
575
|
-
if (
|
|
1325
|
+
if (difference(username.split(''), tilmeld.config.validChars.split(''))
|
|
1326
|
+
.length) {
|
|
576
1327
|
return {
|
|
577
1328
|
result: false,
|
|
578
1329
|
message: tilmeld.config.validCharsNotice,
|
|
579
1330
|
};
|
|
580
1331
|
}
|
|
581
|
-
if (!tilmeld.config.validRegex.test(
|
|
1332
|
+
if (!tilmeld.config.validRegex.test(username)) {
|
|
582
1333
|
return {
|
|
583
1334
|
result: false,
|
|
584
1335
|
message: tilmeld.config.validRegexNotice,
|
|
585
1336
|
};
|
|
586
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
|
+
}
|
|
587
1354
|
const selector = {
|
|
588
1355
|
type: '&',
|
|
589
1356
|
ilike: [
|
|
@@ -598,6 +1365,17 @@ class User extends AbleObject_1.default {
|
|
|
598
1365
|
if (test != null) {
|
|
599
1366
|
return { result: false, message: 'That username is taken.' };
|
|
600
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
|
+
}
|
|
601
1379
|
return {
|
|
602
1380
|
result: true,
|
|
603
1381
|
message: this.guid != null ? 'Username is valid.' : 'Username is available!',
|
|
@@ -608,8 +1386,15 @@ class User extends AbleObject_1.default {
|
|
|
608
1386
|
if (this.$data.username == null || !this.$data.username.length) {
|
|
609
1387
|
return { result: false, message: 'Please specify an email.' };
|
|
610
1388
|
}
|
|
611
|
-
if (tilmeld.config.
|
|
612
|
-
|
|
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) {
|
|
613
1398
|
return {
|
|
614
1399
|
result: false,
|
|
615
1400
|
message: 'Emails must not exceed ' +
|
|
@@ -620,8 +1405,14 @@ class User extends AbleObject_1.default {
|
|
|
620
1405
|
return await this.$checkEmail();
|
|
621
1406
|
}
|
|
622
1407
|
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Check that an email is unique.
|
|
1410
|
+
*
|
|
1411
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1412
|
+
*/
|
|
623
1413
|
async $checkEmail() {
|
|
624
|
-
const tilmeld = this
|
|
1414
|
+
const tilmeld = enforceTilmeld(this);
|
|
1415
|
+
this.$nymph.config.debugInfo('tilmeld', `Email check "${this.$data.email}".`);
|
|
625
1416
|
if (this.$data.email == null || !this.$data.email.length) {
|
|
626
1417
|
if (tilmeld.config.verifyEmail) {
|
|
627
1418
|
return { result: false, message: 'Please specify an email.' };
|
|
@@ -655,8 +1446,14 @@ class User extends AbleObject_1.default {
|
|
|
655
1446
|
message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
|
|
656
1447
|
};
|
|
657
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
|
+
*/
|
|
658
1454
|
async $checkPhone() {
|
|
659
|
-
const tilmeld = this
|
|
1455
|
+
const tilmeld = enforceTilmeld(this);
|
|
1456
|
+
this.$nymph.config.debugInfo('tilmeld', `Phone check "${this.$data.phone}".`);
|
|
660
1457
|
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
661
1458
|
return { result: false, message: 'Please specify a phone number.' };
|
|
662
1459
|
}
|
|
@@ -684,7 +1481,7 @@ class User extends AbleObject_1.default {
|
|
|
684
1481
|
};
|
|
685
1482
|
}
|
|
686
1483
|
async $register(data) {
|
|
687
|
-
let tilmeld = this
|
|
1484
|
+
let tilmeld = enforceTilmeld(this);
|
|
688
1485
|
if (!tilmeld.config.allowRegistration) {
|
|
689
1486
|
return {
|
|
690
1487
|
result: false,
|
|
@@ -699,21 +1496,6 @@ class User extends AbleObject_1.default {
|
|
|
699
1496
|
message: 'This is already a registered user.',
|
|
700
1497
|
};
|
|
701
1498
|
}
|
|
702
|
-
try {
|
|
703
|
-
for (let callback of this.constructor
|
|
704
|
-
.beforeRegisterCallbacks) {
|
|
705
|
-
if (callback) {
|
|
706
|
-
await callback(this, data);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
catch (e) {
|
|
711
|
-
return {
|
|
712
|
-
result: false,
|
|
713
|
-
loggedin: false,
|
|
714
|
-
message: e.message,
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
1499
|
if (!('password' in data) || !data.password.length) {
|
|
718
1500
|
return {
|
|
719
1501
|
result: false,
|
|
@@ -726,7 +1508,7 @@ class User extends AbleObject_1.default {
|
|
|
726
1508
|
return { ...unCheck, loggedin: false };
|
|
727
1509
|
}
|
|
728
1510
|
this.$password(data.password);
|
|
729
|
-
if (tilmeld.config.regFields.
|
|
1511
|
+
if (tilmeld.config.regFields.includes('name')) {
|
|
730
1512
|
this.$data.name =
|
|
731
1513
|
this.$data.nameFirst +
|
|
732
1514
|
(this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
|
|
@@ -738,59 +1520,35 @@ class User extends AbleObject_1.default {
|
|
|
738
1520
|
if (tilmeld.config.emailUsernames) {
|
|
739
1521
|
this.$data.email = this.$data.username;
|
|
740
1522
|
}
|
|
741
|
-
|
|
1523
|
+
// Start transaction.
|
|
1524
|
+
const transaction = 'tilmeld-register-' + nanoid();
|
|
742
1525
|
const nymph = this.$nymph;
|
|
743
1526
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
744
|
-
this.$
|
|
745
|
-
tilmeld = this
|
|
1527
|
+
this.$setNymph(tnymph);
|
|
1528
|
+
tilmeld = enforceTilmeld(this);
|
|
1529
|
+
let message = '';
|
|
1530
|
+
let loggedin = false;
|
|
746
1531
|
try {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
generatedPrimaryGroup.email = this.$data.email;
|
|
754
|
-
const parent = await tnymph.getEntity({ class: tilmeld.Group }, {
|
|
755
|
-
type: '&',
|
|
756
|
-
equal: ['defaultPrimary', true],
|
|
757
|
-
});
|
|
758
|
-
if (parent != null) {
|
|
759
|
-
generatedPrimaryGroup.parent = parent;
|
|
760
|
-
}
|
|
761
|
-
if (!(await generatedPrimaryGroup.$saveSkipAC())) {
|
|
762
|
-
await tnymph.rollback(transaction);
|
|
763
|
-
this.$nymph = nymph;
|
|
764
|
-
return {
|
|
765
|
-
result: false,
|
|
766
|
-
loggedin: false,
|
|
767
|
-
message: 'Error creating primary group for user.',
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
this.$data.group = generatedPrimaryGroup;
|
|
771
|
-
}
|
|
772
|
-
else {
|
|
773
|
-
const group = await tnymph.getEntity({ class: tilmeld.Group }, {
|
|
774
|
-
type: '&',
|
|
775
|
-
equal: ['defaultPrimary', true],
|
|
776
|
-
});
|
|
777
|
-
if (group != null) {
|
|
778
|
-
this.$data.group = group;
|
|
1532
|
+
try {
|
|
1533
|
+
for (let callback of this.constructor
|
|
1534
|
+
.beforeRegisterCallbacks) {
|
|
1535
|
+
if (callback) {
|
|
1536
|
+
await callback(this, data);
|
|
1537
|
+
}
|
|
779
1538
|
}
|
|
780
1539
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
type: '&',
|
|
790
|
-
equal: ['defaultSecondary', true],
|
|
791
|
-
});
|
|
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
|
+
};
|
|
792
1548
|
}
|
|
793
|
-
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.
|
|
794
1552
|
if (!tilmeld.config.unverifiedAccess) {
|
|
795
1553
|
this.$data.enabled = false;
|
|
796
1554
|
}
|
|
@@ -798,6 +1556,8 @@ class User extends AbleObject_1.default {
|
|
|
798
1556
|
else {
|
|
799
1557
|
this.$data.enabled = true;
|
|
800
1558
|
}
|
|
1559
|
+
// If createAdmin is true and there are no other users, grant
|
|
1560
|
+
// "system/admin".
|
|
801
1561
|
let madeAdmin = false;
|
|
802
1562
|
if (tilmeld.config.createAdmin) {
|
|
803
1563
|
const otherUsers = await tnymph.getEntities({
|
|
@@ -806,13 +1566,26 @@ class User extends AbleObject_1.default {
|
|
|
806
1566
|
limit: 1,
|
|
807
1567
|
return: 'guid',
|
|
808
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
|
+
}
|
|
809
1579
|
if (!otherUsers.length) {
|
|
810
1580
|
this.$grant('system/admin');
|
|
811
1581
|
this.$data.enabled = true;
|
|
812
1582
|
madeAdmin = true;
|
|
813
1583
|
}
|
|
814
1584
|
}
|
|
1585
|
+
this.$nymph.config.debugLog('tilmeld', `Registering new user "${this.$data.username}".`);
|
|
815
1586
|
if (await this.$saveSkipAC()) {
|
|
1587
|
+
this.$nymph.config.debugLog('tilmeld', `New user registered "${this.$data.username}".`);
|
|
1588
|
+
// Send the new user registered email.
|
|
816
1589
|
if (tilmeld.config.userRegisteredRecipient != null) {
|
|
817
1590
|
await tilmeld.config.sendEmail(tilmeld, {
|
|
818
1591
|
template: 'UserRegistered',
|
|
@@ -829,31 +1602,20 @@ class User extends AbleObject_1.default {
|
|
|
829
1602
|
},
|
|
830
1603
|
}, this);
|
|
831
1604
|
}
|
|
832
|
-
|
|
833
|
-
let loggedin = false;
|
|
834
|
-
if (generatedPrimaryGroup != null) {
|
|
835
|
-
generatedPrimaryGroup.user = this;
|
|
836
|
-
if (!(await generatedPrimaryGroup.$saveSkipAC())) {
|
|
837
|
-
await tnymph.rollback(transaction);
|
|
838
|
-
this.$nymph = nymph;
|
|
839
|
-
return {
|
|
840
|
-
result: false,
|
|
841
|
-
loggedin: false,
|
|
842
|
-
message: 'Your primary group could not be assigned. Please try again later.',
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
}
|
|
1605
|
+
// Finish up.
|
|
846
1606
|
if (tilmeld.config.verifyEmail &&
|
|
847
1607
|
!tilmeld.config.unverifiedAccess &&
|
|
848
|
-
!madeAdmin
|
|
1608
|
+
!madeAdmin &&
|
|
1609
|
+
tilmeld.config.userFields.includes('email')) {
|
|
849
1610
|
message +=
|
|
850
1611
|
`Almost there. An email has been sent to ${this.$data.email} ` +
|
|
851
1612
|
'with a verification link for you to finish registration.';
|
|
852
1613
|
}
|
|
853
1614
|
else if (tilmeld.config.verifyEmail &&
|
|
854
1615
|
tilmeld.config.unverifiedAccess &&
|
|
855
|
-
!madeAdmin
|
|
856
|
-
|
|
1616
|
+
!madeAdmin &&
|
|
1617
|
+
tilmeld.config.userFields.includes('email')) {
|
|
1618
|
+
if (!(await tilmeld.login(this, true))) {
|
|
857
1619
|
throw new Error('An error occurred trying to log you in.');
|
|
858
1620
|
}
|
|
859
1621
|
message +=
|
|
@@ -863,7 +1625,7 @@ class User extends AbleObject_1.default {
|
|
|
863
1625
|
loggedin = true;
|
|
864
1626
|
}
|
|
865
1627
|
else {
|
|
866
|
-
if (!tilmeld.login(this, true)) {
|
|
1628
|
+
if (!(await tilmeld.login(this, true))) {
|
|
867
1629
|
throw new Error('An error occurred trying to log you in.');
|
|
868
1630
|
}
|
|
869
1631
|
message += "You're now registered and logged in!";
|
|
@@ -878,23 +1640,11 @@ class User extends AbleObject_1.default {
|
|
|
878
1640
|
});
|
|
879
1641
|
}
|
|
880
1642
|
}
|
|
881
|
-
try {
|
|
882
|
-
await tnymph.commit(transaction);
|
|
883
|
-
this.$nymph = nymph;
|
|
884
|
-
this.$nymph.tilmeld?.fillSession(this);
|
|
885
|
-
}
|
|
886
|
-
catch (e) {
|
|
887
|
-
throw e;
|
|
888
|
-
}
|
|
889
|
-
return {
|
|
890
|
-
result: true,
|
|
891
|
-
loggedin,
|
|
892
|
-
message,
|
|
893
|
-
};
|
|
894
1643
|
}
|
|
895
1644
|
else {
|
|
1645
|
+
this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}".`);
|
|
896
1646
|
await tnymph.rollback(transaction);
|
|
897
|
-
this.$nymph
|
|
1647
|
+
this.$setNymph(nymph);
|
|
898
1648
|
return {
|
|
899
1649
|
result: false,
|
|
900
1650
|
loggedin: false,
|
|
@@ -903,22 +1653,69 @@ class User extends AbleObject_1.default {
|
|
|
903
1653
|
}
|
|
904
1654
|
}
|
|
905
1655
|
catch (e) {
|
|
1656
|
+
this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}": ${e}`);
|
|
906
1657
|
await tnymph.rollback(transaction);
|
|
907
|
-
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) {
|
|
908
1667
|
throw e;
|
|
909
1668
|
}
|
|
1669
|
+
return {
|
|
1670
|
+
result: true,
|
|
1671
|
+
loggedin,
|
|
1672
|
+
message,
|
|
1673
|
+
};
|
|
910
1674
|
}
|
|
911
1675
|
async $save() {
|
|
912
|
-
let tilmeld = this
|
|
1676
|
+
let tilmeld = enforceTilmeld(this);
|
|
913
1677
|
if (this.$data.username == null || !this.$data.username.trim().length) {
|
|
914
1678
|
return false;
|
|
915
1679
|
}
|
|
916
|
-
if (
|
|
1680
|
+
if (!this.$skipAcWhenSaving &&
|
|
1681
|
+
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
917
1682
|
!tilmeld.gatekeeper('system/admin') &&
|
|
918
|
-
this.$
|
|
919
|
-
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
|
+
}
|
|
920
1716
|
}
|
|
921
1717
|
let sendVerification = false;
|
|
1718
|
+
// Formatting.
|
|
922
1719
|
this.$data.username = this.$data.username.trim();
|
|
923
1720
|
if (tilmeld.config.emailUsernames) {
|
|
924
1721
|
this.$data.email = this.$data.username;
|
|
@@ -932,49 +1729,63 @@ class User extends AbleObject_1.default {
|
|
|
932
1729
|
(this.$data.nameMiddle ? ' ' + this.$data.nameMiddle : '') +
|
|
933
1730
|
(this.$data.nameLast ? ' ' + this.$data.nameLast : '');
|
|
934
1731
|
this.$data.enabled = !!this.$data.enabled;
|
|
935
|
-
|
|
1732
|
+
// Clear empty values.
|
|
1733
|
+
if (this.$data.nameFirst === '' ||
|
|
1734
|
+
!tilmeld.config.userFields.includes('name')) {
|
|
936
1735
|
delete this.$data.nameFirst;
|
|
937
1736
|
}
|
|
938
|
-
if (this.$data.nameMiddle === ''
|
|
1737
|
+
if (this.$data.nameMiddle === '' ||
|
|
1738
|
+
!tilmeld.config.userFields.includes('name')) {
|
|
939
1739
|
delete this.$data.nameMiddle;
|
|
940
1740
|
}
|
|
941
|
-
if (this.$data.nameLast === ''
|
|
1741
|
+
if (this.$data.nameLast === '' ||
|
|
1742
|
+
!tilmeld.config.userFields.includes('name')) {
|
|
942
1743
|
delete this.$data.nameLast;
|
|
943
1744
|
}
|
|
944
|
-
if (this.$data.name === '') {
|
|
1745
|
+
if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
945
1746
|
delete this.$data.name;
|
|
946
1747
|
}
|
|
947
|
-
if (this.$data.email === ''
|
|
1748
|
+
if (this.$data.email === '' ||
|
|
1749
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
948
1750
|
delete this.$data.email;
|
|
949
1751
|
}
|
|
950
1752
|
if (this.$data.avatar === '') {
|
|
951
1753
|
delete this.$data.avatar;
|
|
952
1754
|
}
|
|
953
|
-
if (this.$data.phone === ''
|
|
1755
|
+
if (this.$data.phone === '' ||
|
|
1756
|
+
!tilmeld.config.userFields.includes('phone')) {
|
|
954
1757
|
delete this.$data.phone;
|
|
955
1758
|
}
|
|
956
|
-
if (this.$data.secret === ''
|
|
1759
|
+
if (this.$data.secret === '' ||
|
|
1760
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
957
1761
|
delete this.$data.secret;
|
|
958
1762
|
}
|
|
959
|
-
if (this.$data.emailChangeDate === 0
|
|
1763
|
+
if (this.$data.emailChangeDate === 0 ||
|
|
1764
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
960
1765
|
delete this.$data.emailChangeDate;
|
|
961
1766
|
}
|
|
962
|
-
if (this.$data.newEmailSecret === ''
|
|
1767
|
+
if (this.$data.newEmailSecret === '' ||
|
|
1768
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
963
1769
|
delete this.$data.newEmailSecret;
|
|
964
1770
|
}
|
|
965
|
-
if (this.$data.newEmailAddress === ''
|
|
1771
|
+
if (this.$data.newEmailAddress === '' ||
|
|
1772
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
966
1773
|
delete this.$data.newEmailAddress;
|
|
967
1774
|
}
|
|
968
|
-
if (this.$data.cancelEmailSecret === ''
|
|
1775
|
+
if (this.$data.cancelEmailSecret === '' ||
|
|
1776
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
969
1777
|
delete this.$data.cancelEmailSecret;
|
|
970
1778
|
}
|
|
971
|
-
if (this.$data.cancelEmailAddress === ''
|
|
1779
|
+
if (this.$data.cancelEmailAddress === '' ||
|
|
1780
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
972
1781
|
delete this.$data.cancelEmailAddress;
|
|
973
1782
|
}
|
|
974
|
-
if (this.$data.recoverSecret === ''
|
|
1783
|
+
if (this.$data.recoverSecret === '' ||
|
|
1784
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
975
1785
|
delete this.$data.recoverSecret;
|
|
976
1786
|
}
|
|
977
|
-
if (this.$data.recoverSecretDate === 0
|
|
1787
|
+
if (this.$data.recoverSecretDate === 0 ||
|
|
1788
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
978
1789
|
delete this.$data.recoverSecretDate;
|
|
979
1790
|
}
|
|
980
1791
|
if (this.$data.password === '') {
|
|
@@ -986,50 +1797,65 @@ class User extends AbleObject_1.default {
|
|
|
986
1797
|
if (this.$data.inheritAbilities === false) {
|
|
987
1798
|
delete this.$data.inheritAbilities;
|
|
988
1799
|
}
|
|
1800
|
+
// Verification.
|
|
989
1801
|
const unCheck = await this.$checkUsername();
|
|
990
1802
|
if (!unCheck.result) {
|
|
991
|
-
throw new
|
|
1803
|
+
throw new BadUsernameError(unCheck.message);
|
|
992
1804
|
}
|
|
993
|
-
if (!tilmeld.config.emailUsernames
|
|
1805
|
+
if (!tilmeld.config.emailUsernames &&
|
|
1806
|
+
tilmeld.config.userFields.includes('email')) {
|
|
994
1807
|
const emCheck = await this.$checkEmail();
|
|
995
1808
|
if (!emCheck.result) {
|
|
996
|
-
throw new
|
|
1809
|
+
throw new BadEmailError(emCheck.message);
|
|
997
1810
|
}
|
|
998
1811
|
}
|
|
999
|
-
|
|
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.
|
|
1000
1817
|
if (tilmeld.config.verifyEmail) {
|
|
1818
|
+
// The user needs to verify this new email address.
|
|
1001
1819
|
if (this.guid == null) {
|
|
1002
|
-
|
|
1003
|
-
|
|
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();
|
|
1004
1824
|
sendVerification = true;
|
|
1005
1825
|
}
|
|
1006
1826
|
}
|
|
1007
1827
|
else if (this.$originalEmail != null &&
|
|
1008
1828
|
this.$originalEmail !== this.$data.email) {
|
|
1829
|
+
// The user already has an old email address.
|
|
1009
1830
|
if (tilmeld.config.emailRateLimit !== '' &&
|
|
1010
1831
|
this.$data.emailChangeDate != null &&
|
|
1011
1832
|
this.$data.emailChangeDate >=
|
|
1012
|
-
(
|
|
1013
|
-
throw new
|
|
1014
|
-
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() +
|
|
1015
1836
|
' to change your email address again.');
|
|
1016
1837
|
}
|
|
1017
1838
|
else {
|
|
1018
1839
|
if (this.$data.newEmailSecret == null &&
|
|
1019
1840
|
tilmeld.config.emailRateLimit !== '' &&
|
|
1841
|
+
// Make sure the user has at least the rate limit time to cancel
|
|
1842
|
+
// an email change.
|
|
1020
1843
|
(this.$data.emailChangeDate == null ||
|
|
1021
1844
|
this.$data.emailChangeDate <
|
|
1022
|
-
(
|
|
1845
|
+
strtotime('-' + tilmeld.config.emailRateLimit) * 1000)) {
|
|
1846
|
+
// Save the old email in case the cancel change link is clicked.
|
|
1023
1847
|
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
1024
|
-
this.$data.cancelEmailSecret =
|
|
1848
|
+
this.$data.cancelEmailSecret = nanoid();
|
|
1025
1849
|
this.$data.emailChangeDate = Date.now();
|
|
1026
1850
|
}
|
|
1851
|
+
// Save the new address and reset to the old one (until they verify
|
|
1852
|
+
// it).
|
|
1027
1853
|
this.$data.newEmailAddress = this.$data.email;
|
|
1028
1854
|
this.$data.email = this.$originalEmail;
|
|
1029
1855
|
if (tilmeld.config.emailUsernames) {
|
|
1030
1856
|
this.$data.username = this.$data.email;
|
|
1031
1857
|
}
|
|
1032
|
-
this.$data.newEmailSecret =
|
|
1858
|
+
this.$data.newEmailSecret = nanoid();
|
|
1033
1859
|
sendVerification = true;
|
|
1034
1860
|
}
|
|
1035
1861
|
}
|
|
@@ -1037,11 +1863,15 @@ class User extends AbleObject_1.default {
|
|
|
1037
1863
|
else if (this.guid != null &&
|
|
1038
1864
|
this.$originalEmail != null &&
|
|
1039
1865
|
this.$originalEmail !== this.$data.email &&
|
|
1866
|
+
// Make sure the user has at least the rate limit time to cancel an
|
|
1867
|
+
// email change.
|
|
1040
1868
|
(this.$data.emailChangeDate == null ||
|
|
1041
1869
|
this.$data.emailChangeDate <
|
|
1042
|
-
(
|
|
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.
|
|
1043
1873
|
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
1044
|
-
this.$data.cancelEmailSecret =
|
|
1874
|
+
this.$data.cancelEmailSecret = nanoid();
|
|
1045
1875
|
sendVerification = true;
|
|
1046
1876
|
}
|
|
1047
1877
|
}
|
|
@@ -1050,72 +1880,188 @@ class User extends AbleObject_1.default {
|
|
|
1050
1880
|
}
|
|
1051
1881
|
delete this.$data.passwordTemp;
|
|
1052
1882
|
if (this.$data.password == null) {
|
|
1053
|
-
throw new
|
|
1054
|
-
}
|
|
1055
|
-
try {
|
|
1056
|
-
tilmeld.config.validatorUser(this);
|
|
1057
|
-
}
|
|
1058
|
-
catch (e) {
|
|
1059
|
-
throw new errors_1.BadDataError(e?.message);
|
|
1883
|
+
throw new BadDataError('A password is required.');
|
|
1060
1884
|
}
|
|
1061
|
-
|
|
1885
|
+
// Start transaction.
|
|
1886
|
+
const transaction = 'tilmeld-save-user-' + (this.guid || nanoid());
|
|
1062
1887
|
const nymph = this.$nymph;
|
|
1063
1888
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1064
|
-
this.$
|
|
1065
|
-
tilmeld = this
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
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
|
+
}
|
|
1069
1982
|
try {
|
|
1070
|
-
|
|
1071
|
-
this.$data.group.avatar = this.$data.avatar;
|
|
1072
|
-
this.$data.group.email = this.$data.email;
|
|
1073
|
-
this.$data.group.name = this.$data.name;
|
|
1074
|
-
this.$data.group.phone = this.$data.phone;
|
|
1075
|
-
await this.$data.group.$saveSkipAC();
|
|
1983
|
+
tilmeld.config.validatorUser(tilmeld, this);
|
|
1076
1984
|
}
|
|
1077
1985
|
catch (e) {
|
|
1078
|
-
|
|
1079
|
-
this.$nymph = nymph;
|
|
1080
|
-
throw e;
|
|
1986
|
+
throw new BadDataError(e?.message);
|
|
1081
1987
|
}
|
|
1082
1988
|
}
|
|
1083
|
-
|
|
1989
|
+
catch (e) {
|
|
1990
|
+
await tnymph.rollback(transaction);
|
|
1991
|
+
this.$setNymph(nymph);
|
|
1992
|
+
throw e;
|
|
1993
|
+
}
|
|
1994
|
+
let ret = false;
|
|
1084
1995
|
let preGuid = this.guid;
|
|
1085
1996
|
let preCdate = this.cdate;
|
|
1086
1997
|
let preMdate = this.mdate;
|
|
1087
1998
|
try {
|
|
1999
|
+
for (let callback of this.constructor
|
|
2000
|
+
.beforeSaveCallbacks) {
|
|
2001
|
+
if (callback) {
|
|
2002
|
+
await callback(this);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
1088
2005
|
ret = await super.$save();
|
|
1089
2006
|
}
|
|
1090
2007
|
catch (e) {
|
|
1091
2008
|
await tnymph.rollback(transaction);
|
|
1092
|
-
this
|
|
2009
|
+
this.guid = preGuid;
|
|
2010
|
+
this.cdate = preCdate;
|
|
2011
|
+
this.mdate = preMdate;
|
|
2012
|
+
this.$setNymph(nymph);
|
|
1093
2013
|
throw e;
|
|
1094
2014
|
}
|
|
1095
2015
|
if (ret) {
|
|
1096
2016
|
if (sendVerification) {
|
|
2017
|
+
// The email has changed, so send a new verification email.
|
|
1097
2018
|
if (!(await this.$sendEmailVerification())) {
|
|
1098
2019
|
await tnymph.rollback(transaction);
|
|
1099
2020
|
this.guid = preGuid;
|
|
1100
2021
|
this.cdate = preCdate;
|
|
1101
2022
|
this.mdate = preMdate;
|
|
1102
|
-
this.$nymph
|
|
2023
|
+
this.$setNymph(nymph);
|
|
1103
2024
|
throw new Error("Couldn't send verification email.");
|
|
1104
2025
|
}
|
|
1105
2026
|
}
|
|
1106
2027
|
this.$descendantGroups = undefined;
|
|
1107
2028
|
this.$gatekeeperCache = undefined;
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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;
|
|
1111
2044
|
}
|
|
2045
|
+
ret = await tnymph.commit(transaction);
|
|
1112
2046
|
}
|
|
1113
2047
|
else {
|
|
1114
2048
|
await tnymph.rollback(transaction);
|
|
1115
2049
|
}
|
|
1116
|
-
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
|
+
}
|
|
1117
2060
|
return ret;
|
|
1118
2061
|
}
|
|
2062
|
+
/**
|
|
2063
|
+
* This should *never* be accessible on the client.
|
|
2064
|
+
*/
|
|
1119
2065
|
async $saveSkipAC() {
|
|
1120
2066
|
this.$skipAcWhenSaving = true;
|
|
1121
2067
|
return await this.$save();
|
|
@@ -1128,24 +2074,109 @@ class User extends AbleObject_1.default {
|
|
|
1128
2074
|
return false;
|
|
1129
2075
|
}
|
|
1130
2076
|
async $delete() {
|
|
1131
|
-
const tilmeld = this
|
|
1132
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1133
|
-
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.");
|
|
1134
2080
|
}
|
|
1135
|
-
if (!
|
|
1136
|
-
|
|
1137
|
-
|
|
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.");
|
|
1138
2088
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
+
}
|
|
1141
2132
|
}
|
|
1142
|
-
|
|
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);
|
|
2153
|
+
}
|
|
2154
|
+
else {
|
|
2155
|
+
await tnymph.rollback(transaction);
|
|
2156
|
+
}
|
|
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;
|
|
2171
|
+
}
|
|
2172
|
+
return false;
|
|
1143
2173
|
}
|
|
1144
2174
|
static on(event, callback) {
|
|
1145
2175
|
const prop = (event + 'Callbacks');
|
|
1146
2176
|
if (!(prop in this)) {
|
|
1147
2177
|
throw new Error('Invalid event type.');
|
|
1148
2178
|
}
|
|
2179
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1149
2180
|
this[prop].push(callback);
|
|
1150
2181
|
return () => this.off(event, callback);
|
|
1151
2182
|
}
|
|
@@ -1154,55 +2185,13 @@ class User extends AbleObject_1.default {
|
|
|
1154
2185
|
if (!(prop in this)) {
|
|
1155
2186
|
return false;
|
|
1156
2187
|
}
|
|
2188
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1157
2189
|
const i = this[prop].indexOf(callback);
|
|
1158
2190
|
if (i > -1) {
|
|
2191
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1159
2192
|
this[prop].splice(i, 1);
|
|
1160
2193
|
}
|
|
1161
2194
|
return true;
|
|
1162
2195
|
}
|
|
1163
2196
|
}
|
|
1164
|
-
exports.default = User;
|
|
1165
|
-
User.ETYPE = 'tilmeld_user';
|
|
1166
|
-
User.class = 'User';
|
|
1167
|
-
User.beforeRegisterCallbacks = [];
|
|
1168
|
-
User.afterRegisterCallbacks = [];
|
|
1169
|
-
User.beforeLoginCallbacks = [];
|
|
1170
|
-
User.afterLoginCallbacks = [];
|
|
1171
|
-
User.beforeLogoutCallbacks = [];
|
|
1172
|
-
User.afterLogoutCallbacks = [];
|
|
1173
|
-
User.DEFAULT_CLIENT_ENABLED_METHODS = [
|
|
1174
|
-
'$checkUsername',
|
|
1175
|
-
'$checkEmail',
|
|
1176
|
-
'$checkPhone',
|
|
1177
|
-
'$getAvatar',
|
|
1178
|
-
'$register',
|
|
1179
|
-
];
|
|
1180
|
-
User.DEFAULT_PRIVATE_DATA = [
|
|
1181
|
-
'email',
|
|
1182
|
-
'originalEmail',
|
|
1183
|
-
'phone',
|
|
1184
|
-
'group',
|
|
1185
|
-
'groups',
|
|
1186
|
-
'abilities',
|
|
1187
|
-
'inheritAbilities',
|
|
1188
|
-
'recoverSecret',
|
|
1189
|
-
'recoverSecretDate',
|
|
1190
|
-
'password',
|
|
1191
|
-
'salt',
|
|
1192
|
-
'secret',
|
|
1193
|
-
'newEmailAddress',
|
|
1194
|
-
'newEmailSecret',
|
|
1195
|
-
'cancelEmailAddress',
|
|
1196
|
-
'cancelEmailSecret',
|
|
1197
|
-
'emailChangeDate',
|
|
1198
|
-
];
|
|
1199
|
-
User.DEFAULT_ALLOWLIST_DATA = [];
|
|
1200
|
-
User.clientEnabledStaticMethods = [
|
|
1201
|
-
'current',
|
|
1202
|
-
'loginUser',
|
|
1203
|
-
'sendRecovery',
|
|
1204
|
-
'recover',
|
|
1205
|
-
'getClientConfig',
|
|
1206
|
-
];
|
|
1207
|
-
User.searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
|
|
1208
2197
|
//# sourceMappingURL=User.js.map
|