@nymphjs/tilmeld 1.0.0-beta.7 → 1.0.0-beta.70
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 +326 -0
- package/README.md +14 -1
- package/dist/AbleObject.d.ts +15 -0
- package/dist/AbleObject.js +16 -1
- package/dist/AbleObject.js.map +1 -1
- package/dist/Group.d.ts +130 -10
- package/dist/Group.js +311 -129
- package/dist/Group.js.map +1 -1
- package/dist/Group.test.js +77 -2
- package/dist/Group.test.js.map +1 -1
- package/dist/Tilmeld.d.ts +237 -9
- package/dist/Tilmeld.js +569 -201
- package/dist/Tilmeld.js.map +1 -1
- package/dist/User.d.ts +344 -16
- package/dist/User.js +979 -313
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +63 -2
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +250 -3
- package/dist/conf/defaults.js +146 -98
- package/dist/conf/defaults.js.map +1 -1
- package/dist/enforceTilmeld.d.ts +3 -0
- package/dist/enforceTilmeld.js +22 -0
- package/dist/enforceTilmeld.js.map +1 -0
- package/dist/errors/AccessControlError.d.ts +2 -1
- package/dist/errors/AccessControlError.js +3 -2
- package/dist/errors/AccessControlError.js.map +1 -1
- package/dist/errors/BadDataError.d.ts +2 -1
- package/dist/errors/BadDataError.js +3 -2
- package/dist/errors/BadDataError.js.map +1 -1
- package/dist/errors/BadEmailError.d.ts +2 -1
- package/dist/errors/BadEmailError.js +3 -2
- package/dist/errors/BadEmailError.js.map +1 -1
- package/dist/errors/BadUsernameError.d.ts +2 -1
- package/dist/errors/BadUsernameError.js +3 -2
- package/dist/errors/BadUsernameError.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +30 -26
- package/src/AbleObject.ts +2 -2
- package/src/Group.test.ts +102 -4
- package/src/Group.ts +274 -158
- package/src/Tilmeld.ts +383 -249
- package/src/User.test.ts +80 -4
- package/src/User.ts +1061 -382
- package/src/conf/d.ts +26 -13
- package/src/conf/defaults.ts +151 -104
- 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/index.ts +2 -0
- package/src/tilmeldJoiProps.ts +9 -9
- package/tsconfig.json +3 -3
- package/typedoc.json +4 -0
package/src/User.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {
|
|
2
|
+
Nymph,
|
|
2
3
|
EntityData,
|
|
3
4
|
EntityJson,
|
|
4
5
|
EntityPatch,
|
|
5
6
|
Selector,
|
|
6
7
|
SerializedEntityData,
|
|
7
8
|
} from '@nymphjs/nymph';
|
|
8
|
-
import {
|
|
9
|
+
import { humanSecret, nanoid } from '@nymphjs/guid';
|
|
10
|
+
import type { EmailOptions } from 'email-templates';
|
|
9
11
|
import strtotime from 'locutus/php/datetime/strtotime';
|
|
10
12
|
import Base64 from 'crypto-js/enc-base64';
|
|
11
13
|
import sha256 from 'crypto-js/sha256';
|
|
12
14
|
import md5 from 'crypto-js/md5';
|
|
13
15
|
import { difference } from 'lodash';
|
|
14
|
-
import {
|
|
16
|
+
import { TOTP, Secret } from 'otpauth';
|
|
17
|
+
import { toDataURL } from 'qrcode';
|
|
15
18
|
|
|
16
|
-
import
|
|
19
|
+
import { enforceTilmeld } from './enforceTilmeld';
|
|
17
20
|
import AbleObject from './AbleObject';
|
|
18
|
-
import Group
|
|
21
|
+
import type Group from './Group';
|
|
22
|
+
import type { GroupData } from './Group';
|
|
19
23
|
import {
|
|
20
24
|
BadDataError,
|
|
21
25
|
BadEmailError,
|
|
@@ -24,23 +28,33 @@ import {
|
|
|
24
28
|
} from './errors';
|
|
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 = {
|
|
@@ -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
|
|
|
@@ -241,9 +275,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
241
275
|
*/
|
|
242
276
|
private $gatekeeperCache?: { [k: string]: true };
|
|
243
277
|
/**
|
|
244
|
-
* This
|
|
278
|
+
* This should only be used by the backend.
|
|
245
279
|
*/
|
|
246
280
|
private $skipAcWhenSaving = false;
|
|
281
|
+
/**
|
|
282
|
+
* This should only be used by the backend.
|
|
283
|
+
*/
|
|
284
|
+
private $skipAcWhenDeleting = false;
|
|
247
285
|
/**
|
|
248
286
|
* The user's group descendants.
|
|
249
287
|
*/
|
|
@@ -257,10 +295,6 @@ export default class User extends AbleObject<UserData> {
|
|
|
257
295
|
*/
|
|
258
296
|
public $originalEmail?: string;
|
|
259
297
|
|
|
260
|
-
static async factory(guid?: string): Promise<User & UserData> {
|
|
261
|
-
return (await super.factory(guid)) as User & UserData;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
298
|
static async factoryUsername(username?: string): Promise<User & UserData> {
|
|
265
299
|
const entity = new this();
|
|
266
300
|
if (username != null) {
|
|
@@ -271,7 +305,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
271
305
|
{
|
|
272
306
|
type: '&',
|
|
273
307
|
ilike: ['username', username.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
274
|
-
}
|
|
308
|
+
},
|
|
275
309
|
);
|
|
276
310
|
if (entity != null) {
|
|
277
311
|
return entity;
|
|
@@ -280,34 +314,18 @@ export default class User extends AbleObject<UserData> {
|
|
|
280
314
|
return entity;
|
|
281
315
|
}
|
|
282
316
|
|
|
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
317
|
public static current(returnObjectIfNotExist: true): User & UserData;
|
|
301
318
|
public static current(
|
|
302
|
-
returnObjectIfNotExist?: false
|
|
319
|
+
returnObjectIfNotExist?: false,
|
|
303
320
|
): (User & UserData) | null;
|
|
304
321
|
public static current(
|
|
305
|
-
returnObjectIfNotExist?: boolean
|
|
322
|
+
returnObjectIfNotExist?: boolean,
|
|
306
323
|
): (User & UserData) | null {
|
|
307
|
-
|
|
324
|
+
const tilmeld = enforceTilmeld(this);
|
|
325
|
+
if (tilmeld.currentUser == null) {
|
|
308
326
|
return returnObjectIfNotExist ? this.factorySync() : null;
|
|
309
327
|
}
|
|
310
|
-
return
|
|
328
|
+
return tilmeld.currentUser;
|
|
311
329
|
}
|
|
312
330
|
|
|
313
331
|
/**
|
|
@@ -320,7 +338,8 @@ export default class User extends AbleObject<UserData> {
|
|
|
320
338
|
recoveryType: 'username' | 'password';
|
|
321
339
|
account: string;
|
|
322
340
|
}): Promise<{ result: boolean; message: string }> {
|
|
323
|
-
|
|
341
|
+
const tilmeld = enforceTilmeld(this);
|
|
342
|
+
if (!tilmeld.config.pwRecovery) {
|
|
324
343
|
return {
|
|
325
344
|
result: false,
|
|
326
345
|
message: 'Account recovery is not allowed.',
|
|
@@ -330,18 +349,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
330
349
|
let user: User & UserData;
|
|
331
350
|
const options: EmailOptions = {};
|
|
332
351
|
|
|
333
|
-
if (
|
|
334
|
-
!this.tilmeld.config.emailUsernames &&
|
|
335
|
-
data.recoveryType === 'username'
|
|
336
|
-
) {
|
|
352
|
+
if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
|
|
337
353
|
// Create a username recovery email.
|
|
338
354
|
|
|
339
355
|
const getUser = await this.nymph.getEntity(
|
|
340
|
-
{ class:
|
|
356
|
+
{ class: tilmeld.User, skipAc: true },
|
|
341
357
|
{
|
|
342
358
|
type: '&',
|
|
343
359
|
ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
344
|
-
}
|
|
360
|
+
},
|
|
345
361
|
);
|
|
346
362
|
|
|
347
363
|
if (getUser == null) {
|
|
@@ -364,7 +380,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
364
380
|
} else if (data.recoveryType === 'password') {
|
|
365
381
|
// Create a password recovery email.
|
|
366
382
|
|
|
367
|
-
const getUser = await
|
|
383
|
+
const getUser = await tilmeld.User.factoryUsername(data.account);
|
|
368
384
|
|
|
369
385
|
if (getUser.guid == null) {
|
|
370
386
|
return {
|
|
@@ -391,14 +407,19 @@ export default class User extends AbleObject<UserData> {
|
|
|
391
407
|
};
|
|
392
408
|
options.locals = {
|
|
393
409
|
recoverCode: getUser.recoverSecret,
|
|
394
|
-
timeLimit:
|
|
410
|
+
timeLimit: tilmeld.config.pwRecoveryTimeLimit,
|
|
395
411
|
};
|
|
396
412
|
} else {
|
|
397
413
|
return { result: false, message: 'Invalid recovery type.' };
|
|
398
414
|
}
|
|
399
415
|
|
|
416
|
+
this.nymph.config.debugLog(
|
|
417
|
+
'tilmeld',
|
|
418
|
+
`User account recovery (${options.template}) request for "${user.username}".`,
|
|
419
|
+
);
|
|
420
|
+
|
|
400
421
|
// Send the email.
|
|
401
|
-
if (await
|
|
422
|
+
if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
|
|
402
423
|
return {
|
|
403
424
|
result: true,
|
|
404
425
|
message:
|
|
@@ -421,22 +442,23 @@ export default class User extends AbleObject<UserData> {
|
|
|
421
442
|
secret: string;
|
|
422
443
|
password: string;
|
|
423
444
|
}): Promise<{ result: boolean; message: string }> {
|
|
424
|
-
|
|
445
|
+
const tilmeld = enforceTilmeld(this);
|
|
446
|
+
if (!tilmeld.config.pwRecovery) {
|
|
425
447
|
return {
|
|
426
448
|
result: false,
|
|
427
449
|
message: 'Account recovery is not allowed.',
|
|
428
450
|
};
|
|
429
451
|
}
|
|
430
452
|
|
|
431
|
-
const user = await
|
|
453
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
432
454
|
|
|
433
455
|
if (
|
|
434
456
|
user.guid == null ||
|
|
435
457
|
user.recoverSecret == null ||
|
|
436
458
|
data.secret !== user.recoverSecret ||
|
|
437
459
|
strtotime(
|
|
438
|
-
'+' +
|
|
439
|
-
Math.floor((user.recoverSecretDate ?? 0) / 1000)
|
|
460
|
+
'+' + tilmeld.config.pwRecoveryTimeLimit,
|
|
461
|
+
Math.floor((user.recoverSecretDate ?? 0) / 1000),
|
|
440
462
|
) *
|
|
441
463
|
1000 <
|
|
442
464
|
Date.now()
|
|
@@ -451,6 +473,11 @@ export default class User extends AbleObject<UserData> {
|
|
|
451
473
|
return { result: false, message: 'Password cannot be empty.' };
|
|
452
474
|
}
|
|
453
475
|
|
|
476
|
+
this.nymph.config.debugLog(
|
|
477
|
+
'tilmeld',
|
|
478
|
+
`User account recovery for "${user.username}".`,
|
|
479
|
+
);
|
|
480
|
+
|
|
454
481
|
user.$password(data.password);
|
|
455
482
|
delete user.recoverSecret;
|
|
456
483
|
delete user.recoverSecretDate;
|
|
@@ -466,27 +493,30 @@ export default class User extends AbleObject<UserData> {
|
|
|
466
493
|
}
|
|
467
494
|
|
|
468
495
|
public static getClientConfig() {
|
|
496
|
+
const tilmeld = enforceTilmeld(this);
|
|
469
497
|
return {
|
|
470
|
-
regFields:
|
|
471
|
-
userFields:
|
|
472
|
-
emailUsernames:
|
|
473
|
-
allowRegistration:
|
|
474
|
-
allowUsernameChange:
|
|
475
|
-
pwRecovery:
|
|
476
|
-
verifyEmail:
|
|
477
|
-
unverifiedAccess:
|
|
498
|
+
regFields: tilmeld.config.regFields,
|
|
499
|
+
userFields: tilmeld.config.userFields,
|
|
500
|
+
emailUsernames: tilmeld.config.emailUsernames,
|
|
501
|
+
allowRegistration: tilmeld.config.allowRegistration,
|
|
502
|
+
allowUsernameChange: tilmeld.config.allowUsernameChange,
|
|
503
|
+
pwRecovery: tilmeld.config.pwRecovery,
|
|
504
|
+
verifyEmail: tilmeld.config.verifyEmail,
|
|
505
|
+
unverifiedAccess: tilmeld.config.unverifiedAccess,
|
|
478
506
|
};
|
|
479
507
|
}
|
|
480
508
|
|
|
481
509
|
public static async loginUser(data: {
|
|
482
510
|
username: string;
|
|
483
511
|
password: string;
|
|
512
|
+
code?: string;
|
|
484
513
|
additionalData?: { [k: string]: any };
|
|
485
514
|
}) {
|
|
515
|
+
const tilmeld = enforceTilmeld(this);
|
|
486
516
|
if (!('username' in data) || !data.username.length) {
|
|
487
517
|
return { result: false, message: 'Incorrect login/password.' };
|
|
488
518
|
}
|
|
489
|
-
const user = await
|
|
519
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
490
520
|
const result: { result: boolean; message: string; user?: User & UserData } =
|
|
491
521
|
await user.$login(data);
|
|
492
522
|
if (result.result) {
|
|
@@ -496,25 +526,85 @@ export default class User extends AbleObject<UserData> {
|
|
|
496
526
|
return result;
|
|
497
527
|
}
|
|
498
528
|
|
|
529
|
+
constructor() {
|
|
530
|
+
super();
|
|
531
|
+
|
|
532
|
+
// Defaults.
|
|
533
|
+
this.$data.enabled = true;
|
|
534
|
+
this.$data.abilities = [];
|
|
535
|
+
this.$data.groups = [];
|
|
536
|
+
this.$data.inheritAbilities = true;
|
|
537
|
+
this.$updateDataProtection();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
$setNymph(nymph: Nymph) {
|
|
541
|
+
this.$nymph = nymph;
|
|
542
|
+
if (!this.$asleep()) {
|
|
543
|
+
if (this.$data.group && this.$data.group.$nymph !== nymph) {
|
|
544
|
+
this.$data.group.$setNymph(nymph);
|
|
545
|
+
}
|
|
546
|
+
for (const group of this.$data.groups ?? []) {
|
|
547
|
+
if (group && group.$nymph !== nymph) {
|
|
548
|
+
group.$setNymph(nymph);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async $getUniques(): Promise<string[]> {
|
|
555
|
+
const tilmeld = enforceTilmeld(this);
|
|
556
|
+
const uniques = [`u:${this.$data.username}`];
|
|
557
|
+
if (
|
|
558
|
+
!tilmeld.config.emailUsernames &&
|
|
559
|
+
tilmeld.config.userFields.includes('email')
|
|
560
|
+
) {
|
|
561
|
+
uniques.push(`e:${this.$data.email}`);
|
|
562
|
+
|
|
563
|
+
if (this.$data.newEmailAddress != null) {
|
|
564
|
+
uniques.push(`e:${this.$data.newEmailAddress}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return uniques;
|
|
568
|
+
}
|
|
569
|
+
|
|
499
570
|
public async $login(data: {
|
|
500
571
|
username: string;
|
|
501
572
|
password: string;
|
|
573
|
+
code?: string;
|
|
502
574
|
additionalData?: { [k: string]: any };
|
|
503
575
|
}) {
|
|
504
|
-
const tilmeld = this
|
|
576
|
+
const tilmeld = enforceTilmeld(this);
|
|
505
577
|
if (this.guid == null) {
|
|
506
578
|
return { result: false, message: 'Incorrect login/password.' };
|
|
507
579
|
}
|
|
508
580
|
if (!this.$data.enabled) {
|
|
509
581
|
return { result: false, message: 'This user is disabled.' };
|
|
510
582
|
}
|
|
511
|
-
if (this.$gatekeeper()) {
|
|
583
|
+
if (await this.$gatekeeper()) {
|
|
512
584
|
return { result: true, message: 'You are already logged in.' };
|
|
513
585
|
}
|
|
514
586
|
if (!this.$checkPassword(data.password)) {
|
|
515
587
|
return { result: false, message: 'Incorrect login/password.' };
|
|
516
588
|
}
|
|
517
589
|
|
|
590
|
+
if (this.$data.totpSecret != null) {
|
|
591
|
+
if (
|
|
592
|
+
data.code == null ||
|
|
593
|
+
typeof data.code !== 'string' ||
|
|
594
|
+
data.code.length !== 6
|
|
595
|
+
) {
|
|
596
|
+
return {
|
|
597
|
+
result: false,
|
|
598
|
+
needTOTP: true,
|
|
599
|
+
message: 'You need to provide a 2FA code.',
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (!this.$checkTOTPCode(data.code)) {
|
|
604
|
+
return { result: false, message: 'Incorrect 2FA code.' };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
518
608
|
try {
|
|
519
609
|
for (let callback of (this.constructor as typeof User)
|
|
520
610
|
.beforeLoginCallbacks) {
|
|
@@ -529,8 +619,90 @@ export default class User extends AbleObject<UserData> {
|
|
|
529
619
|
};
|
|
530
620
|
}
|
|
531
621
|
|
|
622
|
+
this.$nymph.config.debugLog(
|
|
623
|
+
'tilmeld',
|
|
624
|
+
`User login for "${this.$data.username}".`,
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
// Authentication was successful, attempt to login.
|
|
628
|
+
if (!(await tilmeld.login(this, true))) {
|
|
629
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
for (let callback of (this.constructor as typeof User)
|
|
633
|
+
.afterLoginCallbacks) {
|
|
634
|
+
if (callback) {
|
|
635
|
+
await callback(this);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Login was successful.
|
|
640
|
+
return { result: true, message: 'You are logged in.' };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
public async $switchUser(data?: { additionalData?: { [k: string]: any } }) {
|
|
644
|
+
const tilmeld = enforceTilmeld(this);
|
|
645
|
+
if (this.guid == null) {
|
|
646
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
let user = tilmeld.User.current();
|
|
650
|
+
|
|
651
|
+
if (!user || user.guid == null) {
|
|
652
|
+
return { result: false, message: "You're not logged in." };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
656
|
+
|
|
657
|
+
if (isCurrentUser) {
|
|
658
|
+
return { result: false, message: "You can't switch to yourself." };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const toAbilities = this.$data.abilities || [];
|
|
662
|
+
const fromAbilities = user.abilities || [];
|
|
663
|
+
|
|
664
|
+
if (
|
|
665
|
+
!fromAbilities.includes('tilmeld/switch') &&
|
|
666
|
+
!fromAbilities.includes('system/admin')
|
|
667
|
+
) {
|
|
668
|
+
return {
|
|
669
|
+
result: false,
|
|
670
|
+
message: "You don't have permission to switch users.",
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (
|
|
675
|
+
toAbilities.includes('system/admin') ||
|
|
676
|
+
toAbilities.includes('tilmeld/admin')
|
|
677
|
+
) {
|
|
678
|
+
return { result: false, message: "You can't switch to an admin." };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
for (let callback of (this.constructor as typeof User)
|
|
683
|
+
.beforeLoginCallbacks) {
|
|
684
|
+
if (callback) {
|
|
685
|
+
await callback(this, {
|
|
686
|
+
username: this.$data.username || '',
|
|
687
|
+
password: '',
|
|
688
|
+
...(data || {}),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
} catch (e: any) {
|
|
693
|
+
return {
|
|
694
|
+
result: false,
|
|
695
|
+
message: e.message,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
this.$nymph.config.debugLog(
|
|
700
|
+
'tilmeld',
|
|
701
|
+
`User login switch for "${user.username}" to "${this.$data.username}".`,
|
|
702
|
+
);
|
|
703
|
+
|
|
532
704
|
// Authentication was successful, attempt to login.
|
|
533
|
-
if (!tilmeld.
|
|
705
|
+
if (!(await tilmeld.loginSwitch(this, true))) {
|
|
534
706
|
return { result: false, message: 'Incorrect login/password.' };
|
|
535
707
|
}
|
|
536
708
|
|
|
@@ -550,7 +722,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
550
722
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
551
723
|
*/
|
|
552
724
|
public async $logout() {
|
|
553
|
-
const tilmeld = this
|
|
725
|
+
const tilmeld = enforceTilmeld(this);
|
|
554
726
|
|
|
555
727
|
try {
|
|
556
728
|
for (let callback of (this.constructor as typeof User)
|
|
@@ -566,7 +738,12 @@ export default class User extends AbleObject<UserData> {
|
|
|
566
738
|
};
|
|
567
739
|
}
|
|
568
740
|
|
|
569
|
-
|
|
741
|
+
this.$nymph.config.debugLog(
|
|
742
|
+
'tilmeld',
|
|
743
|
+
`User logout for "${this.$data.username}".`,
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
await tilmeld.logout();
|
|
570
747
|
|
|
571
748
|
for (let callback of (this.constructor as typeof User)
|
|
572
749
|
.afterLogoutCallbacks) {
|
|
@@ -592,36 +769,36 @@ export default class User extends AbleObject<UserData> {
|
|
|
592
769
|
);
|
|
593
770
|
}
|
|
594
771
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
public async $getDescendantGroups(): Promise<(Group & GroupData)[]> {
|
|
599
|
-
if (this.$descendantGroups == null) {
|
|
600
|
-
this.$descendantGroups = [];
|
|
601
|
-
if (this.$data.group != null) {
|
|
602
|
-
this.$descendantGroups = await this.$data.group.$getDescendants();
|
|
603
|
-
}
|
|
604
|
-
for (let curGroup of this.$data.groups ?? []) {
|
|
605
|
-
this.$descendantGroups = this.$descendantGroups?.concat(
|
|
606
|
-
await curGroup.$getDescendants()
|
|
607
|
-
);
|
|
608
|
-
}
|
|
772
|
+
public $getGid() {
|
|
773
|
+
if (this.$data.group) {
|
|
774
|
+
return this.$data.group.guid;
|
|
609
775
|
}
|
|
610
|
-
return
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
public $getGids() {
|
|
780
|
+
if (this.$data.groups) {
|
|
781
|
+
return this.$data.groups
|
|
782
|
+
.map((group) => group.guid)
|
|
783
|
+
.filter((guid) => guid != null) as string[];
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
611
786
|
}
|
|
612
787
|
|
|
613
788
|
/**
|
|
614
789
|
* Get the user's group descendants.
|
|
615
790
|
*/
|
|
616
|
-
public $
|
|
791
|
+
public async $getDescendantGroups(): Promise<(Group & GroupData)[]> {
|
|
617
792
|
if (this.$descendantGroups == null) {
|
|
618
793
|
this.$descendantGroups = [];
|
|
619
794
|
if (this.$data.group != null) {
|
|
620
|
-
|
|
795
|
+
await this.$data.group.$wake();
|
|
796
|
+
this.$descendantGroups = await this.$data.group.$getDescendants();
|
|
621
797
|
}
|
|
798
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
622
799
|
for (let curGroup of this.$data.groups ?? []) {
|
|
623
800
|
this.$descendantGroups = this.$descendantGroups?.concat(
|
|
624
|
-
curGroup.$
|
|
801
|
+
await curGroup.$getDescendants(),
|
|
625
802
|
);
|
|
626
803
|
}
|
|
627
804
|
}
|
|
@@ -629,18 +806,42 @@ export default class User extends AbleObject<UserData> {
|
|
|
629
806
|
}
|
|
630
807
|
|
|
631
808
|
public $jsonAcceptData(input: EntityJson, allowConflict = false) {
|
|
632
|
-
const tilmeld = this
|
|
633
|
-
this.$
|
|
809
|
+
const tilmeld = enforceTilmeld(this);
|
|
810
|
+
this.$check();
|
|
634
811
|
|
|
635
812
|
if (
|
|
636
813
|
'abilities' in input.data &&
|
|
637
|
-
input.data.abilities
|
|
638
|
-
|
|
639
|
-
tilmeld.
|
|
640
|
-
|
|
814
|
+
input.data.abilities?.includes('system/admin') !==
|
|
815
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
816
|
+
(!tilmeld.currentUser ||
|
|
817
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
818
|
+
) {
|
|
819
|
+
throw new BadDataError(
|
|
820
|
+
"You don't have the authority to grant or revoke system/admin.",
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (
|
|
825
|
+
'abilities' in input.data &&
|
|
826
|
+
input.data.abilities?.includes('tilmeld/admin') !==
|
|
827
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
828
|
+
(!tilmeld.currentUser ||
|
|
829
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
830
|
+
) {
|
|
831
|
+
throw new BadDataError(
|
|
832
|
+
"You don't have the authority to grant or revoke tilmeld/admin.",
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (
|
|
837
|
+
'abilities' in input.data &&
|
|
838
|
+
input.data.abilities?.includes('tilmeld/switch') !==
|
|
839
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
840
|
+
(!tilmeld.currentUser ||
|
|
841
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
641
842
|
) {
|
|
642
843
|
throw new BadDataError(
|
|
643
|
-
"You don't have the authority to
|
|
844
|
+
"You don't have the authority to grant or revoke tilmeld/switch.",
|
|
644
845
|
);
|
|
645
846
|
}
|
|
646
847
|
|
|
@@ -649,18 +850,42 @@ export default class User extends AbleObject<UserData> {
|
|
|
649
850
|
}
|
|
650
851
|
|
|
651
852
|
public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
|
|
652
|
-
const tilmeld = this
|
|
653
|
-
this.$
|
|
853
|
+
const tilmeld = enforceTilmeld(this);
|
|
854
|
+
this.$check();
|
|
654
855
|
|
|
655
856
|
if (
|
|
656
857
|
'abilities' in patch.set &&
|
|
657
|
-
patch.set.abilities
|
|
658
|
-
|
|
659
|
-
tilmeld.
|
|
660
|
-
|
|
858
|
+
patch.set.abilities?.includes('system/admin') !==
|
|
859
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
860
|
+
(!tilmeld.currentUser ||
|
|
861
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
862
|
+
) {
|
|
863
|
+
throw new BadDataError(
|
|
864
|
+
"You don't have the authority to grant or revoke system/admin.",
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (
|
|
869
|
+
'abilities' in patch.set &&
|
|
870
|
+
patch.set.abilities?.includes('tilmeld/admin') !==
|
|
871
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
872
|
+
(!tilmeld.currentUser ||
|
|
873
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
874
|
+
) {
|
|
875
|
+
throw new BadDataError(
|
|
876
|
+
"You don't have the authority to grant or revoke tilmeld/admin.",
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (
|
|
881
|
+
'abilities' in patch.set &&
|
|
882
|
+
patch.set.abilities?.includes('tilmeld/switch') !==
|
|
883
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
884
|
+
(!tilmeld.currentUser ||
|
|
885
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
661
886
|
) {
|
|
662
887
|
throw new BadDataError(
|
|
663
|
-
"You don't have the authority to
|
|
888
|
+
"You don't have the authority to grant or revoke tilmeld/switch.",
|
|
664
889
|
);
|
|
665
890
|
}
|
|
666
891
|
|
|
@@ -679,7 +904,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
679
904
|
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
680
905
|
*/
|
|
681
906
|
public $updateDataProtection(givenUser?: User & UserData) {
|
|
682
|
-
const tilmeld = this
|
|
907
|
+
const tilmeld = enforceTilmeld(this);
|
|
683
908
|
let user = givenUser ?? tilmeld.User.current();
|
|
684
909
|
|
|
685
910
|
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
@@ -703,17 +928,24 @@ export default class User extends AbleObject<UserData> {
|
|
|
703
928
|
// Users can check to see what abilities they have.
|
|
704
929
|
this.$clientEnabledMethods.push('$gatekeeper');
|
|
705
930
|
this.$clientEnabledMethods.push('$changePassword');
|
|
931
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
706
932
|
this.$clientEnabledMethods.push('$logout');
|
|
707
933
|
this.$clientEnabledMethods.push('$sendEmailVerification');
|
|
934
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
935
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
936
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
937
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
708
938
|
}
|
|
709
939
|
|
|
710
940
|
if (
|
|
711
941
|
user != null &&
|
|
712
|
-
(abilities.
|
|
713
|
-
abilities.
|
|
942
|
+
(abilities.includes('tilmeld/admin') ||
|
|
943
|
+
abilities.includes('system/admin'))
|
|
714
944
|
) {
|
|
715
945
|
// Users who can edit other users can see most of their data.
|
|
716
|
-
this.$privateData = ['password', 'salt'];
|
|
946
|
+
this.$privateData = ['password', 'salt', 'totpSecret'];
|
|
947
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
948
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
717
949
|
this.$allowlistData = undefined;
|
|
718
950
|
} else if (isCurrentUser || isNewUser) {
|
|
719
951
|
// Users can see their own data, and edit some of it.
|
|
@@ -721,30 +953,69 @@ export default class User extends AbleObject<UserData> {
|
|
|
721
953
|
this.$allowlistData.push('username');
|
|
722
954
|
}
|
|
723
955
|
this.$allowlistData.push('avatar');
|
|
724
|
-
if (tilmeld.config.userFields.
|
|
956
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
725
957
|
this.$allowlistData.push('nameFirst');
|
|
726
958
|
this.$allowlistData.push('nameMiddle');
|
|
727
959
|
this.$allowlistData.push('nameLast');
|
|
728
960
|
this.$allowlistData.push('name');
|
|
729
961
|
}
|
|
730
|
-
if (tilmeld.config.userFields.
|
|
962
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
731
963
|
this.$allowlistData.push('email');
|
|
732
964
|
}
|
|
733
|
-
if (tilmeld.config.userFields.
|
|
965
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
734
966
|
this.$allowlistData.push('phone');
|
|
735
967
|
}
|
|
736
968
|
this.$privateData = [
|
|
969
|
+
// The things that are commented are there to show what is _not_ private.
|
|
970
|
+
// 'abilities',
|
|
971
|
+
// 'username',
|
|
972
|
+
// 'nameFirst',
|
|
973
|
+
// 'nameMiddle',
|
|
974
|
+
// 'nameLast',
|
|
975
|
+
// 'name',
|
|
976
|
+
// 'email',
|
|
977
|
+
// 'avatar',
|
|
978
|
+
// 'phone',
|
|
979
|
+
// 'group',
|
|
980
|
+
// 'groups',
|
|
981
|
+
// 'inheritAbilities',
|
|
982
|
+
'enabled',
|
|
737
983
|
'secret',
|
|
984
|
+
'emailChangeDate',
|
|
738
985
|
'newEmailSecret',
|
|
739
|
-
'
|
|
986
|
+
// 'newEmailAddress',
|
|
740
987
|
'cancelEmailSecret',
|
|
741
|
-
'
|
|
988
|
+
'cancelEmailAddress',
|
|
742
989
|
'recoverSecret',
|
|
743
990
|
'recoverSecretDate',
|
|
744
|
-
'password',
|
|
745
991
|
'salt',
|
|
992
|
+
'password',
|
|
993
|
+
'passwordTemp',
|
|
994
|
+
// 'revokeTokenDate',
|
|
995
|
+
'totpSecret',
|
|
746
996
|
];
|
|
747
997
|
}
|
|
998
|
+
|
|
999
|
+
if (
|
|
1000
|
+
user != null &&
|
|
1001
|
+
!isCurrentUser &&
|
|
1002
|
+
(abilities.includes('tilmeld/switch') ||
|
|
1003
|
+
abilities.includes('system/admin'))
|
|
1004
|
+
) {
|
|
1005
|
+
// Users with this ability can switch to other users.
|
|
1006
|
+
this.$clientEnabledMethods.push('$switchUser');
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
public $updateGroupDataProtection() {
|
|
1011
|
+
if (this.$data.group != null && !this.$data.group.$asleep()) {
|
|
1012
|
+
this.$data.group.$updateDataProtection();
|
|
1013
|
+
}
|
|
1014
|
+
for (let group of this.$data.groups ?? []) {
|
|
1015
|
+
if (!group.$asleep()) {
|
|
1016
|
+
group.$updateDataProtection();
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
748
1019
|
}
|
|
749
1020
|
|
|
750
1021
|
/**
|
|
@@ -761,35 +1032,43 @@ export default class User extends AbleObject<UserData> {
|
|
|
761
1032
|
* @param ability The ability.
|
|
762
1033
|
* @returns True or false.
|
|
763
1034
|
*/
|
|
764
|
-
public $gatekeeper(ability?: string): boolean {
|
|
765
|
-
|
|
1035
|
+
public async $gatekeeper(ability?: string): Promise<boolean> {
|
|
1036
|
+
await this.$wake();
|
|
1037
|
+
const tilmeld = enforceTilmeld(this);
|
|
766
1038
|
if (ability == null) {
|
|
767
1039
|
return tilmeld.User.current()?.$is(this) ?? false;
|
|
768
1040
|
}
|
|
769
1041
|
// Check the cache to see if we've already checked this user.
|
|
770
1042
|
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
|
-
);
|
|
1043
|
+
this.$gatekeeperCache = await this.$getGatekeeperCache();
|
|
789
1044
|
}
|
|
790
1045
|
return (
|
|
791
1046
|
(ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
|
|
792
|
-
!!this.$
|
|
1047
|
+
!!this.$data.abilities?.includes('system/admin')
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Build a gatekeeper cache object.
|
|
1053
|
+
*/
|
|
1054
|
+
public async $getGatekeeperCache() {
|
|
1055
|
+
await this.$wake();
|
|
1056
|
+
let abilities = this.$data.abilities ?? [];
|
|
1057
|
+
if (this.$data.inheritAbilities) {
|
|
1058
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1059
|
+
for (let curGroup of this.$data.groups ?? []) {
|
|
1060
|
+
if (curGroup.enabled) {
|
|
1061
|
+
abilities = abilities.concat(curGroup.abilities ?? []);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
await this.$data.group?.$wake();
|
|
1065
|
+
const group = this.$data.group;
|
|
1066
|
+
if (group != null && group.cdate != null && group.enabled) {
|
|
1067
|
+
abilities = abilities.concat(group.abilities ?? []);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return Object.fromEntries(
|
|
1071
|
+
abilities.map((ability) => [ability, true as true]),
|
|
793
1072
|
);
|
|
794
1073
|
}
|
|
795
1074
|
|
|
@@ -806,7 +1085,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
806
1085
|
* @returns True on success, false on failure.
|
|
807
1086
|
*/
|
|
808
1087
|
public async $sendEmailVerification() {
|
|
809
|
-
const tilmeld = this
|
|
1088
|
+
const tilmeld = enforceTilmeld(this);
|
|
810
1089
|
if (this.guid == null) {
|
|
811
1090
|
return false;
|
|
812
1091
|
}
|
|
@@ -818,7 +1097,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
818
1097
|
|
|
819
1098
|
if (this.$data.secret != null) {
|
|
820
1099
|
const link = `${verifyUrl}?action=verify&id=${encodeURIComponent(
|
|
821
|
-
this.guid
|
|
1100
|
+
this.guid,
|
|
822
1101
|
)}&secret=${encodeURIComponent(this.$data.secret)}`;
|
|
823
1102
|
success =
|
|
824
1103
|
success &&
|
|
@@ -836,13 +1115,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
836
1115
|
verifyLink: link,
|
|
837
1116
|
},
|
|
838
1117
|
},
|
|
839
|
-
this
|
|
1118
|
+
this,
|
|
840
1119
|
));
|
|
841
1120
|
}
|
|
842
1121
|
|
|
843
1122
|
if (this.$data.newEmailSecret != null) {
|
|
844
1123
|
const link = `${verifyUrl}?action=verifychange&id=${encodeURIComponent(
|
|
845
|
-
this.guid
|
|
1124
|
+
this.guid,
|
|
846
1125
|
)}&secret=${encodeURIComponent(this.$data.newEmailSecret)}`;
|
|
847
1126
|
success =
|
|
848
1127
|
success &&
|
|
@@ -862,13 +1141,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
862
1141
|
newEmail: this.$data.newEmailAddress,
|
|
863
1142
|
},
|
|
864
1143
|
},
|
|
865
|
-
this
|
|
1144
|
+
this,
|
|
866
1145
|
));
|
|
867
1146
|
}
|
|
868
1147
|
|
|
869
1148
|
if (this.$data.cancelEmailSecret != null) {
|
|
870
1149
|
const link = `${verifyUrl}?action=cancelchange&id=${encodeURIComponent(
|
|
871
|
-
this.guid
|
|
1150
|
+
this.guid,
|
|
872
1151
|
)}&secret=${encodeURIComponent(this.$data.cancelEmailSecret)}`;
|
|
873
1152
|
success =
|
|
874
1153
|
success &&
|
|
@@ -888,7 +1167,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
888
1167
|
newEmail: this.$data.newEmailAddress ?? this.$data.email,
|
|
889
1168
|
},
|
|
890
1169
|
},
|
|
891
|
-
this
|
|
1170
|
+
this,
|
|
892
1171
|
));
|
|
893
1172
|
}
|
|
894
1173
|
|
|
@@ -901,13 +1180,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
901
1180
|
* @param group The group.
|
|
902
1181
|
* @returns True if the user is already in the group. The resulting array of groups if the user was not.
|
|
903
1182
|
*/
|
|
904
|
-
public $addGroup(group: Group & GroupData) {
|
|
905
|
-
|
|
906
|
-
|
|
1183
|
+
public async $addGroup(group: Group & GroupData) {
|
|
1184
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1185
|
+
let groups = this.$data.groups;
|
|
1186
|
+
if (groups == null) {
|
|
1187
|
+
groups = [];
|
|
907
1188
|
}
|
|
908
|
-
if (!group.$inArray(
|
|
909
|
-
|
|
910
|
-
|
|
1189
|
+
if (!group.$inArray(groups)) {
|
|
1190
|
+
groups.push(group);
|
|
1191
|
+
this.$data.groups = groups;
|
|
1192
|
+
return groups;
|
|
911
1193
|
}
|
|
912
1194
|
return true;
|
|
913
1195
|
}
|
|
@@ -919,7 +1201,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
919
1201
|
* @returns True if the passwords match, otherwise false.
|
|
920
1202
|
*/
|
|
921
1203
|
public $checkPassword(password: string) {
|
|
922
|
-
const tilmeld = this
|
|
1204
|
+
const tilmeld = enforceTilmeld(this);
|
|
923
1205
|
switch (tilmeld.config.pwMethod) {
|
|
924
1206
|
case 'plain':
|
|
925
1207
|
return this.$data.password === password;
|
|
@@ -940,10 +1222,12 @@ export default class User extends AbleObject<UserData> {
|
|
|
940
1222
|
* @param group The group.
|
|
941
1223
|
* @returns True if the user wasn't in the group. The resulting array of groups if the user was.
|
|
942
1224
|
*/
|
|
943
|
-
public $delGroup(group: Group & GroupData) {
|
|
944
|
-
|
|
1225
|
+
public async $delGroup(group: Group & GroupData) {
|
|
1226
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1227
|
+
let groups = this.$data.groups || [];
|
|
1228
|
+
if (this.$data.groups != null && group.$inArray(groups)) {
|
|
945
1229
|
const newGroups: (Group & GroupData)[] = [];
|
|
946
|
-
for (let curGroup of
|
|
1230
|
+
for (let curGroup of groups) {
|
|
947
1231
|
if (!group.$is(curGroup)) {
|
|
948
1232
|
newGroups.push(curGroup);
|
|
949
1233
|
}
|
|
@@ -960,7 +1244,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
960
1244
|
* @param group The group, or the group's GUID.
|
|
961
1245
|
* @returns True or false.
|
|
962
1246
|
*/
|
|
963
|
-
public $inGroup(group: (Group & GroupData) | string) {
|
|
1247
|
+
public async $inGroup(group: (Group & GroupData) | string) {
|
|
964
1248
|
if (typeof group === 'string') {
|
|
965
1249
|
if (this.$data.group?.guid === group) {
|
|
966
1250
|
return true;
|
|
@@ -983,16 +1267,19 @@ export default class User extends AbleObject<UserData> {
|
|
|
983
1267
|
* @param group The group, or the group's GUID.
|
|
984
1268
|
* @returns True or false.
|
|
985
1269
|
*/
|
|
986
|
-
public $isDescendant(group: (Group & GroupData) | string) {
|
|
1270
|
+
public async $isDescendant(group: (Group & GroupData) | string) {
|
|
987
1271
|
// Check to see if the user is in a descendant group of the given group.
|
|
1272
|
+
await this.$data.group?.$wake();
|
|
988
1273
|
if (
|
|
1274
|
+
this.$data.group != null &&
|
|
989
1275
|
this.$data.group?.cdate != null &&
|
|
990
|
-
this.$data.group
|
|
1276
|
+
(await this.$data.group?.$isDescendant(group))
|
|
991
1277
|
) {
|
|
992
1278
|
return true;
|
|
993
1279
|
}
|
|
1280
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
994
1281
|
for (let curGroup of this.$data.groups ?? []) {
|
|
995
|
-
if (curGroup.$isDescendant(group)) {
|
|
1282
|
+
if (await curGroup.$isDescendant(group)) {
|
|
996
1283
|
return true;
|
|
997
1284
|
}
|
|
998
1285
|
}
|
|
@@ -1008,15 +1295,31 @@ export default class User extends AbleObject<UserData> {
|
|
|
1008
1295
|
public async $changePassword(data: {
|
|
1009
1296
|
newPassword: string;
|
|
1010
1297
|
currentPassword: string;
|
|
1298
|
+
revokeCurrentTokens?: boolean;
|
|
1011
1299
|
}): Promise<{ result: boolean; message: string }> {
|
|
1300
|
+
this.$nymph.config.debugLog(
|
|
1301
|
+
'tilmeld',
|
|
1302
|
+
`Password change request for "${this.$data.username}".`,
|
|
1303
|
+
);
|
|
1012
1304
|
if (!('newPassword' in data) || !data.newPassword.length) {
|
|
1013
1305
|
return { result: false, message: 'Please specify a password.' };
|
|
1014
1306
|
}
|
|
1015
1307
|
if (!this.$checkPassword(data.currentPassword ?? '')) {
|
|
1308
|
+
this.$nymph.config.debugLog(
|
|
1309
|
+
'tilmeld',
|
|
1310
|
+
`Incorrect password for change request for "${this.$data.username}".`,
|
|
1311
|
+
);
|
|
1016
1312
|
return { result: false, message: 'Incorrect password.' };
|
|
1017
1313
|
}
|
|
1018
1314
|
this.$data.passwordTemp = data.newPassword;
|
|
1315
|
+
if (data.revokeCurrentTokens) {
|
|
1316
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1317
|
+
}
|
|
1019
1318
|
if (await this.$save()) {
|
|
1319
|
+
if (data.revokeCurrentTokens) {
|
|
1320
|
+
const tilmeld = enforceTilmeld(this);
|
|
1321
|
+
await tilmeld.login(this, true);
|
|
1322
|
+
}
|
|
1020
1323
|
return { result: true, message: 'Your password has been changed.' };
|
|
1021
1324
|
} else {
|
|
1022
1325
|
return { result: false, message: "Couldn't save new password." };
|
|
@@ -1030,7 +1333,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1030
1333
|
* @returns The resulting password or hash which is stored in the entity.
|
|
1031
1334
|
*/
|
|
1032
1335
|
public $password(password: string) {
|
|
1033
|
-
const tilmeld = this
|
|
1336
|
+
const tilmeld = enforceTilmeld(this);
|
|
1034
1337
|
switch (tilmeld.config.pwMethod) {
|
|
1035
1338
|
case 'plain':
|
|
1036
1339
|
delete this.$data.salt;
|
|
@@ -1044,7 +1347,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1044
1347
|
default:
|
|
1045
1348
|
this.$data.salt = nanoid();
|
|
1046
1349
|
this.$data.password = sha256(password + this.$data.salt).toString(
|
|
1047
|
-
Base64
|
|
1350
|
+
Base64,
|
|
1048
1351
|
);
|
|
1049
1352
|
break;
|
|
1050
1353
|
}
|
|
@@ -1052,39 +1355,261 @@ export default class User extends AbleObject<UserData> {
|
|
|
1052
1355
|
}
|
|
1053
1356
|
|
|
1054
1357
|
/**
|
|
1055
|
-
*
|
|
1358
|
+
* A frontend accessible method to revoke all currently issued tokens.
|
|
1056
1359
|
*
|
|
1360
|
+
* @param data The input data from the client.
|
|
1057
1361
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1058
1362
|
*/
|
|
1059
|
-
public async $
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
if (
|
|
1066
|
-
tilmeld.config.maxUsernameLength > 0 &&
|
|
1067
|
-
this.$data.username.length > tilmeld.config.maxUsernameLength
|
|
1068
|
-
) {
|
|
1069
|
-
return {
|
|
1070
|
-
result: false,
|
|
1071
|
-
message:
|
|
1072
|
-
'Usernames must not exceed ' +
|
|
1073
|
-
tilmeld.config.maxUsernameLength +
|
|
1074
|
-
' characters.',
|
|
1075
|
-
};
|
|
1076
|
-
}
|
|
1363
|
+
public async $revokeCurrentTokens(data: {
|
|
1364
|
+
password: string;
|
|
1365
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1366
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1367
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1368
|
+
}
|
|
1077
1369
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1370
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1371
|
+
|
|
1372
|
+
if (await this.$save()) {
|
|
1373
|
+
const tilmeld = enforceTilmeld(this);
|
|
1374
|
+
await tilmeld.login(this, true);
|
|
1375
|
+
return {
|
|
1376
|
+
result: true,
|
|
1377
|
+
message: 'You have logged out of all other sessions.',
|
|
1378
|
+
};
|
|
1379
|
+
} else {
|
|
1380
|
+
return { result: false, message: "Couldn't save revocation date." };
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
public async $hasTOTPSecret() {
|
|
1385
|
+
return this.$data.totpSecret != null;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Check the given code against the user's TOTP secret.
|
|
1390
|
+
*
|
|
1391
|
+
* @param code The code in question.
|
|
1392
|
+
* @returns True if the code is valid, otherwise false.
|
|
1393
|
+
*/
|
|
1394
|
+
public $checkTOTPCode(code: string) {
|
|
1395
|
+
if (this.$data.totpSecret == null) {
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const tilmeld = enforceTilmeld(this);
|
|
1400
|
+
|
|
1401
|
+
const totp = new TOTP({
|
|
1402
|
+
issuer: tilmeld.config.appName,
|
|
1403
|
+
label: this.$data.username,
|
|
1404
|
+
algorithm: 'SHA1',
|
|
1405
|
+
digits: 6,
|
|
1406
|
+
period: 30,
|
|
1407
|
+
secret: this.$data.totpSecret,
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
return totp.validate({ token: code, window: 2 }) != null;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* A frontend accessible method to generate a new TOTP secret.
|
|
1415
|
+
*
|
|
1416
|
+
* @returns An object with 'uri', 'qrcode', and 'secret'.
|
|
1417
|
+
*/
|
|
1418
|
+
public async $getNewTOTPSecret() {
|
|
1419
|
+
const tilmeld = enforceTilmeld(this);
|
|
1420
|
+
|
|
1421
|
+
if (this.$data.totpSecret != null) {
|
|
1422
|
+
throw new BadDataError('You already have a 2FA secret.');
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const secret = new Secret();
|
|
1426
|
+
|
|
1427
|
+
const totp = new TOTP({
|
|
1428
|
+
issuer: tilmeld.config.appName,
|
|
1429
|
+
label: this.$data.username,
|
|
1430
|
+
algorithm: 'SHA1',
|
|
1431
|
+
digits: 6,
|
|
1432
|
+
period: 30,
|
|
1433
|
+
secret,
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
const uri = totp.toString();
|
|
1437
|
+
|
|
1438
|
+
const qrcode = await toDataURL(uri);
|
|
1439
|
+
|
|
1440
|
+
return {
|
|
1441
|
+
uri,
|
|
1442
|
+
qrcode,
|
|
1443
|
+
secret: secret.base32,
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* A frontend accessible method to save a TOTP secret into the user's account.
|
|
1449
|
+
*
|
|
1450
|
+
* @param data The input data from the client.
|
|
1451
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1452
|
+
*/
|
|
1453
|
+
public async $saveTOTPSecret(data: {
|
|
1454
|
+
password: string;
|
|
1455
|
+
secret: string;
|
|
1456
|
+
code: string;
|
|
1457
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1458
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1459
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (this.$data.totpSecret != null) {
|
|
1463
|
+
return { result: false, message: 'You already have a 2FA secret.' };
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (
|
|
1467
|
+
data.secret == null ||
|
|
1468
|
+
typeof data.secret !== 'string' ||
|
|
1469
|
+
data.secret === ''
|
|
1470
|
+
) {
|
|
1471
|
+
return { result: false, message: '2FA secret is invalid.' };
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
if (
|
|
1475
|
+
data.code == null ||
|
|
1476
|
+
typeof data.code !== 'string' ||
|
|
1477
|
+
data.code.length !== 6
|
|
1478
|
+
) {
|
|
1479
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const tilmeld = enforceTilmeld(this);
|
|
1483
|
+
|
|
1484
|
+
const totp = new TOTP({
|
|
1485
|
+
issuer: tilmeld.config.appName,
|
|
1486
|
+
label: this.$data.username,
|
|
1487
|
+
algorithm: 'SHA1',
|
|
1488
|
+
digits: 6,
|
|
1489
|
+
period: 30,
|
|
1490
|
+
secret: data.secret,
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1494
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
this.$data.totpSecret = data.secret;
|
|
1498
|
+
|
|
1499
|
+
if (await this.$save()) {
|
|
1500
|
+
return { result: true, message: 'Your two factor secret is now set.' };
|
|
1501
|
+
} else {
|
|
1502
|
+
return { result: false, message: "Couldn't save two factor secret." };
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* A frontend accessible method to remove the TOTP secret from the user's
|
|
1508
|
+
* account.
|
|
1509
|
+
*
|
|
1510
|
+
* @param data The input data from the client.
|
|
1511
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1512
|
+
*/
|
|
1513
|
+
public async $removeTOTPSecret(data?: {
|
|
1514
|
+
password: string;
|
|
1515
|
+
code: string;
|
|
1516
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1517
|
+
const tilmeld = enforceTilmeld(this);
|
|
1518
|
+
|
|
1519
|
+
if (data) {
|
|
1520
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1521
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
if (
|
|
1525
|
+
data.code == null ||
|
|
1526
|
+
typeof data.code !== 'string' ||
|
|
1527
|
+
data.code.length !== 6
|
|
1528
|
+
) {
|
|
1529
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
if (this.$data.totpSecret == null) {
|
|
1533
|
+
return { result: false, message: "You don't have a 2FA secret." };
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const totp = new TOTP({
|
|
1537
|
+
issuer: tilmeld.config.appName,
|
|
1538
|
+
label: this.$data.username,
|
|
1539
|
+
algorithm: 'SHA1',
|
|
1540
|
+
digits: 6,
|
|
1541
|
+
period: 30,
|
|
1542
|
+
secret: this.$data.totpSecret,
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1546
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1547
|
+
}
|
|
1548
|
+
} else if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1549
|
+
return {
|
|
1550
|
+
result: false,
|
|
1551
|
+
message: 'You must provide your password and 2FA code.',
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
delete this.$data.totpSecret;
|
|
1556
|
+
|
|
1557
|
+
if (await this.$save()) {
|
|
1558
|
+
return {
|
|
1559
|
+
result: true,
|
|
1560
|
+
message: 'Two factor secret has been removed.',
|
|
1561
|
+
};
|
|
1562
|
+
} else {
|
|
1563
|
+
return { result: false, message: "Couldn't remove two factor secret." };
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/**
|
|
1568
|
+
* Check that a username is valid.
|
|
1569
|
+
*
|
|
1570
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1571
|
+
*/
|
|
1572
|
+
public async $checkUsername(): Promise<{ result: boolean; message: string }> {
|
|
1573
|
+
const tilmeld = enforceTilmeld(this);
|
|
1574
|
+
|
|
1575
|
+
this.$nymph.config.debugLog(
|
|
1576
|
+
'tilmeld',
|
|
1577
|
+
`Username check "${this.$data.username}".`,
|
|
1578
|
+
);
|
|
1579
|
+
|
|
1580
|
+
if (!tilmeld.config.emailUsernames) {
|
|
1581
|
+
if (this.$data.username == null || !this.$data.username.length) {
|
|
1582
|
+
return { result: false, message: 'Please specify a username.' };
|
|
1583
|
+
}
|
|
1584
|
+
if (this.$data.username.length < tilmeld.config.minUsernameLength) {
|
|
1585
|
+
return {
|
|
1586
|
+
result: false,
|
|
1587
|
+
message:
|
|
1588
|
+
'Usernames must be at least ' +
|
|
1589
|
+
tilmeld.config.minUsernameLength +
|
|
1590
|
+
' characters.',
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
|
|
1594
|
+
return {
|
|
1595
|
+
result: false,
|
|
1596
|
+
message:
|
|
1597
|
+
'Usernames must not exceed ' +
|
|
1598
|
+
tilmeld.config.maxUsernameLength +
|
|
1599
|
+
' characters.',
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (
|
|
1604
|
+
difference(
|
|
1605
|
+
this.$data.username.split(''),
|
|
1606
|
+
tilmeld.config.validChars.split(''),
|
|
1607
|
+
).length
|
|
1608
|
+
) {
|
|
1609
|
+
return {
|
|
1610
|
+
result: false,
|
|
1611
|
+
message: tilmeld.config.validCharsNotice,
|
|
1612
|
+
};
|
|
1088
1613
|
}
|
|
1089
1614
|
if (!tilmeld.config.validRegex.test(this.$data.username)) {
|
|
1090
1615
|
return {
|
|
@@ -1105,12 +1630,24 @@ export default class User extends AbleObject<UserData> {
|
|
|
1105
1630
|
}
|
|
1106
1631
|
const test = await this.$nymph.getEntity(
|
|
1107
1632
|
{ class: tilmeld.User, skipAc: true },
|
|
1108
|
-
selector
|
|
1633
|
+
selector,
|
|
1109
1634
|
);
|
|
1110
1635
|
if (test != null) {
|
|
1111
1636
|
return { result: false, message: 'That username is taken.' };
|
|
1112
1637
|
}
|
|
1113
1638
|
|
|
1639
|
+
for (let callback of (this.constructor as typeof User)
|
|
1640
|
+
.checkUsernameCallbacks) {
|
|
1641
|
+
if (callback) {
|
|
1642
|
+
const result = await callback(this, {
|
|
1643
|
+
username: this.$data.username,
|
|
1644
|
+
});
|
|
1645
|
+
if (!result.result) {
|
|
1646
|
+
return { message: 'Username is not available.', ...result };
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1114
1651
|
return {
|
|
1115
1652
|
result: true,
|
|
1116
1653
|
message:
|
|
@@ -1121,10 +1658,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
1121
1658
|
if (this.$data.username == null || !this.$data.username.length) {
|
|
1122
1659
|
return { result: false, message: 'Please specify an email.' };
|
|
1123
1660
|
}
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1661
|
+
if (this.$data.username.length < tilmeld.config.minUsernameLength) {
|
|
1662
|
+
return {
|
|
1663
|
+
result: false,
|
|
1664
|
+
message:
|
|
1665
|
+
'Emails must be at least ' +
|
|
1666
|
+
tilmeld.config.minUsernameLength +
|
|
1667
|
+
' characters.',
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
|
|
1128
1671
|
return {
|
|
1129
1672
|
result: false,
|
|
1130
1673
|
message:
|
|
@@ -1144,7 +1687,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1144
1687
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1145
1688
|
*/
|
|
1146
1689
|
public async $checkEmail(): Promise<{ result: boolean; message: string }> {
|
|
1147
|
-
const tilmeld = this
|
|
1690
|
+
const tilmeld = enforceTilmeld(this);
|
|
1691
|
+
|
|
1692
|
+
this.$nymph.config.debugLog(
|
|
1693
|
+
'tilmeld',
|
|
1694
|
+
`Email check "${this.$data.email}".`,
|
|
1695
|
+
);
|
|
1696
|
+
|
|
1148
1697
|
if (this.$data.email == null || !this.$data.email.length) {
|
|
1149
1698
|
if (tilmeld.config.verifyEmail) {
|
|
1150
1699
|
return { result: false, message: 'Please specify an email.' };
|
|
@@ -1167,7 +1716,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1167
1716
|
}
|
|
1168
1717
|
const test = await this.$nymph.getEntity(
|
|
1169
1718
|
{ class: tilmeld.User, skipAc: true },
|
|
1170
|
-
selector
|
|
1719
|
+
selector,
|
|
1171
1720
|
);
|
|
1172
1721
|
if (test != null) {
|
|
1173
1722
|
return {
|
|
@@ -1189,7 +1738,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1189
1738
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1190
1739
|
*/
|
|
1191
1740
|
public async $checkPhone(): Promise<{ result: boolean; message: string }> {
|
|
1192
|
-
const tilmeld = this
|
|
1741
|
+
const tilmeld = enforceTilmeld(this);
|
|
1742
|
+
|
|
1743
|
+
this.$nymph.config.debugLog(
|
|
1744
|
+
'tilmeld',
|
|
1745
|
+
`Phone check "${this.$data.phone}".`,
|
|
1746
|
+
);
|
|
1747
|
+
|
|
1193
1748
|
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
1194
1749
|
return { result: false, message: 'Please specify a phone number.' };
|
|
1195
1750
|
}
|
|
@@ -1211,7 +1766,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1211
1766
|
}
|
|
1212
1767
|
const test = await this.$nymph.getEntity(
|
|
1213
1768
|
{ class: tilmeld.User, skipAc: true },
|
|
1214
|
-
selector
|
|
1769
|
+
selector,
|
|
1215
1770
|
);
|
|
1216
1771
|
if (test != null) {
|
|
1217
1772
|
return { result: false, message: 'Phone number is in use.' };
|
|
@@ -1228,7 +1783,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1228
1783
|
password: string;
|
|
1229
1784
|
additionalData?: { [k: string]: any };
|
|
1230
1785
|
}): Promise<{ result: boolean; loggedin: boolean; message: string }> {
|
|
1231
|
-
let tilmeld = this
|
|
1786
|
+
let tilmeld = enforceTilmeld(this);
|
|
1232
1787
|
if (!tilmeld.config.allowRegistration) {
|
|
1233
1788
|
return {
|
|
1234
1789
|
result: false,
|
|
@@ -1244,21 +1799,6 @@ export default class User extends AbleObject<UserData> {
|
|
|
1244
1799
|
};
|
|
1245
1800
|
}
|
|
1246
1801
|
|
|
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
1802
|
if (!('password' in data) || !data.password.length) {
|
|
1263
1803
|
return {
|
|
1264
1804
|
result: false,
|
|
@@ -1272,7 +1812,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1272
1812
|
}
|
|
1273
1813
|
|
|
1274
1814
|
this.$password(data.password);
|
|
1275
|
-
if (tilmeld.config.regFields.
|
|
1815
|
+
if (tilmeld.config.regFields.includes('name')) {
|
|
1276
1816
|
this.$data.name =
|
|
1277
1817
|
this.$data.nameFirst +
|
|
1278
1818
|
(this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
|
|
@@ -1289,75 +1829,34 @@ export default class User extends AbleObject<UserData> {
|
|
|
1289
1829
|
const transaction = 'tilmeld-register-' + nanoid();
|
|
1290
1830
|
const nymph = this.$nymph;
|
|
1291
1831
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1292
|
-
this.$
|
|
1293
|
-
tilmeld = this
|
|
1832
|
+
this.$setNymph(tnymph);
|
|
1833
|
+
tilmeld = enforceTilmeld(this);
|
|
1834
|
+
|
|
1835
|
+
let message = '';
|
|
1836
|
+
let loggedin = false;
|
|
1294
1837
|
|
|
1295
1838
|
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],
|
|
1839
|
+
try {
|
|
1840
|
+
for (let callback of (this.constructor as typeof User)
|
|
1841
|
+
.beforeRegisterCallbacks) {
|
|
1842
|
+
if (callback) {
|
|
1843
|
+
await callback(this, data);
|
|
1332
1844
|
}
|
|
1333
|
-
);
|
|
1334
|
-
if (group != null) {
|
|
1335
|
-
this.$data.group = group;
|
|
1336
1845
|
}
|
|
1846
|
+
} catch (e: any) {
|
|
1847
|
+
await tnymph.rollback(transaction);
|
|
1848
|
+
this.$setNymph(nymph);
|
|
1849
|
+
return {
|
|
1850
|
+
result: false,
|
|
1851
|
+
loggedin: false,
|
|
1852
|
+
message: e.message,
|
|
1853
|
+
};
|
|
1337
1854
|
}
|
|
1338
1855
|
|
|
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) {
|
|
1856
|
+
if (
|
|
1857
|
+
tilmeld.config.userFields.includes('email') &&
|
|
1858
|
+
tilmeld.config.verifyEmail
|
|
1859
|
+
) {
|
|
1361
1860
|
// The user will be enabled after verifying their e-mail address.
|
|
1362
1861
|
if (!tilmeld.config.unverifiedAccess) {
|
|
1363
1862
|
this.$data.enabled = false;
|
|
@@ -1366,7 +1865,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1366
1865
|
this.$data.enabled = true;
|
|
1367
1866
|
}
|
|
1368
1867
|
|
|
1369
|
-
// If
|
|
1868
|
+
// If createAdmin is true and there are no other users, grant
|
|
1370
1869
|
// "system/admin".
|
|
1371
1870
|
let madeAdmin = false;
|
|
1372
1871
|
if (tilmeld.config.createAdmin) {
|
|
@@ -1384,7 +1883,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
1384
1883
|
}
|
|
1385
1884
|
}
|
|
1386
1885
|
|
|
1886
|
+
this.$nymph.config.debugLog(
|
|
1887
|
+
'tilmeld',
|
|
1888
|
+
`Registering new user "${this.$data.username}".`,
|
|
1889
|
+
);
|
|
1387
1890
|
if (await this.$saveSkipAC()) {
|
|
1891
|
+
this.$nymph.config.debugLog(
|
|
1892
|
+
'tilmeld',
|
|
1893
|
+
`New user registered "${this.$data.username}".`,
|
|
1894
|
+
);
|
|
1388
1895
|
// Send the new user registered email.
|
|
1389
1896
|
if (tilmeld.config.userRegisteredRecipient != null) {
|
|
1390
1897
|
await tilmeld.config.sendEmail(
|
|
@@ -1403,33 +1910,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
1403
1910
|
userPhone: this.$data.phone,
|
|
1404
1911
|
},
|
|
1405
1912
|
},
|
|
1406
|
-
this
|
|
1913
|
+
this,
|
|
1407
1914
|
);
|
|
1408
1915
|
}
|
|
1409
1916
|
|
|
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
1917
|
// Finish up.
|
|
1429
1918
|
if (
|
|
1430
1919
|
tilmeld.config.verifyEmail &&
|
|
1431
1920
|
!tilmeld.config.unverifiedAccess &&
|
|
1432
|
-
!madeAdmin
|
|
1921
|
+
!madeAdmin &&
|
|
1922
|
+
tilmeld.config.userFields.includes('email')
|
|
1433
1923
|
) {
|
|
1434
1924
|
message +=
|
|
1435
1925
|
`Almost there. An email has been sent to ${this.$data.email} ` +
|
|
@@ -1437,9 +1927,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1437
1927
|
} else if (
|
|
1438
1928
|
tilmeld.config.verifyEmail &&
|
|
1439
1929
|
tilmeld.config.unverifiedAccess &&
|
|
1440
|
-
!madeAdmin
|
|
1930
|
+
!madeAdmin &&
|
|
1931
|
+
tilmeld.config.userFields.includes('email')
|
|
1441
1932
|
) {
|
|
1442
|
-
if (!tilmeld.login(this, true)) {
|
|
1933
|
+
if (!(await tilmeld.login(this, true))) {
|
|
1443
1934
|
throw new Error('An error occurred trying to log you in.');
|
|
1444
1935
|
}
|
|
1445
1936
|
message +=
|
|
@@ -1448,7 +1939,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1448
1939
|
'registration.';
|
|
1449
1940
|
loggedin = true;
|
|
1450
1941
|
} else {
|
|
1451
|
-
if (!tilmeld.login(this, true)) {
|
|
1942
|
+
if (!(await tilmeld.login(this, true))) {
|
|
1452
1943
|
throw new Error('An error occurred trying to log you in.');
|
|
1453
1944
|
}
|
|
1454
1945
|
message += "You're now registered and logged in!";
|
|
@@ -1464,23 +1955,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1464
1955
|
});
|
|
1465
1956
|
}
|
|
1466
1957
|
}
|
|
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
1958
|
} else {
|
|
1959
|
+
this.$nymph.config.debugError(
|
|
1960
|
+
'tilmeld',
|
|
1961
|
+
`Error registering new user "${this.$data.username}".`,
|
|
1962
|
+
);
|
|
1482
1963
|
await tnymph.rollback(transaction);
|
|
1483
|
-
this.$nymph
|
|
1964
|
+
this.$setNymph(nymph);
|
|
1484
1965
|
return {
|
|
1485
1966
|
result: false,
|
|
1486
1967
|
loggedin: false,
|
|
@@ -1488,25 +1969,44 @@ export default class User extends AbleObject<UserData> {
|
|
|
1488
1969
|
};
|
|
1489
1970
|
}
|
|
1490
1971
|
} catch (e: any) {
|
|
1972
|
+
this.$nymph.config.debugError(
|
|
1973
|
+
'tilmeld',
|
|
1974
|
+
`Error registering new user "${this.$data.username}": ${e}`,
|
|
1975
|
+
);
|
|
1491
1976
|
await tnymph.rollback(transaction);
|
|
1492
|
-
this.$nymph
|
|
1977
|
+
this.$setNymph(nymph);
|
|
1978
|
+
throw e;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
try {
|
|
1982
|
+
await tnymph.commit(transaction);
|
|
1983
|
+
this.$setNymph(nymph);
|
|
1984
|
+
await this.$nymph.tilmeld?.fillSession(this);
|
|
1985
|
+
} catch (e: any) {
|
|
1493
1986
|
throw e;
|
|
1494
1987
|
}
|
|
1988
|
+
|
|
1989
|
+
return {
|
|
1990
|
+
result: true,
|
|
1991
|
+
loggedin,
|
|
1992
|
+
message,
|
|
1993
|
+
};
|
|
1495
1994
|
}
|
|
1496
1995
|
|
|
1497
1996
|
public async $save() {
|
|
1498
|
-
let tilmeld = this
|
|
1997
|
+
let tilmeld = enforceTilmeld(this);
|
|
1499
1998
|
if (this.$data.username == null || !this.$data.username.trim().length) {
|
|
1500
1999
|
return false;
|
|
1501
2000
|
}
|
|
1502
2001
|
|
|
1503
2002
|
if (
|
|
2003
|
+
!this.$skipAcWhenSaving &&
|
|
1504
2004
|
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1505
2005
|
!tilmeld.gatekeeper('system/admin') &&
|
|
1506
|
-
this.$
|
|
2006
|
+
this.$data.abilities?.includes('system/admin')
|
|
1507
2007
|
) {
|
|
1508
2008
|
throw new BadDataError(
|
|
1509
|
-
"You don't have the authority to modify system admins."
|
|
2009
|
+
"You don't have the authority to modify system admins.",
|
|
1510
2010
|
);
|
|
1511
2011
|
}
|
|
1512
2012
|
|
|
@@ -1528,49 +2028,88 @@ export default class User extends AbleObject<UserData> {
|
|
|
1528
2028
|
this.$data.enabled = !!this.$data.enabled;
|
|
1529
2029
|
|
|
1530
2030
|
// Clear empty values.
|
|
1531
|
-
if (
|
|
2031
|
+
if (
|
|
2032
|
+
this.$data.nameFirst === '' ||
|
|
2033
|
+
!tilmeld.config.userFields.includes('name')
|
|
2034
|
+
) {
|
|
1532
2035
|
delete this.$data.nameFirst;
|
|
1533
2036
|
}
|
|
1534
|
-
if (
|
|
2037
|
+
if (
|
|
2038
|
+
this.$data.nameMiddle === '' ||
|
|
2039
|
+
!tilmeld.config.userFields.includes('name')
|
|
2040
|
+
) {
|
|
1535
2041
|
delete this.$data.nameMiddle;
|
|
1536
2042
|
}
|
|
1537
|
-
if (
|
|
2043
|
+
if (
|
|
2044
|
+
this.$data.nameLast === '' ||
|
|
2045
|
+
!tilmeld.config.userFields.includes('name')
|
|
2046
|
+
) {
|
|
1538
2047
|
delete this.$data.nameLast;
|
|
1539
2048
|
}
|
|
1540
|
-
if (this.$data.name === '') {
|
|
2049
|
+
if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
1541
2050
|
delete this.$data.name;
|
|
1542
2051
|
}
|
|
1543
|
-
if (
|
|
2052
|
+
if (
|
|
2053
|
+
this.$data.email === '' ||
|
|
2054
|
+
!tilmeld.config.userFields.includes('email')
|
|
2055
|
+
) {
|
|
1544
2056
|
delete this.$data.email;
|
|
1545
2057
|
}
|
|
1546
2058
|
if (this.$data.avatar === '') {
|
|
1547
2059
|
delete this.$data.avatar;
|
|
1548
2060
|
}
|
|
1549
|
-
if (
|
|
2061
|
+
if (
|
|
2062
|
+
this.$data.phone === '' ||
|
|
2063
|
+
!tilmeld.config.userFields.includes('phone')
|
|
2064
|
+
) {
|
|
1550
2065
|
delete this.$data.phone;
|
|
1551
2066
|
}
|
|
1552
|
-
if (
|
|
2067
|
+
if (
|
|
2068
|
+
this.$data.secret === '' ||
|
|
2069
|
+
!tilmeld.config.userFields.includes('email')
|
|
2070
|
+
) {
|
|
1553
2071
|
delete this.$data.secret;
|
|
1554
2072
|
}
|
|
1555
|
-
if (
|
|
2073
|
+
if (
|
|
2074
|
+
this.$data.emailChangeDate === 0 ||
|
|
2075
|
+
!tilmeld.config.userFields.includes('email')
|
|
2076
|
+
) {
|
|
1556
2077
|
delete this.$data.emailChangeDate;
|
|
1557
2078
|
}
|
|
1558
|
-
if (
|
|
2079
|
+
if (
|
|
2080
|
+
this.$data.newEmailSecret === '' ||
|
|
2081
|
+
!tilmeld.config.userFields.includes('email')
|
|
2082
|
+
) {
|
|
1559
2083
|
delete this.$data.newEmailSecret;
|
|
1560
2084
|
}
|
|
1561
|
-
if (
|
|
2085
|
+
if (
|
|
2086
|
+
this.$data.newEmailAddress === '' ||
|
|
2087
|
+
!tilmeld.config.userFields.includes('email')
|
|
2088
|
+
) {
|
|
1562
2089
|
delete this.$data.newEmailAddress;
|
|
1563
2090
|
}
|
|
1564
|
-
if (
|
|
2091
|
+
if (
|
|
2092
|
+
this.$data.cancelEmailSecret === '' ||
|
|
2093
|
+
!tilmeld.config.userFields.includes('email')
|
|
2094
|
+
) {
|
|
1565
2095
|
delete this.$data.cancelEmailSecret;
|
|
1566
2096
|
}
|
|
1567
|
-
if (
|
|
2097
|
+
if (
|
|
2098
|
+
this.$data.cancelEmailAddress === '' ||
|
|
2099
|
+
!tilmeld.config.userFields.includes('email')
|
|
2100
|
+
) {
|
|
1568
2101
|
delete this.$data.cancelEmailAddress;
|
|
1569
2102
|
}
|
|
1570
|
-
if (
|
|
2103
|
+
if (
|
|
2104
|
+
this.$data.recoverSecret === '' ||
|
|
2105
|
+
!tilmeld.config.userFields.includes('email')
|
|
2106
|
+
) {
|
|
1571
2107
|
delete this.$data.recoverSecret;
|
|
1572
2108
|
}
|
|
1573
|
-
if (
|
|
2109
|
+
if (
|
|
2110
|
+
this.$data.recoverSecretDate === 0 ||
|
|
2111
|
+
!tilmeld.config.userFields.includes('email')
|
|
2112
|
+
) {
|
|
1574
2113
|
delete this.$data.recoverSecretDate;
|
|
1575
2114
|
}
|
|
1576
2115
|
if (this.$data.password === '') {
|
|
@@ -1588,7 +2127,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1588
2127
|
if (!unCheck.result) {
|
|
1589
2128
|
throw new BadUsernameError(unCheck.message);
|
|
1590
2129
|
}
|
|
1591
|
-
if (
|
|
2130
|
+
if (
|
|
2131
|
+
!tilmeld.config.emailUsernames &&
|
|
2132
|
+
tilmeld.config.userFields.includes('email')
|
|
2133
|
+
) {
|
|
1592
2134
|
const emCheck = await this.$checkEmail();
|
|
1593
2135
|
if (!emCheck.result) {
|
|
1594
2136
|
throw new BadEmailError(emCheck.message);
|
|
@@ -1596,7 +2138,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1596
2138
|
}
|
|
1597
2139
|
|
|
1598
2140
|
// Email changes.
|
|
1599
|
-
if (
|
|
2141
|
+
if (
|
|
2142
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
2143
|
+
tilmeld.config.userFields.includes('email')
|
|
2144
|
+
) {
|
|
1600
2145
|
// The user isn't an admin, so email address changes should contain some
|
|
1601
2146
|
// security measures.
|
|
1602
2147
|
if (tilmeld.config.verifyEmail) {
|
|
@@ -1604,7 +2149,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1604
2149
|
if (this.guid == null) {
|
|
1605
2150
|
// If this is the first user, they'll be made an admin, so they don't
|
|
1606
2151
|
// need to verify.
|
|
1607
|
-
if (
|
|
2152
|
+
if (!this.$data.abilities?.includes('system/admin')) {
|
|
1608
2153
|
this.$data.secret = nanoid();
|
|
1609
2154
|
sendVerification = true;
|
|
1610
2155
|
}
|
|
@@ -1624,10 +2169,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1624
2169
|
new Date(
|
|
1625
2170
|
strtotime(
|
|
1626
2171
|
'+' + tilmeld.config.emailRateLimit,
|
|
1627
|
-
Math.floor(this.$data.emailChangeDate / 1000)
|
|
1628
|
-
) * 1000
|
|
2172
|
+
Math.floor(this.$data.emailChangeDate / 1000),
|
|
2173
|
+
) * 1000,
|
|
1629
2174
|
).toString() +
|
|
1630
|
-
' to change your email address again.'
|
|
2175
|
+
' to change your email address again.',
|
|
1631
2176
|
);
|
|
1632
2177
|
} else {
|
|
1633
2178
|
if (
|
|
@@ -1682,37 +2227,129 @@ export default class User extends AbleObject<UserData> {
|
|
|
1682
2227
|
throw new BadDataError('A password is required.');
|
|
1683
2228
|
}
|
|
1684
2229
|
|
|
1685
|
-
try {
|
|
1686
|
-
tilmeld.config.validatorUser(this);
|
|
1687
|
-
} catch (e: any) {
|
|
1688
|
-
throw new BadDataError(e?.message);
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
2230
|
// Start transaction.
|
|
1692
2231
|
const transaction = 'tilmeld-save-' + nanoid();
|
|
1693
2232
|
const nymph = this.$nymph;
|
|
1694
2233
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1695
|
-
this.$
|
|
1696
|
-
tilmeld = this
|
|
2234
|
+
this.$setNymph(tnymph);
|
|
2235
|
+
tilmeld = enforceTilmeld(this);
|
|
1697
2236
|
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2237
|
+
// If the primary group is generated now, the user assigned to it won't have
|
|
2238
|
+
// a guid yet, so it needs to be assigned after the user is saved.
|
|
2239
|
+
let saveGeneratedPrimaryGroupAgain = false;
|
|
2240
|
+
|
|
2241
|
+
try {
|
|
2242
|
+
let group = this.$data.group;
|
|
2243
|
+
await group?.$wake();
|
|
2244
|
+
if (group == null && this.guid == null) {
|
|
2245
|
+
if (tilmeld.config.generatePrimary) {
|
|
2246
|
+
// Generate a new primary group for the user.
|
|
2247
|
+
group = await tilmeld.Group.factory();
|
|
2248
|
+
// This user doesn't have a guid yet, so that's why we set
|
|
2249
|
+
// saveGeneratedPrimaryGroupAgain.
|
|
2250
|
+
group.user = this;
|
|
2251
|
+
saveGeneratedPrimaryGroupAgain = true;
|
|
2252
|
+
const parent = await tnymph.getEntity(
|
|
2253
|
+
{ class: tilmeld.Group },
|
|
2254
|
+
{
|
|
2255
|
+
type: '&',
|
|
2256
|
+
equal: ['defaultPrimary', true],
|
|
2257
|
+
},
|
|
2258
|
+
);
|
|
2259
|
+
if (parent != null) {
|
|
2260
|
+
group.parent = parent;
|
|
2261
|
+
}
|
|
2262
|
+
} else {
|
|
2263
|
+
// Add the default primary.
|
|
2264
|
+
const group = await tnymph.getEntity(
|
|
2265
|
+
{ class: tilmeld.Group },
|
|
2266
|
+
{
|
|
2267
|
+
type: '&',
|
|
2268
|
+
equal: ['defaultPrimary', true],
|
|
2269
|
+
},
|
|
2270
|
+
);
|
|
2271
|
+
if (group != null) {
|
|
2272
|
+
this.$data.group = group;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (group != null && group.user != null && this.$is(group.user)) {
|
|
1704
2277
|
// Update the user's generated primary group.
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
2278
|
+
let changed = false;
|
|
2279
|
+
if (group.groupname !== this.$data.username) {
|
|
2280
|
+
group.groupname = this.$data.username;
|
|
2281
|
+
changed = true;
|
|
2282
|
+
}
|
|
2283
|
+
if (group.avatar !== this.$data.avatar) {
|
|
2284
|
+
group.avatar = this.$data.avatar;
|
|
2285
|
+
changed = true;
|
|
2286
|
+
}
|
|
2287
|
+
if (
|
|
2288
|
+
tilmeld.config.userFields.includes('email') &&
|
|
2289
|
+
group.email !== this.$data.email
|
|
2290
|
+
) {
|
|
2291
|
+
group.email = this.$data.email;
|
|
2292
|
+
changed = true;
|
|
2293
|
+
}
|
|
2294
|
+
if (
|
|
2295
|
+
tilmeld.config.userFields.includes('name') &&
|
|
2296
|
+
group.name !== this.$data.name
|
|
2297
|
+
) {
|
|
2298
|
+
group.name = this.$data.name;
|
|
2299
|
+
changed = true;
|
|
2300
|
+
}
|
|
2301
|
+
if (
|
|
2302
|
+
tilmeld.config.userFields.includes('phone') &&
|
|
2303
|
+
group.phone !== this.$data.phone
|
|
2304
|
+
) {
|
|
2305
|
+
group.phone = this.$data.phone;
|
|
2306
|
+
changed = true;
|
|
2307
|
+
}
|
|
2308
|
+
if (changed) {
|
|
2309
|
+
if (!(await group.$saveSkipAC())) {
|
|
2310
|
+
throw Error('Error updating primary group for user.');
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
this.$data.group = group;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
if (this.$data.groups == null || this.$data.groups.length === 0) {
|
|
2317
|
+
// Add secondary groups.
|
|
2318
|
+
if (
|
|
2319
|
+
tilmeld.config.userFields.includes('email') &&
|
|
2320
|
+
tilmeld.config.verifyEmail &&
|
|
2321
|
+
tilmeld.config.unverifiedAccess &&
|
|
2322
|
+
this.$data.secret != null
|
|
2323
|
+
) {
|
|
2324
|
+
// Add the default secondaries for unverified users.
|
|
2325
|
+
this.$data.groups = await tnymph.getEntities(
|
|
2326
|
+
{ class: tilmeld.Group },
|
|
2327
|
+
{
|
|
2328
|
+
type: '&',
|
|
2329
|
+
equal: ['unverifiedSecondary', true],
|
|
2330
|
+
},
|
|
2331
|
+
);
|
|
2332
|
+
} else {
|
|
2333
|
+
// Add the default secondaries.
|
|
2334
|
+
this.$data.groups = await tnymph.getEntities(
|
|
2335
|
+
{ class: tilmeld.Group },
|
|
2336
|
+
{
|
|
2337
|
+
type: '&',
|
|
2338
|
+
equal: ['defaultSecondary', true],
|
|
2339
|
+
},
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
try {
|
|
2345
|
+
tilmeld.config.validatorUser(tilmeld, this);
|
|
1711
2346
|
} catch (e: any) {
|
|
1712
|
-
|
|
1713
|
-
this.$nymph = nymph;
|
|
1714
|
-
throw e;
|
|
2347
|
+
throw new BadDataError(e?.message);
|
|
1715
2348
|
}
|
|
2349
|
+
} catch (e: any) {
|
|
2350
|
+
await tnymph.rollback(transaction);
|
|
2351
|
+
this.$setNymph(nymph);
|
|
2352
|
+
throw e;
|
|
1716
2353
|
}
|
|
1717
2354
|
|
|
1718
2355
|
let ret: boolean;
|
|
@@ -1722,11 +2359,26 @@ export default class User extends AbleObject<UserData> {
|
|
|
1722
2359
|
|
|
1723
2360
|
try {
|
|
1724
2361
|
ret = await super.$save();
|
|
2362
|
+
|
|
2363
|
+
if (ret && saveGeneratedPrimaryGroupAgain) {
|
|
2364
|
+
const group = this.$data.group;
|
|
2365
|
+
if (group == null) {
|
|
2366
|
+
throw Error('Generated primary group not found.');
|
|
2367
|
+
}
|
|
2368
|
+
await group.$wake();
|
|
2369
|
+
// Now this user has a GUID, so saving it on the group will succeed.
|
|
2370
|
+
group.user = this;
|
|
2371
|
+
if (!(await group.$saveSkipAC())) {
|
|
2372
|
+
throw Error("Generated primary group couldn't be saved.");
|
|
2373
|
+
}
|
|
2374
|
+
this.$data.group = group;
|
|
2375
|
+
}
|
|
1725
2376
|
} catch (e: any) {
|
|
1726
2377
|
await tnymph.rollback(transaction);
|
|
1727
|
-
this.$nymph
|
|
2378
|
+
this.$setNymph(nymph);
|
|
1728
2379
|
throw e;
|
|
1729
2380
|
}
|
|
2381
|
+
|
|
1730
2382
|
if (ret) {
|
|
1731
2383
|
if (sendVerification) {
|
|
1732
2384
|
// The email has changed, so send a new verification email.
|
|
@@ -1735,7 +2387,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1735
2387
|
this.guid = preGuid;
|
|
1736
2388
|
this.cdate = preCdate;
|
|
1737
2389
|
this.mdate = preMdate;
|
|
1738
|
-
this.$nymph
|
|
2390
|
+
this.$setNymph(nymph);
|
|
1739
2391
|
throw new Error("Couldn't send verification email.");
|
|
1740
2392
|
}
|
|
1741
2393
|
}
|
|
@@ -1744,14 +2396,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
1744
2396
|
this.$gatekeeperCache = undefined;
|
|
1745
2397
|
await tnymph.commit(transaction);
|
|
1746
2398
|
|
|
1747
|
-
|
|
2399
|
+
const tilmeld = enforceTilmeld(nymph);
|
|
2400
|
+
|
|
2401
|
+
if (tilmeld.User.current(true).$is(this)) {
|
|
1748
2402
|
// Update the user in the session cache.
|
|
1749
|
-
|
|
2403
|
+
await tilmeld.fillSession(this);
|
|
1750
2404
|
}
|
|
1751
2405
|
} else {
|
|
1752
2406
|
await tnymph.rollback(transaction);
|
|
1753
2407
|
}
|
|
1754
|
-
this.$nymph
|
|
2408
|
+
this.$setNymph(nymph);
|
|
1755
2409
|
return ret;
|
|
1756
2410
|
}
|
|
1757
2411
|
|
|
@@ -1772,16 +2426,17 @@ export default class User extends AbleObject<UserData> {
|
|
|
1772
2426
|
}
|
|
1773
2427
|
|
|
1774
2428
|
public async $delete() {
|
|
1775
|
-
const tilmeld = this
|
|
1776
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
2429
|
+
const tilmeld = enforceTilmeld(this);
|
|
2430
|
+
if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1777
2431
|
throw new BadDataError("You don't have the authority to delete users.");
|
|
1778
2432
|
}
|
|
1779
2433
|
if (
|
|
2434
|
+
!this.$skipAcWhenDeleting &&
|
|
1780
2435
|
!tilmeld.gatekeeper('system/admin') &&
|
|
1781
|
-
this.$
|
|
2436
|
+
this.$data.abilities?.includes('system/admin')
|
|
1782
2437
|
) {
|
|
1783
2438
|
throw new BadDataError(
|
|
1784
|
-
"You don't have the authority to delete system admins."
|
|
2439
|
+
"You don't have the authority to delete system admins.",
|
|
1785
2440
|
);
|
|
1786
2441
|
}
|
|
1787
2442
|
if (tilmeld.User.current(true).$is(this)) {
|
|
@@ -1790,9 +2445,27 @@ export default class User extends AbleObject<UserData> {
|
|
|
1790
2445
|
return await super.$delete();
|
|
1791
2446
|
}
|
|
1792
2447
|
|
|
2448
|
+
/*
|
|
2449
|
+
* This should *never* be accessible on the client.
|
|
2450
|
+
*/
|
|
2451
|
+
async $deleteSkipAC() {
|
|
2452
|
+
this.$skipAcWhenDeleting = true;
|
|
2453
|
+
return await this.$delete();
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
$tilmeldDeleteSkipAC() {
|
|
2457
|
+
if (this.$skipAcWhenDeleting) {
|
|
2458
|
+
this.$skipAcWhenDeleting = false;
|
|
2459
|
+
return true;
|
|
2460
|
+
}
|
|
2461
|
+
return false;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
1793
2464
|
public static on<T extends EventType>(
|
|
1794
2465
|
event: T,
|
|
1795
|
-
callback: T extends '
|
|
2466
|
+
callback: T extends 'checkUsername'
|
|
2467
|
+
? TilmeldCheckUsernameCallback
|
|
2468
|
+
: T extends 'beforeRegister'
|
|
1796
2469
|
? TilmeldBeforeRegisterCallback
|
|
1797
2470
|
: T extends 'afterRegister'
|
|
1798
2471
|
? TilmeldAfterRegisterCallback
|
|
@@ -1804,9 +2477,11 @@ export default class User extends AbleObject<UserData> {
|
|
|
1804
2477
|
? TilmeldBeforeLogoutCallback
|
|
1805
2478
|
: T extends 'afterLogout'
|
|
1806
2479
|
? TilmeldAfterLogoutCallback
|
|
1807
|
-
: never
|
|
2480
|
+
: never,
|
|
1808
2481
|
) {
|
|
1809
|
-
const prop = (event + 'Callbacks') as T extends '
|
|
2482
|
+
const prop = (event + 'Callbacks') as T extends 'checkUsername'
|
|
2483
|
+
? 'checkUsernameCallbacks'
|
|
2484
|
+
: T extends 'beforeRegister'
|
|
1810
2485
|
? 'beforeRegisterCallbacks'
|
|
1811
2486
|
: T extends 'afterRegister'
|
|
1812
2487
|
? 'afterRegisterCallbacks'
|
|
@@ -1829,7 +2504,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
1829
2504
|
|
|
1830
2505
|
public static off<T extends EventType>(
|
|
1831
2506
|
event: T,
|
|
1832
|
-
callback: T extends '
|
|
2507
|
+
callback: T extends 'checkUsername'
|
|
2508
|
+
? TilmeldCheckUsernameCallback
|
|
2509
|
+
: T extends 'beforeRegister'
|
|
1833
2510
|
? TilmeldBeforeRegisterCallback
|
|
1834
2511
|
: T extends 'afterRegister'
|
|
1835
2512
|
? TilmeldAfterRegisterCallback
|
|
@@ -1841,9 +2518,11 @@ export default class User extends AbleObject<UserData> {
|
|
|
1841
2518
|
? TilmeldBeforeLogoutCallback
|
|
1842
2519
|
: T extends 'afterLogout'
|
|
1843
2520
|
? TilmeldAfterLogoutCallback
|
|
1844
|
-
: never
|
|
2521
|
+
: never,
|
|
1845
2522
|
) {
|
|
1846
|
-
const prop = (event + 'Callbacks') as T extends '
|
|
2523
|
+
const prop = (event + 'Callbacks') as T extends 'checkUsername'
|
|
2524
|
+
? 'checkUsernameCallbacks'
|
|
2525
|
+
: T extends 'beforeRegister'
|
|
1847
2526
|
? 'beforeRegisterCallbacks'
|
|
1848
2527
|
: T extends 'afterRegister'
|
|
1849
2528
|
? 'afterRegisterCallbacks'
|