@nymphjs/tilmeld 1.0.0-beta.7 → 1.0.0-beta.70

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 (57) hide show
  1. package/CHANGELOG.md +326 -0
  2. package/README.md +14 -1
  3. package/dist/AbleObject.d.ts +15 -0
  4. package/dist/AbleObject.js +16 -1
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +130 -10
  7. package/dist/Group.js +311 -129
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.test.js +77 -2
  10. package/dist/Group.test.js.map +1 -1
  11. package/dist/Tilmeld.d.ts +237 -9
  12. package/dist/Tilmeld.js +569 -201
  13. package/dist/Tilmeld.js.map +1 -1
  14. package/dist/User.d.ts +344 -16
  15. package/dist/User.js +979 -313
  16. package/dist/User.js.map +1 -1
  17. package/dist/User.test.js +63 -2
  18. package/dist/User.test.js.map +1 -1
  19. package/dist/conf/d.d.ts +250 -3
  20. package/dist/conf/defaults.js +146 -98
  21. package/dist/conf/defaults.js.map +1 -1
  22. package/dist/enforceTilmeld.d.ts +3 -0
  23. package/dist/enforceTilmeld.js +22 -0
  24. package/dist/enforceTilmeld.js.map +1 -0
  25. package/dist/errors/AccessControlError.d.ts +2 -1
  26. package/dist/errors/AccessControlError.js +3 -2
  27. package/dist/errors/AccessControlError.js.map +1 -1
  28. package/dist/errors/BadDataError.d.ts +2 -1
  29. package/dist/errors/BadDataError.js +3 -2
  30. package/dist/errors/BadDataError.js.map +1 -1
  31. package/dist/errors/BadEmailError.d.ts +2 -1
  32. package/dist/errors/BadEmailError.js +3 -2
  33. package/dist/errors/BadEmailError.js.map +1 -1
  34. package/dist/errors/BadUsernameError.d.ts +2 -1
  35. package/dist/errors/BadUsernameError.js +3 -2
  36. package/dist/errors/BadUsernameError.js.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +1 -0
  39. package/dist/index.js.map +1 -1
  40. package/package.json +30 -26
  41. package/src/AbleObject.ts +2 -2
  42. package/src/Group.test.ts +102 -4
  43. package/src/Group.ts +274 -158
  44. package/src/Tilmeld.ts +383 -249
  45. package/src/User.test.ts +80 -4
  46. package/src/User.ts +1061 -382
  47. package/src/conf/d.ts +26 -13
  48. package/src/conf/defaults.ts +151 -104
  49. package/src/enforceTilmeld.ts +20 -0
  50. package/src/errors/AccessControlError.ts +4 -2
  51. package/src/errors/BadDataError.ts +4 -2
  52. package/src/errors/BadEmailError.ts +4 -2
  53. package/src/errors/BadUsernameError.ts +4 -2
  54. package/src/index.ts +2 -0
  55. package/src/tilmeldJoiProps.ts +9 -9
  56. package/tsconfig.json +3 -3
  57. package/typedoc.json +4 -0
package/src/Group.ts CHANGED
@@ -1,6 +1,6 @@
1
- import {
1
+ import type {
2
+ Nymph,
2
3
  EntityData,
3
- EntityInterface,
4
4
  EntityJson,
5
5
  EntityPatch,
6
6
  Options,
@@ -10,7 +10,7 @@ import {
10
10
  import md5 from 'crypto-js/md5';
11
11
  import { difference } from 'lodash';
12
12
 
13
- import type Tilmeld from './Tilmeld';
13
+ import { enforceTilmeld } from './enforceTilmeld';
14
14
  import AbleObject from './AbleObject';
15
15
  import {
16
16
  BadDataError,
@@ -18,7 +18,8 @@ import {
18
18
  BadUsernameError,
19
19
  CouldNotChangeDefaultPrimaryGroupError,
20
20
  } from './errors';
21
- import User, { UserData } from './User';
21
+ import type User from './User';
22
+ import type { UserData } from './User';
22
23
 
23
24
  export type GroupData = {
24
25
  /**
@@ -85,7 +86,6 @@ export default class Group extends AbleObject<GroupData> {
85
86
  /**
86
87
  * The instance of Tilmeld to use for queries.
87
88
  */
88
- static tilmeld: Tilmeld;
89
89
  static ETYPE = 'tilmeld_group';
90
90
  static class = 'Group';
91
91
 
@@ -112,16 +112,16 @@ export default class Group extends AbleObject<GroupData> {
112
112
  protected $allowlistTags?: string[] = [];
113
113
 
114
114
  /**
115
- * This is explicitly used only during the registration proccess.
115
+ * This should only be used by the backend.
116
116
  */
117
117
  private $skipAcWhenSaving = false;
118
-
119
- static async factory(guid?: string): Promise<Group & GroupData> {
120
- return (await super.factory(guid)) as Group & GroupData;
121
- }
118
+ /**
119
+ * This should only be used by the backend.
120
+ */
121
+ private $skipAcWhenDeleting = false;
122
122
 
123
123
  static async factoryGroupname(
124
- groupname?: string
124
+ groupname?: string,
125
125
  ): Promise<Group & GroupData> {
126
126
  const entity = new this();
127
127
  if (groupname != null) {
@@ -132,7 +132,7 @@ export default class Group extends AbleObject<GroupData> {
132
132
  {
133
133
  type: '&',
134
134
  ilike: ['groupname', groupname.replace(/([\\%_])/g, (s) => `\\${s}`)],
135
- }
135
+ },
136
136
  );
137
137
  if (entity != null) {
138
138
  return entity;
@@ -141,21 +141,6 @@ export default class Group extends AbleObject<GroupData> {
141
141
  return entity;
142
142
  }
143
143
 
144
- static factorySync(guid?: string): Group & GroupData {
145
- return super.factorySync(guid) as Group & GroupData;
146
- }
147
-
148
- constructor(guid?: string) {
149
- super(guid);
150
-
151
- if (this.guid == null) {
152
- // Defaults.
153
- this.$data.enabled = true;
154
- this.$data.abilities = [];
155
- this.$updateDataProtection();
156
- }
157
- }
158
-
159
144
  /**
160
145
  * Get all the groups that can be assigned as primary groups.
161
146
  *
@@ -165,16 +150,17 @@ export default class Group extends AbleObject<GroupData> {
165
150
  */
166
151
  public static async getPrimaryGroups(
167
152
  options?: Options,
168
- selectors?: Selector[]
153
+ selectors?: Selector[],
169
154
  ) {
170
- if (!this.tilmeld.gatekeeper('tilmeld/admin')) {
155
+ const tilmeld = enforceTilmeld(this);
156
+ if (!tilmeld.gatekeeper('tilmeld/admin')) {
171
157
  throw new Error("You don't have permission to do that.");
172
158
  }
173
159
 
174
160
  return await this.getAssignableGroups(
175
- this.tilmeld.config.highestPrimary,
176
- { ...options, class: this.tilmeld.Group, return: 'entity' },
177
- [...(selectors ?? [])]
161
+ tilmeld.config.highestPrimary,
162
+ { ...options, class: tilmeld.Group, return: 'entity' },
163
+ [...(selectors ?? [])],
178
164
  );
179
165
  }
180
166
 
@@ -187,24 +173,26 @@ export default class Group extends AbleObject<GroupData> {
187
173
  */
188
174
  public static async getSecondaryGroups(
189
175
  options?: Options,
190
- selectors?: Selector[]
176
+ selectors?: Selector[],
191
177
  ) {
192
- if (!this.tilmeld.gatekeeper('tilmeld/admin')) {
178
+ const tilmeld = enforceTilmeld(this);
179
+ if (!tilmeld.gatekeeper('tilmeld/admin')) {
193
180
  throw new Error("You don't have permission to do that.");
194
181
  }
195
182
 
196
183
  return await this.getAssignableGroups(
197
- this.tilmeld.config.highestSecondary,
198
- { ...options, class: this.tilmeld.Group, return: 'entity' },
199
- [...(selectors ?? [])]
184
+ tilmeld.config.highestSecondary,
185
+ { ...options, class: tilmeld.Group, return: 'entity' },
186
+ [...(selectors ?? [])],
200
187
  );
201
188
  }
202
189
 
203
190
  private static async getAssignableGroups(
204
191
  highestParent: string | boolean,
205
192
  options: Options<typeof Group>,
206
- selectors: Selector[]
193
+ selectors: Selector[],
207
194
  ) {
195
+ const tilmeld = enforceTilmeld(this);
208
196
  let assignableGroups: (Group & GroupData)[] = [];
209
197
 
210
198
  if (highestParent === false) {
@@ -212,24 +200,68 @@ export default class Group extends AbleObject<GroupData> {
212
200
  }
213
201
 
214
202
  assignableGroups = await this.nymph.getEntities(
215
- { ...options, class: this.tilmeld.Group },
216
- ...selectors
203
+ { ...options, class: tilmeld.Group },
204
+ ...selectors,
217
205
  );
218
206
  if (highestParent !== true) {
219
- assignableGroups = assignableGroups.filter((group) => {
220
- let curGroup = group;
221
- while (curGroup.parent != null && curGroup.parent.cdate != null) {
222
- if (curGroup.parent.guid === highestParent) {
223
- return true;
224
- }
225
- curGroup = curGroup.parent;
226
- }
227
- return false;
228
- });
207
+ assignableGroups = (
208
+ await Promise.all(
209
+ assignableGroups.map(async (group) => {
210
+ let curGroup = group;
211
+ let parent = curGroup.parent;
212
+ await parent?.$wake();
213
+ while (parent != null && parent?.cdate != null) {
214
+ if (parent?.guid === highestParent) {
215
+ return group;
216
+ }
217
+ curGroup = parent;
218
+ parent = curGroup.parent;
219
+ await parent?.$wake();
220
+ }
221
+ return null;
222
+ }),
223
+ )
224
+ ).filter((group) => group != null) as (Group & GroupData)[];
229
225
  }
230
226
  return assignableGroups;
231
227
  }
232
228
 
229
+ constructor() {
230
+ super();
231
+
232
+ // Defaults.
233
+ this.$data.enabled = true;
234
+ this.$data.abilities = [];
235
+ this.$updateDataProtection();
236
+ }
237
+
238
+ $setNymph(nymph: Nymph) {
239
+ this.$nymph = nymph;
240
+ if (!this.$asleep()) {
241
+ if (this.$data.user && this.$data.user.$nymph !== nymph) {
242
+ this.$data.user.$setNymph(nymph);
243
+ }
244
+ if (this.$data.parent && this.$data.parent.$nymph !== nymph) {
245
+ this.$data.parent.$setNymph(nymph);
246
+ }
247
+ }
248
+ }
249
+
250
+ async $getUniques(): Promise<string[]> {
251
+ const tilmeld = enforceTilmeld(this);
252
+ const uniques = [`u:${this.$data.groupname}`];
253
+ if (
254
+ !tilmeld.config.emailUsernames &&
255
+ tilmeld.config.userFields.includes('email')
256
+ ) {
257
+ uniques.push(`e:${this.$data.email}`);
258
+ }
259
+ if (this.$data.defaultPrimary) {
260
+ uniques.push(`o:default-primary`);
261
+ }
262
+ return uniques;
263
+ }
264
+
233
265
  public $getAvatar() {
234
266
  if (this.$data.avatar != null && this.$data.avatar !== '') {
235
267
  return this.$data.avatar;
@@ -245,17 +277,42 @@ export default class Group extends AbleObject<GroupData> {
245
277
  }
246
278
 
247
279
  public $jsonAcceptData(input: EntityJson, allowConflict = false) {
248
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
249
- this.$referenceWake();
280
+ const tilmeld = enforceTilmeld(this);
281
+ this.$check();
282
+
283
+ if (
284
+ 'abilities' in input.data &&
285
+ input.data.abilities?.includes('system/admin') !==
286
+ this.$data.abilities?.includes('system/admin') &&
287
+ (!tilmeld.currentUser ||
288
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
289
+ ) {
290
+ throw new BadDataError(
291
+ "You don't have the authority to grant or revoke system/admin.",
292
+ );
293
+ }
250
294
 
251
295
  if (
252
- input.data.abilities?.indexOf('system/admin') !== -1 &&
253
- this.$data.abilities?.indexOf('system/admin') === -1 &&
254
- tilmeld.gatekeeper('tilmeld/admin') &&
255
- !tilmeld.gatekeeper('system/admin')
296
+ 'abilities' in input.data &&
297
+ input.data.abilities?.includes('tilmeld/admin') !==
298
+ this.$data.abilities?.includes('tilmeld/admin') &&
299
+ (!tilmeld.currentUser ||
300
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
256
301
  ) {
257
302
  throw new BadDataError(
258
- "You don't have the authority to make this group a system admin."
303
+ "You don't have the authority to grant or revoke tilmeld/admin.",
304
+ );
305
+ }
306
+
307
+ if (
308
+ 'abilities' in input.data &&
309
+ input.data.abilities?.includes('tilmeld/switch') !==
310
+ this.$data.abilities?.includes('tilmeld/switch') &&
311
+ (!tilmeld.currentUser ||
312
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
313
+ ) {
314
+ throw new BadDataError(
315
+ "You don't have the authority to grant or revoke tilmeld/switch.",
259
316
  );
260
317
  }
261
318
 
@@ -263,17 +320,42 @@ export default class Group extends AbleObject<GroupData> {
263
320
  }
264
321
 
265
322
  public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
266
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
267
- this.$referenceWake();
323
+ const tilmeld = enforceTilmeld(this);
324
+ this.$check();
325
+
326
+ if (
327
+ 'abilities' in patch.set &&
328
+ patch.set.abilities?.includes('system/admin') !==
329
+ this.$data.abilities?.includes('system/admin') &&
330
+ (!tilmeld.currentUser ||
331
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
332
+ ) {
333
+ throw new BadDataError(
334
+ "You don't have the authority to grant or revoke system/admin.",
335
+ );
336
+ }
337
+
338
+ if (
339
+ 'abilities' in patch.set &&
340
+ patch.set.abilities?.includes('tilmeld/admin') !==
341
+ this.$data.abilities?.includes('tilmeld/admin') &&
342
+ (!tilmeld.currentUser ||
343
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
344
+ ) {
345
+ throw new BadDataError(
346
+ "You don't have the authority to grant or revoke tilmeld/admin.",
347
+ );
348
+ }
268
349
 
269
350
  if (
270
- patch.set.abilities?.indexOf('system/admin') !== -1 &&
271
- this.$data.abilities?.indexOf('system/admin') === -1 &&
272
- tilmeld.gatekeeper('tilmeld/admin') &&
273
- !tilmeld.gatekeeper('system/admin')
351
+ 'abilities' in patch.set &&
352
+ patch.set.abilities?.includes('tilmeld/switch') !==
353
+ this.$data.abilities?.includes('tilmeld/switch') &&
354
+ (!tilmeld.currentUser ||
355
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
274
356
  ) {
275
357
  throw new BadDataError(
276
- "You don't have the authority to make this group a system admin."
358
+ "You don't have the authority to grant or revoke tilmeld/switch.",
277
359
  );
278
360
  }
279
361
 
@@ -291,7 +373,7 @@ export default class Group extends AbleObject<GroupData> {
291
373
  * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
292
374
  */
293
375
  public $updateDataProtection(givenUser?: User & UserData) {
294
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
376
+ const tilmeld = enforceTilmeld(this);
295
377
  let user = givenUser ?? tilmeld.User.current();
296
378
 
297
379
  this.$privateData = [...Group.DEFAULT_PRIVATE_DATA];
@@ -303,8 +385,8 @@ export default class Group extends AbleObject<GroupData> {
303
385
 
304
386
  if (
305
387
  user != null &&
306
- ((user.abilities?.indexOf('tilmeld/admin') ?? -1) !== -1 ||
307
- (user.abilities?.indexOf('system/admin') ?? -1) !== -1)
388
+ (user.abilities?.includes('tilmeld/admin') ||
389
+ user.abilities?.includes('system/admin'))
308
390
  ) {
309
391
  // Users who can edit groups can see their data.
310
392
  this.$privateData = [];
@@ -312,7 +394,7 @@ export default class Group extends AbleObject<GroupData> {
312
394
  } else if (
313
395
  this.$data.user != null &&
314
396
  user != null &&
315
- this.$data.user.$is(user)
397
+ this.$data.user.guid === user.guid
316
398
  ) {
317
399
  // Users can see their group's data.
318
400
  this.$privateData = [];
@@ -325,11 +407,13 @@ export default class Group extends AbleObject<GroupData> {
325
407
  * @param group The group, or the group's GUID.
326
408
  * @returns True or false.
327
409
  */
328
- public $isDescendant(givenGroup: (Group & GroupData) | string): boolean {
329
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
410
+ public async $isDescendant(
411
+ givenGroup: (Group & GroupData) | string,
412
+ ): Promise<boolean> {
413
+ const tilmeld = enforceTilmeld(this);
330
414
  let group: Group & GroupData;
331
415
  if (typeof givenGroup === 'string') {
332
- group = tilmeld.Group.factorySync(givenGroup);
416
+ group = await tilmeld.Group.factory(givenGroup);
333
417
  } else {
334
418
  group = givenGroup;
335
419
  }
@@ -337,13 +421,15 @@ export default class Group extends AbleObject<GroupData> {
337
421
  return false;
338
422
  }
339
423
  // Check to see if the group is a descendant of the given group.
340
- if (this.$data.parent == null) {
424
+ const parent = this.$data.parent;
425
+ await parent?.$wake();
426
+ if (parent == null) {
341
427
  return false;
342
428
  }
343
- if (this.$data.parent.$is(group)) {
429
+ if (parent.$is(group)) {
344
430
  return true;
345
431
  }
346
- if (this.$data.parent.$isDescendant(group)) {
432
+ if (await parent.$isDescendant(group)) {
347
433
  return true;
348
434
  }
349
435
  return false;
@@ -355,14 +441,14 @@ export default class Group extends AbleObject<GroupData> {
355
441
  * @returns An array of groups.
356
442
  */
357
443
  public async $getChildren() {
358
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
444
+ const tilmeld = enforceTilmeld(this);
359
445
  return await this.$nymph.getEntities(
360
446
  { class: tilmeld.Group },
361
447
  {
362
448
  type: '&',
363
449
  equal: ['enabled', true],
364
450
  ref: ['parent', this],
365
- }
451
+ },
366
452
  );
367
453
  }
368
454
 
@@ -373,9 +459,9 @@ export default class Group extends AbleObject<GroupData> {
373
459
  * @returns An array of groups.
374
460
  */
375
461
  public async $getDescendants(
376
- andSelf = false
462
+ andSelf = false,
377
463
  ): Promise<(Group & GroupData)[]> {
378
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
464
+ const tilmeld = enforceTilmeld(this);
379
465
  let groups: (Group & GroupData)[] = [];
380
466
  const entities = await this.$nymph.getEntities(
381
467
  { class: tilmeld.Group },
@@ -383,7 +469,7 @@ export default class Group extends AbleObject<GroupData> {
383
469
  type: '&',
384
470
  equal: ['enabled', true],
385
471
  ref: ['parent', this],
386
- }
472
+ },
387
473
  );
388
474
  for (let entity of entities) {
389
475
  groups = (await entity.$getDescendants(true)).concat(groups);
@@ -394,37 +480,6 @@ export default class Group extends AbleObject<GroupData> {
394
480
  return groups;
395
481
  }
396
482
 
397
- /**
398
- * Gets an array of the group's descendant groups.
399
- *
400
- * @param andSelf Include this group in the returned array.
401
- * @returns An array of groups.
402
- */
403
- public $getDescendantsSync(andSelf = false): (Group & GroupData)[] {
404
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
405
- let groups: (Group & GroupData)[] = [];
406
- let entity: EntityInterface | null;
407
- let offset = 0;
408
- do {
409
- entity = this.$nymph.driver.getEntitySync(
410
- { class: tilmeld.Group, offset },
411
- {
412
- type: '&',
413
- equal: ['enabled', true],
414
- ref: ['parent', this],
415
- }
416
- );
417
- if (entity != null) {
418
- groups = entity.$getDescendantsSync(true).concat(groups);
419
- }
420
- offset++;
421
- } while (entity != null);
422
- if (andSelf) {
423
- groups.push(this);
424
- }
425
- return groups;
426
- }
427
-
428
483
  /**
429
484
  * Get the number of parents the group has.
430
485
  *
@@ -436,13 +491,17 @@ export default class Group extends AbleObject<GroupData> {
436
491
  *
437
492
  * @returns The level of the group.
438
493
  */
439
- public $getLevel() {
440
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
441
- let group = tilmeld.Group.factorySync(this.guid ?? undefined);
494
+ public async $getLevel() {
495
+ const tilmeld = enforceTilmeld(this);
496
+ let group = await tilmeld.Group.factory(this.guid ?? undefined);
497
+ let parent = group.parent;
498
+ await parent?.$wake();
442
499
  let level = 0;
443
- while (group.parent != null && group.parent.cdate != null && level < 1024) {
500
+ while (parent != null && parent.cdate != null && level < 1024) {
444
501
  level++;
445
- group = group.parent;
502
+ group = parent;
503
+ parent = group.parent;
504
+ await parent?.$wake();
446
505
  }
447
506
  return level;
448
507
  }
@@ -458,9 +517,9 @@ export default class Group extends AbleObject<GroupData> {
458
517
  public async $getUsers(
459
518
  descendants = false,
460
519
  limit?: number,
461
- offset?: number
520
+ offset?: number,
462
521
  ): Promise<(User & UserData)[]> {
463
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
522
+ const tilmeld = enforceTilmeld(this);
464
523
  let groups: (Group & GroupData)[] = [];
465
524
  if (descendants) {
466
525
  groups = await this.$getDescendants();
@@ -489,7 +548,7 @@ export default class Group extends AbleObject<GroupData> {
489
548
  group,
490
549
  ]),
491
550
  ],
492
- }
551
+ },
493
552
  );
494
553
  }
495
554
 
@@ -502,15 +561,21 @@ export default class Group extends AbleObject<GroupData> {
502
561
  result: boolean;
503
562
  message: string;
504
563
  }> {
505
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
564
+ const tilmeld = enforceTilmeld(this);
506
565
  if (!tilmeld.config.emailUsernames) {
507
566
  if (this.$data.groupname == null || !this.$data.groupname.length) {
508
567
  return { result: false, message: 'Please specify a groupname.' };
509
568
  }
510
- if (
511
- tilmeld.config.maxUsernameLength > 0 &&
512
- this.$data.groupname.length > tilmeld.config.maxUsernameLength
513
- ) {
569
+ if (this.$data.groupname.length < tilmeld.config.minUsernameLength) {
570
+ return {
571
+ result: false,
572
+ message:
573
+ 'Groupnames must be at least ' +
574
+ tilmeld.config.minUsernameLength +
575
+ ' characters.',
576
+ };
577
+ }
578
+ if (this.$data.groupname.length > tilmeld.config.maxUsernameLength) {
514
579
  return {
515
580
  result: false,
516
581
  message:
@@ -522,7 +587,7 @@ export default class Group extends AbleObject<GroupData> {
522
587
  if (
523
588
  difference(
524
589
  this.$data.groupname.split(''),
525
- tilmeld.config.validChars.split('')
590
+ tilmeld.config.validChars.split(''),
526
591
  ).length
527
592
  ) {
528
593
  return {
@@ -549,7 +614,7 @@ export default class Group extends AbleObject<GroupData> {
549
614
  }
550
615
  const test = await this.$nymph.getEntity(
551
616
  { class: tilmeld.Group, skipAc: true },
552
- selector
617
+ selector,
553
618
  );
554
619
  if (test != null) {
555
620
  return { result: false, message: 'That groupname is taken.' };
@@ -565,10 +630,16 @@ export default class Group extends AbleObject<GroupData> {
565
630
  if (this.$data.groupname == null || !this.$data.groupname.length) {
566
631
  return { result: false, message: 'Please specify an email.' };
567
632
  }
568
- if (
569
- tilmeld.config.maxUsernameLength > 0 &&
570
- this.$data.groupname.length > tilmeld.config.maxUsernameLength
571
- ) {
633
+ if (this.$data.groupname.length < tilmeld.config.minUsernameLength) {
634
+ return {
635
+ result: false,
636
+ message:
637
+ 'Emails must be at least ' +
638
+ tilmeld.config.minUsernameLength +
639
+ ' characters.',
640
+ };
641
+ }
642
+ if (this.$data.groupname.length > tilmeld.config.maxUsernameLength) {
572
643
  return {
573
644
  result: false,
574
645
  message:
@@ -588,7 +659,7 @@ export default class Group extends AbleObject<GroupData> {
588
659
  * @returns An object with a boolean 'result' entry and a 'message' entry.
589
660
  */
590
661
  public async $checkEmail(): Promise<{ result: boolean; message: string }> {
591
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
662
+ const tilmeld = enforceTilmeld(this);
592
663
  if (this.$data.email === '') {
593
664
  return { result: true, message: '' };
594
665
  }
@@ -610,7 +681,7 @@ export default class Group extends AbleObject<GroupData> {
610
681
  }
611
682
  const test = await this.$nymph.getEntity(
612
683
  { class: tilmeld.Group, skipAc: true },
613
- selector
684
+ selector,
614
685
  );
615
686
  if (test != null) {
616
687
  return {
@@ -627,7 +698,7 @@ export default class Group extends AbleObject<GroupData> {
627
698
  }
628
699
 
629
700
  public async $save() {
630
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
701
+ const tilmeld = enforceTilmeld(this);
631
702
  if (this.$data.groupname == null || !this.$data.groupname.trim().length) {
632
703
  return false;
633
704
  }
@@ -643,16 +714,22 @@ export default class Group extends AbleObject<GroupData> {
643
714
  this.$data.enabled = !!this.$data.enabled;
644
715
 
645
716
  // Clear empty values.
646
- if (this.$data.name === '') {
717
+ if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
647
718
  delete this.$data.name;
648
719
  }
649
- if (this.$data.email === '') {
720
+ if (
721
+ this.$data.email === '' ||
722
+ !tilmeld.config.userFields.includes('email')
723
+ ) {
650
724
  delete this.$data.email;
651
725
  }
652
726
  if (this.$data.avatar === '') {
653
727
  delete this.$data.avatar;
654
728
  }
655
- if (this.$data.phone === '') {
729
+ if (
730
+ this.$data.phone === '' ||
731
+ !tilmeld.config.userFields.includes('phone')
732
+ ) {
656
733
  delete this.$data.phone;
657
734
  }
658
735
  if (this.$data.parent == null) {
@@ -671,10 +748,12 @@ export default class Group extends AbleObject<GroupData> {
671
748
  delete this.$data.unverifiedSecondary;
672
749
  }
673
750
 
674
- // Groups should never have 'system/admin' or 'tilmeld/admin' abilities.
751
+ // Groups should never have 'system/admin', 'tilmeld/admin', or
752
+ // 'tilmeld/switch' abilities.
675
753
  this.$data.abilities = difference(this.$data.abilities ?? [], [
676
754
  'system/admin',
677
755
  'tilmeld/admin',
756
+ 'tilmeld/switch',
678
757
  null,
679
758
  undefined,
680
759
  ]) as string[];
@@ -684,7 +763,10 @@ export default class Group extends AbleObject<GroupData> {
684
763
  if (!unCheck.result) {
685
764
  throw new BadUsernameError(unCheck.message);
686
765
  }
687
- if (!tilmeld.config.emailUsernames) {
766
+ if (
767
+ !tilmeld.config.emailUsernames &&
768
+ tilmeld.config.userFields.includes('email')
769
+ ) {
688
770
  const emCheck = await this.$checkEmail();
689
771
  if (!emCheck.result) {
690
772
  throw new BadEmailError(emCheck.message);
@@ -692,19 +774,21 @@ export default class Group extends AbleObject<GroupData> {
692
774
  }
693
775
 
694
776
  // Validate group parent. Make sure it's not a descendant of this group.
777
+ const parent = this.$data.parent;
778
+ await parent?.$wake();
695
779
  if (
696
- this.$data.parent &&
697
- (this.$data.parent.cdate == null ||
698
- this.$is(this.$data.parent) ||
699
- this.$data.parent.$isDescendant(this))
780
+ parent &&
781
+ (parent.cdate == null ||
782
+ this.$is(parent) ||
783
+ (await parent.$isDescendant(this)))
700
784
  ) {
701
785
  throw new BadDataError(
702
- "Group parent can't be itself or descendant of itself."
786
+ "Group parent can't be itself or descendant of itself.",
703
787
  );
704
788
  }
705
789
 
706
790
  try {
707
- tilmeld.config.validatorGroup(this);
791
+ tilmeld.config.validatorGroup(tilmeld, this);
708
792
  } catch (e: any) {
709
793
  throw new BadDataError(e?.message);
710
794
  }
@@ -713,14 +797,18 @@ export default class Group extends AbleObject<GroupData> {
713
797
  if (this.$data.defaultPrimary) {
714
798
  const currentPrimary = await this.$nymph.getEntity(
715
799
  { class: tilmeld.Group },
716
- { type: '&', truthy: 'defaultPrimary' }
800
+ { type: '&', truthy: 'defaultPrimary' },
717
801
  );
718
802
  if (currentPrimary != null && !this.$is(currentPrimary)) {
719
803
  currentPrimary.defaultPrimary = false;
720
- if (!(await currentPrimary.$save())) {
804
+ if (
805
+ this.$skipAcWhenSaving
806
+ ? !(await currentPrimary.$saveSkipAC())
807
+ : !(await currentPrimary.$save())
808
+ ) {
721
809
  throw new CouldNotChangeDefaultPrimaryGroupError(
722
810
  'Could not change new user primary group from ' +
723
- `${currentPrimary.groupname}.`
811
+ `${currentPrimary.groupname}.`,
724
812
  );
725
813
  }
726
814
  }
@@ -746,24 +834,28 @@ export default class Group extends AbleObject<GroupData> {
746
834
  }
747
835
 
748
836
  public async $delete() {
749
- let tilmeld = this.$nymph.tilmeld as Tilmeld;
750
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
837
+ let tilmeld = enforceTilmeld(this);
838
+ if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
751
839
  throw new BadDataError("You don't have the authority to delete groups.");
752
840
  }
753
841
 
754
842
  const transaction = 'tilmeld-delete-' + this.guid;
755
843
  const nymph = this.$nymph;
756
844
  const tnymph = await nymph.startTransaction(transaction);
757
- this.$nymph = tnymph;
758
- tilmeld = this.$nymph.tilmeld as Tilmeld;
845
+ this.$setNymph(tnymph);
846
+ tilmeld = enforceTilmeld(this);
759
847
 
760
848
  // Delete descendants.
761
849
  const descendants = await this.$getDescendants();
762
850
  if (descendants.length) {
763
851
  for (let curGroup of descendants) {
764
- if (!(await curGroup.$delete())) {
852
+ if (
853
+ this.$skipAcWhenDeleting
854
+ ? !(await curGroup.$deleteSkipAC())
855
+ : !(await curGroup.$delete())
856
+ ) {
765
857
  await tnymph.rollback(transaction);
766
- this.$nymph = nymph;
858
+ this.$setNymph(nymph);
767
859
  return false;
768
860
  }
769
861
  }
@@ -778,13 +870,17 @@ export default class Group extends AbleObject<GroupData> {
778
870
  {
779
871
  type: '&',
780
872
  ref: ['group', this],
781
- }
873
+ },
782
874
  );
783
875
  for (let user of primaryUsers) {
784
876
  delete user.group;
785
- if (!(await user.$save())) {
877
+ if (
878
+ this.$skipAcWhenDeleting
879
+ ? !(await user.$saveSkipAC())
880
+ : !(await user.$save())
881
+ ) {
786
882
  await tnymph.rollback(transaction);
787
- this.$nymph = nymph;
883
+ this.$setNymph(nymph);
788
884
  return false;
789
885
  }
790
886
  }
@@ -798,11 +894,15 @@ export default class Group extends AbleObject<GroupData> {
798
894
  {
799
895
  type: '&',
800
896
  ref: ['groups', this],
801
- }
897
+ },
802
898
  );
803
899
  for (let user of secondaryUsers) {
804
900
  user.$delGroup(this);
805
- if (!(await user.$save())) {
901
+ if (
902
+ this.$skipAcWhenDeleting
903
+ ? !(await user.$saveSkipAC())
904
+ : !(await user.$save())
905
+ ) {
806
906
  await tnymph.rollback(transaction);
807
907
  return false;
808
908
  }
@@ -815,7 +915,23 @@ export default class Group extends AbleObject<GroupData> {
815
915
  } else {
816
916
  await tnymph.rollback(transaction);
817
917
  }
818
- this.$nymph = nymph;
918
+ this.$setNymph(nymph);
819
919
  return success;
820
920
  }
921
+
922
+ /*
923
+ * This should *never* be accessible on the client.
924
+ */
925
+ async $deleteSkipAC() {
926
+ this.$skipAcWhenDeleting = true;
927
+ return await this.$delete();
928
+ }
929
+
930
+ $tilmeldDeleteSkipAC() {
931
+ if (this.$skipAcWhenDeleting) {
932
+ this.$skipAcWhenDeleting = false;
933
+ return true;
934
+ }
935
+ return false;
936
+ }
821
937
  }