@nymphjs/tilmeld 1.0.0-beta.9 → 1.0.0-beta.90
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 +408 -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 +140 -15
- package/dist/Group.js +466 -191
- 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 +239 -12
- package/dist/Tilmeld.js +634 -254
- 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 +365 -22
- package/dist/User.js +1265 -364
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +132 -13
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +287 -7
- package/dist/conf/d.js +1 -2
- package/dist/conf/defaults.d.ts +1 -1
- package/dist/conf/defaults.js +188 -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 +478 -226
- package/src/Tilmeld.ts +418 -254
- package/src/Tilmeld.types.ts +2 -2
- package/src/User.test.ts +163 -6
- package/src/User.ts +1428 -444
- package/src/conf/d.ts +63 -17
- package/src/conf/defaults.ts +162 -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/src/User.ts
CHANGED
|
@@ -1,46 +1,60 @@
|
|
|
1
|
-
import
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import type {
|
|
3
|
+
Nymph,
|
|
2
4
|
EntityData,
|
|
3
5
|
EntityJson,
|
|
4
6
|
EntityPatch,
|
|
5
7
|
Selector,
|
|
6
8
|
SerializedEntityData,
|
|
7
9
|
} from '@nymphjs/nymph';
|
|
8
|
-
import { EmailOptions } from 'email-templates';
|
|
9
|
-
import strtotime from 'locutus/php/datetime/strtotime';
|
|
10
|
-
import Base64 from 'crypto-js/enc-base64';
|
|
11
|
-
import sha256 from 'crypto-js/sha256';
|
|
12
|
-
import md5 from 'crypto-js/md5';
|
|
13
|
-
import { difference } from 'lodash';
|
|
14
10
|
import { humanSecret, nanoid } from '@nymphjs/guid';
|
|
15
|
-
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
11
|
+
import type { EmailOptions } from 'email-templates';
|
|
12
|
+
import strtotime from 'locutus/php/datetime/strtotime.js';
|
|
13
|
+
import { difference, xor } from 'lodash-es';
|
|
14
|
+
import { TOTP, Secret } from 'otpauth';
|
|
15
|
+
import { toDataURL } from 'qrcode';
|
|
16
|
+
import { splitn } from '@sciactive/splitn';
|
|
17
|
+
|
|
18
|
+
import { enforceTilmeld } from './enforceTilmeld.js';
|
|
19
|
+
import AbleObject from './AbleObject.js';
|
|
20
|
+
import GroupClass from './Group.js';
|
|
21
|
+
import type { GroupData } from './Group.js';
|
|
19
22
|
import {
|
|
23
|
+
AccessControlError,
|
|
20
24
|
BadDataError,
|
|
21
25
|
BadEmailError,
|
|
22
26
|
BadUsernameError,
|
|
23
27
|
EmailChangeRateLimitExceededError,
|
|
24
|
-
} from './errors';
|
|
28
|
+
} from './errors/index.js';
|
|
25
29
|
|
|
26
30
|
export type EventType =
|
|
31
|
+
| 'checkUsername'
|
|
27
32
|
| 'beforeRegister'
|
|
28
33
|
| 'afterRegister'
|
|
29
34
|
| 'beforeLogin'
|
|
30
35
|
| 'afterLogin'
|
|
31
36
|
| 'beforeLogout'
|
|
32
37
|
| 'afterLogout';
|
|
38
|
+
/**
|
|
39
|
+
* This is run when the user has entered an otherwise valid username into the
|
|
40
|
+
* signup form. It should return a result, which when false will stop the
|
|
41
|
+
* process and return the included message, disallowing the username.
|
|
42
|
+
*/
|
|
43
|
+
export type TilmeldCheckUsernameCallback = (
|
|
44
|
+
user: User & UserData,
|
|
45
|
+
data: { username: string },
|
|
46
|
+
) => Promise<{ result: boolean; message?: string }>;
|
|
33
47
|
/**
|
|
34
48
|
* Theses are run before the user data checks, so the only checks before are
|
|
35
49
|
* whether registration is allowed and whether the user is already registered.
|
|
36
50
|
*/
|
|
37
51
|
export type TilmeldBeforeRegisterCallback = (
|
|
38
52
|
user: User & UserData,
|
|
39
|
-
data: { password: string; additionalData?: { [k: string]: any } }
|
|
53
|
+
data: { password: string; additionalData?: { [k: string]: any } },
|
|
40
54
|
) => Promise<void>;
|
|
41
55
|
export type TilmeldAfterRegisterCallback = (
|
|
42
56
|
user: User & UserData,
|
|
43
|
-
result: { loggedin: boolean; message: string }
|
|
57
|
+
result: { loggedin: boolean; message: string },
|
|
44
58
|
) => Promise<void>;
|
|
45
59
|
/**
|
|
46
60
|
* These are run after the authentication checks, but before the login action.
|
|
@@ -51,20 +65,20 @@ export type TilmeldBeforeLoginCallback = (
|
|
|
51
65
|
username: string;
|
|
52
66
|
password: string;
|
|
53
67
|
additionalData?: { [k: string]: any };
|
|
54
|
-
}
|
|
68
|
+
},
|
|
55
69
|
) => Promise<void>;
|
|
56
70
|
/**
|
|
57
71
|
* This is run before the transaction is committed, and you can perform
|
|
58
72
|
* additional functions on the transaction, which is available in `user.$nymph`.
|
|
59
73
|
*/
|
|
60
74
|
export type TilmeldAfterLoginCallback = (
|
|
61
|
-
user: User & UserData
|
|
75
|
+
user: User & UserData,
|
|
62
76
|
) => Promise<void>;
|
|
63
77
|
export type TilmeldBeforeLogoutCallback = (
|
|
64
|
-
user: User & UserData
|
|
78
|
+
user: User & UserData,
|
|
65
79
|
) => Promise<void>;
|
|
66
80
|
export type TilmeldAfterLogoutCallback = (
|
|
67
|
-
user: User & UserData
|
|
81
|
+
user: User & UserData,
|
|
68
82
|
) => Promise<void>;
|
|
69
83
|
|
|
70
84
|
export type UserData = {
|
|
@@ -108,11 +122,11 @@ export type UserData = {
|
|
|
108
122
|
/**
|
|
109
123
|
* The user's primary group.
|
|
110
124
|
*/
|
|
111
|
-
group?:
|
|
125
|
+
group?: GroupClass & GroupData;
|
|
112
126
|
/**
|
|
113
127
|
* The user's secondary groups.
|
|
114
128
|
*/
|
|
115
|
-
groups?: (
|
|
129
|
+
groups?: (GroupClass & GroupData)[];
|
|
116
130
|
/**
|
|
117
131
|
* Whether the user should inherit the abilities of his groups.
|
|
118
132
|
*/
|
|
@@ -135,7 +149,8 @@ export type UserData = {
|
|
|
135
149
|
*/
|
|
136
150
|
newEmailSecret?: string;
|
|
137
151
|
/**
|
|
138
|
-
*
|
|
152
|
+
* If the user has changed their email address, this is the new one, awaiting
|
|
153
|
+
* verification.
|
|
139
154
|
*/
|
|
140
155
|
newEmailAddress?: string;
|
|
141
156
|
/**
|
|
@@ -167,6 +182,15 @@ export type UserData = {
|
|
|
167
182
|
* database.
|
|
168
183
|
*/
|
|
169
184
|
passwordTemp?: string;
|
|
185
|
+
/**
|
|
186
|
+
* If set, this timestamp is the cutoff point for JWT issue dates. Any token
|
|
187
|
+
* issued before this date will not authenticate the user.
|
|
188
|
+
*/
|
|
189
|
+
revokeTokenDate?: number;
|
|
190
|
+
/**
|
|
191
|
+
* Two factor auth secret.
|
|
192
|
+
*/
|
|
193
|
+
totpSecret?: string;
|
|
170
194
|
};
|
|
171
195
|
|
|
172
196
|
/**
|
|
@@ -182,9 +206,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
182
206
|
/**
|
|
183
207
|
* The instance of Tilmeld to use for queries.
|
|
184
208
|
*/
|
|
185
|
-
static tilmeld: Tilmeld;
|
|
186
209
|
static ETYPE = 'tilmeld_user';
|
|
187
210
|
static class = 'User';
|
|
211
|
+
private static checkUsernameCallbacks: TilmeldCheckUsernameCallback[] = [];
|
|
188
212
|
private static beforeRegisterCallbacks: TilmeldBeforeRegisterCallback[] = [];
|
|
189
213
|
private static afterRegisterCallbacks: TilmeldAfterRegisterCallback[] = [];
|
|
190
214
|
private static beforeLoginCallbacks: TilmeldBeforeLoginCallback[] = [];
|
|
@@ -200,23 +224,33 @@ export default class User extends AbleObject<UserData> {
|
|
|
200
224
|
'$register',
|
|
201
225
|
];
|
|
202
226
|
private static DEFAULT_PRIVATE_DATA = [
|
|
227
|
+
// The things that are commented are there to show what is _not_ private.
|
|
228
|
+
'abilities',
|
|
229
|
+
// 'username',
|
|
230
|
+
// 'nameFirst',
|
|
231
|
+
// 'nameMiddle',
|
|
232
|
+
// 'nameLast',
|
|
233
|
+
// 'name',
|
|
203
234
|
'email',
|
|
204
|
-
'
|
|
235
|
+
// 'avatar',
|
|
205
236
|
'phone',
|
|
206
237
|
'group',
|
|
207
238
|
'groups',
|
|
208
|
-
'abilities',
|
|
209
239
|
'inheritAbilities',
|
|
210
|
-
'
|
|
211
|
-
'recoverSecretDate',
|
|
212
|
-
'password',
|
|
213
|
-
'salt',
|
|
240
|
+
'enabled',
|
|
214
241
|
'secret',
|
|
215
|
-
'
|
|
242
|
+
'emailChangeDate',
|
|
216
243
|
'newEmailSecret',
|
|
217
|
-
'
|
|
244
|
+
'newEmailAddress',
|
|
218
245
|
'cancelEmailSecret',
|
|
219
|
-
'
|
|
246
|
+
'cancelEmailAddress',
|
|
247
|
+
'recoverSecret',
|
|
248
|
+
'recoverSecretDate',
|
|
249
|
+
'salt',
|
|
250
|
+
'password',
|
|
251
|
+
'passwordTemp',
|
|
252
|
+
'revokeTokenDate',
|
|
253
|
+
'totpSecret',
|
|
220
254
|
];
|
|
221
255
|
private static DEFAULT_ALLOWLIST_DATA: string[] = [];
|
|
222
256
|
|
|
@@ -227,6 +261,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
227
261
|
'sendRecovery',
|
|
228
262
|
'recover',
|
|
229
263
|
'getClientConfig',
|
|
264
|
+
'getDomainUsers',
|
|
230
265
|
];
|
|
231
266
|
protected $privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
232
267
|
public static searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
|
|
@@ -241,13 +276,17 @@ export default class User extends AbleObject<UserData> {
|
|
|
241
276
|
*/
|
|
242
277
|
private $gatekeeperCache?: { [k: string]: true };
|
|
243
278
|
/**
|
|
244
|
-
* This
|
|
279
|
+
* This should only be used by the backend.
|
|
245
280
|
*/
|
|
246
281
|
private $skipAcWhenSaving = false;
|
|
282
|
+
/**
|
|
283
|
+
* This should only be used by the backend.
|
|
284
|
+
*/
|
|
285
|
+
private $skipAcWhenDeleting = false;
|
|
247
286
|
/**
|
|
248
287
|
* The user's group descendants.
|
|
249
288
|
*/
|
|
250
|
-
private $descendantGroups?: (
|
|
289
|
+
private $descendantGroups?: (GroupClass & GroupData)[];
|
|
251
290
|
/**
|
|
252
291
|
* Used to save the current email address to send verification if it changes
|
|
253
292
|
* from the frontend.
|
|
@@ -256,10 +295,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
256
295
|
* verification, don't set this.
|
|
257
296
|
*/
|
|
258
297
|
public $originalEmail?: string;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Used to save the current username for domain admin permissions.
|
|
300
|
+
*/
|
|
301
|
+
public $originalUsername?: string;
|
|
263
302
|
|
|
264
303
|
static async factoryUsername(username?: string): Promise<User & UserData> {
|
|
265
304
|
const entity = new this();
|
|
@@ -271,7 +310,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
271
310
|
{
|
|
272
311
|
type: '&',
|
|
273
312
|
ilike: ['username', username.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
274
|
-
}
|
|
313
|
+
},
|
|
275
314
|
);
|
|
276
315
|
if (entity != null) {
|
|
277
316
|
return entity;
|
|
@@ -280,34 +319,18 @@ export default class User extends AbleObject<UserData> {
|
|
|
280
319
|
return entity;
|
|
281
320
|
}
|
|
282
321
|
|
|
283
|
-
static factorySync(guid?: string): User & UserData {
|
|
284
|
-
return super.factorySync(guid) as User & UserData;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
constructor(guid?: string) {
|
|
288
|
-
super(guid);
|
|
289
|
-
|
|
290
|
-
if (this.guid == null) {
|
|
291
|
-
// Defaults.
|
|
292
|
-
this.$data.enabled = true;
|
|
293
|
-
this.$data.abilities = [];
|
|
294
|
-
this.$data.groups = [];
|
|
295
|
-
this.$data.inheritAbilities = true;
|
|
296
|
-
this.$updateDataProtection();
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
322
|
public static current(returnObjectIfNotExist: true): User & UserData;
|
|
301
323
|
public static current(
|
|
302
|
-
returnObjectIfNotExist?: false
|
|
324
|
+
returnObjectIfNotExist?: false,
|
|
303
325
|
): (User & UserData) | null;
|
|
304
326
|
public static current(
|
|
305
|
-
returnObjectIfNotExist?: boolean
|
|
327
|
+
returnObjectIfNotExist?: boolean,
|
|
306
328
|
): (User & UserData) | null {
|
|
307
|
-
|
|
329
|
+
const tilmeld = enforceTilmeld(this);
|
|
330
|
+
if (tilmeld.currentUser == null) {
|
|
308
331
|
return returnObjectIfNotExist ? this.factorySync() : null;
|
|
309
332
|
}
|
|
310
|
-
return
|
|
333
|
+
return tilmeld.currentUser;
|
|
311
334
|
}
|
|
312
335
|
|
|
313
336
|
/**
|
|
@@ -320,7 +343,8 @@ export default class User extends AbleObject<UserData> {
|
|
|
320
343
|
recoveryType: 'username' | 'password';
|
|
321
344
|
account: string;
|
|
322
345
|
}): Promise<{ result: boolean; message: string }> {
|
|
323
|
-
|
|
346
|
+
const tilmeld = enforceTilmeld(this);
|
|
347
|
+
if (!tilmeld.config.pwRecovery) {
|
|
324
348
|
return {
|
|
325
349
|
result: false,
|
|
326
350
|
message: 'Account recovery is not allowed.',
|
|
@@ -330,18 +354,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
330
354
|
let user: User & UserData;
|
|
331
355
|
const options: EmailOptions = {};
|
|
332
356
|
|
|
333
|
-
if (
|
|
334
|
-
!this.tilmeld.config.emailUsernames &&
|
|
335
|
-
data.recoveryType === 'username'
|
|
336
|
-
) {
|
|
357
|
+
if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
|
|
337
358
|
// Create a username recovery email.
|
|
338
359
|
|
|
339
360
|
const getUser = await this.nymph.getEntity(
|
|
340
|
-
{ class:
|
|
361
|
+
{ class: tilmeld.User, skipAc: true },
|
|
341
362
|
{
|
|
342
363
|
type: '&',
|
|
343
364
|
ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
344
|
-
}
|
|
365
|
+
},
|
|
345
366
|
);
|
|
346
367
|
|
|
347
368
|
if (getUser == null) {
|
|
@@ -364,7 +385,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
364
385
|
} else if (data.recoveryType === 'password') {
|
|
365
386
|
// Create a password recovery email.
|
|
366
387
|
|
|
367
|
-
const getUser = await
|
|
388
|
+
const getUser = await tilmeld.User.factoryUsername(data.account);
|
|
368
389
|
|
|
369
390
|
if (getUser.guid == null) {
|
|
370
391
|
return {
|
|
@@ -391,14 +412,19 @@ export default class User extends AbleObject<UserData> {
|
|
|
391
412
|
};
|
|
392
413
|
options.locals = {
|
|
393
414
|
recoverCode: getUser.recoverSecret,
|
|
394
|
-
timeLimit:
|
|
415
|
+
timeLimit: tilmeld.config.pwRecoveryTimeLimit,
|
|
395
416
|
};
|
|
396
417
|
} else {
|
|
397
418
|
return { result: false, message: 'Invalid recovery type.' };
|
|
398
419
|
}
|
|
399
420
|
|
|
421
|
+
this.nymph.config.debugLog(
|
|
422
|
+
'tilmeld',
|
|
423
|
+
`User account recovery (${options.template}) request for "${user.username}".`,
|
|
424
|
+
);
|
|
425
|
+
|
|
400
426
|
// Send the email.
|
|
401
|
-
if (await
|
|
427
|
+
if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
|
|
402
428
|
return {
|
|
403
429
|
result: true,
|
|
404
430
|
message:
|
|
@@ -421,22 +447,23 @@ export default class User extends AbleObject<UserData> {
|
|
|
421
447
|
secret: string;
|
|
422
448
|
password: string;
|
|
423
449
|
}): Promise<{ result: boolean; message: string }> {
|
|
424
|
-
|
|
450
|
+
const tilmeld = enforceTilmeld(this);
|
|
451
|
+
if (!tilmeld.config.pwRecovery) {
|
|
425
452
|
return {
|
|
426
453
|
result: false,
|
|
427
454
|
message: 'Account recovery is not allowed.',
|
|
428
455
|
};
|
|
429
456
|
}
|
|
430
457
|
|
|
431
|
-
const user = await
|
|
458
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
432
459
|
|
|
433
460
|
if (
|
|
434
461
|
user.guid == null ||
|
|
435
462
|
user.recoverSecret == null ||
|
|
436
463
|
data.secret !== user.recoverSecret ||
|
|
437
464
|
strtotime(
|
|
438
|
-
'+' +
|
|
439
|
-
Math.floor((user.recoverSecretDate ?? 0) / 1000)
|
|
465
|
+
'+' + tilmeld.config.pwRecoveryTimeLimit,
|
|
466
|
+
Math.floor((user.recoverSecretDate ?? 0) / 1000),
|
|
440
467
|
) *
|
|
441
468
|
1000 <
|
|
442
469
|
Date.now()
|
|
@@ -451,6 +478,11 @@ export default class User extends AbleObject<UserData> {
|
|
|
451
478
|
return { result: false, message: 'Password cannot be empty.' };
|
|
452
479
|
}
|
|
453
480
|
|
|
481
|
+
this.nymph.config.debugLog(
|
|
482
|
+
'tilmeld',
|
|
483
|
+
`User account recovery for "${user.username}".`,
|
|
484
|
+
);
|
|
485
|
+
|
|
454
486
|
user.$password(data.password);
|
|
455
487
|
delete user.recoverSecret;
|
|
456
488
|
delete user.recoverSecretDate;
|
|
@@ -466,27 +498,31 @@ export default class User extends AbleObject<UserData> {
|
|
|
466
498
|
}
|
|
467
499
|
|
|
468
500
|
public static getClientConfig() {
|
|
501
|
+
const tilmeld = enforceTilmeld(this);
|
|
469
502
|
return {
|
|
470
|
-
regFields:
|
|
471
|
-
userFields:
|
|
472
|
-
emailUsernames:
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
503
|
+
regFields: tilmeld.config.regFields,
|
|
504
|
+
userFields: tilmeld.config.userFields,
|
|
505
|
+
emailUsernames: tilmeld.config.emailUsernames,
|
|
506
|
+
domainSupport: tilmeld.config.domainSupport,
|
|
507
|
+
allowRegistration: tilmeld.config.allowRegistration,
|
|
508
|
+
allowUsernameChange: tilmeld.config.allowUsernameChange,
|
|
509
|
+
pwRecovery: tilmeld.config.pwRecovery,
|
|
510
|
+
verifyEmail: tilmeld.config.verifyEmail,
|
|
511
|
+
unverifiedAccess: tilmeld.config.unverifiedAccess,
|
|
478
512
|
};
|
|
479
513
|
}
|
|
480
514
|
|
|
481
515
|
public static async loginUser(data: {
|
|
482
516
|
username: string;
|
|
483
517
|
password: string;
|
|
518
|
+
code?: string;
|
|
484
519
|
additionalData?: { [k: string]: any };
|
|
485
520
|
}) {
|
|
521
|
+
const tilmeld = enforceTilmeld(this);
|
|
486
522
|
if (!('username' in data) || !data.username.length) {
|
|
487
523
|
return { result: false, message: 'Incorrect login/password.' };
|
|
488
524
|
}
|
|
489
|
-
const user = await
|
|
525
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
490
526
|
const result: { result: boolean; message: string; user?: User & UserData } =
|
|
491
527
|
await user.$login(data);
|
|
492
528
|
if (result.result) {
|
|
@@ -496,25 +532,149 @@ export default class User extends AbleObject<UserData> {
|
|
|
496
532
|
return result;
|
|
497
533
|
}
|
|
498
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Get the users in a domain. This is only accessible to the domain's admins.
|
|
537
|
+
*/
|
|
538
|
+
public static async getDomainUsers(
|
|
539
|
+
domain: string,
|
|
540
|
+
options?: {
|
|
541
|
+
limit?: number;
|
|
542
|
+
offset?: number;
|
|
543
|
+
sort?: string;
|
|
544
|
+
reverse?: boolean;
|
|
545
|
+
},
|
|
546
|
+
) {
|
|
547
|
+
const tilmeld = enforceTilmeld(this);
|
|
548
|
+
|
|
549
|
+
if (!tilmeld.config.domainSupport) {
|
|
550
|
+
throw new AccessControlError('Domain support is not enabled.');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!domain) {
|
|
554
|
+
throw new BadDataError('Domain is required.');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
domain = domain.toLowerCase();
|
|
558
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
559
|
+
throw new BadDataError(tilmeld.config.validDomainRegexNotice);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
563
|
+
throw new AccessControlError(
|
|
564
|
+
'You do not have the authority to manage this domain.',
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return await this.nymph.getEntities(
|
|
569
|
+
{
|
|
570
|
+
class: this.nymph.getEntityClass(User),
|
|
571
|
+
...(options?.limit != null
|
|
572
|
+
? {
|
|
573
|
+
limit: options.limit,
|
|
574
|
+
}
|
|
575
|
+
: {}),
|
|
576
|
+
...(options?.offset != null
|
|
577
|
+
? {
|
|
578
|
+
offset: options.offset,
|
|
579
|
+
}
|
|
580
|
+
: {}),
|
|
581
|
+
...(options?.sort != null
|
|
582
|
+
? {
|
|
583
|
+
sort: options.sort,
|
|
584
|
+
}
|
|
585
|
+
: {}),
|
|
586
|
+
...(options?.reverse != null
|
|
587
|
+
? {
|
|
588
|
+
reverse: options.reverse,
|
|
589
|
+
}
|
|
590
|
+
: {}),
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
type: '&',
|
|
594
|
+
like: ['username', `%@${domain.replace(/([\\%_])/g, (s) => `\\${s}`)}`],
|
|
595
|
+
},
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
constructor() {
|
|
600
|
+
super();
|
|
601
|
+
|
|
602
|
+
// Defaults.
|
|
603
|
+
this.$data.enabled = true;
|
|
604
|
+
this.$data.abilities = [];
|
|
605
|
+
this.$data.groups = [];
|
|
606
|
+
this.$data.inheritAbilities = true;
|
|
607
|
+
this.$updateDataProtection();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
$setNymph(nymph: Nymph) {
|
|
611
|
+
this.$nymph = nymph;
|
|
612
|
+
if (!this.$asleep()) {
|
|
613
|
+
if (this.$data.group && this.$data.group.$nymph !== nymph) {
|
|
614
|
+
this.$data.group.$setNymph(nymph);
|
|
615
|
+
}
|
|
616
|
+
for (const group of this.$data.groups ?? []) {
|
|
617
|
+
if (group && group.$nymph !== nymph) {
|
|
618
|
+
group.$setNymph(nymph);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async $getUniques(): Promise<string[]> {
|
|
625
|
+
const tilmeld = enforceTilmeld(this);
|
|
626
|
+
const uniques = [`u:${this.$data.username}`];
|
|
627
|
+
if (
|
|
628
|
+
!tilmeld.config.emailUsernames &&
|
|
629
|
+
tilmeld.config.userFields.includes('email')
|
|
630
|
+
) {
|
|
631
|
+
uniques.push(`e:${this.$data.email}`);
|
|
632
|
+
|
|
633
|
+
if (this.$data.newEmailAddress != null) {
|
|
634
|
+
uniques.push(`e:${this.$data.newEmailAddress}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return uniques;
|
|
638
|
+
}
|
|
639
|
+
|
|
499
640
|
public async $login(data: {
|
|
500
641
|
username: string;
|
|
501
642
|
password: string;
|
|
643
|
+
code?: string;
|
|
502
644
|
additionalData?: { [k: string]: any };
|
|
503
645
|
}) {
|
|
504
|
-
const tilmeld = this
|
|
646
|
+
const tilmeld = enforceTilmeld(this);
|
|
505
647
|
if (this.guid == null) {
|
|
506
648
|
return { result: false, message: 'Incorrect login/password.' };
|
|
507
649
|
}
|
|
508
650
|
if (!this.$data.enabled) {
|
|
509
651
|
return { result: false, message: 'This user is disabled.' };
|
|
510
652
|
}
|
|
511
|
-
if (this.$gatekeeper()) {
|
|
653
|
+
if (await this.$gatekeeper()) {
|
|
512
654
|
return { result: true, message: 'You are already logged in.' };
|
|
513
655
|
}
|
|
514
656
|
if (!this.$checkPassword(data.password)) {
|
|
515
657
|
return { result: false, message: 'Incorrect login/password.' };
|
|
516
658
|
}
|
|
517
659
|
|
|
660
|
+
if (this.$data.totpSecret != null) {
|
|
661
|
+
if (
|
|
662
|
+
data.code == null ||
|
|
663
|
+
typeof data.code !== 'string' ||
|
|
664
|
+
data.code.length !== 6
|
|
665
|
+
) {
|
|
666
|
+
return {
|
|
667
|
+
result: false,
|
|
668
|
+
needTOTP: true,
|
|
669
|
+
message: 'You need to provide a 2FA code.',
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (!this.$checkTOTPCode(data.code)) {
|
|
674
|
+
return { result: false, message: 'Incorrect 2FA code.' };
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
518
678
|
try {
|
|
519
679
|
for (let callback of (this.constructor as typeof User)
|
|
520
680
|
.beforeLoginCallbacks) {
|
|
@@ -529,8 +689,90 @@ export default class User extends AbleObject<UserData> {
|
|
|
529
689
|
};
|
|
530
690
|
}
|
|
531
691
|
|
|
692
|
+
this.$nymph.config.debugLog(
|
|
693
|
+
'tilmeld',
|
|
694
|
+
`User login for "${this.$data.username}".`,
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
// Authentication was successful, attempt to login.
|
|
698
|
+
if (!(await tilmeld.login(this, true))) {
|
|
699
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
for (let callback of (this.constructor as typeof User)
|
|
703
|
+
.afterLoginCallbacks) {
|
|
704
|
+
if (callback) {
|
|
705
|
+
await callback(this);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Login was successful.
|
|
710
|
+
return { result: true, message: 'You are logged in.' };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
public async $switchUser(data?: { additionalData?: { [k: string]: any } }) {
|
|
714
|
+
const tilmeld = enforceTilmeld(this);
|
|
715
|
+
if (this.guid == null) {
|
|
716
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
let user = tilmeld.User.current();
|
|
720
|
+
|
|
721
|
+
if (!user || user.guid == null) {
|
|
722
|
+
return { result: false, message: "You're not logged in." };
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
726
|
+
|
|
727
|
+
if (isCurrentUser) {
|
|
728
|
+
return { result: false, message: "You can't switch to yourself." };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const toAbilities = this.$data.abilities || [];
|
|
732
|
+
const fromAbilities = user.abilities || [];
|
|
733
|
+
|
|
734
|
+
if (
|
|
735
|
+
!fromAbilities.includes('tilmeld/switch') &&
|
|
736
|
+
!fromAbilities.includes('system/admin')
|
|
737
|
+
) {
|
|
738
|
+
return {
|
|
739
|
+
result: false,
|
|
740
|
+
message: "You don't have permission to switch users.",
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (
|
|
745
|
+
toAbilities.includes('system/admin') ||
|
|
746
|
+
toAbilities.includes('tilmeld/admin')
|
|
747
|
+
) {
|
|
748
|
+
return { result: false, message: "You can't switch to an admin." };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
try {
|
|
752
|
+
for (let callback of (this.constructor as typeof User)
|
|
753
|
+
.beforeLoginCallbacks) {
|
|
754
|
+
if (callback) {
|
|
755
|
+
await callback(this, {
|
|
756
|
+
username: this.$data.username || '',
|
|
757
|
+
password: '',
|
|
758
|
+
...(data || {}),
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
} catch (e: any) {
|
|
763
|
+
return {
|
|
764
|
+
result: false,
|
|
765
|
+
message: e.message,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
this.$nymph.config.debugLog(
|
|
770
|
+
'tilmeld',
|
|
771
|
+
`User login switch for "${user.username}" to "${this.$data.username}".`,
|
|
772
|
+
);
|
|
773
|
+
|
|
532
774
|
// Authentication was successful, attempt to login.
|
|
533
|
-
if (!tilmeld.
|
|
775
|
+
if (!(await tilmeld.loginSwitch(this, true))) {
|
|
534
776
|
return { result: false, message: 'Incorrect login/password.' };
|
|
535
777
|
}
|
|
536
778
|
|
|
@@ -550,7 +792,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
550
792
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
551
793
|
*/
|
|
552
794
|
public async $logout() {
|
|
553
|
-
const tilmeld = this
|
|
795
|
+
const tilmeld = enforceTilmeld(this);
|
|
554
796
|
|
|
555
797
|
try {
|
|
556
798
|
for (let callback of (this.constructor as typeof User)
|
|
@@ -566,7 +808,12 @@ export default class User extends AbleObject<UserData> {
|
|
|
566
808
|
};
|
|
567
809
|
}
|
|
568
810
|
|
|
569
|
-
|
|
811
|
+
this.$nymph.config.debugLog(
|
|
812
|
+
'tilmeld',
|
|
813
|
+
`User logout for "${this.$data.username}".`,
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
await tilmeld.logout();
|
|
570
817
|
|
|
571
818
|
for (let callback of (this.constructor as typeof User)
|
|
572
819
|
.afterLogoutCallbacks) {
|
|
@@ -587,89 +834,176 @@ export default class User extends AbleObject<UserData> {
|
|
|
587
834
|
}
|
|
588
835
|
return (
|
|
589
836
|
'https://secure.gravatar.com/avatar/' +
|
|
590
|
-
|
|
837
|
+
crypto
|
|
838
|
+
.createHash('sha256')
|
|
839
|
+
.update(this.$data.email.trim().toLowerCase())
|
|
840
|
+
.digest('hex')
|
|
841
|
+
.toLowerCase() +
|
|
591
842
|
'?d=identicon&s=40'
|
|
592
843
|
);
|
|
593
844
|
}
|
|
594
845
|
|
|
846
|
+
public $getGid() {
|
|
847
|
+
if (this.$data.group) {
|
|
848
|
+
return this.$data.group.guid;
|
|
849
|
+
}
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
public $getGids() {
|
|
854
|
+
if (this.$data.groups) {
|
|
855
|
+
return this.$data.groups
|
|
856
|
+
.map((group) => group.guid)
|
|
857
|
+
.filter((guid) => guid != null) as string[];
|
|
858
|
+
}
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
|
|
595
862
|
/**
|
|
596
863
|
* Get the user's group descendants.
|
|
597
864
|
*/
|
|
598
|
-
public async $getDescendantGroups(): Promise<(
|
|
865
|
+
public async $getDescendantGroups(): Promise<(GroupClass & GroupData)[]> {
|
|
599
866
|
if (this.$descendantGroups == null) {
|
|
600
867
|
this.$descendantGroups = [];
|
|
601
868
|
if (this.$data.group != null) {
|
|
869
|
+
await this.$data.group.$wake();
|
|
602
870
|
this.$descendantGroups = await this.$data.group.$getDescendants();
|
|
603
871
|
}
|
|
872
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
604
873
|
for (let curGroup of this.$data.groups ?? []) {
|
|
605
874
|
this.$descendantGroups = this.$descendantGroups?.concat(
|
|
606
|
-
await curGroup.$getDescendants()
|
|
875
|
+
await curGroup.$getDescendants(),
|
|
607
876
|
);
|
|
608
877
|
}
|
|
609
878
|
}
|
|
610
879
|
return this.$descendantGroups;
|
|
611
880
|
}
|
|
612
881
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (
|
|
618
|
-
this.$
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
882
|
+
public $jsonAcceptData(input: EntityJson, allowConflict = false) {
|
|
883
|
+
const tilmeld = enforceTilmeld(this);
|
|
884
|
+
this.$check();
|
|
885
|
+
|
|
886
|
+
if ('abilities' in input.data) {
|
|
887
|
+
this.$checkIncomingAbilities(input.data.abilities);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// If the user is a new domain user, the data protection needs to be updated
|
|
891
|
+
// before the data is accepted, or else some things won't be accepted
|
|
892
|
+
// correctly (enabled, passwordTemp).
|
|
893
|
+
let reloadDataProtection = false;
|
|
894
|
+
if (tilmeld.config.domainSupport) {
|
|
895
|
+
let [_username, domain] = splitn(input.data.username ?? '', '@', 2);
|
|
896
|
+
// Lowercase the domain part.
|
|
897
|
+
if (domain) {
|
|
898
|
+
domain = domain.toLowerCase();
|
|
626
899
|
}
|
|
900
|
+
const user = tilmeld.User.current();
|
|
901
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
902
|
+
// While loading, accessing abilities on the current user will infinitely
|
|
903
|
+
// loop, but if this is the current user, we can use its abilities.
|
|
904
|
+
const abilities =
|
|
905
|
+
(isCurrentUser || user == null
|
|
906
|
+
? this.$data.abilities
|
|
907
|
+
: user?.abilities) ?? [];
|
|
908
|
+
const isNewUser = input.guid == null;
|
|
909
|
+
const isDomainUser =
|
|
910
|
+
!isCurrentUser &&
|
|
911
|
+
user != null &&
|
|
912
|
+
domain &&
|
|
913
|
+
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
914
|
+
|
|
915
|
+
reloadDataProtection = !!(isNewUser && isDomainUser);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
super.$jsonAcceptData(input, allowConflict);
|
|
919
|
+
|
|
920
|
+
if (reloadDataProtection) {
|
|
921
|
+
// Load it again after data protection is run.
|
|
922
|
+
this.$updateDataProtection();
|
|
923
|
+
super.$jsonAcceptData(input, allowConflict);
|
|
627
924
|
}
|
|
628
|
-
return this.$descendantGroups;
|
|
629
925
|
}
|
|
630
926
|
|
|
631
|
-
public $
|
|
632
|
-
|
|
633
|
-
|
|
927
|
+
public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
|
|
928
|
+
this.$check();
|
|
929
|
+
|
|
930
|
+
if ('abilities' in patch.set) {
|
|
931
|
+
this.$checkIncomingAbilities(patch.set.abilities);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
super.$jsonAcceptPatch(patch, allowConflict);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private $checkIncomingAbilities(incoming: any) {
|
|
938
|
+
const tilmeld = enforceTilmeld(this);
|
|
634
939
|
|
|
635
940
|
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
941
|
+
!Array.isArray(incoming) ||
|
|
942
|
+
incoming.find((ability) => typeof ability !== 'string')
|
|
943
|
+
) {
|
|
944
|
+
throw new BadDataError('Abilities must be an array of strings.');
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (
|
|
948
|
+
incoming.includes('system/admin') !==
|
|
949
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
950
|
+
(!tilmeld.currentUser ||
|
|
951
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
641
952
|
) {
|
|
642
953
|
throw new BadDataError(
|
|
643
|
-
"You don't have the authority to
|
|
954
|
+
"You don't have the authority to grant or revoke system/admin.",
|
|
644
955
|
);
|
|
645
956
|
}
|
|
646
957
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
958
|
+
if (
|
|
959
|
+
incoming.includes('tilmeld/admin') !==
|
|
960
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
961
|
+
(!tilmeld.currentUser ||
|
|
962
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
963
|
+
) {
|
|
964
|
+
throw new BadDataError(
|
|
965
|
+
"You don't have the authority to grant or revoke tilmeld/admin.",
|
|
966
|
+
);
|
|
967
|
+
}
|
|
654
968
|
|
|
655
969
|
if (
|
|
656
|
-
'
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
!tilmeld.gatekeeper('system/admin')
|
|
970
|
+
incoming.includes('tilmeld/switch') !==
|
|
971
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
972
|
+
(!tilmeld.currentUser ||
|
|
973
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
661
974
|
) {
|
|
662
975
|
throw new BadDataError(
|
|
663
|
-
"You don't have the authority to
|
|
976
|
+
"You don't have the authority to grant or revoke tilmeld/switch.",
|
|
664
977
|
);
|
|
665
978
|
}
|
|
666
979
|
|
|
667
|
-
|
|
668
|
-
|
|
980
|
+
if (
|
|
981
|
+
xor(
|
|
982
|
+
incoming.filter((ability: string) => ability.startsWith('tilmeld/')),
|
|
983
|
+
this.$data.abilities?.filter((ability: string) =>
|
|
984
|
+
ability.startsWith('tilmeld/'),
|
|
985
|
+
) ?? [],
|
|
986
|
+
).length &&
|
|
987
|
+
(!tilmeld.currentUser ||
|
|
988
|
+
(!tilmeld.currentUser.abilities?.includes('system/admin') &&
|
|
989
|
+
!tilmeld.currentUser.abilities?.includes('tilmeld/admin')))
|
|
990
|
+
) {
|
|
991
|
+
throw new BadDataError(
|
|
992
|
+
"You don't have the authority to grant or revoke Tilmeld abilities.",
|
|
993
|
+
);
|
|
994
|
+
}
|
|
669
995
|
}
|
|
670
996
|
|
|
671
|
-
public $putData(
|
|
672
|
-
|
|
997
|
+
public $putData(
|
|
998
|
+
data: EntityData,
|
|
999
|
+
sdata?: SerializedEntityData,
|
|
1000
|
+
source?: 'server',
|
|
1001
|
+
) {
|
|
1002
|
+
super.$putData(data, sdata, source);
|
|
1003
|
+
if (source === 'server') {
|
|
1004
|
+
this.$originalEmail = this.$data.email;
|
|
1005
|
+
this.$originalUsername = this.$data.username;
|
|
1006
|
+
}
|
|
673
1007
|
this.$updateDataProtection();
|
|
674
1008
|
}
|
|
675
1009
|
|
|
@@ -679,8 +1013,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
679
1013
|
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
680
1014
|
*/
|
|
681
1015
|
public $updateDataProtection(givenUser?: User & UserData) {
|
|
682
|
-
const tilmeld = this
|
|
1016
|
+
const tilmeld = enforceTilmeld(this);
|
|
683
1017
|
let user = givenUser ?? tilmeld.User.current();
|
|
1018
|
+
let [_username, domain] = tilmeld.config.domainSupport
|
|
1019
|
+
? splitn(this.$data.username ?? '', '@', 2)
|
|
1020
|
+
: [this.$data.username, null];
|
|
1021
|
+
// Lowercase the domain part.
|
|
1022
|
+
if (domain) {
|
|
1023
|
+
domain = domain.toLowerCase();
|
|
1024
|
+
}
|
|
684
1025
|
|
|
685
1026
|
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
686
1027
|
this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
@@ -698,53 +1039,165 @@ export default class User extends AbleObject<UserData> {
|
|
|
698
1039
|
? this.$data.abilities
|
|
699
1040
|
: user?.abilities) ?? [];
|
|
700
1041
|
const isNewUser = this.guid == null;
|
|
1042
|
+
const isDomainUser =
|
|
1043
|
+
tilmeld.config.domainSupport &&
|
|
1044
|
+
!isCurrentUser &&
|
|
1045
|
+
user != null &&
|
|
1046
|
+
domain &&
|
|
1047
|
+
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
701
1048
|
|
|
702
1049
|
if (isCurrentUser) {
|
|
703
1050
|
// Users can check to see what abilities they have.
|
|
704
1051
|
this.$clientEnabledMethods.push('$gatekeeper');
|
|
705
1052
|
this.$clientEnabledMethods.push('$changePassword');
|
|
1053
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
706
1054
|
this.$clientEnabledMethods.push('$logout');
|
|
707
1055
|
this.$clientEnabledMethods.push('$sendEmailVerification');
|
|
1056
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
1057
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
1058
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
1059
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (isDomainUser) {
|
|
1063
|
+
// Domain admins can check to see what abilities their users have.
|
|
1064
|
+
this.$clientEnabledMethods.push('$gatekeeper');
|
|
1065
|
+
this.$clientEnabledMethods.push('$changePassword');
|
|
1066
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
1067
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
1068
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
1069
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
1070
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
708
1071
|
}
|
|
709
1072
|
|
|
710
1073
|
if (
|
|
711
1074
|
user != null &&
|
|
712
|
-
(abilities.
|
|
713
|
-
abilities.
|
|
1075
|
+
(abilities.includes('tilmeld/admin') ||
|
|
1076
|
+
abilities.includes('system/admin'))
|
|
714
1077
|
) {
|
|
715
1078
|
// Users who can edit other users can see most of their data.
|
|
716
|
-
this.$privateData = ['password', 'salt'];
|
|
1079
|
+
this.$privateData = ['password', 'salt', 'totpSecret'];
|
|
1080
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
1081
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
717
1082
|
this.$allowlistData = undefined;
|
|
1083
|
+
} else if (isDomainUser) {
|
|
1084
|
+
// Domain admins can see their users' data, and edit some of it.
|
|
1085
|
+
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
1086
|
+
this.$allowlistData.push('username');
|
|
1087
|
+
}
|
|
1088
|
+
this.$allowlistData.push('avatar');
|
|
1089
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
1090
|
+
this.$allowlistData.push('nameFirst');
|
|
1091
|
+
this.$allowlistData.push('nameMiddle');
|
|
1092
|
+
this.$allowlistData.push('nameLast');
|
|
1093
|
+
this.$allowlistData.push('name');
|
|
1094
|
+
}
|
|
1095
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
1096
|
+
this.$allowlistData.push('email');
|
|
1097
|
+
}
|
|
1098
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
1099
|
+
this.$allowlistData.push('phone');
|
|
1100
|
+
}
|
|
1101
|
+
this.$allowlistData.push('enabled');
|
|
1102
|
+
this.$allowlistData.push('passwordTemp');
|
|
1103
|
+
this.$privateData = [
|
|
1104
|
+
// The things commented are there to show what is _not_ private.
|
|
1105
|
+
// 'abilities',
|
|
1106
|
+
// 'username',
|
|
1107
|
+
// 'nameFirst',
|
|
1108
|
+
// 'nameMiddle',
|
|
1109
|
+
// 'nameLast',
|
|
1110
|
+
// 'name',
|
|
1111
|
+
// 'email',
|
|
1112
|
+
// 'avatar',
|
|
1113
|
+
// 'phone',
|
|
1114
|
+
// 'group',
|
|
1115
|
+
// 'groups',
|
|
1116
|
+
// 'inheritAbilities',
|
|
1117
|
+
// 'enabled',
|
|
1118
|
+
'secret',
|
|
1119
|
+
'emailChangeDate',
|
|
1120
|
+
'newEmailSecret',
|
|
1121
|
+
// 'newEmailAddress',
|
|
1122
|
+
'cancelEmailSecret',
|
|
1123
|
+
'cancelEmailAddress',
|
|
1124
|
+
'recoverSecret',
|
|
1125
|
+
'recoverSecretDate',
|
|
1126
|
+
'salt',
|
|
1127
|
+
'password',
|
|
1128
|
+
// 'passwordTemp',
|
|
1129
|
+
// 'revokeTokenDate',
|
|
1130
|
+
'totpSecret',
|
|
1131
|
+
];
|
|
718
1132
|
} else if (isCurrentUser || isNewUser) {
|
|
719
1133
|
// Users can see their own data, and edit some of it.
|
|
720
1134
|
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
721
1135
|
this.$allowlistData.push('username');
|
|
722
1136
|
}
|
|
723
1137
|
this.$allowlistData.push('avatar');
|
|
724
|
-
if (tilmeld.config.userFields.
|
|
1138
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
725
1139
|
this.$allowlistData.push('nameFirst');
|
|
726
1140
|
this.$allowlistData.push('nameMiddle');
|
|
727
1141
|
this.$allowlistData.push('nameLast');
|
|
728
1142
|
this.$allowlistData.push('name');
|
|
729
1143
|
}
|
|
730
|
-
if (tilmeld.config.userFields.
|
|
1144
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
731
1145
|
this.$allowlistData.push('email');
|
|
732
1146
|
}
|
|
733
|
-
if (tilmeld.config.userFields.
|
|
1147
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
734
1148
|
this.$allowlistData.push('phone');
|
|
735
1149
|
}
|
|
736
1150
|
this.$privateData = [
|
|
1151
|
+
// The things commented are there to show what is _not_ private.
|
|
1152
|
+
// 'abilities',
|
|
1153
|
+
// 'username',
|
|
1154
|
+
// 'nameFirst',
|
|
1155
|
+
// 'nameMiddle',
|
|
1156
|
+
// 'nameLast',
|
|
1157
|
+
// 'name',
|
|
1158
|
+
// 'email',
|
|
1159
|
+
// 'avatar',
|
|
1160
|
+
// 'phone',
|
|
1161
|
+
// 'group',
|
|
1162
|
+
// 'groups',
|
|
1163
|
+
// 'inheritAbilities',
|
|
1164
|
+
'enabled',
|
|
737
1165
|
'secret',
|
|
1166
|
+
'emailChangeDate',
|
|
738
1167
|
'newEmailSecret',
|
|
739
|
-
'
|
|
1168
|
+
// 'newEmailAddress',
|
|
740
1169
|
'cancelEmailSecret',
|
|
741
|
-
'
|
|
1170
|
+
'cancelEmailAddress',
|
|
742
1171
|
'recoverSecret',
|
|
743
1172
|
'recoverSecretDate',
|
|
744
|
-
'password',
|
|
745
1173
|
'salt',
|
|
1174
|
+
'password',
|
|
1175
|
+
'passwordTemp',
|
|
1176
|
+
// 'revokeTokenDate',
|
|
1177
|
+
'totpSecret',
|
|
746
1178
|
];
|
|
747
1179
|
}
|
|
1180
|
+
|
|
1181
|
+
if (
|
|
1182
|
+
user != null &&
|
|
1183
|
+
!isCurrentUser &&
|
|
1184
|
+
(abilities.includes('tilmeld/switch') ||
|
|
1185
|
+
abilities.includes('system/admin'))
|
|
1186
|
+
) {
|
|
1187
|
+
// Users with this ability can switch to other users.
|
|
1188
|
+
this.$clientEnabledMethods.push('$switchUser');
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
public $updateGroupDataProtection() {
|
|
1193
|
+
if (this.$data.group != null && !this.$data.group.$asleep()) {
|
|
1194
|
+
this.$data.group.$updateDataProtection();
|
|
1195
|
+
}
|
|
1196
|
+
for (let group of this.$data.groups ?? []) {
|
|
1197
|
+
if (!group.$asleep()) {
|
|
1198
|
+
group.$updateDataProtection();
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
748
1201
|
}
|
|
749
1202
|
|
|
750
1203
|
/**
|
|
@@ -761,35 +1214,43 @@ export default class User extends AbleObject<UserData> {
|
|
|
761
1214
|
* @param ability The ability.
|
|
762
1215
|
* @returns True or false.
|
|
763
1216
|
*/
|
|
764
|
-
public $gatekeeper(ability?: string): boolean {
|
|
765
|
-
|
|
1217
|
+
public async $gatekeeper(ability?: string): Promise<boolean> {
|
|
1218
|
+
await this.$wake();
|
|
1219
|
+
const tilmeld = enforceTilmeld(this);
|
|
766
1220
|
if (ability == null) {
|
|
767
1221
|
return tilmeld.User.current()?.$is(this) ?? false;
|
|
768
1222
|
}
|
|
769
1223
|
// Check the cache to see if we've already checked this user.
|
|
770
1224
|
if (this.$gatekeeperCache == null) {
|
|
771
|
-
|
|
772
|
-
if (this.$data.inheritAbilities) {
|
|
773
|
-
for (let curGroup of this.$data.groups ?? []) {
|
|
774
|
-
if (curGroup.enabled) {
|
|
775
|
-
abilities = abilities.concat(curGroup.abilities ?? []);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
if (
|
|
779
|
-
this.$data.group != null &&
|
|
780
|
-
this.$data.group.cdate != null &&
|
|
781
|
-
this.$data.group.enabled
|
|
782
|
-
) {
|
|
783
|
-
abilities = abilities.concat(this.$data.group.abilities ?? []);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
this.$gatekeeperCache = Object.fromEntries(
|
|
787
|
-
abilities.map((ability) => [ability, true])
|
|
788
|
-
);
|
|
1225
|
+
this.$gatekeeperCache = await this.$getGatekeeperCache();
|
|
789
1226
|
}
|
|
790
1227
|
return (
|
|
791
1228
|
(ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
|
|
792
|
-
!!this.$
|
|
1229
|
+
!!this.$data.abilities?.includes('system/admin')
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Build a gatekeeper cache object.
|
|
1235
|
+
*/
|
|
1236
|
+
public async $getGatekeeperCache() {
|
|
1237
|
+
await this.$wake();
|
|
1238
|
+
let abilities = this.$data.abilities ?? [];
|
|
1239
|
+
if (this.$data.inheritAbilities) {
|
|
1240
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1241
|
+
for (let curGroup of this.$data.groups ?? []) {
|
|
1242
|
+
if (curGroup.enabled) {
|
|
1243
|
+
abilities = abilities.concat(curGroup.abilities ?? []);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
await this.$data.group?.$wake();
|
|
1247
|
+
const group = this.$data.group;
|
|
1248
|
+
if (group != null && group.cdate != null && group.enabled) {
|
|
1249
|
+
abilities = abilities.concat(group.abilities ?? []);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return Object.fromEntries(
|
|
1253
|
+
abilities.map((ability) => [ability, true as true]),
|
|
793
1254
|
);
|
|
794
1255
|
}
|
|
795
1256
|
|
|
@@ -806,7 +1267,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
806
1267
|
* @returns True on success, false on failure.
|
|
807
1268
|
*/
|
|
808
1269
|
public async $sendEmailVerification() {
|
|
809
|
-
const tilmeld = this
|
|
1270
|
+
const tilmeld = enforceTilmeld(this);
|
|
810
1271
|
if (this.guid == null) {
|
|
811
1272
|
return false;
|
|
812
1273
|
}
|
|
@@ -818,7 +1279,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
818
1279
|
|
|
819
1280
|
if (this.$data.secret != null) {
|
|
820
1281
|
const link = `${verifyUrl}?action=verify&id=${encodeURIComponent(
|
|
821
|
-
this.guid
|
|
1282
|
+
this.guid,
|
|
822
1283
|
)}&secret=${encodeURIComponent(this.$data.secret)}`;
|
|
823
1284
|
success =
|
|
824
1285
|
success &&
|
|
@@ -836,13 +1297,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
836
1297
|
verifyLink: link,
|
|
837
1298
|
},
|
|
838
1299
|
},
|
|
839
|
-
this
|
|
1300
|
+
this,
|
|
840
1301
|
));
|
|
841
1302
|
}
|
|
842
1303
|
|
|
843
1304
|
if (this.$data.newEmailSecret != null) {
|
|
844
1305
|
const link = `${verifyUrl}?action=verifychange&id=${encodeURIComponent(
|
|
845
|
-
this.guid
|
|
1306
|
+
this.guid,
|
|
846
1307
|
)}&secret=${encodeURIComponent(this.$data.newEmailSecret)}`;
|
|
847
1308
|
success =
|
|
848
1309
|
success &&
|
|
@@ -862,13 +1323,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
862
1323
|
newEmail: this.$data.newEmailAddress,
|
|
863
1324
|
},
|
|
864
1325
|
},
|
|
865
|
-
this
|
|
1326
|
+
this,
|
|
866
1327
|
));
|
|
867
1328
|
}
|
|
868
1329
|
|
|
869
1330
|
if (this.$data.cancelEmailSecret != null) {
|
|
870
1331
|
const link = `${verifyUrl}?action=cancelchange&id=${encodeURIComponent(
|
|
871
|
-
this.guid
|
|
1332
|
+
this.guid,
|
|
872
1333
|
)}&secret=${encodeURIComponent(this.$data.cancelEmailSecret)}`;
|
|
873
1334
|
success =
|
|
874
1335
|
success &&
|
|
@@ -888,7 +1349,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
888
1349
|
newEmail: this.$data.newEmailAddress ?? this.$data.email,
|
|
889
1350
|
},
|
|
890
1351
|
},
|
|
891
|
-
this
|
|
1352
|
+
this,
|
|
892
1353
|
));
|
|
893
1354
|
}
|
|
894
1355
|
|
|
@@ -901,13 +1362,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
901
1362
|
* @param group The group.
|
|
902
1363
|
* @returns True if the user is already in the group. The resulting array of groups if the user was not.
|
|
903
1364
|
*/
|
|
904
|
-
public $addGroup(group:
|
|
905
|
-
|
|
906
|
-
|
|
1365
|
+
public async $addGroup(group: GroupClass & GroupData) {
|
|
1366
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1367
|
+
let groups = this.$data.groups;
|
|
1368
|
+
if (groups == null) {
|
|
1369
|
+
groups = [];
|
|
907
1370
|
}
|
|
908
|
-
if (!group.$inArray(
|
|
909
|
-
|
|
910
|
-
|
|
1371
|
+
if (!group.$inArray(groups)) {
|
|
1372
|
+
groups.push(group);
|
|
1373
|
+
this.$data.groups = groups;
|
|
1374
|
+
return groups;
|
|
911
1375
|
}
|
|
912
1376
|
return true;
|
|
913
1377
|
}
|
|
@@ -919,17 +1383,23 @@ export default class User extends AbleObject<UserData> {
|
|
|
919
1383
|
* @returns True if the passwords match, otherwise false.
|
|
920
1384
|
*/
|
|
921
1385
|
public $checkPassword(password: string) {
|
|
922
|
-
const tilmeld = this
|
|
1386
|
+
const tilmeld = enforceTilmeld(this);
|
|
923
1387
|
switch (tilmeld.config.pwMethod) {
|
|
924
1388
|
case 'plain':
|
|
925
1389
|
return this.$data.password === password;
|
|
926
1390
|
case 'digest':
|
|
927
|
-
return
|
|
1391
|
+
return (
|
|
1392
|
+
this.$data.password ===
|
|
1393
|
+
crypto.createHash('sha256').update(password).digest('base64')
|
|
1394
|
+
);
|
|
928
1395
|
case 'salt':
|
|
929
1396
|
default:
|
|
930
1397
|
return (
|
|
931
1398
|
this.$data.password ===
|
|
932
|
-
|
|
1399
|
+
crypto
|
|
1400
|
+
.createHash('sha256')
|
|
1401
|
+
.update(password + this.$data.salt)
|
|
1402
|
+
.digest('base64')
|
|
933
1403
|
);
|
|
934
1404
|
}
|
|
935
1405
|
}
|
|
@@ -940,10 +1410,12 @@ export default class User extends AbleObject<UserData> {
|
|
|
940
1410
|
* @param group The group.
|
|
941
1411
|
* @returns True if the user wasn't in the group. The resulting array of groups if the user was.
|
|
942
1412
|
*/
|
|
943
|
-
public $delGroup(group:
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1413
|
+
public async $delGroup(group: GroupClass & GroupData) {
|
|
1414
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1415
|
+
let groups = this.$data.groups || [];
|
|
1416
|
+
if (this.$data.groups != null && group.$inArray(groups)) {
|
|
1417
|
+
const newGroups: (GroupClass & GroupData)[] = [];
|
|
1418
|
+
for (let curGroup of groups) {
|
|
947
1419
|
if (!group.$is(curGroup)) {
|
|
948
1420
|
newGroups.push(curGroup);
|
|
949
1421
|
}
|
|
@@ -960,7 +1432,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
960
1432
|
* @param group The group, or the group's GUID.
|
|
961
1433
|
* @returns True or false.
|
|
962
1434
|
*/
|
|
963
|
-
public $inGroup(group: (
|
|
1435
|
+
public async $inGroup(group: (GroupClass & GroupData) | string) {
|
|
964
1436
|
if (typeof group === 'string') {
|
|
965
1437
|
if (this.$data.group?.guid === group) {
|
|
966
1438
|
return true;
|
|
@@ -983,16 +1455,19 @@ export default class User extends AbleObject<UserData> {
|
|
|
983
1455
|
* @param group The group, or the group's GUID.
|
|
984
1456
|
* @returns True or false.
|
|
985
1457
|
*/
|
|
986
|
-
public $isDescendant(group: (
|
|
1458
|
+
public async $isDescendant(group: (GroupClass & GroupData) | string) {
|
|
987
1459
|
// Check to see if the user is in a descendant group of the given group.
|
|
1460
|
+
await this.$data.group?.$wake();
|
|
988
1461
|
if (
|
|
1462
|
+
this.$data.group != null &&
|
|
989
1463
|
this.$data.group?.cdate != null &&
|
|
990
|
-
this.$data.group
|
|
1464
|
+
(await this.$data.group?.$isDescendant(group))
|
|
991
1465
|
) {
|
|
992
1466
|
return true;
|
|
993
1467
|
}
|
|
1468
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
994
1469
|
for (let curGroup of this.$data.groups ?? []) {
|
|
995
|
-
if (curGroup.$isDescendant(group)) {
|
|
1470
|
+
if (await curGroup.$isDescendant(group)) {
|
|
996
1471
|
return true;
|
|
997
1472
|
}
|
|
998
1473
|
}
|
|
@@ -1008,15 +1483,31 @@ export default class User extends AbleObject<UserData> {
|
|
|
1008
1483
|
public async $changePassword(data: {
|
|
1009
1484
|
newPassword: string;
|
|
1010
1485
|
currentPassword: string;
|
|
1486
|
+
revokeCurrentTokens?: boolean;
|
|
1011
1487
|
}): Promise<{ result: boolean; message: string }> {
|
|
1488
|
+
this.$nymph.config.debugLog(
|
|
1489
|
+
'tilmeld',
|
|
1490
|
+
`Password change request for "${this.$data.username}".`,
|
|
1491
|
+
);
|
|
1012
1492
|
if (!('newPassword' in data) || !data.newPassword.length) {
|
|
1013
1493
|
return { result: false, message: 'Please specify a password.' };
|
|
1014
1494
|
}
|
|
1015
1495
|
if (!this.$checkPassword(data.currentPassword ?? '')) {
|
|
1496
|
+
this.$nymph.config.debugLog(
|
|
1497
|
+
'tilmeld',
|
|
1498
|
+
`Incorrect password for change request for "${this.$data.username}".`,
|
|
1499
|
+
);
|
|
1016
1500
|
return { result: false, message: 'Incorrect password.' };
|
|
1017
1501
|
}
|
|
1018
1502
|
this.$data.passwordTemp = data.newPassword;
|
|
1503
|
+
if (data.revokeCurrentTokens) {
|
|
1504
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1505
|
+
}
|
|
1019
1506
|
if (await this.$save()) {
|
|
1507
|
+
if (data.revokeCurrentTokens) {
|
|
1508
|
+
const tilmeld = enforceTilmeld(this);
|
|
1509
|
+
await tilmeld.login(this, true);
|
|
1510
|
+
}
|
|
1020
1511
|
return { result: true, message: 'Your password has been changed.' };
|
|
1021
1512
|
} else {
|
|
1022
1513
|
return { result: false, message: "Couldn't save new password." };
|
|
@@ -1030,7 +1521,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1030
1521
|
* @returns The resulting password or hash which is stored in the entity.
|
|
1031
1522
|
*/
|
|
1032
1523
|
public $password(password: string) {
|
|
1033
|
-
const tilmeld = this
|
|
1524
|
+
const tilmeld = enforceTilmeld(this);
|
|
1034
1525
|
switch (tilmeld.config.pwMethod) {
|
|
1035
1526
|
case 'plain':
|
|
1036
1527
|
delete this.$data.salt;
|
|
@@ -1038,34 +1529,275 @@ export default class User extends AbleObject<UserData> {
|
|
|
1038
1529
|
break;
|
|
1039
1530
|
case 'digest':
|
|
1040
1531
|
delete this.$data.salt;
|
|
1041
|
-
this.$data.password =
|
|
1532
|
+
this.$data.password = crypto
|
|
1533
|
+
.createHash('sha256')
|
|
1534
|
+
.update(password)
|
|
1535
|
+
.digest('base64');
|
|
1042
1536
|
break;
|
|
1043
1537
|
case 'salt':
|
|
1044
1538
|
default:
|
|
1045
1539
|
this.$data.salt = nanoid();
|
|
1046
|
-
this.$data.password =
|
|
1047
|
-
|
|
1048
|
-
|
|
1540
|
+
this.$data.password = crypto
|
|
1541
|
+
.createHash('sha256')
|
|
1542
|
+
.update(password + this.$data.salt)
|
|
1543
|
+
.digest('base64');
|
|
1049
1544
|
break;
|
|
1050
1545
|
}
|
|
1051
1546
|
return this.$data.password;
|
|
1052
1547
|
}
|
|
1053
1548
|
|
|
1549
|
+
/**
|
|
1550
|
+
* A frontend accessible method to revoke all currently issued tokens.
|
|
1551
|
+
*
|
|
1552
|
+
* @param data The input data from the client.
|
|
1553
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1554
|
+
*/
|
|
1555
|
+
public async $revokeCurrentTokens(data: {
|
|
1556
|
+
password: string;
|
|
1557
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1558
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1559
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1563
|
+
|
|
1564
|
+
if (await this.$save()) {
|
|
1565
|
+
const tilmeld = enforceTilmeld(this);
|
|
1566
|
+
await tilmeld.login(this, true);
|
|
1567
|
+
return {
|
|
1568
|
+
result: true,
|
|
1569
|
+
message: 'You have logged out of all other sessions.',
|
|
1570
|
+
};
|
|
1571
|
+
} else {
|
|
1572
|
+
return { result: false, message: "Couldn't save revocation date." };
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
public async $hasTOTPSecret() {
|
|
1577
|
+
return this.$data.totpSecret != null;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* Check the given code against the user's TOTP secret.
|
|
1582
|
+
*
|
|
1583
|
+
* @param code The code in question.
|
|
1584
|
+
* @returns True if the code is valid, otherwise false.
|
|
1585
|
+
*/
|
|
1586
|
+
public $checkTOTPCode(code: string) {
|
|
1587
|
+
if (this.$data.totpSecret == null) {
|
|
1588
|
+
return false;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
const tilmeld = enforceTilmeld(this);
|
|
1592
|
+
|
|
1593
|
+
const totp = new TOTP({
|
|
1594
|
+
issuer: tilmeld.config.appName,
|
|
1595
|
+
label: this.$data.username,
|
|
1596
|
+
algorithm: 'SHA1',
|
|
1597
|
+
digits: 6,
|
|
1598
|
+
period: 30,
|
|
1599
|
+
secret: this.$data.totpSecret,
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
return totp.validate({ token: code, window: 2 }) != null;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
/**
|
|
1606
|
+
* A frontend accessible method to generate a new TOTP secret.
|
|
1607
|
+
*
|
|
1608
|
+
* @returns An object with 'uri', 'qrcode', and 'secret'.
|
|
1609
|
+
*/
|
|
1610
|
+
public async $getNewTOTPSecret() {
|
|
1611
|
+
const tilmeld = enforceTilmeld(this);
|
|
1612
|
+
|
|
1613
|
+
if (this.$data.totpSecret != null) {
|
|
1614
|
+
throw new BadDataError('You already have a 2FA secret.');
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
const secret = new Secret();
|
|
1618
|
+
|
|
1619
|
+
const totp = new TOTP({
|
|
1620
|
+
issuer: tilmeld.config.appName,
|
|
1621
|
+
label: this.$data.username,
|
|
1622
|
+
algorithm: 'SHA1',
|
|
1623
|
+
digits: 6,
|
|
1624
|
+
period: 30,
|
|
1625
|
+
secret,
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
const uri = totp.toString();
|
|
1629
|
+
|
|
1630
|
+
const qrcode = await toDataURL(uri);
|
|
1631
|
+
|
|
1632
|
+
return {
|
|
1633
|
+
uri,
|
|
1634
|
+
qrcode,
|
|
1635
|
+
secret: secret.base32,
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* A frontend accessible method to save a TOTP secret into the user's account.
|
|
1641
|
+
*
|
|
1642
|
+
* @param data The input data from the client.
|
|
1643
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1644
|
+
*/
|
|
1645
|
+
public async $saveTOTPSecret(data: {
|
|
1646
|
+
password: string;
|
|
1647
|
+
secret: string;
|
|
1648
|
+
code: string;
|
|
1649
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1650
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1651
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
if (this.$data.totpSecret != null) {
|
|
1655
|
+
return { result: false, message: 'You already have a 2FA secret.' };
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (
|
|
1659
|
+
data.secret == null ||
|
|
1660
|
+
typeof data.secret !== 'string' ||
|
|
1661
|
+
data.secret === ''
|
|
1662
|
+
) {
|
|
1663
|
+
return { result: false, message: '2FA secret is invalid.' };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
if (
|
|
1667
|
+
data.code == null ||
|
|
1668
|
+
typeof data.code !== 'string' ||
|
|
1669
|
+
data.code.length !== 6
|
|
1670
|
+
) {
|
|
1671
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
const tilmeld = enforceTilmeld(this);
|
|
1675
|
+
|
|
1676
|
+
const totp = new TOTP({
|
|
1677
|
+
issuer: tilmeld.config.appName,
|
|
1678
|
+
label: this.$data.username,
|
|
1679
|
+
algorithm: 'SHA1',
|
|
1680
|
+
digits: 6,
|
|
1681
|
+
period: 30,
|
|
1682
|
+
secret: data.secret,
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1686
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
this.$data.totpSecret = data.secret;
|
|
1690
|
+
|
|
1691
|
+
if (await this.$save()) {
|
|
1692
|
+
return { result: true, message: 'Your two factor secret is now set.' };
|
|
1693
|
+
} else {
|
|
1694
|
+
return { result: false, message: "Couldn't save two factor secret." };
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* A frontend accessible method to remove the TOTP secret from the user's
|
|
1700
|
+
* account.
|
|
1701
|
+
*
|
|
1702
|
+
* @param data The input data from the client.
|
|
1703
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1704
|
+
*/
|
|
1705
|
+
public async $removeTOTPSecret(data?: {
|
|
1706
|
+
password: string;
|
|
1707
|
+
code: string;
|
|
1708
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1709
|
+
const tilmeld = enforceTilmeld(this);
|
|
1710
|
+
|
|
1711
|
+
if (data) {
|
|
1712
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1713
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
if (
|
|
1717
|
+
data.code == null ||
|
|
1718
|
+
typeof data.code !== 'string' ||
|
|
1719
|
+
data.code.length !== 6
|
|
1720
|
+
) {
|
|
1721
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
if (this.$data.totpSecret == null) {
|
|
1725
|
+
return { result: false, message: "You don't have a 2FA secret." };
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
const totp = new TOTP({
|
|
1729
|
+
issuer: tilmeld.config.appName,
|
|
1730
|
+
label: this.$data.username,
|
|
1731
|
+
algorithm: 'SHA1',
|
|
1732
|
+
digits: 6,
|
|
1733
|
+
period: 30,
|
|
1734
|
+
secret: this.$data.totpSecret,
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1738
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1739
|
+
}
|
|
1740
|
+
} else if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1741
|
+
return {
|
|
1742
|
+
result: false,
|
|
1743
|
+
message: 'You must provide your password and 2FA code.',
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
delete this.$data.totpSecret;
|
|
1748
|
+
|
|
1749
|
+
if (await this.$save()) {
|
|
1750
|
+
return {
|
|
1751
|
+
result: true,
|
|
1752
|
+
message: 'Two factor secret has been removed.',
|
|
1753
|
+
};
|
|
1754
|
+
} else {
|
|
1755
|
+
return { result: false, message: "Couldn't remove two factor secret." };
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1054
1759
|
/**
|
|
1055
1760
|
* Check that a username is valid.
|
|
1056
1761
|
*
|
|
1057
1762
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1058
1763
|
*/
|
|
1059
1764
|
public async $checkUsername(): Promise<{ result: boolean; message: string }> {
|
|
1060
|
-
const tilmeld = this
|
|
1765
|
+
const tilmeld = enforceTilmeld(this);
|
|
1766
|
+
|
|
1767
|
+
this.$nymph.config.debugInfo(
|
|
1768
|
+
'tilmeld',
|
|
1769
|
+
`Username check "${this.$data.username}".`,
|
|
1770
|
+
);
|
|
1771
|
+
|
|
1061
1772
|
if (!tilmeld.config.emailUsernames) {
|
|
1062
|
-
|
|
1063
|
-
|
|
1773
|
+
let [username, domain] = tilmeld.config.domainSupport
|
|
1774
|
+
? splitn(this.$data.username ?? '', '@', 2)
|
|
1775
|
+
: [this.$data.username, null];
|
|
1776
|
+
|
|
1777
|
+
// Lowercase the domain part.
|
|
1778
|
+
if (domain) {
|
|
1779
|
+
domain = domain.toLowerCase();
|
|
1780
|
+
this.$data.username = `${username}@${domain}`;
|
|
1064
1781
|
}
|
|
1782
|
+
|
|
1065
1783
|
if (
|
|
1066
|
-
|
|
1067
|
-
this.$data.username.length
|
|
1784
|
+
this.$data.username == null ||
|
|
1785
|
+
!this.$data.username.length ||
|
|
1786
|
+
username == null ||
|
|
1787
|
+
!username.length
|
|
1068
1788
|
) {
|
|
1789
|
+
return { result: false, message: 'Please specify a username.' };
|
|
1790
|
+
}
|
|
1791
|
+
if (username.length < tilmeld.config.minUsernameLength) {
|
|
1792
|
+
return {
|
|
1793
|
+
result: false,
|
|
1794
|
+
message:
|
|
1795
|
+
'Usernames must be at least ' +
|
|
1796
|
+
tilmeld.config.minUsernameLength +
|
|
1797
|
+
' characters.',
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
if (username.length > tilmeld.config.maxUsernameLength) {
|
|
1069
1801
|
return {
|
|
1070
1802
|
result: false,
|
|
1071
1803
|
message:
|
|
@@ -1076,22 +1808,38 @@ export default class User extends AbleObject<UserData> {
|
|
|
1076
1808
|
}
|
|
1077
1809
|
|
|
1078
1810
|
if (
|
|
1079
|
-
difference(
|
|
1080
|
-
|
|
1081
|
-
tilmeld.config.validChars.split('')
|
|
1082
|
-
).length
|
|
1811
|
+
difference(username.split(''), tilmeld.config.validChars.split(''))
|
|
1812
|
+
.length
|
|
1083
1813
|
) {
|
|
1084
1814
|
return {
|
|
1085
1815
|
result: false,
|
|
1086
1816
|
message: tilmeld.config.validCharsNotice,
|
|
1087
1817
|
};
|
|
1088
1818
|
}
|
|
1089
|
-
if (!tilmeld.config.validRegex.test(
|
|
1819
|
+
if (!tilmeld.config.validRegex.test(username)) {
|
|
1090
1820
|
return {
|
|
1091
1821
|
result: false,
|
|
1092
1822
|
message: tilmeld.config.validRegexNotice,
|
|
1093
1823
|
};
|
|
1094
1824
|
}
|
|
1825
|
+
if (domain != null) {
|
|
1826
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
1827
|
+
return {
|
|
1828
|
+
result: false,
|
|
1829
|
+
message: tilmeld.config.validDomainRegexNotice,
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
if (
|
|
1833
|
+
!this.$is(tilmeld.currentUser) &&
|
|
1834
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
|
|
1835
|
+
) {
|
|
1836
|
+
return {
|
|
1837
|
+
result: false,
|
|
1838
|
+
message:
|
|
1839
|
+
"You don't have the authority to manage users on this domain.",
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1095
1843
|
|
|
1096
1844
|
const selector: Selector = {
|
|
1097
1845
|
type: '&',
|
|
@@ -1105,12 +1853,24 @@ export default class User extends AbleObject<UserData> {
|
|
|
1105
1853
|
}
|
|
1106
1854
|
const test = await this.$nymph.getEntity(
|
|
1107
1855
|
{ class: tilmeld.User, skipAc: true },
|
|
1108
|
-
selector
|
|
1856
|
+
selector,
|
|
1109
1857
|
);
|
|
1110
1858
|
if (test != null) {
|
|
1111
1859
|
return { result: false, message: 'That username is taken.' };
|
|
1112
1860
|
}
|
|
1113
1861
|
|
|
1862
|
+
for (let callback of (this.constructor as typeof User)
|
|
1863
|
+
.checkUsernameCallbacks) {
|
|
1864
|
+
if (callback) {
|
|
1865
|
+
const result = await callback(this, {
|
|
1866
|
+
username: this.$data.username,
|
|
1867
|
+
});
|
|
1868
|
+
if (!result.result) {
|
|
1869
|
+
return { message: 'Username is not available.', ...result };
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1114
1874
|
return {
|
|
1115
1875
|
result: true,
|
|
1116
1876
|
message:
|
|
@@ -1121,10 +1881,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
1121
1881
|
if (this.$data.username == null || !this.$data.username.length) {
|
|
1122
1882
|
return { result: false, message: 'Please specify an email.' };
|
|
1123
1883
|
}
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1884
|
+
if (this.$data.username.length < tilmeld.config.minUsernameLength) {
|
|
1885
|
+
return {
|
|
1886
|
+
result: false,
|
|
1887
|
+
message:
|
|
1888
|
+
'Emails must be at least ' +
|
|
1889
|
+
tilmeld.config.minUsernameLength +
|
|
1890
|
+
' characters.',
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
|
|
1128
1894
|
return {
|
|
1129
1895
|
result: false,
|
|
1130
1896
|
message:
|
|
@@ -1144,7 +1910,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1144
1910
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1145
1911
|
*/
|
|
1146
1912
|
public async $checkEmail(): Promise<{ result: boolean; message: string }> {
|
|
1147
|
-
const tilmeld = this
|
|
1913
|
+
const tilmeld = enforceTilmeld(this);
|
|
1914
|
+
|
|
1915
|
+
this.$nymph.config.debugInfo(
|
|
1916
|
+
'tilmeld',
|
|
1917
|
+
`Email check "${this.$data.email}".`,
|
|
1918
|
+
);
|
|
1919
|
+
|
|
1148
1920
|
if (this.$data.email == null || !this.$data.email.length) {
|
|
1149
1921
|
if (tilmeld.config.verifyEmail) {
|
|
1150
1922
|
return { result: false, message: 'Please specify an email.' };
|
|
@@ -1167,7 +1939,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1167
1939
|
}
|
|
1168
1940
|
const test = await this.$nymph.getEntity(
|
|
1169
1941
|
{ class: tilmeld.User, skipAc: true },
|
|
1170
|
-
selector
|
|
1942
|
+
selector,
|
|
1171
1943
|
);
|
|
1172
1944
|
if (test != null) {
|
|
1173
1945
|
return {
|
|
@@ -1189,7 +1961,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1189
1961
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1190
1962
|
*/
|
|
1191
1963
|
public async $checkPhone(): Promise<{ result: boolean; message: string }> {
|
|
1192
|
-
const tilmeld = this
|
|
1964
|
+
const tilmeld = enforceTilmeld(this);
|
|
1965
|
+
|
|
1966
|
+
this.$nymph.config.debugInfo(
|
|
1967
|
+
'tilmeld',
|
|
1968
|
+
`Phone check "${this.$data.phone}".`,
|
|
1969
|
+
);
|
|
1970
|
+
|
|
1193
1971
|
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
1194
1972
|
return { result: false, message: 'Please specify a phone number.' };
|
|
1195
1973
|
}
|
|
@@ -1211,7 +1989,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1211
1989
|
}
|
|
1212
1990
|
const test = await this.$nymph.getEntity(
|
|
1213
1991
|
{ class: tilmeld.User, skipAc: true },
|
|
1214
|
-
selector
|
|
1992
|
+
selector,
|
|
1215
1993
|
);
|
|
1216
1994
|
if (test != null) {
|
|
1217
1995
|
return { result: false, message: 'Phone number is in use.' };
|
|
@@ -1228,7 +2006,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1228
2006
|
password: string;
|
|
1229
2007
|
additionalData?: { [k: string]: any };
|
|
1230
2008
|
}): Promise<{ result: boolean; loggedin: boolean; message: string }> {
|
|
1231
|
-
let tilmeld = this
|
|
2009
|
+
let tilmeld = enforceTilmeld(this);
|
|
1232
2010
|
if (!tilmeld.config.allowRegistration) {
|
|
1233
2011
|
return {
|
|
1234
2012
|
result: false,
|
|
@@ -1244,21 +2022,6 @@ export default class User extends AbleObject<UserData> {
|
|
|
1244
2022
|
};
|
|
1245
2023
|
}
|
|
1246
2024
|
|
|
1247
|
-
try {
|
|
1248
|
-
for (let callback of (this.constructor as typeof User)
|
|
1249
|
-
.beforeRegisterCallbacks) {
|
|
1250
|
-
if (callback) {
|
|
1251
|
-
await callback(this, data);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
} catch (e: any) {
|
|
1255
|
-
return {
|
|
1256
|
-
result: false,
|
|
1257
|
-
loggedin: false,
|
|
1258
|
-
message: e.message,
|
|
1259
|
-
};
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
2025
|
if (!('password' in data) || !data.password.length) {
|
|
1263
2026
|
return {
|
|
1264
2027
|
result: false,
|
|
@@ -1272,7 +2035,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1272
2035
|
}
|
|
1273
2036
|
|
|
1274
2037
|
this.$password(data.password);
|
|
1275
|
-
if (tilmeld.config.regFields.
|
|
2038
|
+
if (tilmeld.config.regFields.includes('name')) {
|
|
1276
2039
|
this.$data.name =
|
|
1277
2040
|
this.$data.nameFirst +
|
|
1278
2041
|
(this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
|
|
@@ -1289,75 +2052,34 @@ export default class User extends AbleObject<UserData> {
|
|
|
1289
2052
|
const transaction = 'tilmeld-register-' + nanoid();
|
|
1290
2053
|
const nymph = this.$nymph;
|
|
1291
2054
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1292
|
-
this.$
|
|
1293
|
-
tilmeld = this
|
|
2055
|
+
this.$setNymph(tnymph);
|
|
2056
|
+
tilmeld = enforceTilmeld(this);
|
|
2057
|
+
|
|
2058
|
+
let message = '';
|
|
2059
|
+
let loggedin = false;
|
|
1294
2060
|
|
|
1295
2061
|
try {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
generatedPrimaryGroup.groupname = this.$data.username;
|
|
1302
|
-
generatedPrimaryGroup.avatar = this.$data.avatar;
|
|
1303
|
-
generatedPrimaryGroup.name = this.$data.name;
|
|
1304
|
-
generatedPrimaryGroup.email = this.$data.email;
|
|
1305
|
-
const parent = await tnymph.getEntity(
|
|
1306
|
-
{ class: tilmeld.Group },
|
|
1307
|
-
{
|
|
1308
|
-
type: '&',
|
|
1309
|
-
equal: ['defaultPrimary', true],
|
|
1310
|
-
}
|
|
1311
|
-
);
|
|
1312
|
-
if (parent != null) {
|
|
1313
|
-
generatedPrimaryGroup.parent = parent;
|
|
1314
|
-
}
|
|
1315
|
-
if (!(await generatedPrimaryGroup.$saveSkipAC())) {
|
|
1316
|
-
await tnymph.rollback(transaction);
|
|
1317
|
-
this.$nymph = nymph;
|
|
1318
|
-
return {
|
|
1319
|
-
result: false,
|
|
1320
|
-
loggedin: false,
|
|
1321
|
-
message: 'Error creating primary group for user.',
|
|
1322
|
-
};
|
|
1323
|
-
}
|
|
1324
|
-
this.$data.group = generatedPrimaryGroup;
|
|
1325
|
-
} else {
|
|
1326
|
-
// Add the default primary.
|
|
1327
|
-
const group = await tnymph.getEntity(
|
|
1328
|
-
{ class: tilmeld.Group },
|
|
1329
|
-
{
|
|
1330
|
-
type: '&',
|
|
1331
|
-
equal: ['defaultPrimary', true],
|
|
2062
|
+
try {
|
|
2063
|
+
for (let callback of (this.constructor as typeof User)
|
|
2064
|
+
.beforeRegisterCallbacks) {
|
|
2065
|
+
if (callback) {
|
|
2066
|
+
await callback(this, data);
|
|
1332
2067
|
}
|
|
1333
|
-
);
|
|
1334
|
-
if (group != null) {
|
|
1335
|
-
this.$data.group = group;
|
|
1336
2068
|
}
|
|
2069
|
+
} catch (e: any) {
|
|
2070
|
+
await tnymph.rollback(transaction);
|
|
2071
|
+
this.$setNymph(nymph);
|
|
2072
|
+
return {
|
|
2073
|
+
result: false,
|
|
2074
|
+
loggedin: false,
|
|
2075
|
+
message: e.message,
|
|
2076
|
+
};
|
|
1337
2077
|
}
|
|
1338
2078
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
{ class: tilmeld.Group },
|
|
1344
|
-
{
|
|
1345
|
-
type: '&',
|
|
1346
|
-
equal: ['unverifiedSecondary', true],
|
|
1347
|
-
}
|
|
1348
|
-
);
|
|
1349
|
-
} else {
|
|
1350
|
-
// Add the default secondaries.
|
|
1351
|
-
this.$data.groups = await tnymph.getEntities(
|
|
1352
|
-
{ class: tilmeld.Group },
|
|
1353
|
-
{
|
|
1354
|
-
type: '&',
|
|
1355
|
-
equal: ['defaultSecondary', true],
|
|
1356
|
-
}
|
|
1357
|
-
);
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
if (tilmeld.config.verifyEmail) {
|
|
2079
|
+
if (
|
|
2080
|
+
tilmeld.config.userFields.includes('email') &&
|
|
2081
|
+
tilmeld.config.verifyEmail
|
|
2082
|
+
) {
|
|
1361
2083
|
// The user will be enabled after verifying their e-mail address.
|
|
1362
2084
|
if (!tilmeld.config.unverifiedAccess) {
|
|
1363
2085
|
this.$data.enabled = false;
|
|
@@ -1366,7 +2088,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1366
2088
|
this.$data.enabled = true;
|
|
1367
2089
|
}
|
|
1368
2090
|
|
|
1369
|
-
// If
|
|
2091
|
+
// If createAdmin is true and there are no other users, grant
|
|
1370
2092
|
// "system/admin".
|
|
1371
2093
|
let madeAdmin = false;
|
|
1372
2094
|
if (tilmeld.config.createAdmin) {
|
|
@@ -1384,7 +2106,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
1384
2106
|
}
|
|
1385
2107
|
}
|
|
1386
2108
|
|
|
2109
|
+
this.$nymph.config.debugLog(
|
|
2110
|
+
'tilmeld',
|
|
2111
|
+
`Registering new user "${this.$data.username}".`,
|
|
2112
|
+
);
|
|
1387
2113
|
if (await this.$saveSkipAC()) {
|
|
2114
|
+
this.$nymph.config.debugLog(
|
|
2115
|
+
'tilmeld',
|
|
2116
|
+
`New user registered "${this.$data.username}".`,
|
|
2117
|
+
);
|
|
1388
2118
|
// Send the new user registered email.
|
|
1389
2119
|
if (tilmeld.config.userRegisteredRecipient != null) {
|
|
1390
2120
|
await tilmeld.config.sendEmail(
|
|
@@ -1403,33 +2133,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
1403
2133
|
userPhone: this.$data.phone,
|
|
1404
2134
|
},
|
|
1405
2135
|
},
|
|
1406
|
-
this
|
|
2136
|
+
this,
|
|
1407
2137
|
);
|
|
1408
2138
|
}
|
|
1409
2139
|
|
|
1410
|
-
let message = '';
|
|
1411
|
-
let loggedin = false;
|
|
1412
|
-
|
|
1413
|
-
// Save the primary group.
|
|
1414
|
-
if (generatedPrimaryGroup != null) {
|
|
1415
|
-
generatedPrimaryGroup.user = this;
|
|
1416
|
-
if (!(await generatedPrimaryGroup.$saveSkipAC())) {
|
|
1417
|
-
await tnymph.rollback(transaction);
|
|
1418
|
-
this.$nymph = nymph;
|
|
1419
|
-
return {
|
|
1420
|
-
result: false,
|
|
1421
|
-
loggedin: false,
|
|
1422
|
-
message:
|
|
1423
|
-
'Your primary group could not be assigned. Please try again later.',
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
2140
|
// Finish up.
|
|
1429
2141
|
if (
|
|
1430
2142
|
tilmeld.config.verifyEmail &&
|
|
1431
2143
|
!tilmeld.config.unverifiedAccess &&
|
|
1432
|
-
!madeAdmin
|
|
2144
|
+
!madeAdmin &&
|
|
2145
|
+
tilmeld.config.userFields.includes('email')
|
|
1433
2146
|
) {
|
|
1434
2147
|
message +=
|
|
1435
2148
|
`Almost there. An email has been sent to ${this.$data.email} ` +
|
|
@@ -1437,9 +2150,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1437
2150
|
} else if (
|
|
1438
2151
|
tilmeld.config.verifyEmail &&
|
|
1439
2152
|
tilmeld.config.unverifiedAccess &&
|
|
1440
|
-
!madeAdmin
|
|
2153
|
+
!madeAdmin &&
|
|
2154
|
+
tilmeld.config.userFields.includes('email')
|
|
1441
2155
|
) {
|
|
1442
|
-
if (!tilmeld.login(this, true)) {
|
|
2156
|
+
if (!(await tilmeld.login(this, true))) {
|
|
1443
2157
|
throw new Error('An error occurred trying to log you in.');
|
|
1444
2158
|
}
|
|
1445
2159
|
message +=
|
|
@@ -1448,7 +2162,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1448
2162
|
'registration.';
|
|
1449
2163
|
loggedin = true;
|
|
1450
2164
|
} else {
|
|
1451
|
-
if (!tilmeld.login(this, true)) {
|
|
2165
|
+
if (!(await tilmeld.login(this, true))) {
|
|
1452
2166
|
throw new Error('An error occurred trying to log you in.');
|
|
1453
2167
|
}
|
|
1454
2168
|
message += "You're now registered and logged in!";
|
|
@@ -1464,23 +2178,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1464
2178
|
});
|
|
1465
2179
|
}
|
|
1466
2180
|
}
|
|
1467
|
-
|
|
1468
|
-
try {
|
|
1469
|
-
await tnymph.commit(transaction);
|
|
1470
|
-
this.$nymph = nymph;
|
|
1471
|
-
this.$nymph.tilmeld?.fillSession(this);
|
|
1472
|
-
} catch (e: any) {
|
|
1473
|
-
throw e;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
return {
|
|
1477
|
-
result: true,
|
|
1478
|
-
loggedin,
|
|
1479
|
-
message,
|
|
1480
|
-
};
|
|
1481
2181
|
} else {
|
|
2182
|
+
this.$nymph.config.debugError(
|
|
2183
|
+
'tilmeld',
|
|
2184
|
+
`Error registering new user "${this.$data.username}".`,
|
|
2185
|
+
);
|
|
1482
2186
|
await tnymph.rollback(transaction);
|
|
1483
|
-
this.$nymph
|
|
2187
|
+
this.$setNymph(nymph);
|
|
1484
2188
|
return {
|
|
1485
2189
|
result: false,
|
|
1486
2190
|
loggedin: false,
|
|
@@ -1488,28 +2192,100 @@ export default class User extends AbleObject<UserData> {
|
|
|
1488
2192
|
};
|
|
1489
2193
|
}
|
|
1490
2194
|
} catch (e: any) {
|
|
2195
|
+
this.$nymph.config.debugError(
|
|
2196
|
+
'tilmeld',
|
|
2197
|
+
`Error registering new user "${this.$data.username}": ${e}`,
|
|
2198
|
+
);
|
|
1491
2199
|
await tnymph.rollback(transaction);
|
|
1492
|
-
this.$nymph
|
|
2200
|
+
this.$setNymph(nymph);
|
|
2201
|
+
throw e;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
try {
|
|
2205
|
+
await tnymph.commit(transaction);
|
|
2206
|
+
this.$setNymph(nymph);
|
|
2207
|
+
await this.$nymph.tilmeld?.fillSession(this);
|
|
2208
|
+
} catch (e: any) {
|
|
1493
2209
|
throw e;
|
|
1494
2210
|
}
|
|
2211
|
+
|
|
2212
|
+
return {
|
|
2213
|
+
result: true,
|
|
2214
|
+
loggedin,
|
|
2215
|
+
message,
|
|
2216
|
+
};
|
|
1495
2217
|
}
|
|
1496
2218
|
|
|
1497
2219
|
public async $save() {
|
|
1498
|
-
let tilmeld = this
|
|
2220
|
+
let tilmeld = enforceTilmeld(this);
|
|
1499
2221
|
if (this.$data.username == null || !this.$data.username.trim().length) {
|
|
1500
2222
|
return false;
|
|
1501
2223
|
}
|
|
1502
2224
|
|
|
1503
2225
|
if (
|
|
2226
|
+
!this.$skipAcWhenSaving &&
|
|
1504
2227
|
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1505
2228
|
!tilmeld.gatekeeper('system/admin') &&
|
|
1506
|
-
this.$
|
|
2229
|
+
this.$data.abilities?.includes('system/admin')
|
|
1507
2230
|
) {
|
|
1508
|
-
throw new
|
|
1509
|
-
"You don't have the authority to modify system admins."
|
|
2231
|
+
throw new AccessControlError(
|
|
2232
|
+
"You don't have the authority to modify system admins.",
|
|
1510
2233
|
);
|
|
1511
2234
|
}
|
|
1512
2235
|
|
|
2236
|
+
// Lowercase the domain part.
|
|
2237
|
+
if (tilmeld.config.domainSupport) {
|
|
2238
|
+
const [username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
2239
|
+
if (domain) {
|
|
2240
|
+
this.$data.username = `${username}@${domain.toLowerCase()}`;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
if (tilmeld.config.domainSupport) {
|
|
2245
|
+
const [_username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
2246
|
+
const [_originalUsername, originalDomain] = splitn(
|
|
2247
|
+
this.$originalUsername ?? '',
|
|
2248
|
+
'@',
|
|
2249
|
+
2,
|
|
2250
|
+
);
|
|
2251
|
+
|
|
2252
|
+
if (
|
|
2253
|
+
this.guid != null &&
|
|
2254
|
+
((originalDomain == null && domain != null) ||
|
|
2255
|
+
(domain == null && originalDomain != null))
|
|
2256
|
+
) {
|
|
2257
|
+
throw new AccessControlError(
|
|
2258
|
+
"Users can't switch to or from having a domain once created.",
|
|
2259
|
+
);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
if (
|
|
2263
|
+
!this.$skipAcWhenSaving &&
|
|
2264
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
2265
|
+
!tilmeld.gatekeeper('system/admin')
|
|
2266
|
+
) {
|
|
2267
|
+
if (
|
|
2268
|
+
domain != null &&
|
|
2269
|
+
!this.$is(tilmeld.currentUser) &&
|
|
2270
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
|
|
2271
|
+
) {
|
|
2272
|
+
throw new AccessControlError(
|
|
2273
|
+
"You don't have the authority to modify users on this domain.",
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
if (
|
|
2277
|
+
originalDomain != null &&
|
|
2278
|
+
originalDomain !== domain &&
|
|
2279
|
+
(!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
|
|
2280
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))
|
|
2281
|
+
) {
|
|
2282
|
+
throw new AccessControlError(
|
|
2283
|
+
"You don't have the authority to modify users on this domain.",
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
1513
2289
|
let sendVerification = false;
|
|
1514
2290
|
|
|
1515
2291
|
// Formatting.
|
|
@@ -1528,49 +2304,88 @@ export default class User extends AbleObject<UserData> {
|
|
|
1528
2304
|
this.$data.enabled = !!this.$data.enabled;
|
|
1529
2305
|
|
|
1530
2306
|
// Clear empty values.
|
|
1531
|
-
if (
|
|
2307
|
+
if (
|
|
2308
|
+
this.$data.nameFirst === '' ||
|
|
2309
|
+
!tilmeld.config.userFields.includes('name')
|
|
2310
|
+
) {
|
|
1532
2311
|
delete this.$data.nameFirst;
|
|
1533
2312
|
}
|
|
1534
|
-
if (
|
|
2313
|
+
if (
|
|
2314
|
+
this.$data.nameMiddle === '' ||
|
|
2315
|
+
!tilmeld.config.userFields.includes('name')
|
|
2316
|
+
) {
|
|
1535
2317
|
delete this.$data.nameMiddle;
|
|
1536
2318
|
}
|
|
1537
|
-
if (
|
|
2319
|
+
if (
|
|
2320
|
+
this.$data.nameLast === '' ||
|
|
2321
|
+
!tilmeld.config.userFields.includes('name')
|
|
2322
|
+
) {
|
|
1538
2323
|
delete this.$data.nameLast;
|
|
1539
2324
|
}
|
|
1540
|
-
if (this.$data.name === '') {
|
|
2325
|
+
if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
1541
2326
|
delete this.$data.name;
|
|
1542
2327
|
}
|
|
1543
|
-
if (
|
|
2328
|
+
if (
|
|
2329
|
+
this.$data.email === '' ||
|
|
2330
|
+
!tilmeld.config.userFields.includes('email')
|
|
2331
|
+
) {
|
|
1544
2332
|
delete this.$data.email;
|
|
1545
2333
|
}
|
|
1546
2334
|
if (this.$data.avatar === '') {
|
|
1547
2335
|
delete this.$data.avatar;
|
|
1548
2336
|
}
|
|
1549
|
-
if (
|
|
2337
|
+
if (
|
|
2338
|
+
this.$data.phone === '' ||
|
|
2339
|
+
!tilmeld.config.userFields.includes('phone')
|
|
2340
|
+
) {
|
|
1550
2341
|
delete this.$data.phone;
|
|
1551
2342
|
}
|
|
1552
|
-
if (
|
|
2343
|
+
if (
|
|
2344
|
+
this.$data.secret === '' ||
|
|
2345
|
+
!tilmeld.config.userFields.includes('email')
|
|
2346
|
+
) {
|
|
1553
2347
|
delete this.$data.secret;
|
|
1554
2348
|
}
|
|
1555
|
-
if (
|
|
2349
|
+
if (
|
|
2350
|
+
this.$data.emailChangeDate === 0 ||
|
|
2351
|
+
!tilmeld.config.userFields.includes('email')
|
|
2352
|
+
) {
|
|
1556
2353
|
delete this.$data.emailChangeDate;
|
|
1557
2354
|
}
|
|
1558
|
-
if (
|
|
2355
|
+
if (
|
|
2356
|
+
this.$data.newEmailSecret === '' ||
|
|
2357
|
+
!tilmeld.config.userFields.includes('email')
|
|
2358
|
+
) {
|
|
1559
2359
|
delete this.$data.newEmailSecret;
|
|
1560
2360
|
}
|
|
1561
|
-
if (
|
|
2361
|
+
if (
|
|
2362
|
+
this.$data.newEmailAddress === '' ||
|
|
2363
|
+
!tilmeld.config.userFields.includes('email')
|
|
2364
|
+
) {
|
|
1562
2365
|
delete this.$data.newEmailAddress;
|
|
1563
2366
|
}
|
|
1564
|
-
if (
|
|
2367
|
+
if (
|
|
2368
|
+
this.$data.cancelEmailSecret === '' ||
|
|
2369
|
+
!tilmeld.config.userFields.includes('email')
|
|
2370
|
+
) {
|
|
1565
2371
|
delete this.$data.cancelEmailSecret;
|
|
1566
2372
|
}
|
|
1567
|
-
if (
|
|
2373
|
+
if (
|
|
2374
|
+
this.$data.cancelEmailAddress === '' ||
|
|
2375
|
+
!tilmeld.config.userFields.includes('email')
|
|
2376
|
+
) {
|
|
1568
2377
|
delete this.$data.cancelEmailAddress;
|
|
1569
2378
|
}
|
|
1570
|
-
if (
|
|
2379
|
+
if (
|
|
2380
|
+
this.$data.recoverSecret === '' ||
|
|
2381
|
+
!tilmeld.config.userFields.includes('email')
|
|
2382
|
+
) {
|
|
1571
2383
|
delete this.$data.recoverSecret;
|
|
1572
2384
|
}
|
|
1573
|
-
if (
|
|
2385
|
+
if (
|
|
2386
|
+
this.$data.recoverSecretDate === 0 ||
|
|
2387
|
+
!tilmeld.config.userFields.includes('email')
|
|
2388
|
+
) {
|
|
1574
2389
|
delete this.$data.recoverSecretDate;
|
|
1575
2390
|
}
|
|
1576
2391
|
if (this.$data.password === '') {
|
|
@@ -1588,7 +2403,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1588
2403
|
if (!unCheck.result) {
|
|
1589
2404
|
throw new BadUsernameError(unCheck.message);
|
|
1590
2405
|
}
|
|
1591
|
-
if (
|
|
2406
|
+
if (
|
|
2407
|
+
!tilmeld.config.emailUsernames &&
|
|
2408
|
+
tilmeld.config.userFields.includes('email')
|
|
2409
|
+
) {
|
|
1592
2410
|
const emCheck = await this.$checkEmail();
|
|
1593
2411
|
if (!emCheck.result) {
|
|
1594
2412
|
throw new BadEmailError(emCheck.message);
|
|
@@ -1596,7 +2414,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1596
2414
|
}
|
|
1597
2415
|
|
|
1598
2416
|
// Email changes.
|
|
1599
|
-
if (
|
|
2417
|
+
if (
|
|
2418
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
2419
|
+
tilmeld.config.userFields.includes('email')
|
|
2420
|
+
) {
|
|
1600
2421
|
// The user isn't an admin, so email address changes should contain some
|
|
1601
2422
|
// security measures.
|
|
1602
2423
|
if (tilmeld.config.verifyEmail) {
|
|
@@ -1604,7 +2425,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1604
2425
|
if (this.guid == null) {
|
|
1605
2426
|
// If this is the first user, they'll be made an admin, so they don't
|
|
1606
2427
|
// need to verify.
|
|
1607
|
-
if (
|
|
2428
|
+
if (!this.$data.abilities?.includes('system/admin')) {
|
|
1608
2429
|
this.$data.secret = nanoid();
|
|
1609
2430
|
sendVerification = true;
|
|
1610
2431
|
}
|
|
@@ -1624,10 +2445,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1624
2445
|
new Date(
|
|
1625
2446
|
strtotime(
|
|
1626
2447
|
'+' + tilmeld.config.emailRateLimit,
|
|
1627
|
-
Math.floor(this.$data.emailChangeDate / 1000)
|
|
1628
|
-
) * 1000
|
|
2448
|
+
Math.floor(this.$data.emailChangeDate / 1000),
|
|
2449
|
+
) * 1000,
|
|
1629
2450
|
).toString() +
|
|
1630
|
-
' to change your email address again.'
|
|
2451
|
+
' to change your email address again.',
|
|
1631
2452
|
);
|
|
1632
2453
|
} else {
|
|
1633
2454
|
if (
|
|
@@ -1682,40 +2503,126 @@ export default class User extends AbleObject<UserData> {
|
|
|
1682
2503
|
throw new BadDataError('A password is required.');
|
|
1683
2504
|
}
|
|
1684
2505
|
|
|
1685
|
-
try {
|
|
1686
|
-
tilmeld.config.validatorUser(this);
|
|
1687
|
-
} catch (e: any) {
|
|
1688
|
-
throw new BadDataError(e?.message);
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
2506
|
// Start transaction.
|
|
1692
2507
|
const transaction = 'tilmeld-save-' + nanoid();
|
|
1693
2508
|
const nymph = this.$nymph;
|
|
1694
2509
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1695
|
-
this.$
|
|
1696
|
-
tilmeld = this
|
|
2510
|
+
this.$setNymph(tnymph);
|
|
2511
|
+
tilmeld = enforceTilmeld(this);
|
|
2512
|
+
const Group = tnymph.getEntityClass(GroupClass);
|
|
1697
2513
|
|
|
1698
|
-
|
|
1699
|
-
this.$data.group
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2514
|
+
try {
|
|
2515
|
+
let group = this.$data.group;
|
|
2516
|
+
await group?.$wake();
|
|
2517
|
+
if (group == null && this.guid == null) {
|
|
2518
|
+
if (tilmeld.config.generatePrimary) {
|
|
2519
|
+
// Generate a new primary group for the user.
|
|
2520
|
+
group = await Group.factory();
|
|
2521
|
+
group.user = this;
|
|
2522
|
+
const parent = await tnymph.getEntity(
|
|
2523
|
+
{ class: Group },
|
|
2524
|
+
{
|
|
2525
|
+
type: '&',
|
|
2526
|
+
equal: ['defaultPrimary', true],
|
|
2527
|
+
},
|
|
2528
|
+
);
|
|
2529
|
+
if (parent != null) {
|
|
2530
|
+
group.parent = parent;
|
|
2531
|
+
}
|
|
2532
|
+
} else {
|
|
2533
|
+
// Add the default primary.
|
|
2534
|
+
const group = await tnymph.getEntity(
|
|
2535
|
+
{ class: Group },
|
|
2536
|
+
{
|
|
2537
|
+
type: '&',
|
|
2538
|
+
equal: ['defaultPrimary', true],
|
|
2539
|
+
},
|
|
2540
|
+
);
|
|
2541
|
+
if (group != null) {
|
|
2542
|
+
this.$data.group = group;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
if (group != null && group.user != null && this.$is(group.user)) {
|
|
1704
2547
|
// Update the user's generated primary group.
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
2548
|
+
let changed = false;
|
|
2549
|
+
if (group.groupname !== this.$data.username) {
|
|
2550
|
+
group.groupname = this.$data.username;
|
|
2551
|
+
changed = true;
|
|
2552
|
+
}
|
|
2553
|
+
if (group.avatar !== this.$data.avatar) {
|
|
2554
|
+
group.avatar = this.$data.avatar;
|
|
2555
|
+
changed = true;
|
|
2556
|
+
}
|
|
2557
|
+
if (
|
|
2558
|
+
tilmeld.config.userFields.includes('email') &&
|
|
2559
|
+
group.email !== this.$data.email
|
|
2560
|
+
) {
|
|
2561
|
+
group.email = this.$data.email;
|
|
2562
|
+
changed = true;
|
|
2563
|
+
}
|
|
2564
|
+
if (
|
|
2565
|
+
tilmeld.config.userFields.includes('name') &&
|
|
2566
|
+
group.name !== this.$data.name
|
|
2567
|
+
) {
|
|
2568
|
+
group.name = this.$data.name;
|
|
2569
|
+
changed = true;
|
|
2570
|
+
}
|
|
2571
|
+
if (
|
|
2572
|
+
tilmeld.config.userFields.includes('phone') &&
|
|
2573
|
+
group.phone !== this.$data.phone
|
|
2574
|
+
) {
|
|
2575
|
+
group.phone = this.$data.phone;
|
|
2576
|
+
changed = true;
|
|
2577
|
+
}
|
|
2578
|
+
if (changed || group.guid == null) {
|
|
2579
|
+
if (!(await group.$saveSkipAC())) {
|
|
2580
|
+
throw Error('Error updating primary group for user.');
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
this.$data.group = group;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
if (this.$data.groups == null || this.$data.groups.length === 0) {
|
|
2587
|
+
// Add secondary groups.
|
|
2588
|
+
if (
|
|
2589
|
+
tilmeld.config.userFields.includes('email') &&
|
|
2590
|
+
tilmeld.config.verifyEmail &&
|
|
2591
|
+
tilmeld.config.unverifiedAccess &&
|
|
2592
|
+
this.$data.secret != null
|
|
2593
|
+
) {
|
|
2594
|
+
// Add the default secondaries for unverified users.
|
|
2595
|
+
this.$data.groups = await tnymph.getEntities(
|
|
2596
|
+
{ class: Group },
|
|
2597
|
+
{
|
|
2598
|
+
type: '&',
|
|
2599
|
+
equal: ['unverifiedSecondary', true],
|
|
2600
|
+
},
|
|
2601
|
+
);
|
|
2602
|
+
} else {
|
|
2603
|
+
// Add the default secondaries.
|
|
2604
|
+
this.$data.groups = await tnymph.getEntities(
|
|
2605
|
+
{ class: Group },
|
|
2606
|
+
{
|
|
2607
|
+
type: '&',
|
|
2608
|
+
equal: ['defaultSecondary', true],
|
|
2609
|
+
},
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
try {
|
|
2615
|
+
tilmeld.config.validatorUser(tilmeld, this);
|
|
1711
2616
|
} catch (e: any) {
|
|
1712
|
-
|
|
1713
|
-
this.$nymph = nymph;
|
|
1714
|
-
throw e;
|
|
2617
|
+
throw new BadDataError(e?.message);
|
|
1715
2618
|
}
|
|
2619
|
+
} catch (e: any) {
|
|
2620
|
+
await tnymph.rollback(transaction);
|
|
2621
|
+
this.$setNymph(nymph);
|
|
2622
|
+
throw e;
|
|
1716
2623
|
}
|
|
1717
2624
|
|
|
1718
|
-
let ret
|
|
2625
|
+
let ret = false;
|
|
1719
2626
|
let preGuid = this.guid;
|
|
1720
2627
|
let preCdate = this.cdate;
|
|
1721
2628
|
let preMdate = this.mdate;
|
|
@@ -1724,9 +2631,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1724
2631
|
ret = await super.$save();
|
|
1725
2632
|
} catch (e: any) {
|
|
1726
2633
|
await tnymph.rollback(transaction);
|
|
1727
|
-
this
|
|
2634
|
+
this.guid = preGuid;
|
|
2635
|
+
this.cdate = preCdate;
|
|
2636
|
+
this.mdate = preMdate;
|
|
2637
|
+
this.$setNymph(nymph);
|
|
1728
2638
|
throw e;
|
|
1729
2639
|
}
|
|
2640
|
+
|
|
1730
2641
|
if (ret) {
|
|
1731
2642
|
if (sendVerification) {
|
|
1732
2643
|
// The email has changed, so send a new verification email.
|
|
@@ -1735,23 +2646,31 @@ export default class User extends AbleObject<UserData> {
|
|
|
1735
2646
|
this.guid = preGuid;
|
|
1736
2647
|
this.cdate = preCdate;
|
|
1737
2648
|
this.mdate = preMdate;
|
|
1738
|
-
this.$nymph
|
|
2649
|
+
this.$setNymph(nymph);
|
|
1739
2650
|
throw new Error("Couldn't send verification email.");
|
|
1740
2651
|
}
|
|
1741
2652
|
}
|
|
1742
2653
|
|
|
1743
2654
|
this.$descendantGroups = undefined;
|
|
1744
2655
|
this.$gatekeeperCache = undefined;
|
|
1745
|
-
await tnymph.commit(transaction);
|
|
2656
|
+
ret = await tnymph.commit(transaction);
|
|
2657
|
+
} else {
|
|
2658
|
+
await tnymph.rollback(transaction);
|
|
2659
|
+
}
|
|
2660
|
+
this.$setNymph(nymph);
|
|
2661
|
+
|
|
2662
|
+
if (ret) {
|
|
2663
|
+
this.$originalEmail = this.$data.email;
|
|
2664
|
+
this.$originalUsername = this.$data.username;
|
|
1746
2665
|
|
|
1747
|
-
|
|
2666
|
+
const tilmeld = enforceTilmeld(nymph);
|
|
2667
|
+
|
|
2668
|
+
if (tilmeld.User.current(true).$is(this)) {
|
|
1748
2669
|
// Update the user in the session cache.
|
|
1749
|
-
|
|
2670
|
+
await tilmeld.fillSession(this);
|
|
1750
2671
|
}
|
|
1751
|
-
} else {
|
|
1752
|
-
await tnymph.rollback(transaction);
|
|
1753
2672
|
}
|
|
1754
|
-
|
|
2673
|
+
|
|
1755
2674
|
return ret;
|
|
1756
2675
|
}
|
|
1757
2676
|
|
|
@@ -1772,53 +2691,114 @@ export default class User extends AbleObject<UserData> {
|
|
|
1772
2691
|
}
|
|
1773
2692
|
|
|
1774
2693
|
public async $delete() {
|
|
1775
|
-
const tilmeld = this
|
|
1776
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1777
|
-
throw new
|
|
2694
|
+
const tilmeld = enforceTilmeld(this);
|
|
2695
|
+
if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
|
|
2696
|
+
throw new AccessControlError(
|
|
2697
|
+
"You don't have the authority to delete users.",
|
|
2698
|
+
);
|
|
1778
2699
|
}
|
|
1779
2700
|
if (
|
|
2701
|
+
!this.$skipAcWhenDeleting &&
|
|
1780
2702
|
!tilmeld.gatekeeper('system/admin') &&
|
|
1781
|
-
this.$
|
|
2703
|
+
this.$data.abilities?.includes('system/admin')
|
|
1782
2704
|
) {
|
|
1783
|
-
throw new
|
|
1784
|
-
"You don't have the authority to delete system admins."
|
|
2705
|
+
throw new AccessControlError(
|
|
2706
|
+
"You don't have the authority to delete system admins.",
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
if (this.$data.username !== this.$originalUsername) {
|
|
2710
|
+
throw new AccessControlError(
|
|
2711
|
+
"You can't delete a user while switching their username.",
|
|
1785
2712
|
);
|
|
1786
2713
|
}
|
|
2714
|
+
|
|
1787
2715
|
if (tilmeld.User.current(true).$is(this)) {
|
|
1788
2716
|
await this.$logout();
|
|
1789
2717
|
}
|
|
1790
|
-
|
|
2718
|
+
|
|
2719
|
+
if (this.$data.group != null && this.$is(this.$data.group.user)) {
|
|
2720
|
+
const transaction = 'tilmeld-delete-' + this.guid;
|
|
2721
|
+
const nymph = this.$nymph;
|
|
2722
|
+
const tnymph = await nymph.startTransaction(transaction);
|
|
2723
|
+
this.$setNymph(tnymph);
|
|
2724
|
+
|
|
2725
|
+
// Read the skip ac value here, because super.$delete changes it.
|
|
2726
|
+
const $skipAcWhenDeleting = this.$skipAcWhenDeleting;
|
|
2727
|
+
|
|
2728
|
+
// Delete the user.
|
|
2729
|
+
let success = await super.$delete();
|
|
2730
|
+
if (!success) {
|
|
2731
|
+
await tnymph.rollback(transaction);
|
|
2732
|
+
this.$setNymph(nymph);
|
|
2733
|
+
return success;
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// Delete the generated group.
|
|
2737
|
+
success = $skipAcWhenDeleting
|
|
2738
|
+
? await this.$data.group.$deleteSkipAC()
|
|
2739
|
+
: await this.$data.group.$delete();
|
|
2740
|
+
|
|
2741
|
+
if (success) {
|
|
2742
|
+
success = await tnymph.commit(transaction);
|
|
2743
|
+
} else {
|
|
2744
|
+
await tnymph.rollback(transaction);
|
|
2745
|
+
}
|
|
2746
|
+
this.$setNymph(nymph);
|
|
2747
|
+
return success;
|
|
2748
|
+
} else {
|
|
2749
|
+
return await super.$delete();
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
/*
|
|
2754
|
+
* This should *never* be accessible on the client.
|
|
2755
|
+
*/
|
|
2756
|
+
async $deleteSkipAC() {
|
|
2757
|
+
this.$skipAcWhenDeleting = true;
|
|
2758
|
+
return await this.$delete();
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
$tilmeldDeleteSkipAC() {
|
|
2762
|
+
if (this.$skipAcWhenDeleting) {
|
|
2763
|
+
this.$skipAcWhenDeleting = false;
|
|
2764
|
+
return true;
|
|
2765
|
+
}
|
|
2766
|
+
return false;
|
|
1791
2767
|
}
|
|
1792
2768
|
|
|
1793
2769
|
public static on<T extends EventType>(
|
|
1794
2770
|
event: T,
|
|
1795
|
-
callback: T extends '
|
|
1796
|
-
?
|
|
1797
|
-
: T extends '
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
2771
|
+
callback: T extends 'checkUsername'
|
|
2772
|
+
? TilmeldCheckUsernameCallback
|
|
2773
|
+
: T extends 'beforeRegister'
|
|
2774
|
+
? TilmeldBeforeRegisterCallback
|
|
2775
|
+
: T extends 'afterRegister'
|
|
2776
|
+
? TilmeldAfterRegisterCallback
|
|
2777
|
+
: T extends 'beforeLogin'
|
|
2778
|
+
? TilmeldBeforeLoginCallback
|
|
2779
|
+
: T extends 'afterLogin'
|
|
2780
|
+
? TilmeldAfterLoginCallback
|
|
2781
|
+
: T extends 'beforeLogout'
|
|
2782
|
+
? TilmeldBeforeLogoutCallback
|
|
2783
|
+
: T extends 'afterLogout'
|
|
2784
|
+
? TilmeldAfterLogoutCallback
|
|
2785
|
+
: never,
|
|
1808
2786
|
) {
|
|
1809
|
-
const prop = (event + 'Callbacks') as T extends '
|
|
1810
|
-
? '
|
|
1811
|
-
: T extends '
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2787
|
+
const prop = (event + 'Callbacks') as T extends 'checkUsername'
|
|
2788
|
+
? 'checkUsernameCallbacks'
|
|
2789
|
+
: T extends 'beforeRegister'
|
|
2790
|
+
? 'beforeRegisterCallbacks'
|
|
2791
|
+
: T extends 'afterRegister'
|
|
2792
|
+
? 'afterRegisterCallbacks'
|
|
2793
|
+
: T extends 'beforeLogin'
|
|
2794
|
+
? 'beforeLoginCallbacks'
|
|
2795
|
+
: T extends 'afterLogin'
|
|
2796
|
+
? 'afterLoginCallbacks'
|
|
2797
|
+
: T extends 'beforeLogout'
|
|
2798
|
+
? 'beforeLogoutCallbacks'
|
|
2799
|
+
: T extends 'afterLogout'
|
|
2800
|
+
? 'afterLogoutCallbacks'
|
|
2801
|
+
: never;
|
|
1822
2802
|
if (!(prop in this)) {
|
|
1823
2803
|
throw new Error('Invalid event type.');
|
|
1824
2804
|
}
|
|
@@ -1829,33 +2809,37 @@ export default class User extends AbleObject<UserData> {
|
|
|
1829
2809
|
|
|
1830
2810
|
public static off<T extends EventType>(
|
|
1831
2811
|
event: T,
|
|
1832
|
-
callback: T extends '
|
|
1833
|
-
?
|
|
1834
|
-
: T extends '
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2812
|
+
callback: T extends 'checkUsername'
|
|
2813
|
+
? TilmeldCheckUsernameCallback
|
|
2814
|
+
: T extends 'beforeRegister'
|
|
2815
|
+
? TilmeldBeforeRegisterCallback
|
|
2816
|
+
: T extends 'afterRegister'
|
|
2817
|
+
? TilmeldAfterRegisterCallback
|
|
2818
|
+
: T extends 'beforeLogin'
|
|
2819
|
+
? TilmeldBeforeLoginCallback
|
|
2820
|
+
: T extends 'afterLogin'
|
|
2821
|
+
? TilmeldAfterLoginCallback
|
|
2822
|
+
: T extends 'beforeLogout'
|
|
2823
|
+
? TilmeldBeforeLogoutCallback
|
|
2824
|
+
: T extends 'afterLogout'
|
|
2825
|
+
? TilmeldAfterLogoutCallback
|
|
2826
|
+
: never,
|
|
1845
2827
|
) {
|
|
1846
|
-
const prop = (event + 'Callbacks') as T extends '
|
|
1847
|
-
? '
|
|
1848
|
-
: T extends '
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2828
|
+
const prop = (event + 'Callbacks') as T extends 'checkUsername'
|
|
2829
|
+
? 'checkUsernameCallbacks'
|
|
2830
|
+
: T extends 'beforeRegister'
|
|
2831
|
+
? 'beforeRegisterCallbacks'
|
|
2832
|
+
: T extends 'afterRegister'
|
|
2833
|
+
? 'afterRegisterCallbacks'
|
|
2834
|
+
: T extends 'beforeLogin'
|
|
2835
|
+
? 'beforeLoginCallbacks'
|
|
2836
|
+
: T extends 'afterLogin'
|
|
2837
|
+
? 'afterLoginCallbacks'
|
|
2838
|
+
: T extends 'beforeLogout'
|
|
2839
|
+
? 'beforeLogoutCallbacks'
|
|
2840
|
+
: T extends 'afterLogout'
|
|
2841
|
+
? 'afterLogoutCallbacks'
|
|
2842
|
+
: never;
|
|
1859
2843
|
if (!(prop in this)) {
|
|
1860
2844
|
return false;
|
|
1861
2845
|
}
|