@nymphjs/tilmeld 1.0.0-beta.9 → 1.0.0-beta.90

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.
Files changed (78) hide show
  1. package/CHANGELOG.md +408 -0
  2. package/README.md +15 -2
  3. package/dist/AbleObject.d.ts +15 -0
  4. package/dist/AbleObject.js +18 -6
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +140 -15
  7. package/dist/Group.js +466 -191
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.test.js +83 -13
  10. package/dist/Group.test.js.map +1 -1
  11. package/dist/Tilmeld.d.ts +239 -12
  12. package/dist/Tilmeld.js +634 -254
  13. package/dist/Tilmeld.js.map +1 -1
  14. package/dist/Tilmeld.types.d.ts +2 -2
  15. package/dist/Tilmeld.types.js +1 -2
  16. package/dist/User.d.ts +365 -22
  17. package/dist/User.js +1265 -364
  18. package/dist/User.js.map +1 -1
  19. package/dist/User.test.js +132 -13
  20. package/dist/User.test.js.map +1 -1
  21. package/dist/conf/d.d.ts +287 -7
  22. package/dist/conf/d.js +1 -2
  23. package/dist/conf/defaults.d.ts +1 -1
  24. package/dist/conf/defaults.js +188 -141
  25. package/dist/conf/defaults.js.map +1 -1
  26. package/dist/conf/index.d.ts +2 -2
  27. package/dist/conf/index.js +2 -8
  28. package/dist/conf/index.js.map +1 -1
  29. package/dist/enforceTilmeld.d.ts +3 -0
  30. package/dist/enforceTilmeld.js +18 -0
  31. package/dist/enforceTilmeld.js.map +1 -0
  32. package/dist/errors/AccessControlError.d.ts +2 -1
  33. package/dist/errors/AccessControlError.js +3 -6
  34. package/dist/errors/AccessControlError.js.map +1 -1
  35. package/dist/errors/BadDataError.d.ts +2 -1
  36. package/dist/errors/BadDataError.js +3 -6
  37. package/dist/errors/BadDataError.js.map +1 -1
  38. package/dist/errors/BadEmailError.d.ts +2 -1
  39. package/dist/errors/BadEmailError.js +3 -6
  40. package/dist/errors/BadEmailError.js.map +1 -1
  41. package/dist/errors/BadUsernameError.d.ts +2 -1
  42. package/dist/errors/BadUsernameError.js +3 -6
  43. package/dist/errors/BadUsernameError.js.map +1 -1
  44. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js +1 -5
  45. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js.map +1 -1
  46. package/dist/errors/EmailChangeRateLimitExceededError.js +1 -5
  47. package/dist/errors/EmailChangeRateLimitExceededError.js.map +1 -1
  48. package/dist/errors/index.d.ts +6 -6
  49. package/dist/errors/index.js +6 -22
  50. package/dist/errors/index.js.map +1 -1
  51. package/dist/index.d.ts +11 -10
  52. package/dist/index.js +16 -35
  53. package/dist/index.js.map +1 -1
  54. package/dist/tilmeldJoiProps.js +13 -18
  55. package/dist/tilmeldJoiProps.js.map +1 -1
  56. package/jest.config.js +11 -2
  57. package/package.json +34 -31
  58. package/src/AbleObject.ts +2 -2
  59. package/src/Group.test.ts +104 -6
  60. package/src/Group.ts +478 -226
  61. package/src/Tilmeld.ts +418 -254
  62. package/src/Tilmeld.types.ts +2 -2
  63. package/src/User.test.ts +163 -6
  64. package/src/User.ts +1428 -444
  65. package/src/conf/d.ts +63 -17
  66. package/src/conf/defaults.ts +162 -109
  67. package/src/conf/index.ts +2 -2
  68. package/src/enforceTilmeld.ts +20 -0
  69. package/src/errors/AccessControlError.ts +4 -2
  70. package/src/errors/BadDataError.ts +4 -2
  71. package/src/errors/BadEmailError.ts +4 -2
  72. package/src/errors/BadUsernameError.ts +4 -2
  73. package/src/errors/index.ts +6 -6
  74. package/src/index.ts +12 -10
  75. package/src/tilmeldJoiProps.ts +11 -11
  76. package/src/type/locutus.d.ts +3 -0
  77. package/tsconfig.json +5 -3
  78. package/typedoc.json +4 -0
package/dist/Group.js CHANGED
@@ -1,16 +1,57 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const md5_1 = __importDefault(require("crypto-js/md5"));
7
- const lodash_1 = require("lodash");
8
- const AbleObject_1 = __importDefault(require("./AbleObject"));
9
- const errors_1 = require("./errors");
10
- class Group extends AbleObject_1.default {
11
- static async factory(guid) {
12
- return (await super.factory(guid));
13
- }
1
+ import crypto from 'node:crypto';
2
+ import { difference, xor } from 'lodash-es';
3
+ import { splitn } from '@sciactive/splitn';
4
+ import { enforceTilmeld } from './enforceTilmeld.js';
5
+ import AbleObject from './AbleObject.js';
6
+ import { AccessControlError, BadDataError, BadEmailError, BadUsernameError, CouldNotChangeDefaultPrimaryGroupError, } from './errors/index.js';
7
+ import UserClass from './User.js';
8
+ /**
9
+ * A user group data model.
10
+ *
11
+ * Written by Hunter Perrin for SciActive.
12
+ *
13
+ * @author Hunter Perrin <hperrin@gmail.com>
14
+ * @copyright SciActive Inc
15
+ * @see http://nymph.io/
16
+ */
17
+ export default class Group extends AbleObject {
18
+ /**
19
+ * The instance of Tilmeld to use for queries.
20
+ */
21
+ static ETYPE = 'tilmeld_group';
22
+ static class = 'Group';
23
+ static DEFAULT_PRIVATE_DATA = ['email', 'phone', 'abilities', 'user'];
24
+ static DEFAULT_ALLOWLIST_DATA = [];
25
+ $tags = [];
26
+ $clientEnabledMethods = [
27
+ '$checkGroupname',
28
+ '$checkEmail',
29
+ '$getAvatar',
30
+ '$getChildren',
31
+ '$getDescendants',
32
+ '$getLevel',
33
+ '$isDescendant',
34
+ ];
35
+ static clientEnabledStaticMethods = [
36
+ 'getPrimaryGroups',
37
+ 'getSecondaryGroups',
38
+ ];
39
+ $privateData = [...Group.DEFAULT_PRIVATE_DATA];
40
+ static searchRestrictedData = [...Group.DEFAULT_PRIVATE_DATA];
41
+ $allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
42
+ $allowlistTags = [];
43
+ /**
44
+ * This should only be used by the backend.
45
+ */
46
+ $skipAcWhenSaving = false;
47
+ /**
48
+ * This should only be used by the backend.
49
+ */
50
+ $skipAcWhenDeleting = false;
51
+ /**
52
+ * Used to save the current groupname for domain admin permissions.
53
+ */
54
+ $originalGroupname;
14
55
  static async factoryGroupname(groupname) {
15
56
  const entity = new this();
16
57
  if (groupname != null) {
@@ -26,63 +67,89 @@ class Group extends AbleObject_1.default {
26
67
  }
27
68
  return entity;
28
69
  }
29
- static factorySync(guid) {
30
- return super.factorySync(guid);
31
- }
32
- constructor(guid) {
33
- super(guid);
34
- this.$tags = [];
35
- this.$clientEnabledMethods = [
36
- '$checkGroupname',
37
- '$checkEmail',
38
- '$getAvatar',
39
- '$getChildren',
40
- '$getDescendants',
41
- '$getLevel',
42
- '$isDescendant',
43
- ];
44
- this.$privateData = [...Group.DEFAULT_PRIVATE_DATA];
45
- this.$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
46
- this.$allowlistTags = [];
47
- this.$skipAcWhenSaving = false;
48
- if (this.guid == null) {
49
- this.$data.enabled = true;
50
- this.$data.abilities = [];
51
- this.$updateDataProtection();
52
- }
53
- }
70
+ /**
71
+ * Get all the groups that can be assigned as primary groups.
72
+ *
73
+ * @param options The options for an optional search query.
74
+ * @param selectors The selectors for an optional search query.
75
+ * @returns An array of the assignable primary groups.
76
+ */
54
77
  static async getPrimaryGroups(options, selectors) {
55
- if (!this.tilmeld.gatekeeper('tilmeld/admin')) {
78
+ const tilmeld = enforceTilmeld(this);
79
+ if (!tilmeld.gatekeeper('tilmeld/admin')) {
56
80
  throw new Error("You don't have permission to do that.");
57
81
  }
58
- return await this.getAssignableGroups(this.tilmeld.config.highestPrimary, { ...options, class: this.tilmeld.Group, return: 'entity' }, [...(selectors ?? [])]);
82
+ return await this.getAssignableGroups(tilmeld.config.highestPrimary, { ...options, class: tilmeld.Group, return: 'entity' }, [...(selectors ?? [])]);
59
83
  }
84
+ /**
85
+ * Get all the groups that can be assigned as secondary groups.
86
+ *
87
+ * @param options The options for an optional search query.
88
+ * @param selectors The selectors for an optional search query.
89
+ * @returns An array of the assignable secondary groups.
90
+ */
60
91
  static async getSecondaryGroups(options, selectors) {
61
- if (!this.tilmeld.gatekeeper('tilmeld/admin')) {
92
+ const tilmeld = enforceTilmeld(this);
93
+ if (!tilmeld.gatekeeper('tilmeld/admin')) {
62
94
  throw new Error("You don't have permission to do that.");
63
95
  }
64
- return await this.getAssignableGroups(this.tilmeld.config.highestSecondary, { ...options, class: this.tilmeld.Group, return: 'entity' }, [...(selectors ?? [])]);
96
+ return await this.getAssignableGroups(tilmeld.config.highestSecondary, { ...options, class: tilmeld.Group, return: 'entity' }, [...(selectors ?? [])]);
65
97
  }
66
98
  static async getAssignableGroups(highestParent, options, selectors) {
99
+ const tilmeld = enforceTilmeld(this);
67
100
  let assignableGroups = [];
68
101
  if (highestParent === false) {
69
102
  return assignableGroups;
70
103
  }
71
- assignableGroups = await this.nymph.getEntities({ ...options, class: this.tilmeld.Group }, ...selectors);
104
+ assignableGroups = await this.nymph.getEntities({ ...options, class: tilmeld.Group }, ...selectors);
72
105
  if (highestParent !== true) {
73
- assignableGroups = assignableGroups.filter((group) => {
106
+ assignableGroups = (await Promise.all(assignableGroups.map(async (group) => {
74
107
  let curGroup = group;
75
- while (curGroup.parent != null && curGroup.parent.cdate != null) {
76
- if (curGroup.parent.guid === highestParent) {
77
- return true;
108
+ let parent = curGroup.parent;
109
+ await parent?.$wake();
110
+ while (parent != null && parent?.cdate != null) {
111
+ if (parent?.guid === highestParent) {
112
+ return group;
78
113
  }
79
- curGroup = curGroup.parent;
114
+ curGroup = parent;
115
+ parent = curGroup.parent;
116
+ await parent?.$wake();
80
117
  }
81
- return false;
82
- });
118
+ return null;
119
+ }))).filter((group) => group != null);
83
120
  }
84
121
  return assignableGroups;
85
122
  }
123
+ constructor() {
124
+ super();
125
+ // Defaults.
126
+ this.$data.enabled = true;
127
+ this.$data.abilities = [];
128
+ this.$updateDataProtection();
129
+ }
130
+ $setNymph(nymph) {
131
+ this.$nymph = nymph;
132
+ if (!this.$asleep()) {
133
+ if (this.$data.user && this.$data.user.$nymph !== nymph) {
134
+ this.$data.user.$setNymph(nymph);
135
+ }
136
+ if (this.$data.parent && this.$data.parent.$nymph !== nymph) {
137
+ this.$data.parent.$setNymph(nymph);
138
+ }
139
+ }
140
+ }
141
+ async $getUniques() {
142
+ const tilmeld = enforceTilmeld(this);
143
+ const uniques = [`u:${this.$data.groupname}`];
144
+ if (!tilmeld.config.emailUsernames &&
145
+ tilmeld.config.userFields.includes('email')) {
146
+ uniques.push(`e:${this.$data.email}`);
147
+ }
148
+ if (this.$data.defaultPrimary) {
149
+ uniques.push(`o:default-primary`);
150
+ }
151
+ return uniques;
152
+ }
86
153
  $getAvatar() {
87
154
  if (this.$data.avatar != null && this.$data.avatar !== '') {
88
155
  return this.$data.avatar;
@@ -91,60 +158,117 @@ class Group extends AbleObject_1.default {
91
158
  return 'https://secure.gravatar.com/avatar/?d=mm&s=40';
92
159
  }
93
160
  return ('https://secure.gravatar.com/avatar/' +
94
- (0, md5_1.default)(this.$data.email.trim().toLowerCase()).toString() +
161
+ crypto
162
+ .createHash('sha256')
163
+ .update(this.$data.email.trim().toLowerCase())
164
+ .digest('hex')
165
+ .toLowerCase() +
95
166
  '?d=identicon&s=40');
96
167
  }
97
168
  $jsonAcceptData(input, allowConflict = false) {
98
- const tilmeld = this.$nymph.tilmeld;
99
- this.$referenceWake();
100
- if (input.data.abilities?.indexOf('system/admin') !== -1 &&
101
- this.$data.abilities?.indexOf('system/admin') === -1 &&
102
- tilmeld.gatekeeper('tilmeld/admin') &&
103
- !tilmeld.gatekeeper('system/admin')) {
104
- throw new errors_1.BadDataError("You don't have the authority to make this group a system admin.");
169
+ this.$check();
170
+ if ('abilities' in input.data) {
171
+ this.$checkIncomingAbilities(input.data.abilities);
105
172
  }
106
173
  super.$jsonAcceptData(input, allowConflict);
107
174
  }
108
175
  $jsonAcceptPatch(patch, allowConflict = false) {
109
- const tilmeld = this.$nymph.tilmeld;
110
- this.$referenceWake();
111
- if (patch.set.abilities?.indexOf('system/admin') !== -1 &&
112
- this.$data.abilities?.indexOf('system/admin') === -1 &&
113
- tilmeld.gatekeeper('tilmeld/admin') &&
114
- !tilmeld.gatekeeper('system/admin')) {
115
- throw new errors_1.BadDataError("You don't have the authority to make this group a system admin.");
176
+ this.$check();
177
+ if ('abilities' in patch.set) {
178
+ this.$checkIncomingAbilities(patch.set.abilities);
116
179
  }
117
180
  super.$jsonAcceptPatch(patch, allowConflict);
118
181
  }
119
- $putData(data, sdata) {
120
- super.$putData(data, sdata);
182
+ $checkIncomingAbilities(incoming) {
183
+ const tilmeld = enforceTilmeld(this);
184
+ if (!Array.isArray(incoming) ||
185
+ incoming.find((ability) => typeof ability !== 'string')) {
186
+ throw new BadDataError('Abilities must be an array of strings.');
187
+ }
188
+ if (incoming.includes('system/admin') !==
189
+ this.$data.abilities?.includes('system/admin') &&
190
+ (!tilmeld.currentUser ||
191
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
192
+ throw new BadDataError("You don't have the authority to grant or revoke system/admin.");
193
+ }
194
+ if (incoming.includes('tilmeld/admin') !==
195
+ this.$data.abilities?.includes('tilmeld/admin') &&
196
+ (!tilmeld.currentUser ||
197
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
198
+ throw new BadDataError("You don't have the authority to grant or revoke tilmeld/admin.");
199
+ }
200
+ if (incoming.includes('tilmeld/switch') !==
201
+ this.$data.abilities?.includes('tilmeld/switch') &&
202
+ (!tilmeld.currentUser ||
203
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
204
+ throw new BadDataError("You don't have the authority to grant or revoke tilmeld/switch.");
205
+ }
206
+ if (xor(incoming.filter((ability) => ability.startsWith('tilmeld/')), this.$data.abilities?.filter((ability) => ability.startsWith('tilmeld/')) ?? []).length &&
207
+ (!tilmeld.currentUser ||
208
+ (!tilmeld.currentUser.abilities?.includes('system/admin') &&
209
+ !tilmeld.currentUser.abilities?.includes('tilmeld/admin')))) {
210
+ throw new BadDataError("You don't have the authority to grant or revoke Tilmeld abilities.");
211
+ }
212
+ }
213
+ $putData(data, sdata, source) {
214
+ super.$putData(data, sdata, source);
215
+ if (source === 'server') {
216
+ this.$originalGroupname = this.$data.groupname;
217
+ }
121
218
  this.$updateDataProtection();
122
219
  }
220
+ /**
221
+ * Update the data protection arrays for a user.
222
+ *
223
+ * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
224
+ */
123
225
  $updateDataProtection(givenUser) {
124
- const tilmeld = this.$nymph.tilmeld;
226
+ const tilmeld = enforceTilmeld(this);
125
227
  let user = givenUser ?? tilmeld.User.current();
228
+ let [_groupname, domain] = tilmeld.config.domainSupport
229
+ ? splitn(this.$data.groupname ?? '', '@', 2)
230
+ : [this.$data.groupname, null];
231
+ // Lowercase the domain part.
232
+ if (domain) {
233
+ domain = domain.toLowerCase();
234
+ }
126
235
  this.$privateData = [...Group.DEFAULT_PRIVATE_DATA];
127
236
  this.$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
128
237
  if (tilmeld.config.emailUsernames) {
129
238
  this.$privateData.push('groupname');
130
239
  }
131
240
  if (user != null &&
132
- ((user.abilities?.indexOf('tilmeld/admin') ?? -1) !== -1 ||
133
- (user.abilities?.indexOf('system/admin') ?? -1) !== -1)) {
241
+ (user.abilities?.includes('tilmeld/admin') ||
242
+ user.abilities?.includes('system/admin'))) {
243
+ // Users who can edit groups can see their data.
134
244
  this.$privateData = [];
135
245
  this.$allowlistData = undefined;
136
246
  }
137
247
  else if (this.$data.user != null &&
138
248
  user != null &&
139
- this.$data.user.$is(user)) {
249
+ this.$data.user.guid === user.guid) {
250
+ // Users can see their group's data.
251
+ this.$privateData = [];
252
+ }
253
+ else if (user != null &&
254
+ tilmeld.config.domainSupport &&
255
+ domain &&
256
+ user.abilities?.includes(`tilmeld/domain/${domain}/admin`)) {
257
+ // Domain admins can see their groups' data.
140
258
  this.$privateData = [];
141
259
  }
142
260
  }
143
- $isDescendant(givenGroup) {
144
- const tilmeld = this.$nymph.tilmeld;
261
+ /**
262
+ * Check whether the group is a descendant of a group.
263
+ *
264
+ * @param group The group, or the group's GUID.
265
+ * @returns True or false.
266
+ */
267
+ async $isDescendant(givenGroup) {
268
+ const tilmeld = enforceTilmeld(this);
145
269
  let group;
146
270
  if (typeof givenGroup === 'string') {
147
- group = tilmeld.Group.factorySync(givenGroup);
271
+ group = await tilmeld.Group.factory(givenGroup);
148
272
  }
149
273
  else {
150
274
  group = givenGroup;
@@ -152,27 +276,41 @@ class Group extends AbleObject_1.default {
152
276
  if (group.guid == null) {
153
277
  return false;
154
278
  }
155
- if (this.$data.parent == null) {
279
+ // Check to see if the group is a descendant of the given group.
280
+ const parent = this.$data.parent;
281
+ await parent?.$wake();
282
+ if (parent == null) {
156
283
  return false;
157
284
  }
158
- if (this.$data.parent.$is(group)) {
285
+ if (parent.$is(group)) {
159
286
  return true;
160
287
  }
161
- if (this.$data.parent.$isDescendant(group)) {
288
+ if (await parent.$isDescendant(group)) {
162
289
  return true;
163
290
  }
164
291
  return false;
165
292
  }
293
+ /**
294
+ * Gets an array of the group's child groups.
295
+ *
296
+ * @returns An array of groups.
297
+ */
166
298
  async $getChildren() {
167
- const tilmeld = this.$nymph.tilmeld;
299
+ const tilmeld = enforceTilmeld(this);
168
300
  return await this.$nymph.getEntities({ class: tilmeld.Group }, {
169
301
  type: '&',
170
302
  equal: ['enabled', true],
171
303
  ref: ['parent', this],
172
304
  });
173
305
  }
306
+ /**
307
+ * Gets an array of the group's descendant groups.
308
+ *
309
+ * @param andSelf Include this group in the returned array.
310
+ * @returns An array of groups.
311
+ */
174
312
  async $getDescendants(andSelf = false) {
175
- const tilmeld = this.$nymph.tilmeld;
313
+ const tilmeld = enforceTilmeld(this);
176
314
  let groups = [];
177
315
  const entities = await this.$nymph.getEntities({ class: tilmeld.Group }, {
178
316
  type: '&',
@@ -187,39 +325,41 @@ class Group extends AbleObject_1.default {
187
325
  }
188
326
  return groups;
189
327
  }
190
- $getDescendantsSync(andSelf = false) {
191
- const tilmeld = this.$nymph.tilmeld;
192
- let groups = [];
193
- let entity;
194
- let offset = 0;
195
- do {
196
- entity = this.$nymph.driver.getEntitySync({ class: tilmeld.Group, offset }, {
197
- type: '&',
198
- equal: ['enabled', true],
199
- ref: ['parent', this],
200
- });
201
- if (entity != null) {
202
- groups = entity.$getDescendantsSync(true).concat(groups);
203
- }
204
- offset++;
205
- } while (entity != null);
206
- if (andSelf) {
207
- groups.push(this);
208
- }
209
- return groups;
210
- }
211
- $getLevel() {
212
- const tilmeld = this.$nymph.tilmeld;
213
- let group = tilmeld.Group.factorySync(this.guid ?? undefined);
328
+ /**
329
+ * Get the number of parents the group has.
330
+ *
331
+ * If the group is a top level group, this will return 0. If it is a child of
332
+ * a top level group, this will return 1. If it is a grandchild of a top level
333
+ * group, this will return 2, and so on.
334
+ *
335
+ * Levels will max out at 1024 to avoid recursive loops.
336
+ *
337
+ * @returns The level of the group.
338
+ */
339
+ async $getLevel() {
340
+ const tilmeld = enforceTilmeld(this);
341
+ let group = await tilmeld.Group.factory(this.guid ?? undefined);
342
+ let parent = group.parent;
343
+ await parent?.$wake();
214
344
  let level = 0;
215
- while (group.parent != null && group.parent.cdate != null && level < 1024) {
345
+ while (parent != null && parent.cdate != null && level < 1024) {
216
346
  level++;
217
- group = group.parent;
347
+ group = parent;
348
+ parent = group.parent;
349
+ await parent?.$wake();
218
350
  }
219
351
  return level;
220
352
  }
353
+ /**
354
+ * Gets an array of users in the group.
355
+ *
356
+ * @param descendants Include users in all descendant groups too.
357
+ * @param limit The limit for the query.
358
+ * @param offset The offset for the query.
359
+ * @returns An array of users.
360
+ */
221
361
  async $getUsers(descendants = false, limit, offset) {
222
- const tilmeld = this.$nymph.tilmeld;
362
+ const tilmeld = enforceTilmeld(this);
223
363
  let groups = [];
224
364
  if (descendants) {
225
365
  groups = await this.$getDescendants();
@@ -247,14 +387,37 @@ class Group extends AbleObject_1.default {
247
387
  ],
248
388
  });
249
389
  }
390
+ /**
391
+ * Check that a groupname is valid.
392
+ *
393
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
394
+ */
250
395
  async $checkGroupname() {
251
- const tilmeld = this.$nymph.tilmeld;
396
+ const tilmeld = enforceTilmeld(this);
252
397
  if (!tilmeld.config.emailUsernames) {
253
- if (this.$data.groupname == null || !this.$data.groupname.length) {
398
+ let [groupname, domain] = tilmeld.config.domainSupport
399
+ ? splitn(this.$data.groupname ?? '', '@', 2)
400
+ : [this.$data.groupname, null];
401
+ // Lowercase the domain part.
402
+ if (domain) {
403
+ domain = domain.toLowerCase();
404
+ this.$data.groupname = `${groupname}@${domain}`;
405
+ }
406
+ if (this.$data.groupname == null ||
407
+ !this.$data.groupname.length ||
408
+ groupname == null ||
409
+ !groupname.length) {
254
410
  return { result: false, message: 'Please specify a groupname.' };
255
411
  }
256
- if (tilmeld.config.maxUsernameLength > 0 &&
257
- this.$data.groupname.length > tilmeld.config.maxUsernameLength) {
412
+ if (groupname.length < tilmeld.config.minUsernameLength) {
413
+ return {
414
+ result: false,
415
+ message: 'Groupnames must be at least ' +
416
+ tilmeld.config.minUsernameLength +
417
+ ' characters.',
418
+ };
419
+ }
420
+ if (groupname.length > tilmeld.config.maxUsernameLength) {
258
421
  return {
259
422
  result: false,
260
423
  message: 'Groupnames must not exceed ' +
@@ -262,18 +425,34 @@ class Group extends AbleObject_1.default {
262
425
  ' characters.',
263
426
  };
264
427
  }
265
- if ((0, lodash_1.difference)(this.$data.groupname.split(''), tilmeld.config.validChars.split('')).length) {
428
+ if (difference(groupname.split(''), tilmeld.config.validChars.split(''))
429
+ .length) {
266
430
  return {
267
431
  result: false,
268
432
  message: tilmeld.config.validCharsNotice,
269
433
  };
270
434
  }
271
- if (!tilmeld.config.validRegex.test(this.$data.groupname)) {
435
+ if (!tilmeld.config.validRegex.test(groupname)) {
272
436
  return {
273
437
  result: false,
274
438
  message: tilmeld.config.validRegexNotice,
275
439
  };
276
440
  }
441
+ if (domain != null) {
442
+ if (!tilmeld.config.validDomainRegex.test(domain)) {
443
+ return {
444
+ result: false,
445
+ message: tilmeld.config.validDomainRegexNotice,
446
+ };
447
+ }
448
+ if (!this.$is(tilmeld.currentUser?.group) &&
449
+ !tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
450
+ return {
451
+ result: false,
452
+ message: "You don't have the authority to manage groups on this domain.",
453
+ };
454
+ }
455
+ }
277
456
  const selector = {
278
457
  type: '&',
279
458
  ilike: [
@@ -298,8 +477,15 @@ class Group extends AbleObject_1.default {
298
477
  if (this.$data.groupname == null || !this.$data.groupname.length) {
299
478
  return { result: false, message: 'Please specify an email.' };
300
479
  }
301
- if (tilmeld.config.maxUsernameLength > 0 &&
302
- this.$data.groupname.length > tilmeld.config.maxUsernameLength) {
480
+ if (this.$data.groupname.length < tilmeld.config.minUsernameLength) {
481
+ return {
482
+ result: false,
483
+ message: 'Emails must be at least ' +
484
+ tilmeld.config.minUsernameLength +
485
+ ' characters.',
486
+ };
487
+ }
488
+ if (this.$data.groupname.length > tilmeld.config.maxUsernameLength) {
303
489
  return {
304
490
  result: false,
305
491
  message: 'Emails must not exceed ' +
@@ -310,8 +496,13 @@ class Group extends AbleObject_1.default {
310
496
  return await this.$checkEmail();
311
497
  }
312
498
  }
499
+ /**
500
+ * Check that an email is unique.
501
+ *
502
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
503
+ */
313
504
  async $checkEmail() {
314
- const tilmeld = this.$nymph.tilmeld;
505
+ const tilmeld = enforceTilmeld(this);
315
506
  if (this.$data.email === '') {
316
507
  return { result: true, message: '' };
317
508
  }
@@ -344,10 +535,42 @@ class Group extends AbleObject_1.default {
344
535
  };
345
536
  }
346
537
  async $save() {
347
- const tilmeld = this.$nymph.tilmeld;
538
+ const tilmeld = enforceTilmeld(this);
348
539
  if (this.$data.groupname == null || !this.$data.groupname.trim().length) {
349
540
  return false;
350
541
  }
542
+ // Lowercase the domain part.
543
+ if (tilmeld.config.domainSupport) {
544
+ const [groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
545
+ if (domain) {
546
+ this.$data.groupname = `${groupname}@${domain.toLowerCase()}`;
547
+ }
548
+ }
549
+ if (tilmeld.config.domainSupport) {
550
+ const [_groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
551
+ const [_originalGroupname, originalDomain] = splitn(this.$originalGroupname ?? '', '@', 2);
552
+ if (this.guid != null &&
553
+ ((originalDomain == null && domain != null) ||
554
+ (domain == null && originalDomain != null))) {
555
+ throw new AccessControlError("Groups can't switch to or from having a domain once created.");
556
+ }
557
+ if (!this.$skipAcWhenSaving &&
558
+ !tilmeld.gatekeeper('tilmeld/admin') &&
559
+ !tilmeld.gatekeeper('system/admin')) {
560
+ if (domain != null &&
561
+ !this.$is(tilmeld.currentUser) &&
562
+ !tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
563
+ throw new AccessControlError("You don't have the authority to modify groups on this domain.");
564
+ }
565
+ if (originalDomain != null &&
566
+ originalDomain !== domain &&
567
+ (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
568
+ !tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))) {
569
+ throw new AccessControlError("You don't have the authority to modify groups on this domain.");
570
+ }
571
+ }
572
+ }
573
+ // Formatting.
351
574
  this.$data.groupname = this.$data.groupname.trim();
352
575
  if (tilmeld.config.emailUsernames) {
353
576
  this.$data.email = this.$data.groupname;
@@ -356,16 +579,19 @@ class Group extends AbleObject_1.default {
356
579
  this.$data.name = (this.$data.name ?? '').trim();
357
580
  this.$data.phone = (this.$data.phone ?? '').trim();
358
581
  this.$data.enabled = !!this.$data.enabled;
359
- if (this.$data.name === '') {
582
+ // Clear empty values.
583
+ if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
360
584
  delete this.$data.name;
361
585
  }
362
- if (this.$data.email === '') {
586
+ if (this.$data.email === '' ||
587
+ !tilmeld.config.userFields.includes('email')) {
363
588
  delete this.$data.email;
364
589
  }
365
590
  if (this.$data.avatar === '') {
366
591
  delete this.$data.avatar;
367
592
  }
368
- if (this.$data.phone === '') {
593
+ if (this.$data.phone === '' ||
594
+ !tilmeld.config.userFields.includes('phone')) {
369
595
  delete this.$data.phone;
370
596
  }
371
597
  if (this.$data.parent == null) {
@@ -383,46 +609,70 @@ class Group extends AbleObject_1.default {
383
609
  if (!this.$data.unverifiedSecondary) {
384
610
  delete this.$data.unverifiedSecondary;
385
611
  }
386
- this.$data.abilities = (0, lodash_1.difference)(this.$data.abilities ?? [], [
387
- 'system/admin',
388
- 'tilmeld/admin',
389
- null,
390
- undefined,
391
- ]);
612
+ // Groups should never have 'system/admin', 'tilmeld/admin',
613
+ // 'tilmeld/switch', or domain admin abilities.
614
+ this.$data.abilities = (this.$data.abilities ?? []).filter((ability) => {
615
+ if (['system/admin', 'tilmeld/admin', 'tilmeld/switch'].indexOf(ability) !==
616
+ -1) {
617
+ return false;
618
+ }
619
+ if (ability == null) {
620
+ return false;
621
+ }
622
+ if (ability.startsWith('tilmeld/domain/') && ability.endsWith('/admin')) {
623
+ return false;
624
+ }
625
+ return true;
626
+ });
627
+ // Verification.
392
628
  const unCheck = await this.$checkGroupname();
393
629
  if (!unCheck.result) {
394
- throw new errors_1.BadUsernameError(unCheck.message);
630
+ throw new BadUsernameError(unCheck.message);
395
631
  }
396
- if (!tilmeld.config.emailUsernames) {
632
+ if (!tilmeld.config.emailUsernames &&
633
+ tilmeld.config.userFields.includes('email')) {
397
634
  const emCheck = await this.$checkEmail();
398
635
  if (!emCheck.result) {
399
- throw new errors_1.BadEmailError(emCheck.message);
636
+ throw new BadEmailError(emCheck.message);
400
637
  }
401
638
  }
402
- if (this.$data.parent &&
403
- (this.$data.parent.cdate == null ||
404
- this.$is(this.$data.parent) ||
405
- this.$data.parent.$isDescendant(this))) {
406
- throw new errors_1.BadDataError("Group parent can't be itself or descendant of itself.");
639
+ // Validate group parent. Make sure it's not a descendant of this group.
640
+ const parent = this.$data.parent;
641
+ await parent?.$wake();
642
+ if (parent &&
643
+ (parent.cdate == null ||
644
+ this.$is(parent) ||
645
+ (await parent.$isDescendant(this)))) {
646
+ throw new BadDataError("Group parent can't be itself or descendant of itself.");
407
647
  }
408
648
  try {
409
- tilmeld.config.validatorGroup(this);
649
+ tilmeld.config.validatorGroup(tilmeld, this);
410
650
  }
411
651
  catch (e) {
412
- throw new errors_1.BadDataError(e?.message);
652
+ throw new BadDataError(e?.message);
413
653
  }
654
+ // Only one default primary group is allowed.
414
655
  if (this.$data.defaultPrimary) {
415
656
  const currentPrimary = await this.$nymph.getEntity({ class: tilmeld.Group }, { type: '&', truthy: 'defaultPrimary' });
416
657
  if (currentPrimary != null && !this.$is(currentPrimary)) {
417
658
  currentPrimary.defaultPrimary = false;
418
- if (!(await currentPrimary.$save())) {
419
- throw new errors_1.CouldNotChangeDefaultPrimaryGroupError('Could not change new user primary group from ' +
659
+ if (this.$skipAcWhenSaving
660
+ ? !(await currentPrimary.$saveSkipAC())
661
+ : !(await currentPrimary.$save())) {
662
+ throw new CouldNotChangeDefaultPrimaryGroupError('Could not change new user primary group from ' +
420
663
  `${currentPrimary.groupname}.`);
421
664
  }
422
665
  }
423
666
  }
424
- return await super.$save();
667
+ const ret = await super.$save();
668
+ if (ret) {
669
+ this.$originalGroupname = this.$data.groupname;
670
+ }
671
+ return ret;
425
672
  }
673
+ /*
674
+ * This should *never* be accessible on the client.
675
+ */
426
676
  async $saveSkipAC() {
427
677
  this.$skipAcWhenSaving = true;
428
678
  return await this.$save();
@@ -435,73 +685,98 @@ class Group extends AbleObject_1.default {
435
685
  return false;
436
686
  }
437
687
  async $delete() {
438
- let tilmeld = this.$nymph.tilmeld;
439
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
440
- throw new errors_1.BadDataError("You don't have the authority to delete groups.");
688
+ let tilmeld = enforceTilmeld(this);
689
+ if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
690
+ throw new AccessControlError("You don't have the authority to delete groups.");
691
+ }
692
+ if (this.$data.groupname !== this.$originalGroupname) {
693
+ throw new AccessControlError("You can't delete a group while switching its groupname.");
441
694
  }
442
695
  const transaction = 'tilmeld-delete-' + this.guid;
443
696
  const nymph = this.$nymph;
444
697
  const tnymph = await nymph.startTransaction(transaction);
445
- this.$nymph = tnymph;
446
- tilmeld = this.$nymph.tilmeld;
447
- const descendants = await this.$getDescendants();
448
- if (descendants.length) {
449
- for (let curGroup of descendants) {
450
- if (!(await curGroup.$delete())) {
698
+ this.$setNymph(tnymph);
699
+ tilmeld = enforceTilmeld(this);
700
+ const User = tnymph.getEntityClass(UserClass);
701
+ try {
702
+ // Delete descendants.
703
+ const descendants = await this.$getDescendants();
704
+ if (descendants.length) {
705
+ for (let curGroup of descendants) {
706
+ if (this.$skipAcWhenDeleting
707
+ ? !(await curGroup.$deleteSkipAC())
708
+ : !(await curGroup.$delete())) {
709
+ await tnymph.rollback(transaction);
710
+ this.$setNymph(nymph);
711
+ return false;
712
+ }
713
+ }
714
+ }
715
+ // Remove users from this primary group.
716
+ const primaryUsers = await tnymph.getEntities({
717
+ class: User,
718
+ skipAc: true,
719
+ }, {
720
+ type: '&',
721
+ ref: ['group', this],
722
+ });
723
+ for (let user of primaryUsers) {
724
+ delete user.group;
725
+ if (this.$skipAcWhenDeleting
726
+ ? !(await user.$saveSkipAC())
727
+ : !(await user.$save())) {
451
728
  await tnymph.rollback(transaction);
452
- this.$nymph = nymph;
729
+ this.$setNymph(nymph);
453
730
  return false;
454
731
  }
455
732
  }
456
- }
457
- const primaryUsers = await tnymph.getEntities({
458
- class: tilmeld.User,
459
- skipAc: true,
460
- }, {
461
- type: '&',
462
- ref: ['group', this],
463
- });
464
- for (let user of primaryUsers) {
465
- delete user.group;
466
- if (!(await user.$save())) {
467
- await tnymph.rollback(transaction);
468
- this.$nymph = nymph;
469
- return false;
733
+ // Remove users from this secondary group.
734
+ const secondaryUsers = await tnymph.getEntities({
735
+ class: User,
736
+ skipAc: true,
737
+ }, {
738
+ type: '&',
739
+ ref: ['groups', this],
740
+ });
741
+ for (let user of secondaryUsers) {
742
+ user.$delGroup(this);
743
+ if (this.$skipAcWhenDeleting
744
+ ? !(await user.$saveSkipAC())
745
+ : !(await user.$save())) {
746
+ await tnymph.rollback(transaction);
747
+ return false;
748
+ }
470
749
  }
471
- }
472
- const secondaryUsers = await tnymph.getEntities({
473
- class: tilmeld.User,
474
- skipAc: true,
475
- }, {
476
- type: '&',
477
- ref: ['groups', this],
478
- });
479
- for (let user of secondaryUsers) {
480
- user.$delGroup(this);
481
- if (!(await user.$save())) {
750
+ // Delete the group.
751
+ let success = await super.$delete();
752
+ if (success) {
753
+ success = await tnymph.commit(transaction);
754
+ }
755
+ else {
482
756
  await tnymph.rollback(transaction);
483
- return false;
484
757
  }
758
+ this.$setNymph(nymph);
759
+ return success;
485
760
  }
486
- const success = await super.$delete();
487
- if (success) {
488
- await tnymph.commit(transaction);
489
- }
490
- else {
761
+ catch (e) {
491
762
  await tnymph.rollback(transaction);
763
+ this.$setNymph(nymph);
764
+ throw e;
492
765
  }
493
- this.$nymph = nymph;
494
- return success;
766
+ }
767
+ /*
768
+ * This should *never* be accessible on the client.
769
+ */
770
+ async $deleteSkipAC() {
771
+ this.$skipAcWhenDeleting = true;
772
+ return await this.$delete();
773
+ }
774
+ $tilmeldDeleteSkipAC() {
775
+ if (this.$skipAcWhenDeleting) {
776
+ this.$skipAcWhenDeleting = false;
777
+ return true;
778
+ }
779
+ return false;
495
780
  }
496
781
  }
497
- exports.default = Group;
498
- Group.ETYPE = 'tilmeld_group';
499
- Group.class = 'Group';
500
- Group.DEFAULT_PRIVATE_DATA = ['email', 'phone', 'abilities', 'user'];
501
- Group.DEFAULT_ALLOWLIST_DATA = [];
502
- Group.clientEnabledStaticMethods = [
503
- 'getPrimaryGroups',
504
- 'getSecondaryGroups',
505
- ];
506
- Group.searchRestrictedData = [...Group.DEFAULT_PRIVATE_DATA];
507
782
  //# sourceMappingURL=Group.js.map