@nymphjs/tilmeld 1.0.0-beta.55 → 1.0.0-beta.57
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 +16 -0
- package/dist/AbleObject.d.ts +15 -0
- package/dist/AbleObject.js +15 -0
- package/dist/AbleObject.js.map +1 -1
- package/dist/Group.d.ts +116 -0
- package/dist/Group.js +98 -0
- package/dist/Group.js.map +1 -1
- package/dist/Group.test.js +1 -0
- package/dist/Group.test.js.map +1 -1
- package/dist/Tilmeld.d.ts +226 -1
- package/dist/Tilmeld.js +306 -13
- package/dist/Tilmeld.js.map +1 -1
- package/dist/User.d.ts +265 -0
- package/dist/User.js +246 -0
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +1 -0
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +244 -0
- package/dist/conf/defaults.js +7 -3
- package/dist/conf/defaults.js.map +1 -1
- 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/package.json +8 -7
- package/src/Tilmeld.ts +9 -3
- package/src/User.ts +67 -0
- package/src/conf/defaults.ts +1 -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/tsconfig.json +1 -1
package/dist/User.js
CHANGED
|
@@ -14,6 +14,15 @@ const qrcode_1 = require("qrcode");
|
|
|
14
14
|
const enforceTilmeld_1 = require("./enforceTilmeld");
|
|
15
15
|
const AbleObject_1 = __importDefault(require("./AbleObject"));
|
|
16
16
|
const errors_1 = require("./errors");
|
|
17
|
+
/**
|
|
18
|
+
* A user data model.
|
|
19
|
+
*
|
|
20
|
+
* Written by Hunter Perrin for SciActive.
|
|
21
|
+
*
|
|
22
|
+
* @author Hunter Perrin <hperrin@gmail.com>
|
|
23
|
+
* @copyright SciActive Inc
|
|
24
|
+
* @see http://nymph.io/
|
|
25
|
+
*/
|
|
17
26
|
class User extends AbleObject_1.default {
|
|
18
27
|
static async factory(guid) {
|
|
19
28
|
return (await super.factory(guid));
|
|
@@ -42,7 +51,11 @@ class User extends AbleObject_1.default {
|
|
|
42
51
|
this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
43
52
|
this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
|
|
44
53
|
this.$allowlistTags = [];
|
|
54
|
+
/**
|
|
55
|
+
* This is explicitly used only during the registration proccess.
|
|
56
|
+
*/
|
|
45
57
|
this.$skipAcWhenSaving = false;
|
|
58
|
+
// Defaults.
|
|
46
59
|
this.$data.enabled = true;
|
|
47
60
|
this.$data.abilities = [];
|
|
48
61
|
this.$data.groups = [];
|
|
@@ -56,6 +69,12 @@ class User extends AbleObject_1.default {
|
|
|
56
69
|
}
|
|
57
70
|
return tilmeld.currentUser;
|
|
58
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Send an account recovery link.
|
|
74
|
+
*
|
|
75
|
+
* @param data The input data from the client.
|
|
76
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
77
|
+
*/
|
|
59
78
|
static async sendRecovery(data) {
|
|
60
79
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
61
80
|
if (!tilmeld.config.pwRecovery) {
|
|
@@ -67,6 +86,7 @@ class User extends AbleObject_1.default {
|
|
|
67
86
|
let user;
|
|
68
87
|
const options = {};
|
|
69
88
|
if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
|
|
89
|
+
// Create a username recovery email.
|
|
70
90
|
const getUser = await this.nymph.getEntity({ class: tilmeld.User, skipAc: true }, {
|
|
71
91
|
type: '&',
|
|
72
92
|
ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
@@ -77,6 +97,7 @@ class User extends AbleObject_1.default {
|
|
|
77
97
|
message: 'Requested account is not accessible.',
|
|
78
98
|
};
|
|
79
99
|
}
|
|
100
|
+
// Send the recovery email.
|
|
80
101
|
user = getUser;
|
|
81
102
|
options.template = 'RecoverUsername';
|
|
82
103
|
options.message = {
|
|
@@ -88,6 +109,7 @@ class User extends AbleObject_1.default {
|
|
|
88
109
|
options.locals = {};
|
|
89
110
|
}
|
|
90
111
|
else if (data.recoveryType === 'password') {
|
|
112
|
+
// Create a password recovery email.
|
|
91
113
|
const getUser = await tilmeld.User.factoryUsername(data.account);
|
|
92
114
|
if (getUser.guid == null) {
|
|
93
115
|
return {
|
|
@@ -95,11 +117,13 @@ class User extends AbleObject_1.default {
|
|
|
95
117
|
message: 'Requested account is not accessible.',
|
|
96
118
|
};
|
|
97
119
|
}
|
|
120
|
+
// Create a unique secret.
|
|
98
121
|
getUser.recoverSecret = (0, guid_1.humanSecret)();
|
|
99
122
|
getUser.recoverSecretDate = Date.now();
|
|
100
123
|
if (!(await getUser.$saveSkipAC())) {
|
|
101
124
|
return { result: false, message: "Couldn't save recovery secret." };
|
|
102
125
|
}
|
|
126
|
+
// Send the recovery email.
|
|
103
127
|
user = getUser;
|
|
104
128
|
options.template = 'RecoverPassword';
|
|
105
129
|
options.message = {
|
|
@@ -116,6 +140,8 @@ class User extends AbleObject_1.default {
|
|
|
116
140
|
else {
|
|
117
141
|
return { result: false, message: 'Invalid recovery type.' };
|
|
118
142
|
}
|
|
143
|
+
this.nymph.config.debugLog('tilmeld', `User account recovery (${options.template}) request for "${user.username}".`);
|
|
144
|
+
// Send the email.
|
|
119
145
|
if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
|
|
120
146
|
return {
|
|
121
147
|
result: true,
|
|
@@ -127,6 +153,12 @@ class User extends AbleObject_1.default {
|
|
|
127
153
|
return { result: false, message: "Couldn't send recovery email." };
|
|
128
154
|
}
|
|
129
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Recover account details.
|
|
158
|
+
*
|
|
159
|
+
* @param data The input data from the client.
|
|
160
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
161
|
+
*/
|
|
130
162
|
static async recover(data) {
|
|
131
163
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
132
164
|
if (!tilmeld.config.pwRecovery) {
|
|
@@ -150,6 +182,7 @@ class User extends AbleObject_1.default {
|
|
|
150
182
|
if (!('password' in data) || !data.password.length) {
|
|
151
183
|
return { result: false, message: 'Password cannot be empty.' };
|
|
152
184
|
}
|
|
185
|
+
this.nymph.config.debugLog('tilmeld', `User account recovery for "${user.username}".`);
|
|
153
186
|
user.$password(data.password);
|
|
154
187
|
delete user.recoverSecret;
|
|
155
188
|
delete user.recoverSecretDate;
|
|
@@ -231,6 +264,8 @@ class User extends AbleObject_1.default {
|
|
|
231
264
|
message: e.message,
|
|
232
265
|
};
|
|
233
266
|
}
|
|
267
|
+
this.$nymph.config.debugLog('tilmeld', `User login for "${this.$data.username}".`);
|
|
268
|
+
// Authentication was successful, attempt to login.
|
|
234
269
|
if (!(await tilmeld.login(this, true))) {
|
|
235
270
|
return { result: false, message: 'Incorrect login/password.' };
|
|
236
271
|
}
|
|
@@ -240,6 +275,7 @@ class User extends AbleObject_1.default {
|
|
|
240
275
|
await callback(this);
|
|
241
276
|
}
|
|
242
277
|
}
|
|
278
|
+
// Login was successful.
|
|
243
279
|
return { result: true, message: 'You are logged in.' };
|
|
244
280
|
}
|
|
245
281
|
async $switchUser(data) {
|
|
@@ -286,6 +322,8 @@ class User extends AbleObject_1.default {
|
|
|
286
322
|
message: e.message,
|
|
287
323
|
};
|
|
288
324
|
}
|
|
325
|
+
this.$nymph.config.debugLog('tilmeld', `User login switch for "${user.username}" to "${this.$data.username}".`);
|
|
326
|
+
// Authentication was successful, attempt to login.
|
|
289
327
|
if (!(await tilmeld.loginSwitch(this, true))) {
|
|
290
328
|
return { result: false, message: 'Incorrect login/password.' };
|
|
291
329
|
}
|
|
@@ -295,8 +333,13 @@ class User extends AbleObject_1.default {
|
|
|
295
333
|
await callback(this);
|
|
296
334
|
}
|
|
297
335
|
}
|
|
336
|
+
// Login was successful.
|
|
298
337
|
return { result: true, message: 'You are logged in.' };
|
|
299
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Log a user out of the system.
|
|
341
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
342
|
+
*/
|
|
300
343
|
async $logout() {
|
|
301
344
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
302
345
|
try {
|
|
@@ -313,6 +356,7 @@ class User extends AbleObject_1.default {
|
|
|
313
356
|
message: e.message,
|
|
314
357
|
};
|
|
315
358
|
}
|
|
359
|
+
this.$nymph.config.debugLog('tilmeld', `User logout for "${this.$data.username}".`);
|
|
316
360
|
await tilmeld.logout();
|
|
317
361
|
for (let callback of this.constructor
|
|
318
362
|
.afterLogoutCallbacks) {
|
|
@@ -347,6 +391,9 @@ class User extends AbleObject_1.default {
|
|
|
347
391
|
}
|
|
348
392
|
return null;
|
|
349
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Get the user's group descendants.
|
|
396
|
+
*/
|
|
350
397
|
async $getDescendantGroups() {
|
|
351
398
|
if (this.$descendantGroups == null) {
|
|
352
399
|
this.$descendantGroups = [];
|
|
@@ -419,6 +466,11 @@ class User extends AbleObject_1.default {
|
|
|
419
466
|
super.$putData(data, sdata);
|
|
420
467
|
this.$updateDataProtection();
|
|
421
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Update the data protection arrays for a user.
|
|
471
|
+
*
|
|
472
|
+
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
473
|
+
*/
|
|
422
474
|
$updateDataProtection(givenUser) {
|
|
423
475
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
424
476
|
let user = givenUser ?? tilmeld.User.current();
|
|
@@ -429,11 +481,14 @@ class User extends AbleObject_1.default {
|
|
|
429
481
|
this.$privateData.push('username');
|
|
430
482
|
}
|
|
431
483
|
const isCurrentUser = user != null && user.$is(this);
|
|
484
|
+
// While loading, accessing abilities on the current user will infinitely
|
|
485
|
+
// loop, but if this is the current user, we can use its abilities.
|
|
432
486
|
const abilities = (isCurrentUser || user == null
|
|
433
487
|
? this.$data.abilities
|
|
434
488
|
: user?.abilities) ?? [];
|
|
435
489
|
const isNewUser = this.guid == null;
|
|
436
490
|
if (isCurrentUser) {
|
|
491
|
+
// Users can check to see what abilities they have.
|
|
437
492
|
this.$clientEnabledMethods.push('$gatekeeper');
|
|
438
493
|
this.$clientEnabledMethods.push('$changePassword');
|
|
439
494
|
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
@@ -447,12 +502,14 @@ class User extends AbleObject_1.default {
|
|
|
447
502
|
if (user != null &&
|
|
448
503
|
(abilities.includes('tilmeld/admin') ||
|
|
449
504
|
abilities.includes('system/admin'))) {
|
|
505
|
+
// Users who can edit other users can see most of their data.
|
|
450
506
|
this.$privateData = ['password', 'salt', 'totpSecret'];
|
|
451
507
|
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
452
508
|
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
453
509
|
this.$allowlistData = undefined;
|
|
454
510
|
}
|
|
455
511
|
else if (isCurrentUser || isNewUser) {
|
|
512
|
+
// Users can see their own data, and edit some of it.
|
|
456
513
|
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
457
514
|
this.$allowlistData.push('username');
|
|
458
515
|
}
|
|
@@ -470,10 +527,24 @@ class User extends AbleObject_1.default {
|
|
|
470
527
|
this.$allowlistData.push('phone');
|
|
471
528
|
}
|
|
472
529
|
this.$privateData = [
|
|
530
|
+
// The things that are commented are there to show what is _not_ private.
|
|
531
|
+
// 'abilities',
|
|
532
|
+
// 'username',
|
|
533
|
+
// 'nameFirst',
|
|
534
|
+
// 'nameMiddle',
|
|
535
|
+
// 'nameLast',
|
|
536
|
+
// 'name',
|
|
537
|
+
// 'email',
|
|
538
|
+
// 'avatar',
|
|
539
|
+
// 'phone',
|
|
540
|
+
// 'group',
|
|
541
|
+
// 'groups',
|
|
542
|
+
// 'inheritAbilities',
|
|
473
543
|
'enabled',
|
|
474
544
|
'secret',
|
|
475
545
|
'emailChangeDate',
|
|
476
546
|
'newEmailSecret',
|
|
547
|
+
// 'newEmailAddress',
|
|
477
548
|
'cancelEmailSecret',
|
|
478
549
|
'cancelEmailAddress',
|
|
479
550
|
'recoverSecret',
|
|
@@ -481,6 +552,7 @@ class User extends AbleObject_1.default {
|
|
|
481
552
|
'salt',
|
|
482
553
|
'password',
|
|
483
554
|
'passwordTemp',
|
|
555
|
+
// 'revokeTokenDate',
|
|
484
556
|
'totpSecret',
|
|
485
557
|
];
|
|
486
558
|
}
|
|
@@ -488,6 +560,7 @@ class User extends AbleObject_1.default {
|
|
|
488
560
|
!isCurrentUser &&
|
|
489
561
|
(abilities.includes('tilmeld/switch') ||
|
|
490
562
|
abilities.includes('system/admin'))) {
|
|
563
|
+
// Users with this ability can switch to other users.
|
|
491
564
|
this.$clientEnabledMethods.push('$switchUser');
|
|
492
565
|
}
|
|
493
566
|
}
|
|
@@ -501,18 +574,36 @@ class User extends AbleObject_1.default {
|
|
|
501
574
|
}
|
|
502
575
|
}
|
|
503
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* Check to see if a user has an ability.
|
|
579
|
+
*
|
|
580
|
+
* This function will check both user and group abilities, if the user is
|
|
581
|
+
* marked to inherit the abilities of its group.
|
|
582
|
+
*
|
|
583
|
+
* If `ability` is undefined, it will check to see if the user is currently
|
|
584
|
+
* logged in.
|
|
585
|
+
*
|
|
586
|
+
* If the user has the "system/admin" ability, this function will return true.
|
|
587
|
+
*
|
|
588
|
+
* @param ability The ability.
|
|
589
|
+
* @returns True or false.
|
|
590
|
+
*/
|
|
504
591
|
async $gatekeeper(ability) {
|
|
505
592
|
await this.$wake();
|
|
506
593
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
507
594
|
if (ability == null) {
|
|
508
595
|
return tilmeld.User.current()?.$is(this) ?? false;
|
|
509
596
|
}
|
|
597
|
+
// Check the cache to see if we've already checked this user.
|
|
510
598
|
if (this.$gatekeeperCache == null) {
|
|
511
599
|
this.$gatekeeperCache = await this.$getGatekeeperCache();
|
|
512
600
|
}
|
|
513
601
|
return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
|
|
514
602
|
!!this.$data.abilities?.includes('system/admin'));
|
|
515
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Build a gatekeeper cache object.
|
|
606
|
+
*/
|
|
516
607
|
async $getGatekeeperCache() {
|
|
517
608
|
await this.$wake();
|
|
518
609
|
let abilities = this.$data.abilities ?? [];
|
|
@@ -537,6 +628,11 @@ class User extends AbleObject_1.default {
|
|
|
537
628
|
this.$gatekeeperCache = undefined;
|
|
538
629
|
return ret;
|
|
539
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* Send the user email verification/change/cancellation links.
|
|
633
|
+
*
|
|
634
|
+
* @returns True on success, false on failure.
|
|
635
|
+
*/
|
|
540
636
|
async $sendEmailVerification() {
|
|
541
637
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
542
638
|
if (this.guid == null) {
|
|
@@ -601,6 +697,12 @@ class User extends AbleObject_1.default {
|
|
|
601
697
|
}
|
|
602
698
|
return success;
|
|
603
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Add the user to a (secondary) group.
|
|
702
|
+
*
|
|
703
|
+
* @param group The group.
|
|
704
|
+
* @returns True if the user is already in the group. The resulting array of groups if the user was not.
|
|
705
|
+
*/
|
|
604
706
|
async $addGroup(group) {
|
|
605
707
|
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
606
708
|
let groups = this.$data.groups;
|
|
@@ -614,6 +716,12 @@ class User extends AbleObject_1.default {
|
|
|
614
716
|
}
|
|
615
717
|
return true;
|
|
616
718
|
}
|
|
719
|
+
/**
|
|
720
|
+
* Check the given password against the user's.
|
|
721
|
+
*
|
|
722
|
+
* @param password The password in question.
|
|
723
|
+
* @returns True if the passwords match, otherwise false.
|
|
724
|
+
*/
|
|
617
725
|
$checkPassword(password) {
|
|
618
726
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
619
727
|
switch (tilmeld.config.pwMethod) {
|
|
@@ -627,6 +735,12 @@ class User extends AbleObject_1.default {
|
|
|
627
735
|
(0, sha256_1.default)(password + this.$data.salt).toString(enc_base64_1.default));
|
|
628
736
|
}
|
|
629
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* Remove the user from a (secondary) group.
|
|
740
|
+
*
|
|
741
|
+
* @param group The group.
|
|
742
|
+
* @returns True if the user wasn't in the group. The resulting array of groups if the user was.
|
|
743
|
+
*/
|
|
630
744
|
async $delGroup(group) {
|
|
631
745
|
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
632
746
|
let groups = this.$data.groups || [];
|
|
@@ -642,6 +756,12 @@ class User extends AbleObject_1.default {
|
|
|
642
756
|
}
|
|
643
757
|
return true;
|
|
644
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Check whether the user is in a (primary or secondary) group.
|
|
761
|
+
*
|
|
762
|
+
* @param group The group, or the group's GUID.
|
|
763
|
+
* @returns True or false.
|
|
764
|
+
*/
|
|
645
765
|
async $inGroup(group) {
|
|
646
766
|
if (typeof group === 'string') {
|
|
647
767
|
if (this.$data.group?.guid === group) {
|
|
@@ -656,7 +776,14 @@ class User extends AbleObject_1.default {
|
|
|
656
776
|
}
|
|
657
777
|
return (group.$is(this.$data.group) || group.$inArray(this.$data.groups ?? []));
|
|
658
778
|
}
|
|
779
|
+
/**
|
|
780
|
+
* Check whether the user is a descendant of a group.
|
|
781
|
+
*
|
|
782
|
+
* @param group The group, or the group's GUID.
|
|
783
|
+
* @returns True or false.
|
|
784
|
+
*/
|
|
659
785
|
async $isDescendant(group) {
|
|
786
|
+
// Check to see if the user is in a descendant group of the given group.
|
|
660
787
|
await this.$data.group?.$wake();
|
|
661
788
|
if (this.$data.group != null &&
|
|
662
789
|
this.$data.group?.cdate != null &&
|
|
@@ -671,11 +798,19 @@ class User extends AbleObject_1.default {
|
|
|
671
798
|
}
|
|
672
799
|
return false;
|
|
673
800
|
}
|
|
801
|
+
/**
|
|
802
|
+
* A frontend accessible method to change the user's password.
|
|
803
|
+
*
|
|
804
|
+
* @param data The input data from the client.
|
|
805
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
806
|
+
*/
|
|
674
807
|
async $changePassword(data) {
|
|
808
|
+
this.$nymph.config.debugLog('tilmeld', `Password change request for "${this.$data.username}".`);
|
|
675
809
|
if (!('newPassword' in data) || !data.newPassword.length) {
|
|
676
810
|
return { result: false, message: 'Please specify a password.' };
|
|
677
811
|
}
|
|
678
812
|
if (!this.$checkPassword(data.currentPassword ?? '')) {
|
|
813
|
+
this.$nymph.config.debugLog('tilmeld', `Incorrect password for change request for "${this.$data.username}".`);
|
|
679
814
|
return { result: false, message: 'Incorrect password.' };
|
|
680
815
|
}
|
|
681
816
|
this.$data.passwordTemp = data.newPassword;
|
|
@@ -693,6 +828,12 @@ class User extends AbleObject_1.default {
|
|
|
693
828
|
return { result: false, message: "Couldn't save new password." };
|
|
694
829
|
}
|
|
695
830
|
}
|
|
831
|
+
/**
|
|
832
|
+
* Change the user's password.
|
|
833
|
+
*
|
|
834
|
+
* @param password The new password.
|
|
835
|
+
* @returns The resulting password or hash which is stored in the entity.
|
|
836
|
+
*/
|
|
696
837
|
$password(password) {
|
|
697
838
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
698
839
|
switch (tilmeld.config.pwMethod) {
|
|
@@ -712,6 +853,12 @@ class User extends AbleObject_1.default {
|
|
|
712
853
|
}
|
|
713
854
|
return this.$data.password;
|
|
714
855
|
}
|
|
856
|
+
/**
|
|
857
|
+
* A frontend accessible method to revoke all currently issued tokens.
|
|
858
|
+
*
|
|
859
|
+
* @param data The input data from the client.
|
|
860
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
861
|
+
*/
|
|
715
862
|
async $revokeCurrentTokens(data) {
|
|
716
863
|
if (!this.$checkPassword(data.password ?? '')) {
|
|
717
864
|
return { result: false, message: 'Incorrect password.' };
|
|
@@ -732,6 +879,12 @@ class User extends AbleObject_1.default {
|
|
|
732
879
|
async $hasTOTPSecret() {
|
|
733
880
|
return this.$data.totpSecret != null;
|
|
734
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* Check the given code against the user's TOTP secret.
|
|
884
|
+
*
|
|
885
|
+
* @param code The code in question.
|
|
886
|
+
* @returns True if the code is valid, otherwise false.
|
|
887
|
+
*/
|
|
735
888
|
$checkTOTPCode(code) {
|
|
736
889
|
if (this.$data.totpSecret == null) {
|
|
737
890
|
return false;
|
|
@@ -747,6 +900,11 @@ class User extends AbleObject_1.default {
|
|
|
747
900
|
});
|
|
748
901
|
return totp.validate({ token: code, window: 2 }) != null;
|
|
749
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* A frontend accessible method to generate a new TOTP secret.
|
|
905
|
+
*
|
|
906
|
+
* @returns An object with 'uri', 'qrcode', and 'secret'.
|
|
907
|
+
*/
|
|
750
908
|
async $getNewTOTPSecret() {
|
|
751
909
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
752
910
|
if (this.$data.totpSecret != null) {
|
|
@@ -769,6 +927,12 @@ class User extends AbleObject_1.default {
|
|
|
769
927
|
secret: secret.base32,
|
|
770
928
|
};
|
|
771
929
|
}
|
|
930
|
+
/**
|
|
931
|
+
* A frontend accessible method to save a TOTP secret into the user's account.
|
|
932
|
+
*
|
|
933
|
+
* @param data The input data from the client.
|
|
934
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
935
|
+
*/
|
|
772
936
|
async $saveTOTPSecret(data) {
|
|
773
937
|
if (!this.$checkPassword(data.password ?? '')) {
|
|
774
938
|
return { result: false, message: 'Incorrect password.' };
|
|
@@ -806,6 +970,13 @@ class User extends AbleObject_1.default {
|
|
|
806
970
|
return { result: false, message: "Couldn't save two factor secret." };
|
|
807
971
|
}
|
|
808
972
|
}
|
|
973
|
+
/**
|
|
974
|
+
* A frontend accessible method to remove the TOTP secret from the user's
|
|
975
|
+
* account.
|
|
976
|
+
*
|
|
977
|
+
* @param data The input data from the client.
|
|
978
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
979
|
+
*/
|
|
809
980
|
async $removeTOTPSecret(data) {
|
|
810
981
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
811
982
|
if (data) {
|
|
@@ -849,8 +1020,14 @@ class User extends AbleObject_1.default {
|
|
|
849
1020
|
return { result: false, message: "Couldn't remove two factor secret." };
|
|
850
1021
|
}
|
|
851
1022
|
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Check that a username is valid.
|
|
1025
|
+
*
|
|
1026
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1027
|
+
*/
|
|
852
1028
|
async $checkUsername() {
|
|
853
1029
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
1030
|
+
this.$nymph.config.debugLog('tilmeld', `Username check "${this.$data.username}".`);
|
|
854
1031
|
if (!tilmeld.config.emailUsernames) {
|
|
855
1032
|
if (this.$data.username == null || !this.$data.username.length) {
|
|
856
1033
|
return { result: false, message: 'Please specify a username.' };
|
|
@@ -937,8 +1114,14 @@ class User extends AbleObject_1.default {
|
|
|
937
1114
|
return await this.$checkEmail();
|
|
938
1115
|
}
|
|
939
1116
|
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Check that an email is unique.
|
|
1119
|
+
*
|
|
1120
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1121
|
+
*/
|
|
940
1122
|
async $checkEmail() {
|
|
941
1123
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
1124
|
+
this.$nymph.config.debugLog('tilmeld', `Email check "${this.$data.email}".`);
|
|
942
1125
|
if (this.$data.email == null || !this.$data.email.length) {
|
|
943
1126
|
if (tilmeld.config.verifyEmail) {
|
|
944
1127
|
return { result: false, message: 'Please specify an email.' };
|
|
@@ -972,8 +1155,14 @@ class User extends AbleObject_1.default {
|
|
|
972
1155
|
message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
|
|
973
1156
|
};
|
|
974
1157
|
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Check that a phone number is unique.
|
|
1160
|
+
*
|
|
1161
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1162
|
+
*/
|
|
975
1163
|
async $checkPhone() {
|
|
976
1164
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
1165
|
+
this.$nymph.config.debugLog('tilmeld', `Phone check "${this.$data.phone}".`);
|
|
977
1166
|
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
978
1167
|
return { result: false, message: 'Please specify a phone number.' };
|
|
979
1168
|
}
|
|
@@ -1040,6 +1229,7 @@ class User extends AbleObject_1.default {
|
|
|
1040
1229
|
if (tilmeld.config.emailUsernames) {
|
|
1041
1230
|
this.$data.email = this.$data.username;
|
|
1042
1231
|
}
|
|
1232
|
+
// Start transaction.
|
|
1043
1233
|
const transaction = 'tilmeld-register-' + (0, guid_1.nanoid)();
|
|
1044
1234
|
const nymph = this.$nymph;
|
|
1045
1235
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
@@ -1065,8 +1255,10 @@ class User extends AbleObject_1.default {
|
|
|
1065
1255
|
message: e.message,
|
|
1066
1256
|
};
|
|
1067
1257
|
}
|
|
1258
|
+
// Add primary group.
|
|
1068
1259
|
let generatedPrimaryGroup = null;
|
|
1069
1260
|
if (tilmeld.config.generatePrimary) {
|
|
1261
|
+
// Generate a new primary group for the user.
|
|
1070
1262
|
generatedPrimaryGroup = await tilmeld.Group.factory();
|
|
1071
1263
|
generatedPrimaryGroup.groupname = this.$data.username;
|
|
1072
1264
|
generatedPrimaryGroup.avatar = this.$data.avatar;
|
|
@@ -1095,6 +1287,7 @@ class User extends AbleObject_1.default {
|
|
|
1095
1287
|
this.$data.group = generatedPrimaryGroup;
|
|
1096
1288
|
}
|
|
1097
1289
|
else {
|
|
1290
|
+
// Add the default primary.
|
|
1098
1291
|
const group = await tnymph.getEntity({ class: tilmeld.Group }, {
|
|
1099
1292
|
type: '&',
|
|
1100
1293
|
equal: ['defaultPrimary', true],
|
|
@@ -1103,15 +1296,18 @@ class User extends AbleObject_1.default {
|
|
|
1103
1296
|
this.$data.group = group;
|
|
1104
1297
|
}
|
|
1105
1298
|
}
|
|
1299
|
+
// Add secondary groups.
|
|
1106
1300
|
if (tilmeld.config.userFields.includes('email') &&
|
|
1107
1301
|
tilmeld.config.verifyEmail &&
|
|
1108
1302
|
tilmeld.config.unverifiedAccess) {
|
|
1303
|
+
// Add the default secondaries for unverified users.
|
|
1109
1304
|
this.$data.groups = await tnymph.getEntities({ class: tilmeld.Group }, {
|
|
1110
1305
|
type: '&',
|
|
1111
1306
|
equal: ['unverifiedSecondary', true],
|
|
1112
1307
|
});
|
|
1113
1308
|
}
|
|
1114
1309
|
else {
|
|
1310
|
+
// Add the default secondaries.
|
|
1115
1311
|
this.$data.groups = await tnymph.getEntities({ class: tilmeld.Group }, {
|
|
1116
1312
|
type: '&',
|
|
1117
1313
|
equal: ['defaultSecondary', true],
|
|
@@ -1119,6 +1315,7 @@ class User extends AbleObject_1.default {
|
|
|
1119
1315
|
}
|
|
1120
1316
|
if (tilmeld.config.userFields.includes('email') &&
|
|
1121
1317
|
tilmeld.config.verifyEmail) {
|
|
1318
|
+
// The user will be enabled after verifying their e-mail address.
|
|
1122
1319
|
if (!tilmeld.config.unverifiedAccess) {
|
|
1123
1320
|
this.$data.enabled = false;
|
|
1124
1321
|
}
|
|
@@ -1126,6 +1323,8 @@ class User extends AbleObject_1.default {
|
|
|
1126
1323
|
else {
|
|
1127
1324
|
this.$data.enabled = true;
|
|
1128
1325
|
}
|
|
1326
|
+
// If create_admin is true and there are no other users, grant
|
|
1327
|
+
// "system/admin".
|
|
1129
1328
|
let madeAdmin = false;
|
|
1130
1329
|
if (tilmeld.config.createAdmin) {
|
|
1131
1330
|
const otherUsers = await tnymph.getEntities({
|
|
@@ -1134,13 +1333,17 @@ class User extends AbleObject_1.default {
|
|
|
1134
1333
|
limit: 1,
|
|
1135
1334
|
return: 'guid',
|
|
1136
1335
|
});
|
|
1336
|
+
// Make sure it's not just null, cause that means an error.
|
|
1137
1337
|
if (!otherUsers.length) {
|
|
1138
1338
|
this.$grant('system/admin');
|
|
1139
1339
|
this.$data.enabled = true;
|
|
1140
1340
|
madeAdmin = true;
|
|
1141
1341
|
}
|
|
1142
1342
|
}
|
|
1343
|
+
this.$nymph.config.debugLog('tilmeld', `Registering new user "${this.$data.username}".`);
|
|
1143
1344
|
if (await this.$saveSkipAC()) {
|
|
1345
|
+
this.$nymph.config.debugLog('tilmeld', `New user registered "${this.$data.username}".`);
|
|
1346
|
+
// Send the new user registered email.
|
|
1144
1347
|
if (tilmeld.config.userRegisteredRecipient != null) {
|
|
1145
1348
|
await tilmeld.config.sendEmail(tilmeld, {
|
|
1146
1349
|
template: 'UserRegistered',
|
|
@@ -1157,6 +1360,7 @@ class User extends AbleObject_1.default {
|
|
|
1157
1360
|
},
|
|
1158
1361
|
}, this);
|
|
1159
1362
|
}
|
|
1363
|
+
// Save the primary group.
|
|
1160
1364
|
if (generatedPrimaryGroup != null) {
|
|
1161
1365
|
generatedPrimaryGroup.user = this;
|
|
1162
1366
|
if (!(await generatedPrimaryGroup.$saveSkipAC())) {
|
|
@@ -1169,6 +1373,7 @@ class User extends AbleObject_1.default {
|
|
|
1169
1373
|
};
|
|
1170
1374
|
}
|
|
1171
1375
|
}
|
|
1376
|
+
// Finish up.
|
|
1172
1377
|
if (tilmeld.config.verifyEmail &&
|
|
1173
1378
|
!tilmeld.config.unverifiedAccess &&
|
|
1174
1379
|
!madeAdmin &&
|
|
@@ -1210,6 +1415,7 @@ class User extends AbleObject_1.default {
|
|
|
1210
1415
|
else {
|
|
1211
1416
|
await tnymph.rollback(transaction);
|
|
1212
1417
|
this.$nymph = nymph;
|
|
1418
|
+
this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}".`);
|
|
1213
1419
|
return {
|
|
1214
1420
|
result: false,
|
|
1215
1421
|
loggedin: false,
|
|
@@ -1220,6 +1426,7 @@ class User extends AbleObject_1.default {
|
|
|
1220
1426
|
catch (e) {
|
|
1221
1427
|
await tnymph.rollback(transaction);
|
|
1222
1428
|
this.$nymph = nymph;
|
|
1429
|
+
this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}": ${e}`);
|
|
1223
1430
|
throw e;
|
|
1224
1431
|
}
|
|
1225
1432
|
try {
|
|
@@ -1247,6 +1454,7 @@ class User extends AbleObject_1.default {
|
|
|
1247
1454
|
throw new errors_1.BadDataError("You don't have the authority to modify system admins.");
|
|
1248
1455
|
}
|
|
1249
1456
|
let sendVerification = false;
|
|
1457
|
+
// Formatting.
|
|
1250
1458
|
this.$data.username = this.$data.username.trim();
|
|
1251
1459
|
if (tilmeld.config.emailUsernames) {
|
|
1252
1460
|
this.$data.email = this.$data.username;
|
|
@@ -1260,6 +1468,7 @@ class User extends AbleObject_1.default {
|
|
|
1260
1468
|
(this.$data.nameMiddle ? ' ' + this.$data.nameMiddle : '') +
|
|
1261
1469
|
(this.$data.nameLast ? ' ' + this.$data.nameLast : '');
|
|
1262
1470
|
this.$data.enabled = !!this.$data.enabled;
|
|
1471
|
+
// Clear empty values.
|
|
1263
1472
|
if (this.$data.nameFirst === '' ||
|
|
1264
1473
|
!tilmeld.config.userFields.includes('name')) {
|
|
1265
1474
|
delete this.$data.nameFirst;
|
|
@@ -1327,6 +1536,7 @@ class User extends AbleObject_1.default {
|
|
|
1327
1536
|
if (this.$data.inheritAbilities === false) {
|
|
1328
1537
|
delete this.$data.inheritAbilities;
|
|
1329
1538
|
}
|
|
1539
|
+
// Verification.
|
|
1330
1540
|
const unCheck = await this.$checkUsername();
|
|
1331
1541
|
if (!unCheck.result) {
|
|
1332
1542
|
throw new errors_1.BadUsernameError(unCheck.message);
|
|
@@ -1338,10 +1548,16 @@ class User extends AbleObject_1.default {
|
|
|
1338
1548
|
throw new errors_1.BadEmailError(emCheck.message);
|
|
1339
1549
|
}
|
|
1340
1550
|
}
|
|
1551
|
+
// Email changes.
|
|
1341
1552
|
if (!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1342
1553
|
tilmeld.config.userFields.includes('email')) {
|
|
1554
|
+
// The user isn't an admin, so email address changes should contain some
|
|
1555
|
+
// security measures.
|
|
1343
1556
|
if (tilmeld.config.verifyEmail) {
|
|
1557
|
+
// The user needs to verify this new email address.
|
|
1344
1558
|
if (this.guid == null) {
|
|
1559
|
+
// If this is the first user, they'll be made an admin, so they don't
|
|
1560
|
+
// need to verify.
|
|
1345
1561
|
if (!this.$data.abilities?.includes('system/admin')) {
|
|
1346
1562
|
this.$data.secret = (0, guid_1.nanoid)();
|
|
1347
1563
|
sendVerification = true;
|
|
@@ -1349,6 +1565,7 @@ class User extends AbleObject_1.default {
|
|
|
1349
1565
|
}
|
|
1350
1566
|
else if (this.$originalEmail != null &&
|
|
1351
1567
|
this.$originalEmail !== this.$data.email) {
|
|
1568
|
+
// The user already has an old email address.
|
|
1352
1569
|
if (tilmeld.config.emailRateLimit !== '' &&
|
|
1353
1570
|
this.$data.emailChangeDate != null &&
|
|
1354
1571
|
this.$data.emailChangeDate >=
|
|
@@ -1360,13 +1577,18 @@ class User extends AbleObject_1.default {
|
|
|
1360
1577
|
else {
|
|
1361
1578
|
if (this.$data.newEmailSecret == null &&
|
|
1362
1579
|
tilmeld.config.emailRateLimit !== '' &&
|
|
1580
|
+
// Make sure the user has at least the rate limit time to cancel
|
|
1581
|
+
// an email change.
|
|
1363
1582
|
(this.$data.emailChangeDate == null ||
|
|
1364
1583
|
this.$data.emailChangeDate <
|
|
1365
1584
|
(0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
|
|
1585
|
+
// Save the old email in case the cancel change link is clicked.
|
|
1366
1586
|
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
1367
1587
|
this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
|
|
1368
1588
|
this.$data.emailChangeDate = Date.now();
|
|
1369
1589
|
}
|
|
1590
|
+
// Save the new address and reset to the old one (until they verify
|
|
1591
|
+
// it).
|
|
1370
1592
|
this.$data.newEmailAddress = this.$data.email;
|
|
1371
1593
|
this.$data.email = this.$originalEmail;
|
|
1372
1594
|
if (tilmeld.config.emailUsernames) {
|
|
@@ -1380,9 +1602,13 @@ class User extends AbleObject_1.default {
|
|
|
1380
1602
|
else if (this.guid != null &&
|
|
1381
1603
|
this.$originalEmail != null &&
|
|
1382
1604
|
this.$originalEmail !== this.$data.email &&
|
|
1605
|
+
// Make sure the user has at least the rate limit time to cancel an
|
|
1606
|
+
// email change.
|
|
1383
1607
|
(this.$data.emailChangeDate == null ||
|
|
1384
1608
|
this.$data.emailChangeDate <
|
|
1385
1609
|
(0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
|
|
1610
|
+
// The user doesn't need to verify their new email address, but should
|
|
1611
|
+
// be able to cancel the email change from their old address.
|
|
1386
1612
|
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
1387
1613
|
this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
|
|
1388
1614
|
sendVerification = true;
|
|
@@ -1401,6 +1627,7 @@ class User extends AbleObject_1.default {
|
|
|
1401
1627
|
catch (e) {
|
|
1402
1628
|
throw new errors_1.BadDataError(e?.message);
|
|
1403
1629
|
}
|
|
1630
|
+
// Start transaction.
|
|
1404
1631
|
const transaction = 'tilmeld-save-' + (0, guid_1.nanoid)();
|
|
1405
1632
|
const nymph = this.$nymph;
|
|
1406
1633
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
@@ -1410,6 +1637,7 @@ class User extends AbleObject_1.default {
|
|
|
1410
1637
|
await group?.$wake();
|
|
1411
1638
|
if (group != null && group.user != null && this.$is(group.user)) {
|
|
1412
1639
|
try {
|
|
1640
|
+
// Update the user's generated primary group.
|
|
1413
1641
|
group.groupname = this.$data.username;
|
|
1414
1642
|
group.avatar = this.$data.avatar;
|
|
1415
1643
|
if (tilmeld.config.userFields.includes('email')) {
|
|
@@ -1444,6 +1672,7 @@ class User extends AbleObject_1.default {
|
|
|
1444
1672
|
}
|
|
1445
1673
|
if (ret) {
|
|
1446
1674
|
if (sendVerification) {
|
|
1675
|
+
// The email has changed, so send a new verification email.
|
|
1447
1676
|
if (!(await this.$sendEmailVerification())) {
|
|
1448
1677
|
await tnymph.rollback(transaction);
|
|
1449
1678
|
this.guid = preGuid;
|
|
@@ -1458,6 +1687,7 @@ class User extends AbleObject_1.default {
|
|
|
1458
1687
|
await tnymph.commit(transaction);
|
|
1459
1688
|
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
|
|
1460
1689
|
if (tilmeld.User.current(true).$is(this)) {
|
|
1690
|
+
// Update the user in the session cache.
|
|
1461
1691
|
await tilmeld.fillSession(this);
|
|
1462
1692
|
}
|
|
1463
1693
|
}
|
|
@@ -1467,6 +1697,9 @@ class User extends AbleObject_1.default {
|
|
|
1467
1697
|
this.$nymph = nymph;
|
|
1468
1698
|
return ret;
|
|
1469
1699
|
}
|
|
1700
|
+
/**
|
|
1701
|
+
* This should *never* be accessible on the client.
|
|
1702
|
+
*/
|
|
1470
1703
|
async $saveSkipAC() {
|
|
1471
1704
|
this.$skipAcWhenSaving = true;
|
|
1472
1705
|
return await this.$save();
|
|
@@ -1497,6 +1730,7 @@ class User extends AbleObject_1.default {
|
|
|
1497
1730
|
if (!(prop in this)) {
|
|
1498
1731
|
throw new Error('Invalid event type.');
|
|
1499
1732
|
}
|
|
1733
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1500
1734
|
this[prop].push(callback);
|
|
1501
1735
|
return () => this.off(event, callback);
|
|
1502
1736
|
}
|
|
@@ -1505,13 +1739,18 @@ class User extends AbleObject_1.default {
|
|
|
1505
1739
|
if (!(prop in this)) {
|
|
1506
1740
|
return false;
|
|
1507
1741
|
}
|
|
1742
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1508
1743
|
const i = this[prop].indexOf(callback);
|
|
1509
1744
|
if (i > -1) {
|
|
1745
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1510
1746
|
this[prop].splice(i, 1);
|
|
1511
1747
|
}
|
|
1512
1748
|
return true;
|
|
1513
1749
|
}
|
|
1514
1750
|
}
|
|
1751
|
+
/**
|
|
1752
|
+
* The instance of Tilmeld to use for queries.
|
|
1753
|
+
*/
|
|
1515
1754
|
User.ETYPE = 'tilmeld_user';
|
|
1516
1755
|
User.class = 'User';
|
|
1517
1756
|
User.checkUsernameCallbacks = [];
|
|
@@ -1529,8 +1768,15 @@ User.DEFAULT_CLIENT_ENABLED_METHODS = [
|
|
|
1529
1768
|
'$register',
|
|
1530
1769
|
];
|
|
1531
1770
|
User.DEFAULT_PRIVATE_DATA = [
|
|
1771
|
+
// The things that are commented are there to show what is _not_ private.
|
|
1532
1772
|
'abilities',
|
|
1773
|
+
// 'username',
|
|
1774
|
+
// 'nameFirst',
|
|
1775
|
+
// 'nameMiddle',
|
|
1776
|
+
// 'nameLast',
|
|
1777
|
+
// 'name',
|
|
1533
1778
|
'email',
|
|
1779
|
+
// 'avatar',
|
|
1534
1780
|
'phone',
|
|
1535
1781
|
'group',
|
|
1536
1782
|
'groups',
|