@nymphjs/tilmeld 1.0.0-beta.84 → 1.0.0-beta.86
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 +10 -0
- package/dist/Group.d.ts +6 -1
- package/dist/Group.js +140 -54
- package/dist/Group.js.map +1 -1
- package/dist/Tilmeld.js +18 -0
- package/dist/Tilmeld.js.map +1 -1
- package/dist/User.d.ts +16 -1
- package/dist/User.js +299 -53
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +63 -0
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +31 -0
- package/dist/conf/defaults.js +3 -0
- package/dist/conf/defaults.js.map +1 -1
- package/package.json +9 -8
- package/src/Group.ts +191 -66
- package/src/Tilmeld.ts +31 -1
- package/src/User.test.ts +81 -0
- package/src/User.ts +376 -63
- package/src/conf/d.ts +31 -0
- package/src/conf/defaults.ts +4 -0
package/src/User.ts
CHANGED
|
@@ -10,15 +10,17 @@ import type {
|
|
|
10
10
|
import { humanSecret, nanoid } from '@nymphjs/guid';
|
|
11
11
|
import type { EmailOptions } from 'email-templates';
|
|
12
12
|
import strtotime from 'locutus/php/datetime/strtotime.js';
|
|
13
|
-
import { difference } from 'lodash-es';
|
|
13
|
+
import { difference, xor } from 'lodash-es';
|
|
14
14
|
import { TOTP, Secret } from 'otpauth';
|
|
15
15
|
import { toDataURL } from 'qrcode';
|
|
16
|
+
import { splitn } from '@sciactive/splitn';
|
|
16
17
|
|
|
17
18
|
import { enforceTilmeld } from './enforceTilmeld.js';
|
|
18
19
|
import AbleObject from './AbleObject.js';
|
|
19
20
|
import type Group from './Group.js';
|
|
20
21
|
import type { GroupData } from './Group.js';
|
|
21
22
|
import {
|
|
23
|
+
AccessControlError,
|
|
22
24
|
BadDataError,
|
|
23
25
|
BadEmailError,
|
|
24
26
|
BadUsernameError,
|
|
@@ -259,6 +261,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
259
261
|
'sendRecovery',
|
|
260
262
|
'recover',
|
|
261
263
|
'getClientConfig',
|
|
264
|
+
'getDomainUsers',
|
|
262
265
|
];
|
|
263
266
|
protected $privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
264
267
|
public static searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
|
|
@@ -292,6 +295,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
292
295
|
* verification, don't set this.
|
|
293
296
|
*/
|
|
294
297
|
public $originalEmail?: string;
|
|
298
|
+
/**
|
|
299
|
+
* Used to save the current username for domain admin permissions.
|
|
300
|
+
*/
|
|
301
|
+
public $originalUsername?: string;
|
|
295
302
|
|
|
296
303
|
static async factoryUsername(username?: string): Promise<User & UserData> {
|
|
297
304
|
const entity = new this();
|
|
@@ -496,6 +503,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
496
503
|
regFields: tilmeld.config.regFields,
|
|
497
504
|
userFields: tilmeld.config.userFields,
|
|
498
505
|
emailUsernames: tilmeld.config.emailUsernames,
|
|
506
|
+
domainSupport: tilmeld.config.domainSupport,
|
|
499
507
|
allowRegistration: tilmeld.config.allowRegistration,
|
|
500
508
|
allowUsernameChange: tilmeld.config.allowUsernameChange,
|
|
501
509
|
pwRecovery: tilmeld.config.pwRecovery,
|
|
@@ -524,6 +532,70 @@ export default class User extends AbleObject<UserData> {
|
|
|
524
532
|
return result;
|
|
525
533
|
}
|
|
526
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Get the users in a domain. This is only accessible to the domain's admins.
|
|
537
|
+
*/
|
|
538
|
+
public static async getDomainUsers(
|
|
539
|
+
domain: string,
|
|
540
|
+
options?: {
|
|
541
|
+
limit?: number;
|
|
542
|
+
offset?: number;
|
|
543
|
+
sort?: string;
|
|
544
|
+
reverse?: boolean;
|
|
545
|
+
},
|
|
546
|
+
) {
|
|
547
|
+
const tilmeld = enforceTilmeld(this);
|
|
548
|
+
|
|
549
|
+
if (!tilmeld.config.domainSupport) {
|
|
550
|
+
throw new AccessControlError('Domain support is not enabled.');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!domain) {
|
|
554
|
+
throw new BadDataError('Domain is required.');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
domain = domain.toLowerCase();
|
|
558
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
559
|
+
throw new BadDataError(tilmeld.config.validDomainRegexNotice);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
563
|
+
throw new AccessControlError(
|
|
564
|
+
'You do not have the authority to manage this domain.',
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return await this.nymph.getEntities(
|
|
569
|
+
{
|
|
570
|
+
class: this.nymph.getEntityClass(User),
|
|
571
|
+
...(options?.limit != null
|
|
572
|
+
? {
|
|
573
|
+
limit: options.limit,
|
|
574
|
+
}
|
|
575
|
+
: {}),
|
|
576
|
+
...(options?.offset != null
|
|
577
|
+
? {
|
|
578
|
+
offset: options.offset,
|
|
579
|
+
}
|
|
580
|
+
: {}),
|
|
581
|
+
...(options?.sort != null
|
|
582
|
+
? {
|
|
583
|
+
sort: options.sort,
|
|
584
|
+
}
|
|
585
|
+
: {}),
|
|
586
|
+
...(options?.reverse != null
|
|
587
|
+
? {
|
|
588
|
+
reverse: options.reverse,
|
|
589
|
+
}
|
|
590
|
+
: {}),
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
type: '&',
|
|
594
|
+
like: ['username', `%@${domain.replace(/([\\%_])/g, (s) => `\\${s}`)}`],
|
|
595
|
+
},
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
527
599
|
constructor() {
|
|
528
600
|
super();
|
|
529
601
|
|
|
@@ -811,53 +883,69 @@ export default class User extends AbleObject<UserData> {
|
|
|
811
883
|
const tilmeld = enforceTilmeld(this);
|
|
812
884
|
this.$check();
|
|
813
885
|
|
|
814
|
-
if (
|
|
815
|
-
|
|
816
|
-
input.data.abilities?.includes('system/admin') !==
|
|
817
|
-
this.$data.abilities?.includes('system/admin') &&
|
|
818
|
-
(!tilmeld.currentUser ||
|
|
819
|
-
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
820
|
-
) {
|
|
821
|
-
throw new BadDataError(
|
|
822
|
-
"You don't have the authority to grant or revoke system/admin.",
|
|
823
|
-
);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
if (
|
|
827
|
-
'abilities' in input.data &&
|
|
828
|
-
input.data.abilities?.includes('tilmeld/admin') !==
|
|
829
|
-
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
830
|
-
(!tilmeld.currentUser ||
|
|
831
|
-
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
832
|
-
) {
|
|
833
|
-
throw new BadDataError(
|
|
834
|
-
"You don't have the authority to grant or revoke tilmeld/admin.",
|
|
835
|
-
);
|
|
886
|
+
if ('abilities' in input.data) {
|
|
887
|
+
this.$checkIncomingAbilities(input.data.abilities);
|
|
836
888
|
}
|
|
837
889
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
890
|
+
// If the user is a new domain user, the data protection needs to be updated
|
|
891
|
+
// before the data is accepted, or else some things won't be accepted
|
|
892
|
+
// correctly (enabled, passwordTemp).
|
|
893
|
+
let reloadDataProtection = false;
|
|
894
|
+
if (tilmeld.config.domainSupport) {
|
|
895
|
+
let [_username, domain] = splitn(input.data.username ?? '', '@', 2);
|
|
896
|
+
// Lowercase the domain part.
|
|
897
|
+
if (domain) {
|
|
898
|
+
domain = domain.toLowerCase();
|
|
899
|
+
}
|
|
900
|
+
const user = tilmeld.User.current();
|
|
901
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
902
|
+
// While loading, accessing abilities on the current user will infinitely
|
|
903
|
+
// loop, but if this is the current user, we can use its abilities.
|
|
904
|
+
const abilities =
|
|
905
|
+
(isCurrentUser || user == null
|
|
906
|
+
? this.$data.abilities
|
|
907
|
+
: user?.abilities) ?? [];
|
|
908
|
+
const isNewUser = input.guid == null;
|
|
909
|
+
const isDomainUser =
|
|
910
|
+
!isCurrentUser &&
|
|
911
|
+
user != null &&
|
|
912
|
+
domain &&
|
|
913
|
+
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
914
|
+
|
|
915
|
+
reloadDataProtection = !!(isNewUser && isDomainUser);
|
|
848
916
|
}
|
|
849
917
|
|
|
850
|
-
this.$originalEmail = this.$data.email;
|
|
851
918
|
super.$jsonAcceptData(input, allowConflict);
|
|
919
|
+
|
|
920
|
+
if (reloadDataProtection) {
|
|
921
|
+
// Load it again after data protection is run.
|
|
922
|
+
this.$updateDataProtection();
|
|
923
|
+
super.$jsonAcceptData(input, allowConflict);
|
|
924
|
+
}
|
|
852
925
|
}
|
|
853
926
|
|
|
854
927
|
public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
|
|
855
|
-
const tilmeld = enforceTilmeld(this);
|
|
856
928
|
this.$check();
|
|
857
929
|
|
|
930
|
+
if ('abilities' in patch.set) {
|
|
931
|
+
this.$checkIncomingAbilities(patch.set.abilities);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
super.$jsonAcceptPatch(patch, allowConflict);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private $checkIncomingAbilities(incoming: any) {
|
|
938
|
+
const tilmeld = enforceTilmeld(this);
|
|
939
|
+
|
|
858
940
|
if (
|
|
859
|
-
|
|
860
|
-
|
|
941
|
+
!Array.isArray(incoming) ||
|
|
942
|
+
incoming.find((ability) => typeof ability !== 'string')
|
|
943
|
+
) {
|
|
944
|
+
throw new BadDataError('Abilities must be an array of strings.');
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (
|
|
948
|
+
incoming.includes('system/admin') !==
|
|
861
949
|
this.$data.abilities?.includes('system/admin') &&
|
|
862
950
|
(!tilmeld.currentUser ||
|
|
863
951
|
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
@@ -868,8 +956,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
868
956
|
}
|
|
869
957
|
|
|
870
958
|
if (
|
|
871
|
-
'
|
|
872
|
-
patch.set.abilities?.includes('tilmeld/admin') !==
|
|
959
|
+
incoming.includes('tilmeld/admin') !==
|
|
873
960
|
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
874
961
|
(!tilmeld.currentUser ||
|
|
875
962
|
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
@@ -880,8 +967,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
880
967
|
}
|
|
881
968
|
|
|
882
969
|
if (
|
|
883
|
-
'
|
|
884
|
-
patch.set.abilities?.includes('tilmeld/switch') !==
|
|
970
|
+
incoming.includes('tilmeld/switch') !==
|
|
885
971
|
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
886
972
|
(!tilmeld.currentUser ||
|
|
887
973
|
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
@@ -891,12 +977,33 @@ export default class User extends AbleObject<UserData> {
|
|
|
891
977
|
);
|
|
892
978
|
}
|
|
893
979
|
|
|
894
|
-
|
|
895
|
-
|
|
980
|
+
if (
|
|
981
|
+
xor(
|
|
982
|
+
incoming.filter((ability: string) => ability.startsWith('tilmeld/')),
|
|
983
|
+
this.$data.abilities?.filter((ability: string) =>
|
|
984
|
+
ability.startsWith('tilmeld/'),
|
|
985
|
+
) ?? [],
|
|
986
|
+
).length &&
|
|
987
|
+
(!tilmeld.currentUser ||
|
|
988
|
+
(!tilmeld.currentUser.abilities?.includes('system/admin') &&
|
|
989
|
+
!tilmeld.currentUser.abilities?.includes('tilmeld/admin')))
|
|
990
|
+
) {
|
|
991
|
+
throw new BadDataError(
|
|
992
|
+
"You don't have the authority to grant or revoke Tilmeld abilities.",
|
|
993
|
+
);
|
|
994
|
+
}
|
|
896
995
|
}
|
|
897
996
|
|
|
898
|
-
public $putData(
|
|
899
|
-
|
|
997
|
+
public $putData(
|
|
998
|
+
data: EntityData,
|
|
999
|
+
sdata?: SerializedEntityData,
|
|
1000
|
+
source?: 'server',
|
|
1001
|
+
) {
|
|
1002
|
+
super.$putData(data, sdata, source);
|
|
1003
|
+
if (source === 'server') {
|
|
1004
|
+
this.$originalEmail = this.$data.email;
|
|
1005
|
+
this.$originalUsername = this.$data.username;
|
|
1006
|
+
}
|
|
900
1007
|
this.$updateDataProtection();
|
|
901
1008
|
}
|
|
902
1009
|
|
|
@@ -908,6 +1015,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
908
1015
|
public $updateDataProtection(givenUser?: User & UserData) {
|
|
909
1016
|
const tilmeld = enforceTilmeld(this);
|
|
910
1017
|
let user = givenUser ?? tilmeld.User.current();
|
|
1018
|
+
let [_username, domain] = tilmeld.config.domainSupport
|
|
1019
|
+
? splitn(this.$data.username ?? '', '@', 2)
|
|
1020
|
+
: [this.$data.username, null];
|
|
1021
|
+
// Lowercase the domain part.
|
|
1022
|
+
if (domain) {
|
|
1023
|
+
domain = domain.toLowerCase();
|
|
1024
|
+
}
|
|
911
1025
|
|
|
912
1026
|
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
913
1027
|
this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
@@ -925,6 +1039,12 @@ export default class User extends AbleObject<UserData> {
|
|
|
925
1039
|
? this.$data.abilities
|
|
926
1040
|
: user?.abilities) ?? [];
|
|
927
1041
|
const isNewUser = this.guid == null;
|
|
1042
|
+
const isDomainUser =
|
|
1043
|
+
tilmeld.config.domainSupport &&
|
|
1044
|
+
!isCurrentUser &&
|
|
1045
|
+
user != null &&
|
|
1046
|
+
domain &&
|
|
1047
|
+
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
928
1048
|
|
|
929
1049
|
if (isCurrentUser) {
|
|
930
1050
|
// Users can check to see what abilities they have.
|
|
@@ -939,6 +1059,17 @@ export default class User extends AbleObject<UserData> {
|
|
|
939
1059
|
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
940
1060
|
}
|
|
941
1061
|
|
|
1062
|
+
if (isDomainUser) {
|
|
1063
|
+
// Domain admins can check to see what abilities their users have.
|
|
1064
|
+
this.$clientEnabledMethods.push('$gatekeeper');
|
|
1065
|
+
this.$clientEnabledMethods.push('$changePassword');
|
|
1066
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
1067
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
1068
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
1069
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
1070
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
1071
|
+
}
|
|
1072
|
+
|
|
942
1073
|
if (
|
|
943
1074
|
user != null &&
|
|
944
1075
|
(abilities.includes('tilmeld/admin') ||
|
|
@@ -949,6 +1080,55 @@ export default class User extends AbleObject<UserData> {
|
|
|
949
1080
|
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
950
1081
|
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
951
1082
|
this.$allowlistData = undefined;
|
|
1083
|
+
} else if (isDomainUser) {
|
|
1084
|
+
// Domain admins can see their users' data, and edit some of it.
|
|
1085
|
+
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
1086
|
+
this.$allowlistData.push('username');
|
|
1087
|
+
}
|
|
1088
|
+
this.$allowlistData.push('avatar');
|
|
1089
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
1090
|
+
this.$allowlistData.push('nameFirst');
|
|
1091
|
+
this.$allowlistData.push('nameMiddle');
|
|
1092
|
+
this.$allowlistData.push('nameLast');
|
|
1093
|
+
this.$allowlistData.push('name');
|
|
1094
|
+
}
|
|
1095
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
1096
|
+
this.$allowlistData.push('email');
|
|
1097
|
+
}
|
|
1098
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
1099
|
+
this.$allowlistData.push('phone');
|
|
1100
|
+
}
|
|
1101
|
+
this.$allowlistData.push('enabled');
|
|
1102
|
+
this.$allowlistData.push('passwordTemp');
|
|
1103
|
+
this.$privateData = [
|
|
1104
|
+
// The things commented are there to show what is _not_ private.
|
|
1105
|
+
// 'abilities',
|
|
1106
|
+
// 'username',
|
|
1107
|
+
// 'nameFirst',
|
|
1108
|
+
// 'nameMiddle',
|
|
1109
|
+
// 'nameLast',
|
|
1110
|
+
// 'name',
|
|
1111
|
+
// 'email',
|
|
1112
|
+
// 'avatar',
|
|
1113
|
+
// 'phone',
|
|
1114
|
+
// 'group',
|
|
1115
|
+
// 'groups',
|
|
1116
|
+
// 'inheritAbilities',
|
|
1117
|
+
// 'enabled',
|
|
1118
|
+
'secret',
|
|
1119
|
+
'emailChangeDate',
|
|
1120
|
+
'newEmailSecret',
|
|
1121
|
+
// 'newEmailAddress',
|
|
1122
|
+
'cancelEmailSecret',
|
|
1123
|
+
'cancelEmailAddress',
|
|
1124
|
+
'recoverSecret',
|
|
1125
|
+
'recoverSecretDate',
|
|
1126
|
+
'salt',
|
|
1127
|
+
'password',
|
|
1128
|
+
// 'passwordTemp',
|
|
1129
|
+
// 'revokeTokenDate',
|
|
1130
|
+
'totpSecret',
|
|
1131
|
+
];
|
|
952
1132
|
} else if (isCurrentUser || isNewUser) {
|
|
953
1133
|
// Users can see their own data, and edit some of it.
|
|
954
1134
|
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
@@ -968,7 +1148,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
968
1148
|
this.$allowlistData.push('phone');
|
|
969
1149
|
}
|
|
970
1150
|
this.$privateData = [
|
|
971
|
-
// The things
|
|
1151
|
+
// The things commented are there to show what is _not_ private.
|
|
972
1152
|
// 'abilities',
|
|
973
1153
|
// 'username',
|
|
974
1154
|
// 'nameFirst',
|
|
@@ -1590,10 +1770,25 @@ export default class User extends AbleObject<UserData> {
|
|
|
1590
1770
|
);
|
|
1591
1771
|
|
|
1592
1772
|
if (!tilmeld.config.emailUsernames) {
|
|
1593
|
-
|
|
1773
|
+
let [username, domain] = tilmeld.config.domainSupport
|
|
1774
|
+
? splitn(this.$data.username ?? '', '@', 2)
|
|
1775
|
+
: [this.$data.username, null];
|
|
1776
|
+
|
|
1777
|
+
// Lowercase the domain part.
|
|
1778
|
+
if (domain) {
|
|
1779
|
+
domain = domain.toLowerCase();
|
|
1780
|
+
this.$data.username = `${username}@${domain}`;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (
|
|
1784
|
+
this.$data.username == null ||
|
|
1785
|
+
!this.$data.username.length ||
|
|
1786
|
+
username == null ||
|
|
1787
|
+
!username.length
|
|
1788
|
+
) {
|
|
1594
1789
|
return { result: false, message: 'Please specify a username.' };
|
|
1595
1790
|
}
|
|
1596
|
-
if (
|
|
1791
|
+
if (username.length < tilmeld.config.minUsernameLength) {
|
|
1597
1792
|
return {
|
|
1598
1793
|
result: false,
|
|
1599
1794
|
message:
|
|
@@ -1602,7 +1797,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1602
1797
|
' characters.',
|
|
1603
1798
|
};
|
|
1604
1799
|
}
|
|
1605
|
-
if (
|
|
1800
|
+
if (username.length > tilmeld.config.maxUsernameLength) {
|
|
1606
1801
|
return {
|
|
1607
1802
|
result: false,
|
|
1608
1803
|
message:
|
|
@@ -1613,22 +1808,38 @@ export default class User extends AbleObject<UserData> {
|
|
|
1613
1808
|
}
|
|
1614
1809
|
|
|
1615
1810
|
if (
|
|
1616
|
-
difference(
|
|
1617
|
-
|
|
1618
|
-
tilmeld.config.validChars.split(''),
|
|
1619
|
-
).length
|
|
1811
|
+
difference(username.split(''), tilmeld.config.validChars.split(''))
|
|
1812
|
+
.length
|
|
1620
1813
|
) {
|
|
1621
1814
|
return {
|
|
1622
1815
|
result: false,
|
|
1623
1816
|
message: tilmeld.config.validCharsNotice,
|
|
1624
1817
|
};
|
|
1625
1818
|
}
|
|
1626
|
-
if (!tilmeld.config.validRegex.test(
|
|
1819
|
+
if (!tilmeld.config.validRegex.test(username)) {
|
|
1627
1820
|
return {
|
|
1628
1821
|
result: false,
|
|
1629
1822
|
message: tilmeld.config.validRegexNotice,
|
|
1630
1823
|
};
|
|
1631
1824
|
}
|
|
1825
|
+
if (domain != null) {
|
|
1826
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
1827
|
+
return {
|
|
1828
|
+
result: false,
|
|
1829
|
+
message: tilmeld.config.validDomainRegexNotice,
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
if (
|
|
1833
|
+
!this.$is(tilmeld.currentUser) &&
|
|
1834
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
|
|
1835
|
+
) {
|
|
1836
|
+
return {
|
|
1837
|
+
result: false,
|
|
1838
|
+
message:
|
|
1839
|
+
"You don't have the authority to manage users on this domain.",
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1632
1843
|
|
|
1633
1844
|
const selector: Selector = {
|
|
1634
1845
|
type: '&',
|
|
@@ -2017,11 +2228,64 @@ export default class User extends AbleObject<UserData> {
|
|
|
2017
2228
|
!tilmeld.gatekeeper('system/admin') &&
|
|
2018
2229
|
this.$data.abilities?.includes('system/admin')
|
|
2019
2230
|
) {
|
|
2020
|
-
throw new
|
|
2231
|
+
throw new AccessControlError(
|
|
2021
2232
|
"You don't have the authority to modify system admins.",
|
|
2022
2233
|
);
|
|
2023
2234
|
}
|
|
2024
2235
|
|
|
2236
|
+
// Lowercase the domain part.
|
|
2237
|
+
if (tilmeld.config.domainSupport) {
|
|
2238
|
+
const [username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
2239
|
+
if (domain) {
|
|
2240
|
+
this.$data.username = `${username}@${domain.toLowerCase()}`;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
if (tilmeld.config.domainSupport) {
|
|
2245
|
+
const [_username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
2246
|
+
const [_originalUsername, originalDomain] = splitn(
|
|
2247
|
+
this.$originalUsername ?? '',
|
|
2248
|
+
'@',
|
|
2249
|
+
2,
|
|
2250
|
+
);
|
|
2251
|
+
|
|
2252
|
+
if (
|
|
2253
|
+
this.guid != null &&
|
|
2254
|
+
((originalDomain == null && domain != null) ||
|
|
2255
|
+
(domain == null && originalDomain != null))
|
|
2256
|
+
) {
|
|
2257
|
+
throw new AccessControlError(
|
|
2258
|
+
"Users can't switch to or from having a domain once created.",
|
|
2259
|
+
);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
if (
|
|
2263
|
+
!this.$skipAcWhenSaving &&
|
|
2264
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
2265
|
+
!tilmeld.gatekeeper('system/admin')
|
|
2266
|
+
) {
|
|
2267
|
+
if (
|
|
2268
|
+
domain != null &&
|
|
2269
|
+
!this.$is(tilmeld.currentUser) &&
|
|
2270
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
|
|
2271
|
+
) {
|
|
2272
|
+
throw new AccessControlError(
|
|
2273
|
+
"You don't have the authority to modify users on this domain.",
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
if (
|
|
2277
|
+
originalDomain != null &&
|
|
2278
|
+
originalDomain !== domain &&
|
|
2279
|
+
(!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
|
|
2280
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))
|
|
2281
|
+
) {
|
|
2282
|
+
throw new AccessControlError(
|
|
2283
|
+
"You don't have the authority to modify users on this domain.",
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2025
2289
|
let sendVerification = false;
|
|
2026
2290
|
|
|
2027
2291
|
// Formatting.
|
|
@@ -2364,7 +2628,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
2364
2628
|
throw e;
|
|
2365
2629
|
}
|
|
2366
2630
|
|
|
2367
|
-
let ret
|
|
2631
|
+
let ret = false;
|
|
2368
2632
|
let preGuid = this.guid;
|
|
2369
2633
|
let preCdate = this.cdate;
|
|
2370
2634
|
let preMdate = this.mdate;
|
|
@@ -2387,6 +2651,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
2387
2651
|
}
|
|
2388
2652
|
} catch (e: any) {
|
|
2389
2653
|
await tnymph.rollback(transaction);
|
|
2654
|
+
this.guid = preGuid;
|
|
2655
|
+
this.cdate = preCdate;
|
|
2656
|
+
this.mdate = preMdate;
|
|
2390
2657
|
this.$setNymph(nymph);
|
|
2391
2658
|
throw e;
|
|
2392
2659
|
}
|
|
@@ -2406,7 +2673,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
2406
2673
|
|
|
2407
2674
|
this.$descendantGroups = undefined;
|
|
2408
2675
|
this.$gatekeeperCache = undefined;
|
|
2409
|
-
await tnymph.commit(transaction);
|
|
2676
|
+
ret = await tnymph.commit(transaction);
|
|
2677
|
+
} else {
|
|
2678
|
+
await tnymph.rollback(transaction);
|
|
2679
|
+
}
|
|
2680
|
+
this.$setNymph(nymph);
|
|
2681
|
+
|
|
2682
|
+
if (ret) {
|
|
2683
|
+
this.$originalEmail = this.$data.email;
|
|
2684
|
+
this.$originalUsername = this.$data.username;
|
|
2410
2685
|
|
|
2411
2686
|
const tilmeld = enforceTilmeld(nymph);
|
|
2412
2687
|
|
|
@@ -2414,10 +2689,8 @@ export default class User extends AbleObject<UserData> {
|
|
|
2414
2689
|
// Update the user in the session cache.
|
|
2415
2690
|
await tilmeld.fillSession(this);
|
|
2416
2691
|
}
|
|
2417
|
-
} else {
|
|
2418
|
-
await tnymph.rollback(transaction);
|
|
2419
2692
|
}
|
|
2420
|
-
|
|
2693
|
+
|
|
2421
2694
|
return ret;
|
|
2422
2695
|
}
|
|
2423
2696
|
|
|
@@ -2440,21 +2713,61 @@ export default class User extends AbleObject<UserData> {
|
|
|
2440
2713
|
public async $delete() {
|
|
2441
2714
|
const tilmeld = enforceTilmeld(this);
|
|
2442
2715
|
if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
|
|
2443
|
-
throw new
|
|
2716
|
+
throw new AccessControlError(
|
|
2717
|
+
"You don't have the authority to delete users.",
|
|
2718
|
+
);
|
|
2444
2719
|
}
|
|
2445
2720
|
if (
|
|
2446
2721
|
!this.$skipAcWhenDeleting &&
|
|
2447
2722
|
!tilmeld.gatekeeper('system/admin') &&
|
|
2448
2723
|
this.$data.abilities?.includes('system/admin')
|
|
2449
2724
|
) {
|
|
2450
|
-
throw new
|
|
2725
|
+
throw new AccessControlError(
|
|
2451
2726
|
"You don't have the authority to delete system admins.",
|
|
2452
2727
|
);
|
|
2453
2728
|
}
|
|
2729
|
+
if (this.$data.username !== this.$originalUsername) {
|
|
2730
|
+
throw new AccessControlError(
|
|
2731
|
+
"You can't delete a user while switching their username.",
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2454
2735
|
if (tilmeld.User.current(true).$is(this)) {
|
|
2455
2736
|
await this.$logout();
|
|
2456
2737
|
}
|
|
2457
|
-
|
|
2738
|
+
|
|
2739
|
+
if (this.$data.group != null && this.$is(this.$data.group.user)) {
|
|
2740
|
+
const transaction = 'tilmeld-delete-' + this.guid;
|
|
2741
|
+
const nymph = this.$nymph;
|
|
2742
|
+
const tnymph = await nymph.startTransaction(transaction);
|
|
2743
|
+
this.$setNymph(tnymph);
|
|
2744
|
+
|
|
2745
|
+
// Read the skip ac value here, because super.$delete changes it.
|
|
2746
|
+
const $skipAcWhenDeleting = this.$skipAcWhenDeleting;
|
|
2747
|
+
|
|
2748
|
+
// Delete the user.
|
|
2749
|
+
let success = await super.$delete();
|
|
2750
|
+
if (!success) {
|
|
2751
|
+
await tnymph.rollback(transaction);
|
|
2752
|
+
this.$setNymph(nymph);
|
|
2753
|
+
return success;
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
// Delete the generated group.
|
|
2757
|
+
success = $skipAcWhenDeleting
|
|
2758
|
+
? await this.$data.group.$deleteSkipAC()
|
|
2759
|
+
: await this.$data.group.$delete();
|
|
2760
|
+
|
|
2761
|
+
if (success) {
|
|
2762
|
+
success = await tnymph.commit(transaction);
|
|
2763
|
+
} else {
|
|
2764
|
+
await tnymph.rollback(transaction);
|
|
2765
|
+
}
|
|
2766
|
+
this.$setNymph(nymph);
|
|
2767
|
+
return success;
|
|
2768
|
+
} else {
|
|
2769
|
+
return await super.$delete();
|
|
2770
|
+
}
|
|
2458
2771
|
}
|
|
2459
2772
|
|
|
2460
2773
|
/*
|
package/src/conf/d.ts
CHANGED
|
@@ -47,8 +47,27 @@ export interface Config {
|
|
|
47
47
|
* Instead of a "username", a user logs in and is referred to by their email
|
|
48
48
|
* address. Enabling this after many users have been created can be messy.
|
|
49
49
|
* Make sure they all have email addresses first.
|
|
50
|
+
*
|
|
51
|
+
* Email usernames cannot be enabled if domain support is.
|
|
50
52
|
*/
|
|
51
53
|
emailUsernames: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Enable domain support.
|
|
56
|
+
*
|
|
57
|
+
* If domain support is enabled, usernames can be either bare, meaning they
|
|
58
|
+
* belong to the root domain, or can include a domain part (username@domain),
|
|
59
|
+
* meaning they belong to that domain.
|
|
60
|
+
*
|
|
61
|
+
* It also enables domain admin abilities, meaning a domain admin has the
|
|
62
|
+
* ability to create and modify users on the domain
|
|
63
|
+
* ("tilmeld/domain/example.com/admin").
|
|
64
|
+
*
|
|
65
|
+
* Once a user is created, whether it has a domain part can't be changed.
|
|
66
|
+
* However, it can be moved to a different domain.
|
|
67
|
+
*
|
|
68
|
+
* Domain support cannot be enabled if email usernames is.
|
|
69
|
+
*/
|
|
70
|
+
domainSupport: boolean;
|
|
52
71
|
/**
|
|
53
72
|
* Allow users to register.
|
|
54
73
|
*/
|
|
@@ -207,6 +226,18 @@ export interface Config {
|
|
|
207
226
|
* be displayed.
|
|
208
227
|
*/
|
|
209
228
|
validEmailRegexNotice: string;
|
|
229
|
+
/**
|
|
230
|
+
* Domains must match this regular expression. By default, this uses a regex
|
|
231
|
+
* that should match most domains:
|
|
232
|
+
*
|
|
233
|
+
* https://stackoverflow.com/a/26987741
|
|
234
|
+
*/
|
|
235
|
+
validDomainRegex: RegExp;
|
|
236
|
+
/**
|
|
237
|
+
* When a user enters a domain that doesn't match the regex, this message will
|
|
238
|
+
* be displayed.
|
|
239
|
+
*/
|
|
240
|
+
validDomainRegexNotice: string;
|
|
210
241
|
/**
|
|
211
242
|
* The minimum length for usernames.
|
|
212
243
|
*/
|