@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/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
- 'abilities' in input.data &&
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
- if (
839
- 'abilities' in input.data &&
840
- input.data.abilities?.includes('tilmeld/switch') !==
841
- this.$data.abilities?.includes('tilmeld/switch') &&
842
- (!tilmeld.currentUser ||
843
- !tilmeld.currentUser.abilities?.includes('system/admin'))
844
- ) {
845
- throw new BadDataError(
846
- "You don't have the authority to grant or revoke tilmeld/switch.",
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
- 'abilities' in patch.set &&
860
- patch.set.abilities?.includes('system/admin') !==
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
- 'abilities' in patch.set &&
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
- 'abilities' in patch.set &&
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
- this.$originalEmail = this.$data.email;
895
- super.$jsonAcceptPatch(patch, allowConflict);
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(data: EntityData, sdata?: SerializedEntityData) {
899
- super.$putData(data, sdata);
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 that are commented are there to show what is _not_ private.
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
- if (this.$data.username == null || !this.$data.username.length) {
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 (this.$data.username.length < tilmeld.config.minUsernameLength) {
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 (this.$data.username.length > tilmeld.config.maxUsernameLength) {
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
- this.$data.username.split(''),
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(this.$data.username)) {
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 BadDataError(
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: boolean;
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
- this.$setNymph(nymph);
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 BadDataError("You don't have the authority to delete users.");
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 BadDataError(
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
- return await super.$delete();
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
  */