@nymphjs/tilmeld 1.0.0-beta.2 → 1.0.0-beta.21
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 +106 -0
- package/README.md +13 -0
- package/dist/Group.js +49 -23
- package/dist/Group.js.map +1 -1
- package/dist/Tilmeld.d.ts +5 -2
- package/dist/Tilmeld.js +151 -60
- package/dist/Tilmeld.js.map +1 -1
- package/dist/User.d.ts +56 -3
- package/dist/User.js +306 -45
- package/dist/User.js.map +1 -1
- package/dist/conf/d.d.ts +3 -1
- package/dist/conf/defaults.js +15 -4
- 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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +22 -19
- package/src/Group.ts +70 -23
- package/src/Tilmeld.ts +225 -76
- package/src/User.ts +483 -49
- package/src/conf/d.ts +16 -7
- package/src/conf/defaults.ts +17 -4
- package/src/enforceTilmeld.ts +20 -0
- package/src/index.ts +2 -0
package/src/User.ts
CHANGED
|
@@ -5,15 +5,18 @@ import {
|
|
|
5
5
|
Selector,
|
|
6
6
|
SerializedEntityData,
|
|
7
7
|
} from '@nymphjs/nymph';
|
|
8
|
+
import { humanSecret, nanoid } from '@nymphjs/guid';
|
|
8
9
|
import { EmailOptions } from 'email-templates';
|
|
9
10
|
import strtotime from 'locutus/php/datetime/strtotime';
|
|
10
11
|
import Base64 from 'crypto-js/enc-base64';
|
|
11
12
|
import sha256 from 'crypto-js/sha256';
|
|
12
13
|
import md5 from 'crypto-js/md5';
|
|
13
14
|
import { difference } from 'lodash';
|
|
14
|
-
import {
|
|
15
|
+
import { TOTP, Secret } from 'otpauth';
|
|
16
|
+
import { toDataURL } from 'qrcode';
|
|
15
17
|
|
|
16
18
|
import type Tilmeld from './Tilmeld';
|
|
19
|
+
import { enforceTilmeld } from './enforceTilmeld';
|
|
17
20
|
import AbleObject from './AbleObject';
|
|
18
21
|
import Group, { GroupData } from './Group';
|
|
19
22
|
import {
|
|
@@ -24,12 +27,22 @@ import {
|
|
|
24
27
|
} from './errors';
|
|
25
28
|
|
|
26
29
|
export type EventType =
|
|
30
|
+
| 'checkUsername'
|
|
27
31
|
| 'beforeRegister'
|
|
28
32
|
| 'afterRegister'
|
|
29
33
|
| 'beforeLogin'
|
|
30
34
|
| 'afterLogin'
|
|
31
35
|
| 'beforeLogout'
|
|
32
36
|
| 'afterLogout';
|
|
37
|
+
/**
|
|
38
|
+
* This is run when the user has entered an otherwise valid username into the
|
|
39
|
+
* signup form. It should return a result, which when false will stop the
|
|
40
|
+
* process and return the included message, disallowing the username.
|
|
41
|
+
*/
|
|
42
|
+
export type TilmeldCheckUsernameCallback = (
|
|
43
|
+
user: User & UserData,
|
|
44
|
+
data: { username: string }
|
|
45
|
+
) => Promise<{ result: boolean; message?: string }>;
|
|
33
46
|
/**
|
|
34
47
|
* Theses are run before the user data checks, so the only checks before are
|
|
35
48
|
* whether registration is allowed and whether the user is already registered.
|
|
@@ -135,7 +148,8 @@ export type UserData = {
|
|
|
135
148
|
*/
|
|
136
149
|
newEmailSecret?: string;
|
|
137
150
|
/**
|
|
138
|
-
*
|
|
151
|
+
* If the user has changed their email address, this is the new one, awaiting
|
|
152
|
+
* verification.
|
|
139
153
|
*/
|
|
140
154
|
newEmailAddress?: string;
|
|
141
155
|
/**
|
|
@@ -167,6 +181,15 @@ export type UserData = {
|
|
|
167
181
|
* database.
|
|
168
182
|
*/
|
|
169
183
|
passwordTemp?: string;
|
|
184
|
+
/**
|
|
185
|
+
* If set, this timestamp is the cutoff point for JWT issue dates. Any token
|
|
186
|
+
* issued before this date will not authenticate the user.
|
|
187
|
+
*/
|
|
188
|
+
revokeTokenDate?: number;
|
|
189
|
+
/**
|
|
190
|
+
* Two factor auth secret.
|
|
191
|
+
*/
|
|
192
|
+
totpSecret?: string;
|
|
170
193
|
};
|
|
171
194
|
|
|
172
195
|
/**
|
|
@@ -185,6 +208,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
185
208
|
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
|
|
|
@@ -481,6 +515,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
481
515
|
public static async loginUser(data: {
|
|
482
516
|
username: string;
|
|
483
517
|
password: string;
|
|
518
|
+
code?: string;
|
|
484
519
|
additionalData?: { [k: string]: any };
|
|
485
520
|
}) {
|
|
486
521
|
if (!('username' in data) || !data.username.length) {
|
|
@@ -499,9 +534,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
499
534
|
public async $login(data: {
|
|
500
535
|
username: string;
|
|
501
536
|
password: string;
|
|
537
|
+
code?: string;
|
|
502
538
|
additionalData?: { [k: string]: any };
|
|
503
539
|
}) {
|
|
504
|
-
const tilmeld = this
|
|
540
|
+
const tilmeld = enforceTilmeld(this);
|
|
505
541
|
if (this.guid == null) {
|
|
506
542
|
return { result: false, message: 'Incorrect login/password.' };
|
|
507
543
|
}
|
|
@@ -515,6 +551,24 @@ export default class User extends AbleObject<UserData> {
|
|
|
515
551
|
return { result: false, message: 'Incorrect login/password.' };
|
|
516
552
|
}
|
|
517
553
|
|
|
554
|
+
if (this.$data.totpSecret != null) {
|
|
555
|
+
if (
|
|
556
|
+
data.code == null ||
|
|
557
|
+
typeof data.code !== 'string' ||
|
|
558
|
+
data.code.length !== 6
|
|
559
|
+
) {
|
|
560
|
+
return {
|
|
561
|
+
result: false,
|
|
562
|
+
needTOTP: true,
|
|
563
|
+
message: 'You need to provide a 2FA code.',
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!this.$checkTOTPCode(data.code)) {
|
|
568
|
+
return { result: false, message: 'Incorrect 2FA code.' };
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
518
572
|
try {
|
|
519
573
|
for (let callback of (this.constructor as typeof User)
|
|
520
574
|
.beforeLoginCallbacks) {
|
|
@@ -545,12 +599,84 @@ export default class User extends AbleObject<UserData> {
|
|
|
545
599
|
return { result: true, message: 'You are logged in.' };
|
|
546
600
|
}
|
|
547
601
|
|
|
602
|
+
public async $switchUser(data?: { additionalData?: { [k: string]: any } }) {
|
|
603
|
+
const tilmeld = enforceTilmeld(this);
|
|
604
|
+
if (this.guid == null) {
|
|
605
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
let user = tilmeld.User.current();
|
|
609
|
+
|
|
610
|
+
if (!user || user.guid == null) {
|
|
611
|
+
return { result: false, message: "You're not logged in." };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
615
|
+
|
|
616
|
+
if (isCurrentUser) {
|
|
617
|
+
return { result: false, message: "You can't switch to yourself." };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const toAbilities = this.$data.abilities || [];
|
|
621
|
+
const fromAbilities = user.abilities || [];
|
|
622
|
+
|
|
623
|
+
if (
|
|
624
|
+
fromAbilities.indexOf('tilmeld/switch') === -1 &&
|
|
625
|
+
fromAbilities.indexOf('system/admin') === -1
|
|
626
|
+
) {
|
|
627
|
+
return {
|
|
628
|
+
result: false,
|
|
629
|
+
message: "You don't have permission to switch users.",
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (
|
|
634
|
+
toAbilities.indexOf('system/admin') !== -1 ||
|
|
635
|
+
toAbilities.indexOf('tilmeld/admin') !== -1
|
|
636
|
+
) {
|
|
637
|
+
return { result: false, message: "You can't switch to an admin." };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
for (let callback of (this.constructor as typeof User)
|
|
642
|
+
.beforeLoginCallbacks) {
|
|
643
|
+
if (callback) {
|
|
644
|
+
await callback(this, {
|
|
645
|
+
username: this.$data.username || '',
|
|
646
|
+
password: '',
|
|
647
|
+
...(data || {}),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
} catch (e: any) {
|
|
652
|
+
return {
|
|
653
|
+
result: false,
|
|
654
|
+
message: e.message,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Authentication was successful, attempt to login.
|
|
659
|
+
if (!tilmeld.loginSwitch(this, true)) {
|
|
660
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
for (let callback of (this.constructor as typeof User)
|
|
664
|
+
.afterLoginCallbacks) {
|
|
665
|
+
if (callback) {
|
|
666
|
+
await callback(this);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Login was successful.
|
|
671
|
+
return { result: true, message: 'You are logged in.' };
|
|
672
|
+
}
|
|
673
|
+
|
|
548
674
|
/**
|
|
549
675
|
* Log a user out of the system.
|
|
550
676
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
551
677
|
*/
|
|
552
678
|
public async $logout() {
|
|
553
|
-
const tilmeld = this
|
|
679
|
+
const tilmeld = enforceTilmeld(this);
|
|
554
680
|
|
|
555
681
|
try {
|
|
556
682
|
for (let callback of (this.constructor as typeof User)
|
|
@@ -629,17 +755,39 @@ export default class User extends AbleObject<UserData> {
|
|
|
629
755
|
}
|
|
630
756
|
|
|
631
757
|
public $jsonAcceptData(input: EntityJson, allowConflict = false) {
|
|
632
|
-
const tilmeld = this
|
|
758
|
+
const tilmeld = enforceTilmeld(this);
|
|
633
759
|
this.$referenceWake();
|
|
634
760
|
|
|
635
761
|
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
762
|
+
'abilities' in input.data &&
|
|
763
|
+
input.data.abilities?.includes('system/admin') !==
|
|
764
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
765
|
+
!tilmeld.gatekeeper('system/admin')
|
|
766
|
+
) {
|
|
767
|
+
throw new BadDataError(
|
|
768
|
+
"You don't have the authority to grant or revoke system/admin."
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (
|
|
773
|
+
'abilities' in input.data &&
|
|
774
|
+
input.data.abilities?.includes('tilmeld/admin') !==
|
|
775
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
639
776
|
!tilmeld.gatekeeper('system/admin')
|
|
640
777
|
) {
|
|
641
778
|
throw new BadDataError(
|
|
642
|
-
"You don't have the authority to
|
|
779
|
+
"You don't have the authority to grant or revoke tilmeld/admin."
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (
|
|
784
|
+
'abilities' in input.data &&
|
|
785
|
+
input.data.abilities?.includes('tilmeld/switch') !==
|
|
786
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
787
|
+
!tilmeld.gatekeeper('system/admin')
|
|
788
|
+
) {
|
|
789
|
+
throw new BadDataError(
|
|
790
|
+
"You don't have the authority to grant or revoke tilmeld/switch."
|
|
643
791
|
);
|
|
644
792
|
}
|
|
645
793
|
|
|
@@ -648,17 +796,39 @@ export default class User extends AbleObject<UserData> {
|
|
|
648
796
|
}
|
|
649
797
|
|
|
650
798
|
public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
|
|
651
|
-
const tilmeld = this
|
|
799
|
+
const tilmeld = enforceTilmeld(this);
|
|
652
800
|
this.$referenceWake();
|
|
653
801
|
|
|
654
802
|
if (
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
803
|
+
'abilities' in patch.set &&
|
|
804
|
+
patch.set.abilities?.includes('system/admin') !==
|
|
805
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
806
|
+
!tilmeld.gatekeeper('system/admin')
|
|
807
|
+
) {
|
|
808
|
+
throw new BadDataError(
|
|
809
|
+
"You don't have the authority to grant or revoke system/admin."
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (
|
|
814
|
+
'abilities' in patch.set &&
|
|
815
|
+
patch.set.abilities?.includes('tilmeld/admin') !==
|
|
816
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
658
817
|
!tilmeld.gatekeeper('system/admin')
|
|
659
818
|
) {
|
|
660
819
|
throw new BadDataError(
|
|
661
|
-
"You don't have the authority to
|
|
820
|
+
"You don't have the authority to grant or revoke tilmeld/admin."
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (
|
|
825
|
+
'abilities' in patch.set &&
|
|
826
|
+
patch.set.abilities?.includes('tilmeld/switch') !==
|
|
827
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
828
|
+
!tilmeld.gatekeeper('system/admin')
|
|
829
|
+
) {
|
|
830
|
+
throw new BadDataError(
|
|
831
|
+
"You don't have the authority to grant or revoke tilmeld/switch."
|
|
662
832
|
);
|
|
663
833
|
}
|
|
664
834
|
|
|
@@ -677,7 +847,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
677
847
|
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
678
848
|
*/
|
|
679
849
|
public $updateDataProtection(givenUser?: User & UserData) {
|
|
680
|
-
const tilmeld = this
|
|
850
|
+
const tilmeld = enforceTilmeld(this);
|
|
681
851
|
let user = givenUser ?? tilmeld.User.current();
|
|
682
852
|
|
|
683
853
|
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
@@ -701,8 +871,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
701
871
|
// Users can check to see what abilities they have.
|
|
702
872
|
this.$clientEnabledMethods.push('$gatekeeper');
|
|
703
873
|
this.$clientEnabledMethods.push('$changePassword');
|
|
874
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
704
875
|
this.$clientEnabledMethods.push('$logout');
|
|
705
876
|
this.$clientEnabledMethods.push('$sendEmailVerification');
|
|
877
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
878
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
879
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
880
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
706
881
|
}
|
|
707
882
|
|
|
708
883
|
if (
|
|
@@ -711,7 +886,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
711
886
|
abilities.indexOf('system/admin') !== -1)
|
|
712
887
|
) {
|
|
713
888
|
// Users who can edit other users can see most of their data.
|
|
714
|
-
this.$privateData = ['password', 'salt'];
|
|
889
|
+
this.$privateData = ['password', 'salt', 'totpSecret'];
|
|
715
890
|
this.$allowlistData = undefined;
|
|
716
891
|
} else if (isCurrentUser || isNewUser) {
|
|
717
892
|
// Users can see their own data, and edit some of it.
|
|
@@ -732,17 +907,45 @@ export default class User extends AbleObject<UserData> {
|
|
|
732
907
|
this.$allowlistData.push('phone');
|
|
733
908
|
}
|
|
734
909
|
this.$privateData = [
|
|
910
|
+
// The things that are commented are there to show what is _not_ private.
|
|
911
|
+
// 'abilities',
|
|
912
|
+
// 'username',
|
|
913
|
+
// 'nameFirst',
|
|
914
|
+
// 'nameMiddle',
|
|
915
|
+
// 'nameLast',
|
|
916
|
+
// 'name',
|
|
917
|
+
// 'email',
|
|
918
|
+
// 'avatar',
|
|
919
|
+
// 'phone',
|
|
920
|
+
// 'group',
|
|
921
|
+
// 'groups',
|
|
922
|
+
// 'inheritAbilities',
|
|
923
|
+
'enabled',
|
|
735
924
|
'secret',
|
|
925
|
+
'emailChangeDate',
|
|
736
926
|
'newEmailSecret',
|
|
737
|
-
'
|
|
927
|
+
// 'newEmailAddress',
|
|
738
928
|
'cancelEmailSecret',
|
|
739
|
-
'
|
|
929
|
+
'cancelEmailAddress',
|
|
740
930
|
'recoverSecret',
|
|
741
931
|
'recoverSecretDate',
|
|
742
|
-
'password',
|
|
743
932
|
'salt',
|
|
933
|
+
'password',
|
|
934
|
+
'passwordTemp',
|
|
935
|
+
// 'revokeTokenDate',
|
|
936
|
+
'totpSecret',
|
|
744
937
|
];
|
|
745
938
|
}
|
|
939
|
+
|
|
940
|
+
if (
|
|
941
|
+
user != null &&
|
|
942
|
+
!isCurrentUser &&
|
|
943
|
+
(abilities.indexOf('tilmeld/switch') !== -1 ||
|
|
944
|
+
abilities.indexOf('system/admin') !== -1)
|
|
945
|
+
) {
|
|
946
|
+
// Users with this ability can switch to other users.
|
|
947
|
+
this.$clientEnabledMethods.push('$switchUser');
|
|
948
|
+
}
|
|
746
949
|
}
|
|
747
950
|
|
|
748
951
|
/**
|
|
@@ -760,7 +963,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
760
963
|
* @returns True or false.
|
|
761
964
|
*/
|
|
762
965
|
public $gatekeeper(ability?: string): boolean {
|
|
763
|
-
const tilmeld = this
|
|
966
|
+
const tilmeld = enforceTilmeld(this);
|
|
764
967
|
if (ability == null) {
|
|
765
968
|
return tilmeld.User.current()?.$is(this) ?? false;
|
|
766
969
|
}
|
|
@@ -787,7 +990,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
787
990
|
}
|
|
788
991
|
return (
|
|
789
992
|
(ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
|
|
790
|
-
!!this.$
|
|
993
|
+
!!this.$data.abilities?.includes('system/admin')
|
|
791
994
|
);
|
|
792
995
|
}
|
|
793
996
|
|
|
@@ -804,7 +1007,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
804
1007
|
* @returns True on success, false on failure.
|
|
805
1008
|
*/
|
|
806
1009
|
public async $sendEmailVerification() {
|
|
807
|
-
const tilmeld = this
|
|
1010
|
+
const tilmeld = enforceTilmeld(this);
|
|
808
1011
|
if (this.guid == null) {
|
|
809
1012
|
return false;
|
|
810
1013
|
}
|
|
@@ -917,7 +1120,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
917
1120
|
* @returns True if the passwords match, otherwise false.
|
|
918
1121
|
*/
|
|
919
1122
|
public $checkPassword(password: string) {
|
|
920
|
-
const tilmeld = this
|
|
1123
|
+
const tilmeld = enforceTilmeld(this);
|
|
921
1124
|
switch (tilmeld.config.pwMethod) {
|
|
922
1125
|
case 'plain':
|
|
923
1126
|
return this.$data.password === password;
|
|
@@ -1006,6 +1209,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1006
1209
|
public async $changePassword(data: {
|
|
1007
1210
|
newPassword: string;
|
|
1008
1211
|
currentPassword: string;
|
|
1212
|
+
revokeCurrentTokens?: boolean;
|
|
1009
1213
|
}): Promise<{ result: boolean; message: string }> {
|
|
1010
1214
|
if (!('newPassword' in data) || !data.newPassword.length) {
|
|
1011
1215
|
return { result: false, message: 'Please specify a password.' };
|
|
@@ -1014,7 +1218,14 @@ export default class User extends AbleObject<UserData> {
|
|
|
1014
1218
|
return { result: false, message: 'Incorrect password.' };
|
|
1015
1219
|
}
|
|
1016
1220
|
this.$data.passwordTemp = data.newPassword;
|
|
1221
|
+
if (data.revokeCurrentTokens) {
|
|
1222
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1223
|
+
}
|
|
1017
1224
|
if (await this.$save()) {
|
|
1225
|
+
if (data.revokeCurrentTokens) {
|
|
1226
|
+
const tilmeld = enforceTilmeld(this);
|
|
1227
|
+
tilmeld.login(this, true);
|
|
1228
|
+
}
|
|
1018
1229
|
return { result: true, message: 'Your password has been changed.' };
|
|
1019
1230
|
} else {
|
|
1020
1231
|
return { result: false, message: "Couldn't save new password." };
|
|
@@ -1028,7 +1239,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1028
1239
|
* @returns The resulting password or hash which is stored in the entity.
|
|
1029
1240
|
*/
|
|
1030
1241
|
public $password(password: string) {
|
|
1031
|
-
const tilmeld = this
|
|
1242
|
+
const tilmeld = enforceTilmeld(this);
|
|
1032
1243
|
switch (tilmeld.config.pwMethod) {
|
|
1033
1244
|
case 'plain':
|
|
1034
1245
|
delete this.$data.salt;
|
|
@@ -1049,13 +1260,216 @@ export default class User extends AbleObject<UserData> {
|
|
|
1049
1260
|
return this.$data.password;
|
|
1050
1261
|
}
|
|
1051
1262
|
|
|
1263
|
+
/**
|
|
1264
|
+
* A frontend accessible method to revoke all currently issued tokens.
|
|
1265
|
+
*
|
|
1266
|
+
* @param data The input data from the client.
|
|
1267
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1268
|
+
*/
|
|
1269
|
+
public async $revokeCurrentTokens(data: {
|
|
1270
|
+
password: string;
|
|
1271
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1272
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1273
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1277
|
+
|
|
1278
|
+
if (await this.$save()) {
|
|
1279
|
+
const tilmeld = enforceTilmeld(this);
|
|
1280
|
+
tilmeld.login(this, true);
|
|
1281
|
+
return {
|
|
1282
|
+
result: true,
|
|
1283
|
+
message: 'You have logged out of all other sessions.',
|
|
1284
|
+
};
|
|
1285
|
+
} else {
|
|
1286
|
+
return { result: false, message: "Couldn't save revocation date." };
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
public async $hasTOTPSecret() {
|
|
1291
|
+
return this.$data.totpSecret != null;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Check the given code against the user's TOTP secret.
|
|
1296
|
+
*
|
|
1297
|
+
* @param code The code in question.
|
|
1298
|
+
* @returns True if the code is valid, otherwise false.
|
|
1299
|
+
*/
|
|
1300
|
+
public $checkTOTPCode(code: string) {
|
|
1301
|
+
if (this.$data.totpSecret == null) {
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const tilmeld = enforceTilmeld(this);
|
|
1306
|
+
|
|
1307
|
+
const totp = new TOTP({
|
|
1308
|
+
issuer: tilmeld.config.appName,
|
|
1309
|
+
label: this.$data.username,
|
|
1310
|
+
algorithm: 'SHA1',
|
|
1311
|
+
digits: 6,
|
|
1312
|
+
period: 30,
|
|
1313
|
+
secret: this.$data.totpSecret,
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
return totp.validate({ token: code, window: 2 }) != null;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* A frontend accessible method to generate a new TOTP secret.
|
|
1321
|
+
*
|
|
1322
|
+
* @returns An object with 'uri', 'qrcode', and 'secret'.
|
|
1323
|
+
*/
|
|
1324
|
+
public async $getNewTOTPSecret() {
|
|
1325
|
+
const tilmeld = enforceTilmeld(this);
|
|
1326
|
+
|
|
1327
|
+
if (this.$data.totpSecret != null) {
|
|
1328
|
+
throw new BadDataError('You already have a 2FA secret.');
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const secret = new Secret();
|
|
1332
|
+
|
|
1333
|
+
const totp = new TOTP({
|
|
1334
|
+
issuer: tilmeld.config.appName,
|
|
1335
|
+
label: this.$data.username,
|
|
1336
|
+
algorithm: 'SHA1',
|
|
1337
|
+
digits: 6,
|
|
1338
|
+
period: 30,
|
|
1339
|
+
secret,
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
const uri = totp.toString();
|
|
1343
|
+
|
|
1344
|
+
const qrcode = await toDataURL(uri);
|
|
1345
|
+
|
|
1346
|
+
return {
|
|
1347
|
+
uri,
|
|
1348
|
+
qrcode,
|
|
1349
|
+
secret: secret.base32,
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* A frontend accessible method to save a TOTP secret into the user's account.
|
|
1355
|
+
*
|
|
1356
|
+
* @param data The input data from the client.
|
|
1357
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1358
|
+
*/
|
|
1359
|
+
public async $saveTOTPSecret(data: {
|
|
1360
|
+
password: string;
|
|
1361
|
+
secret: string;
|
|
1362
|
+
code: string;
|
|
1363
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1364
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1365
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (this.$data.totpSecret != null) {
|
|
1369
|
+
return { result: false, message: 'You already have a 2FA secret.' };
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (
|
|
1373
|
+
data.secret == null ||
|
|
1374
|
+
typeof data.secret !== 'string' ||
|
|
1375
|
+
data.secret === ''
|
|
1376
|
+
) {
|
|
1377
|
+
return { result: false, message: '2FA secret is invalid.' };
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (
|
|
1381
|
+
data.code == null ||
|
|
1382
|
+
typeof data.code !== 'string' ||
|
|
1383
|
+
data.code.length !== 6
|
|
1384
|
+
) {
|
|
1385
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
const tilmeld = enforceTilmeld(this);
|
|
1389
|
+
|
|
1390
|
+
const totp = new TOTP({
|
|
1391
|
+
issuer: tilmeld.config.appName,
|
|
1392
|
+
label: this.$data.username,
|
|
1393
|
+
algorithm: 'SHA1',
|
|
1394
|
+
digits: 6,
|
|
1395
|
+
period: 30,
|
|
1396
|
+
secret: data.secret,
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1400
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
this.$data.totpSecret = data.secret;
|
|
1404
|
+
|
|
1405
|
+
if (await this.$save()) {
|
|
1406
|
+
return { result: true, message: 'Your two factor secret is now set.' };
|
|
1407
|
+
} else {
|
|
1408
|
+
return { result: false, message: "Couldn't save two factor secret." };
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* A frontend accessible method to remove the TOTP secret from the user's
|
|
1414
|
+
* account.
|
|
1415
|
+
*
|
|
1416
|
+
* @param data The input data from the client.
|
|
1417
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1418
|
+
*/
|
|
1419
|
+
public async $removeTOTPSecret(data: {
|
|
1420
|
+
password: string;
|
|
1421
|
+
code: string;
|
|
1422
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1423
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1424
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
if (this.$data.totpSecret == null) {
|
|
1428
|
+
return { result: false, message: "You don't have a 2FA secret." };
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
if (
|
|
1432
|
+
data.code == null ||
|
|
1433
|
+
typeof data.code !== 'string' ||
|
|
1434
|
+
data.code.length !== 6
|
|
1435
|
+
) {
|
|
1436
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const tilmeld = enforceTilmeld(this);
|
|
1440
|
+
|
|
1441
|
+
const totp = new TOTP({
|
|
1442
|
+
issuer: tilmeld.config.appName,
|
|
1443
|
+
label: this.$data.username,
|
|
1444
|
+
algorithm: 'SHA1',
|
|
1445
|
+
digits: 6,
|
|
1446
|
+
period: 30,
|
|
1447
|
+
secret: this.$data.totpSecret,
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1451
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
delete this.$data.totpSecret;
|
|
1455
|
+
|
|
1456
|
+
if (await this.$save()) {
|
|
1457
|
+
return {
|
|
1458
|
+
result: true,
|
|
1459
|
+
message: 'Your two factor secret has been removed.',
|
|
1460
|
+
};
|
|
1461
|
+
} else {
|
|
1462
|
+
return { result: false, message: "Couldn't remove two factor secret." };
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1052
1466
|
/**
|
|
1053
1467
|
* Check that a username is valid.
|
|
1054
1468
|
*
|
|
1055
1469
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1056
1470
|
*/
|
|
1057
1471
|
public async $checkUsername(): Promise<{ result: boolean; message: string }> {
|
|
1058
|
-
const tilmeld = this
|
|
1472
|
+
const tilmeld = enforceTilmeld(this);
|
|
1059
1473
|
if (!tilmeld.config.emailUsernames) {
|
|
1060
1474
|
if (this.$data.username == null || !this.$data.username.length) {
|
|
1061
1475
|
return { result: false, message: 'Please specify a username.' };
|
|
@@ -1109,6 +1523,18 @@ export default class User extends AbleObject<UserData> {
|
|
|
1109
1523
|
return { result: false, message: 'That username is taken.' };
|
|
1110
1524
|
}
|
|
1111
1525
|
|
|
1526
|
+
for (let callback of (this.constructor as typeof User)
|
|
1527
|
+
.checkUsernameCallbacks) {
|
|
1528
|
+
if (callback) {
|
|
1529
|
+
const result = await callback(this, {
|
|
1530
|
+
username: this.$data.username,
|
|
1531
|
+
});
|
|
1532
|
+
if (!result.result) {
|
|
1533
|
+
return { message: 'Username is not available.', ...result };
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1112
1538
|
return {
|
|
1113
1539
|
result: true,
|
|
1114
1540
|
message:
|
|
@@ -1142,7 +1568,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1142
1568
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1143
1569
|
*/
|
|
1144
1570
|
public async $checkEmail(): Promise<{ result: boolean; message: string }> {
|
|
1145
|
-
const tilmeld = this
|
|
1571
|
+
const tilmeld = enforceTilmeld(this);
|
|
1146
1572
|
if (this.$data.email == null || !this.$data.email.length) {
|
|
1147
1573
|
if (tilmeld.config.verifyEmail) {
|
|
1148
1574
|
return { result: false, message: 'Please specify an email.' };
|
|
@@ -1187,7 +1613,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1187
1613
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1188
1614
|
*/
|
|
1189
1615
|
public async $checkPhone(): Promise<{ result: boolean; message: string }> {
|
|
1190
|
-
const tilmeld = this
|
|
1616
|
+
const tilmeld = enforceTilmeld(this);
|
|
1191
1617
|
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
1192
1618
|
return { result: false, message: 'Please specify a phone number.' };
|
|
1193
1619
|
}
|
|
@@ -1226,7 +1652,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1226
1652
|
password: string;
|
|
1227
1653
|
additionalData?: { [k: string]: any };
|
|
1228
1654
|
}): Promise<{ result: boolean; loggedin: boolean; message: string }> {
|
|
1229
|
-
let tilmeld = this
|
|
1655
|
+
let tilmeld = enforceTilmeld(this);
|
|
1230
1656
|
if (!tilmeld.config.allowRegistration) {
|
|
1231
1657
|
return {
|
|
1232
1658
|
result: false,
|
|
@@ -1288,7 +1714,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1288
1714
|
const nymph = this.$nymph;
|
|
1289
1715
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1290
1716
|
this.$nymph = tnymph;
|
|
1291
|
-
tilmeld = this
|
|
1717
|
+
tilmeld = enforceTilmeld(this);
|
|
1292
1718
|
|
|
1293
1719
|
try {
|
|
1294
1720
|
// Add primary group.
|
|
@@ -1493,7 +1919,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1493
1919
|
}
|
|
1494
1920
|
|
|
1495
1921
|
public async $save() {
|
|
1496
|
-
let tilmeld = this
|
|
1922
|
+
let tilmeld = enforceTilmeld(this);
|
|
1497
1923
|
if (this.$data.username == null || !this.$data.username.trim().length) {
|
|
1498
1924
|
return false;
|
|
1499
1925
|
}
|
|
@@ -1501,7 +1927,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1501
1927
|
if (
|
|
1502
1928
|
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1503
1929
|
!tilmeld.gatekeeper('system/admin') &&
|
|
1504
|
-
this.$
|
|
1930
|
+
this.$data.abilities?.includes('system/admin')
|
|
1505
1931
|
) {
|
|
1506
1932
|
throw new BadDataError(
|
|
1507
1933
|
"You don't have the authority to modify system admins."
|
|
@@ -1691,7 +2117,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1691
2117
|
const nymph = this.$nymph;
|
|
1692
2118
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1693
2119
|
this.$nymph = tnymph;
|
|
1694
|
-
tilmeld = this
|
|
2120
|
+
tilmeld = enforceTilmeld(this);
|
|
1695
2121
|
|
|
1696
2122
|
if (
|
|
1697
2123
|
this.$data.group != null &&
|
|
@@ -1742,9 +2168,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
1742
2168
|
this.$gatekeeperCache = undefined;
|
|
1743
2169
|
await tnymph.commit(transaction);
|
|
1744
2170
|
|
|
1745
|
-
if ((nymph
|
|
2171
|
+
if (enforceTilmeld(nymph).User.current(true).$is(this)) {
|
|
1746
2172
|
// Update the user in the session cache.
|
|
1747
|
-
(nymph
|
|
2173
|
+
enforceTilmeld(nymph).fillSession(this);
|
|
1748
2174
|
}
|
|
1749
2175
|
} else {
|
|
1750
2176
|
await tnymph.rollback(transaction);
|
|
@@ -1770,13 +2196,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1770
2196
|
}
|
|
1771
2197
|
|
|
1772
2198
|
public async $delete() {
|
|
1773
|
-
const tilmeld = this
|
|
2199
|
+
const tilmeld = enforceTilmeld(this);
|
|
1774
2200
|
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1775
2201
|
throw new BadDataError("You don't have the authority to delete users.");
|
|
1776
2202
|
}
|
|
1777
2203
|
if (
|
|
1778
2204
|
!tilmeld.gatekeeper('system/admin') &&
|
|
1779
|
-
this.$
|
|
2205
|
+
this.$data.abilities?.includes('system/admin')
|
|
1780
2206
|
) {
|
|
1781
2207
|
throw new BadDataError(
|
|
1782
2208
|
"You don't have the authority to delete system admins."
|
|
@@ -1790,7 +2216,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
1790
2216
|
|
|
1791
2217
|
public static on<T extends EventType>(
|
|
1792
2218
|
event: T,
|
|
1793
|
-
callback: T extends '
|
|
2219
|
+
callback: T extends 'checkUsername'
|
|
2220
|
+
? TilmeldCheckUsernameCallback
|
|
2221
|
+
: T extends 'beforeRegister'
|
|
1794
2222
|
? TilmeldBeforeRegisterCallback
|
|
1795
2223
|
: T extends 'afterRegister'
|
|
1796
2224
|
? TilmeldAfterRegisterCallback
|
|
@@ -1804,7 +2232,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
1804
2232
|
? TilmeldAfterLogoutCallback
|
|
1805
2233
|
: never
|
|
1806
2234
|
) {
|
|
1807
|
-
const prop = (event + 'Callbacks') as T extends '
|
|
2235
|
+
const prop = (event + 'Callbacks') as T extends 'checkUsername'
|
|
2236
|
+
? 'checkUsernameCallbacks'
|
|
2237
|
+
: T extends 'beforeRegister'
|
|
1808
2238
|
? 'beforeRegisterCallbacks'
|
|
1809
2239
|
: T extends 'afterRegister'
|
|
1810
2240
|
? 'afterRegisterCallbacks'
|
|
@@ -1827,7 +2257,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
1827
2257
|
|
|
1828
2258
|
public static off<T extends EventType>(
|
|
1829
2259
|
event: T,
|
|
1830
|
-
callback: T extends '
|
|
2260
|
+
callback: T extends 'checkUsername'
|
|
2261
|
+
? TilmeldCheckUsernameCallback
|
|
2262
|
+
: T extends 'beforeRegister'
|
|
1831
2263
|
? TilmeldBeforeRegisterCallback
|
|
1832
2264
|
: T extends 'afterRegister'
|
|
1833
2265
|
? TilmeldAfterRegisterCallback
|
|
@@ -1841,7 +2273,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
1841
2273
|
? TilmeldAfterLogoutCallback
|
|
1842
2274
|
: never
|
|
1843
2275
|
) {
|
|
1844
|
-
const prop = (event + 'Callbacks') as T extends '
|
|
2276
|
+
const prop = (event + 'Callbacks') as T extends 'checkUsername'
|
|
2277
|
+
? 'checkUsernameCallbacks'
|
|
2278
|
+
: T extends 'beforeRegister'
|
|
1845
2279
|
? 'beforeRegisterCallbacks'
|
|
1846
2280
|
: T extends 'afterRegister'
|
|
1847
2281
|
? 'afterRegisterCallbacks'
|