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

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 +414 -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 +181 -15
  7. package/dist/Group.js +551 -192
  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 +241 -12
  12. package/dist/Tilmeld.js +636 -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 +395 -22
  17. package/dist/User.js +1316 -365
  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 +661 -226
  61. package/src/Tilmeld.ts +420 -254
  62. package/src/Tilmeld.types.ts +2 -2
  63. package/src/User.test.ts +163 -6
  64. package/src/User.ts +1557 -454
  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/src/Group.ts CHANGED
@@ -1,24 +1,74 @@
1
- import {
1
+ import crypto from 'node:crypto';
2
+ import type {
3
+ Nymph,
2
4
  EntityData,
3
- EntityInterface,
4
5
  EntityJson,
5
6
  EntityPatch,
6
7
  Options,
7
8
  Selector,
8
9
  SerializedEntityData,
9
10
  } from '@nymphjs/nymph';
10
- import md5 from 'crypto-js/md5';
11
- import { difference } from 'lodash';
11
+ import { nanoid } from '@nymphjs/guid';
12
+ import { difference, xor } from 'lodash-es';
13
+ import { splitn } from '@sciactive/splitn';
12
14
 
13
- import type Tilmeld from './Tilmeld';
14
- import AbleObject from './AbleObject';
15
+ import { enforceTilmeld } from './enforceTilmeld.js';
16
+ import AbleObject from './AbleObject.js';
15
17
  import {
18
+ AccessControlError,
16
19
  BadDataError,
17
20
  BadEmailError,
18
21
  BadUsernameError,
19
22
  CouldNotChangeDefaultPrimaryGroupError,
20
- } from './errors';
21
- import User, { UserData } from './User';
23
+ } from './errors/index.js';
24
+ import UserClass from './User.js';
25
+ import type { UserData } from './User.js';
26
+
27
+ export type GroupEventType =
28
+ | 'checkGroupname'
29
+ | 'beforeSave'
30
+ | 'afterSave'
31
+ | 'beforeDelete'
32
+ | 'afterDelete';
33
+ /**
34
+ * This is run when the user has entered an otherwise valid groupname into the
35
+ * signup form. It should return a result, which when false will stop the
36
+ * process and return the included message, disallowing the groupname.
37
+ */
38
+ export type TilmeldCheckGroupnameCallback = (
39
+ group: Group & GroupData,
40
+ data: { groupname: string },
41
+ ) => Promise<{ result: boolean; message?: string }>;
42
+ /**
43
+ * This is run just before the group is saved, so all checks have passed.
44
+ *
45
+ * The transaction is available in `group.$nymph`.
46
+ */
47
+ export type TilmeldBeforeGroupSaveCallback = (
48
+ group: Group & GroupData,
49
+ ) => Promise<void>;
50
+ /**
51
+ * This is run before the transaction is committed, and you can perform
52
+ * additional functions on the transaction, which is available in
53
+ * `group.$nymph`.
54
+ */
55
+ export type TilmeldAfterGroupSaveCallback = (
56
+ group: Group & GroupData,
57
+ ) => Promise<void>;
58
+ /**
59
+ * The transaction is available in `group.$nymph`.
60
+ */
61
+ export type TilmeldBeforeGroupDeleteCallback = (
62
+ group: Group & GroupData,
63
+ ) => Promise<void>;
64
+ /**
65
+ * This is run before the transaction is committed, and you can perform
66
+ * additional functions on the transaction, which is available in
67
+ * `group.$nymph`.
68
+ */
69
+ export type TilmeldAfterGroupDeleteCallback = (
70
+ group: Group & GroupData,
71
+ ) => Promise<void>;
22
72
 
23
73
  export type GroupData = {
24
74
  /**
@@ -52,7 +102,7 @@ export type GroupData = {
52
102
  /**
53
103
  * If generatePrimary is on, this will be the user who generated this group.
54
104
  */
55
- user?: (User & UserData) | null;
105
+ user?: (UserClass & UserData) | null;
56
106
 
57
107
  /**
58
108
  * Whether the group can be used.
@@ -85,9 +135,13 @@ export default class Group extends AbleObject<GroupData> {
85
135
  /**
86
136
  * The instance of Tilmeld to use for queries.
87
137
  */
88
- static tilmeld: Tilmeld;
89
138
  static ETYPE = 'tilmeld_group';
90
139
  static class = 'Group';
140
+ private static checkGroupnameCallbacks: TilmeldCheckGroupnameCallback[] = [];
141
+ private static beforeSaveCallbacks: TilmeldBeforeGroupSaveCallback[] = [];
142
+ private static afterSaveCallbacks: TilmeldAfterGroupSaveCallback[] = [];
143
+ private static beforeDeleteCallbacks: TilmeldBeforeGroupDeleteCallback[] = [];
144
+ private static afterDeleteCallbacks: TilmeldAfterGroupDeleteCallback[] = [];
91
145
 
92
146
  private static DEFAULT_PRIVATE_DATA = ['email', 'phone', 'abilities', 'user'];
93
147
  private static DEFAULT_ALLOWLIST_DATA: string[] = [];
@@ -112,16 +166,20 @@ export default class Group extends AbleObject<GroupData> {
112
166
  protected $allowlistTags?: string[] = [];
113
167
 
114
168
  /**
115
- * This is explicitly used only during the registration proccess.
169
+ * This should only be used by the backend.
116
170
  */
117
171
  private $skipAcWhenSaving = false;
118
-
119
- static async factory(guid?: string): Promise<Group & GroupData> {
120
- return (await super.factory(guid)) as Group & GroupData;
121
- }
172
+ /**
173
+ * This should only be used by the backend.
174
+ */
175
+ private $skipAcWhenDeleting = false;
176
+ /**
177
+ * Used to save the current groupname for domain admin permissions.
178
+ */
179
+ public $originalGroupname?: string;
122
180
 
123
181
  static async factoryGroupname(
124
- groupname?: string
182
+ groupname?: string,
125
183
  ): Promise<Group & GroupData> {
126
184
  const entity = new this();
127
185
  if (groupname != null) {
@@ -132,7 +190,7 @@ export default class Group extends AbleObject<GroupData> {
132
190
  {
133
191
  type: '&',
134
192
  ilike: ['groupname', groupname.replace(/([\\%_])/g, (s) => `\\${s}`)],
135
- }
193
+ },
136
194
  );
137
195
  if (entity != null) {
138
196
  return entity;
@@ -141,21 +199,6 @@ export default class Group extends AbleObject<GroupData> {
141
199
  return entity;
142
200
  }
143
201
 
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
202
  /**
160
203
  * Get all the groups that can be assigned as primary groups.
161
204
  *
@@ -165,16 +208,17 @@ export default class Group extends AbleObject<GroupData> {
165
208
  */
166
209
  public static async getPrimaryGroups(
167
210
  options?: Options,
168
- selectors?: Selector[]
211
+ selectors?: Selector[],
169
212
  ) {
170
- if (!this.tilmeld.gatekeeper('tilmeld/admin')) {
213
+ const tilmeld = enforceTilmeld(this);
214
+ if (!tilmeld.gatekeeper('tilmeld/admin')) {
171
215
  throw new Error("You don't have permission to do that.");
172
216
  }
173
217
 
174
218
  return await this.getAssignableGroups(
175
- this.tilmeld.config.highestPrimary,
176
- { ...options, class: this.tilmeld.Group, return: 'entity' },
177
- [...(selectors ?? [])]
219
+ tilmeld.config.highestPrimary,
220
+ { ...options, class: tilmeld.Group, return: 'entity' },
221
+ [...(selectors ?? [])],
178
222
  );
179
223
  }
180
224
 
@@ -187,24 +231,26 @@ export default class Group extends AbleObject<GroupData> {
187
231
  */
188
232
  public static async getSecondaryGroups(
189
233
  options?: Options,
190
- selectors?: Selector[]
234
+ selectors?: Selector[],
191
235
  ) {
192
- if (!this.tilmeld.gatekeeper('tilmeld/admin')) {
236
+ const tilmeld = enforceTilmeld(this);
237
+ if (!tilmeld.gatekeeper('tilmeld/admin')) {
193
238
  throw new Error("You don't have permission to do that.");
194
239
  }
195
240
 
196
241
  return await this.getAssignableGroups(
197
- this.tilmeld.config.highestSecondary,
198
- { ...options, class: this.tilmeld.Group, return: 'entity' },
199
- [...(selectors ?? [])]
242
+ tilmeld.config.highestSecondary,
243
+ { ...options, class: tilmeld.Group, return: 'entity' },
244
+ [...(selectors ?? [])],
200
245
  );
201
246
  }
202
247
 
203
248
  private static async getAssignableGroups(
204
249
  highestParent: string | boolean,
205
250
  options: Options<typeof Group>,
206
- selectors: Selector[]
251
+ selectors: Selector[],
207
252
  ) {
253
+ const tilmeld = enforceTilmeld(this);
208
254
  let assignableGroups: (Group & GroupData)[] = [];
209
255
 
210
256
  if (highestParent === false) {
@@ -212,24 +258,68 @@ export default class Group extends AbleObject<GroupData> {
212
258
  }
213
259
 
214
260
  assignableGroups = await this.nymph.getEntities(
215
- { ...options, class: this.tilmeld.Group },
216
- ...selectors
261
+ { ...options, class: tilmeld.Group },
262
+ ...selectors,
217
263
  );
218
264
  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
- });
265
+ assignableGroups = (
266
+ await Promise.all(
267
+ assignableGroups.map(async (group) => {
268
+ let curGroup = group;
269
+ let parent = curGroup.parent;
270
+ await parent?.$wake();
271
+ while (parent != null && parent?.cdate != null) {
272
+ if (parent?.guid === highestParent) {
273
+ return group;
274
+ }
275
+ curGroup = parent;
276
+ parent = curGroup.parent;
277
+ await parent?.$wake();
278
+ }
279
+ return null;
280
+ }),
281
+ )
282
+ ).filter((group) => group != null) as (Group & GroupData)[];
229
283
  }
230
284
  return assignableGroups;
231
285
  }
232
286
 
287
+ constructor() {
288
+ super();
289
+
290
+ // Defaults.
291
+ this.$data.enabled = true;
292
+ this.$data.abilities = [];
293
+ this.$updateDataProtection();
294
+ }
295
+
296
+ $setNymph(nymph: Nymph) {
297
+ this.$nymph = nymph;
298
+ if (!this.$asleep()) {
299
+ if (this.$data.user && this.$data.user.$nymph !== nymph) {
300
+ this.$data.user.$setNymph(nymph);
301
+ }
302
+ if (this.$data.parent && this.$data.parent.$nymph !== nymph) {
303
+ this.$data.parent.$setNymph(nymph);
304
+ }
305
+ }
306
+ }
307
+
308
+ async $getUniques(): Promise<string[]> {
309
+ const tilmeld = enforceTilmeld(this);
310
+ const uniques = [`u:${this.$data.groupname}`];
311
+ if (
312
+ !tilmeld.config.emailUsernames &&
313
+ tilmeld.config.userFields.includes('email')
314
+ ) {
315
+ uniques.push(`e:${this.$data.email}`);
316
+ }
317
+ if (this.$data.defaultPrimary) {
318
+ uniques.push(`o:default-primary`);
319
+ }
320
+ return uniques;
321
+ }
322
+
233
323
  public $getAvatar() {
234
324
  if (this.$data.avatar != null && this.$data.avatar !== '') {
235
325
  return this.$data.avatar;
@@ -239,49 +329,104 @@ export default class Group extends AbleObject<GroupData> {
239
329
  }
240
330
  return (
241
331
  'https://secure.gravatar.com/avatar/' +
242
- md5(this.$data.email.trim().toLowerCase()).toString() +
332
+ crypto
333
+ .createHash('sha256')
334
+ .update(this.$data.email.trim().toLowerCase())
335
+ .digest('hex')
336
+ .toLowerCase() +
243
337
  '?d=identicon&s=40'
244
338
  );
245
339
  }
246
340
 
247
341
  public $jsonAcceptData(input: EntityJson, allowConflict = false) {
248
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
249
- this.$referenceWake();
342
+ this.$check();
250
343
 
251
- 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')
256
- ) {
257
- throw new BadDataError(
258
- "You don't have the authority to make this group a system admin."
259
- );
344
+ if ('abilities' in input.data) {
345
+ this.$checkIncomingAbilities(input.data.abilities);
260
346
  }
261
347
 
262
348
  super.$jsonAcceptData(input, allowConflict);
263
349
  }
264
350
 
265
351
  public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
266
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
267
- this.$referenceWake();
352
+ this.$check();
353
+
354
+ if ('abilities' in patch.set) {
355
+ this.$checkIncomingAbilities(patch.set.abilities);
356
+ }
357
+
358
+ super.$jsonAcceptPatch(patch, allowConflict);
359
+ }
360
+
361
+ private $checkIncomingAbilities(incoming: any) {
362
+ const tilmeld = enforceTilmeld(this);
363
+
364
+ if (
365
+ !Array.isArray(incoming) ||
366
+ incoming.find((ability) => typeof ability !== 'string')
367
+ ) {
368
+ throw new BadDataError('Abilities must be an array of strings.');
369
+ }
268
370
 
269
371
  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')
372
+ incoming.includes('system/admin') !==
373
+ this.$data.abilities?.includes('system/admin') &&
374
+ (!tilmeld.currentUser ||
375
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
274
376
  ) {
275
377
  throw new BadDataError(
276
- "You don't have the authority to make this group a system admin."
378
+ "You don't have the authority to grant or revoke system/admin.",
277
379
  );
278
380
  }
279
381
 
280
- super.$jsonAcceptPatch(patch, allowConflict);
382
+ if (
383
+ incoming.includes('tilmeld/admin') !==
384
+ this.$data.abilities?.includes('tilmeld/admin') &&
385
+ (!tilmeld.currentUser ||
386
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
387
+ ) {
388
+ throw new BadDataError(
389
+ "You don't have the authority to grant or revoke tilmeld/admin.",
390
+ );
391
+ }
392
+
393
+ if (
394
+ incoming.includes('tilmeld/switch') !==
395
+ this.$data.abilities?.includes('tilmeld/switch') &&
396
+ (!tilmeld.currentUser ||
397
+ !tilmeld.currentUser.abilities?.includes('system/admin'))
398
+ ) {
399
+ throw new BadDataError(
400
+ "You don't have the authority to grant or revoke tilmeld/switch.",
401
+ );
402
+ }
403
+
404
+ if (
405
+ xor(
406
+ incoming.filter((ability: string) => ability.startsWith('tilmeld/')),
407
+ this.$data.abilities?.filter((ability: string) =>
408
+ ability.startsWith('tilmeld/'),
409
+ ) ?? [],
410
+ ).length &&
411
+ (!tilmeld.currentUser ||
412
+ (!tilmeld.currentUser.abilities?.includes('system/admin') &&
413
+ !tilmeld.currentUser.abilities?.includes('tilmeld/admin')))
414
+ ) {
415
+ throw new BadDataError(
416
+ "You don't have the authority to grant or revoke Tilmeld abilities.",
417
+ );
418
+ }
281
419
  }
282
420
 
283
- public $putData(data: EntityData, sdata?: SerializedEntityData) {
284
- super.$putData(data, sdata);
421
+ public $putData(
422
+ data: EntityData,
423
+ sdata?: SerializedEntityData,
424
+ source?: 'server',
425
+ ) {
426
+ super.$putData(data, sdata, source);
427
+ if (source === 'server') {
428
+ this.$originalGroupname = this.$data.groupname;
429
+ }
285
430
  this.$updateDataProtection();
286
431
  }
287
432
 
@@ -290,9 +435,16 @@ export default class Group extends AbleObject<GroupData> {
290
435
  *
291
436
  * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
292
437
  */
293
- public $updateDataProtection(givenUser?: User & UserData) {
294
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
438
+ public $updateDataProtection(givenUser?: UserClass & UserData) {
439
+ const tilmeld = enforceTilmeld(this);
295
440
  let user = givenUser ?? tilmeld.User.current();
441
+ let [_groupname, domain] = tilmeld.config.domainSupport
442
+ ? splitn(this.$data.groupname ?? '', '@', 2)
443
+ : [this.$data.groupname, null];
444
+ // Lowercase the domain part.
445
+ if (domain) {
446
+ domain = domain.toLowerCase();
447
+ }
296
448
 
297
449
  this.$privateData = [...Group.DEFAULT_PRIVATE_DATA];
298
450
  this.$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
@@ -303,8 +455,8 @@ export default class Group extends AbleObject<GroupData> {
303
455
 
304
456
  if (
305
457
  user != null &&
306
- ((user.abilities?.indexOf('tilmeld/admin') ?? -1) !== -1 ||
307
- (user.abilities?.indexOf('system/admin') ?? -1) !== -1)
458
+ (user.abilities?.includes('tilmeld/admin') ||
459
+ user.abilities?.includes('system/admin'))
308
460
  ) {
309
461
  // Users who can edit groups can see their data.
310
462
  this.$privateData = [];
@@ -312,10 +464,18 @@ export default class Group extends AbleObject<GroupData> {
312
464
  } else if (
313
465
  this.$data.user != null &&
314
466
  user != null &&
315
- this.$data.user.$is(user)
467
+ this.$data.user.guid === user.guid
316
468
  ) {
317
469
  // Users can see their group's data.
318
470
  this.$privateData = [];
471
+ } else if (
472
+ user != null &&
473
+ tilmeld.config.domainSupport &&
474
+ domain &&
475
+ user.abilities?.includes(`tilmeld/domain/${domain}/admin`)
476
+ ) {
477
+ // Domain admins can see their groups' data.
478
+ this.$privateData = [];
319
479
  }
320
480
  }
321
481
 
@@ -325,11 +485,13 @@ export default class Group extends AbleObject<GroupData> {
325
485
  * @param group The group, or the group's GUID.
326
486
  * @returns True or false.
327
487
  */
328
- public $isDescendant(givenGroup: (Group & GroupData) | string): boolean {
329
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
488
+ public async $isDescendant(
489
+ givenGroup: (Group & GroupData) | string,
490
+ ): Promise<boolean> {
491
+ const tilmeld = enforceTilmeld(this);
330
492
  let group: Group & GroupData;
331
493
  if (typeof givenGroup === 'string') {
332
- group = tilmeld.Group.factorySync(givenGroup);
494
+ group = await tilmeld.Group.factory(givenGroup);
333
495
  } else {
334
496
  group = givenGroup;
335
497
  }
@@ -337,13 +499,15 @@ export default class Group extends AbleObject<GroupData> {
337
499
  return false;
338
500
  }
339
501
  // Check to see if the group is a descendant of the given group.
340
- if (this.$data.parent == null) {
502
+ const parent = this.$data.parent;
503
+ await parent?.$wake();
504
+ if (parent == null) {
341
505
  return false;
342
506
  }
343
- if (this.$data.parent.$is(group)) {
507
+ if (parent.$is(group)) {
344
508
  return true;
345
509
  }
346
- if (this.$data.parent.$isDescendant(group)) {
510
+ if (await parent.$isDescendant(group)) {
347
511
  return true;
348
512
  }
349
513
  return false;
@@ -355,14 +519,14 @@ export default class Group extends AbleObject<GroupData> {
355
519
  * @returns An array of groups.
356
520
  */
357
521
  public async $getChildren() {
358
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
522
+ const tilmeld = enforceTilmeld(this);
359
523
  return await this.$nymph.getEntities(
360
524
  { class: tilmeld.Group },
361
525
  {
362
526
  type: '&',
363
527
  equal: ['enabled', true],
364
528
  ref: ['parent', this],
365
- }
529
+ },
366
530
  );
367
531
  }
368
532
 
@@ -373,9 +537,9 @@ export default class Group extends AbleObject<GroupData> {
373
537
  * @returns An array of groups.
374
538
  */
375
539
  public async $getDescendants(
376
- andSelf = false
540
+ andSelf = false,
377
541
  ): Promise<(Group & GroupData)[]> {
378
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
542
+ const tilmeld = enforceTilmeld(this);
379
543
  let groups: (Group & GroupData)[] = [];
380
544
  const entities = await this.$nymph.getEntities(
381
545
  { class: tilmeld.Group },
@@ -383,7 +547,7 @@ export default class Group extends AbleObject<GroupData> {
383
547
  type: '&',
384
548
  equal: ['enabled', true],
385
549
  ref: ['parent', this],
386
- }
550
+ },
387
551
  );
388
552
  for (let entity of entities) {
389
553
  groups = (await entity.$getDescendants(true)).concat(groups);
@@ -394,37 +558,6 @@ export default class Group extends AbleObject<GroupData> {
394
558
  return groups;
395
559
  }
396
560
 
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
561
  /**
429
562
  * Get the number of parents the group has.
430
563
  *
@@ -436,13 +569,17 @@ export default class Group extends AbleObject<GroupData> {
436
569
  *
437
570
  * @returns The level of the group.
438
571
  */
439
- public $getLevel() {
440
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
441
- let group = tilmeld.Group.factorySync(this.guid ?? undefined);
572
+ public async $getLevel() {
573
+ const tilmeld = enforceTilmeld(this);
574
+ let group = await tilmeld.Group.factory(this.guid ?? undefined);
575
+ let parent = group.parent;
576
+ await parent?.$wake();
442
577
  let level = 0;
443
- while (group.parent != null && group.parent.cdate != null && level < 1024) {
578
+ while (parent != null && parent.cdate != null && level < 1024) {
444
579
  level++;
445
- group = group.parent;
580
+ group = parent;
581
+ parent = group.parent;
582
+ await parent?.$wake();
446
583
  }
447
584
  return level;
448
585
  }
@@ -458,9 +595,9 @@ export default class Group extends AbleObject<GroupData> {
458
595
  public async $getUsers(
459
596
  descendants = false,
460
597
  limit?: number,
461
- offset?: number
462
- ): Promise<(User & UserData)[]> {
463
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
598
+ offset?: number,
599
+ ): Promise<(UserClass & UserData)[]> {
600
+ const tilmeld = enforceTilmeld(this);
464
601
  let groups: (Group & GroupData)[] = [];
465
602
  if (descendants) {
466
603
  groups = await this.$getDescendants();
@@ -489,7 +626,7 @@ export default class Group extends AbleObject<GroupData> {
489
626
  group,
490
627
  ]),
491
628
  ],
492
- }
629
+ },
493
630
  );
494
631
  }
495
632
 
@@ -502,15 +639,36 @@ export default class Group extends AbleObject<GroupData> {
502
639
  result: boolean;
503
640
  message: string;
504
641
  }> {
505
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
642
+ const tilmeld = enforceTilmeld(this);
506
643
  if (!tilmeld.config.emailUsernames) {
507
- if (this.$data.groupname == null || !this.$data.groupname.length) {
508
- return { result: false, message: 'Please specify a groupname.' };
644
+ let [groupname, domain] = tilmeld.config.domainSupport
645
+ ? splitn(this.$data.groupname ?? '', '@', 2)
646
+ : [this.$data.groupname, null];
647
+
648
+ // Lowercase the domain part.
649
+ if (domain) {
650
+ domain = domain.toLowerCase();
651
+ this.$data.groupname = `${groupname}@${domain}`;
509
652
  }
653
+
510
654
  if (
511
- tilmeld.config.maxUsernameLength > 0 &&
512
- this.$data.groupname.length > tilmeld.config.maxUsernameLength
655
+ this.$data.groupname == null ||
656
+ !this.$data.groupname.length ||
657
+ groupname == null ||
658
+ !groupname.length
513
659
  ) {
660
+ return { result: false, message: 'Please specify a groupname.' };
661
+ }
662
+ if (groupname.length < tilmeld.config.minUsernameLength) {
663
+ return {
664
+ result: false,
665
+ message:
666
+ 'Groupnames must be at least ' +
667
+ tilmeld.config.minUsernameLength +
668
+ ' characters.',
669
+ };
670
+ }
671
+ if (groupname.length > tilmeld.config.maxUsernameLength) {
514
672
  return {
515
673
  result: false,
516
674
  message:
@@ -520,22 +678,38 @@ export default class Group extends AbleObject<GroupData> {
520
678
  };
521
679
  }
522
680
  if (
523
- difference(
524
- this.$data.groupname.split(''),
525
- tilmeld.config.validChars.split('')
526
- ).length
681
+ difference(groupname.split(''), tilmeld.config.validChars.split(''))
682
+ .length
527
683
  ) {
528
684
  return {
529
685
  result: false,
530
686
  message: tilmeld.config.validCharsNotice,
531
687
  };
532
688
  }
533
- if (!tilmeld.config.validRegex.test(this.$data.groupname)) {
689
+ if (!tilmeld.config.validRegex.test(groupname)) {
534
690
  return {
535
691
  result: false,
536
692
  message: tilmeld.config.validRegexNotice,
537
693
  };
538
694
  }
695
+ if (domain != null) {
696
+ if (!tilmeld.config.validDomainRegex.test(domain)) {
697
+ return {
698
+ result: false,
699
+ message: tilmeld.config.validDomainRegexNotice,
700
+ };
701
+ }
702
+ if (
703
+ !this.$is(tilmeld.currentUser?.group) &&
704
+ !tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
705
+ ) {
706
+ return {
707
+ result: false,
708
+ message:
709
+ "You don't have the authority to manage groups on this domain.",
710
+ };
711
+ }
712
+ }
539
713
 
540
714
  const selector: Selector = {
541
715
  type: '&',
@@ -549,12 +723,24 @@ export default class Group extends AbleObject<GroupData> {
549
723
  }
550
724
  const test = await this.$nymph.getEntity(
551
725
  { class: tilmeld.Group, skipAc: true },
552
- selector
726
+ selector,
553
727
  );
554
728
  if (test != null) {
555
729
  return { result: false, message: 'That groupname is taken.' };
556
730
  }
557
731
 
732
+ for (let callback of (this.constructor as typeof Group)
733
+ .checkGroupnameCallbacks) {
734
+ if (callback) {
735
+ const result = await callback(this, {
736
+ groupname: this.$data.groupname,
737
+ });
738
+ if (!result.result) {
739
+ return { message: 'Groupname is not available.', ...result };
740
+ }
741
+ }
742
+ }
743
+
558
744
  return {
559
745
  result: true,
560
746
  message:
@@ -565,10 +751,16 @@ export default class Group extends AbleObject<GroupData> {
565
751
  if (this.$data.groupname == null || !this.$data.groupname.length) {
566
752
  return { result: false, message: 'Please specify an email.' };
567
753
  }
568
- if (
569
- tilmeld.config.maxUsernameLength > 0 &&
570
- this.$data.groupname.length > tilmeld.config.maxUsernameLength
571
- ) {
754
+ if (this.$data.groupname.length < tilmeld.config.minUsernameLength) {
755
+ return {
756
+ result: false,
757
+ message:
758
+ 'Emails must be at least ' +
759
+ tilmeld.config.minUsernameLength +
760
+ ' characters.',
761
+ };
762
+ }
763
+ if (this.$data.groupname.length > tilmeld.config.maxUsernameLength) {
572
764
  return {
573
765
  result: false,
574
766
  message:
@@ -588,7 +780,7 @@ export default class Group extends AbleObject<GroupData> {
588
780
  * @returns An object with a boolean 'result' entry and a 'message' entry.
589
781
  */
590
782
  public async $checkEmail(): Promise<{ result: boolean; message: string }> {
591
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
783
+ const tilmeld = enforceTilmeld(this);
592
784
  if (this.$data.email === '') {
593
785
  return { result: true, message: '' };
594
786
  }
@@ -610,7 +802,7 @@ export default class Group extends AbleObject<GroupData> {
610
802
  }
611
803
  const test = await this.$nymph.getEntity(
612
804
  { class: tilmeld.Group, skipAc: true },
613
- selector
805
+ selector,
614
806
  );
615
807
  if (test != null) {
616
808
  return {
@@ -627,11 +819,64 @@ export default class Group extends AbleObject<GroupData> {
627
819
  }
628
820
 
629
821
  public async $save() {
630
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
822
+ let tilmeld = enforceTilmeld(this);
631
823
  if (this.$data.groupname == null || !this.$data.groupname.trim().length) {
632
824
  return false;
633
825
  }
634
826
 
827
+ // Lowercase the domain part.
828
+ if (tilmeld.config.domainSupport) {
829
+ const [groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
830
+ if (domain) {
831
+ this.$data.groupname = `${groupname}@${domain.toLowerCase()}`;
832
+ }
833
+ }
834
+
835
+ if (tilmeld.config.domainSupport) {
836
+ const [_groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
837
+ const [_originalGroupname, originalDomain] = splitn(
838
+ this.$originalGroupname ?? '',
839
+ '@',
840
+ 2,
841
+ );
842
+
843
+ if (
844
+ this.guid != null &&
845
+ ((originalDomain == null && domain != null) ||
846
+ (domain == null && originalDomain != null))
847
+ ) {
848
+ throw new AccessControlError(
849
+ "Groups can't switch to or from having a domain once created.",
850
+ );
851
+ }
852
+
853
+ if (
854
+ !this.$skipAcWhenSaving &&
855
+ !tilmeld.gatekeeper('tilmeld/admin') &&
856
+ !tilmeld.gatekeeper('system/admin')
857
+ ) {
858
+ if (
859
+ domain != null &&
860
+ !this.$is(tilmeld.currentUser) &&
861
+ !tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
862
+ ) {
863
+ throw new AccessControlError(
864
+ "You don't have the authority to modify groups on this domain.",
865
+ );
866
+ }
867
+ if (
868
+ originalDomain != null &&
869
+ originalDomain !== domain &&
870
+ (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
871
+ !tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))
872
+ ) {
873
+ throw new AccessControlError(
874
+ "You don't have the authority to modify groups on this domain.",
875
+ );
876
+ }
877
+ }
878
+ }
879
+
635
880
  // Formatting.
636
881
  this.$data.groupname = this.$data.groupname.trim();
637
882
  if (tilmeld.config.emailUsernames) {
@@ -643,16 +888,22 @@ export default class Group extends AbleObject<GroupData> {
643
888
  this.$data.enabled = !!this.$data.enabled;
644
889
 
645
890
  // Clear empty values.
646
- if (this.$data.name === '') {
891
+ if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
647
892
  delete this.$data.name;
648
893
  }
649
- if (this.$data.email === '') {
894
+ if (
895
+ this.$data.email === '' ||
896
+ !tilmeld.config.userFields.includes('email')
897
+ ) {
650
898
  delete this.$data.email;
651
899
  }
652
900
  if (this.$data.avatar === '') {
653
901
  delete this.$data.avatar;
654
902
  }
655
- if (this.$data.phone === '') {
903
+ if (
904
+ this.$data.phone === '' ||
905
+ !tilmeld.config.userFields.includes('phone')
906
+ ) {
656
907
  delete this.$data.phone;
657
908
  }
658
909
  if (this.$data.parent == null) {
@@ -671,20 +922,33 @@ export default class Group extends AbleObject<GroupData> {
671
922
  delete this.$data.unverifiedSecondary;
672
923
  }
673
924
 
674
- // Groups should never have 'system/admin' or 'tilmeld/admin' abilities.
675
- this.$data.abilities = difference(this.$data.abilities ?? [], [
676
- 'system/admin',
677
- 'tilmeld/admin',
678
- null,
679
- undefined,
680
- ]) as string[];
925
+ // Groups should never have 'system/admin', 'tilmeld/admin',
926
+ // 'tilmeld/switch', or domain admin abilities.
927
+ this.$data.abilities = (this.$data.abilities ?? []).filter((ability) => {
928
+ if (
929
+ ['system/admin', 'tilmeld/admin', 'tilmeld/switch'].indexOf(ability) !==
930
+ -1
931
+ ) {
932
+ return false;
933
+ }
934
+ if (ability == null) {
935
+ return false;
936
+ }
937
+ if (ability.startsWith('tilmeld/domain/') && ability.endsWith('/admin')) {
938
+ return false;
939
+ }
940
+ return true;
941
+ }) as string[];
681
942
 
682
943
  // Verification.
683
944
  const unCheck = await this.$checkGroupname();
684
945
  if (!unCheck.result) {
685
946
  throw new BadUsernameError(unCheck.message);
686
947
  }
687
- if (!tilmeld.config.emailUsernames) {
948
+ if (
949
+ !tilmeld.config.emailUsernames &&
950
+ tilmeld.config.userFields.includes('email')
951
+ ) {
688
952
  const emCheck = await this.$checkEmail();
689
953
  if (!emCheck.result) {
690
954
  throw new BadEmailError(emCheck.message);
@@ -692,19 +956,21 @@ export default class Group extends AbleObject<GroupData> {
692
956
  }
693
957
 
694
958
  // Validate group parent. Make sure it's not a descendant of this group.
959
+ const parent = this.$data.parent;
960
+ await parent?.$wake();
695
961
  if (
696
- this.$data.parent &&
697
- (this.$data.parent.cdate == null ||
698
- this.$is(this.$data.parent) ||
699
- this.$data.parent.$isDescendant(this))
962
+ parent &&
963
+ (parent.cdate == null ||
964
+ this.$is(parent) ||
965
+ (await parent.$isDescendant(this)))
700
966
  ) {
701
967
  throw new BadDataError(
702
- "Group parent can't be itself or descendant of itself."
968
+ "Group parent can't be itself or descendant of itself.",
703
969
  );
704
970
  }
705
971
 
706
972
  try {
707
- tilmeld.config.validatorGroup(this);
973
+ tilmeld.config.validatorGroup(tilmeld, this);
708
974
  } catch (e: any) {
709
975
  throw new BadDataError(e?.message);
710
976
  }
@@ -713,20 +979,62 @@ export default class Group extends AbleObject<GroupData> {
713
979
  if (this.$data.defaultPrimary) {
714
980
  const currentPrimary = await this.$nymph.getEntity(
715
981
  { class: tilmeld.Group },
716
- { type: '&', truthy: 'defaultPrimary' }
982
+ { type: '&', truthy: 'defaultPrimary' },
717
983
  );
718
984
  if (currentPrimary != null && !this.$is(currentPrimary)) {
719
985
  currentPrimary.defaultPrimary = false;
720
- if (!(await currentPrimary.$save())) {
986
+ if (
987
+ this.$skipAcWhenSaving
988
+ ? !(await currentPrimary.$saveSkipAC())
989
+ : !(await currentPrimary.$save())
990
+ ) {
721
991
  throw new CouldNotChangeDefaultPrimaryGroupError(
722
992
  'Could not change new user primary group from ' +
723
- `${currentPrimary.groupname}.`
993
+ `${currentPrimary.groupname}.`,
724
994
  );
725
995
  }
726
996
  }
727
997
  }
728
998
 
729
- return await super.$save();
999
+ const transaction = 'tilmeld-save-group-' + (this.guid || nanoid());
1000
+ const nymph = this.$nymph;
1001
+ const tnymph = await nymph.startTransaction(transaction);
1002
+ this.$setNymph(tnymph);
1003
+ tilmeld = enforceTilmeld(this);
1004
+
1005
+ let preGuid = this.guid;
1006
+ let preCdate = this.cdate;
1007
+ let preMdate = this.mdate;
1008
+
1009
+ try {
1010
+ for (let callback of (this.constructor as typeof Group)
1011
+ .beforeSaveCallbacks) {
1012
+ if (callback) {
1013
+ await callback(this);
1014
+ }
1015
+ }
1016
+
1017
+ let ret = await super.$save();
1018
+ if (ret) {
1019
+ this.$originalGroupname = this.$data.groupname;
1020
+ }
1021
+ for (let callback of (this.constructor as typeof Group)
1022
+ .afterSaveCallbacks) {
1023
+ if (callback) {
1024
+ await callback(this);
1025
+ }
1026
+ }
1027
+ ret = await tnymph.commit(transaction);
1028
+ this.$setNymph(nymph);
1029
+ return ret;
1030
+ } catch (e: any) {
1031
+ await tnymph.rollback(transaction);
1032
+ this.guid = preGuid;
1033
+ this.cdate = preCdate;
1034
+ this.mdate = preMdate;
1035
+ this.$setNymph(nymph);
1036
+ throw e;
1037
+ }
730
1038
  }
731
1039
 
732
1040
  /*
@@ -746,76 +1054,203 @@ export default class Group extends AbleObject<GroupData> {
746
1054
  }
747
1055
 
748
1056
  public async $delete() {
749
- let tilmeld = this.$nymph.tilmeld as Tilmeld;
750
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
751
- throw new BadDataError("You don't have the authority to delete groups.");
1057
+ let tilmeld = enforceTilmeld(this);
1058
+ if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
1059
+ throw new AccessControlError(
1060
+ "You don't have the authority to delete groups.",
1061
+ );
1062
+ }
1063
+ if (this.$data.groupname !== this.$originalGroupname) {
1064
+ throw new AccessControlError(
1065
+ "You can't delete a group while switching its groupname.",
1066
+ );
752
1067
  }
753
1068
 
754
- const transaction = 'tilmeld-delete-' + this.guid;
1069
+ const transaction = 'tilmeld-delete-group-' + this.guid;
755
1070
  const nymph = this.$nymph;
756
1071
  const tnymph = await nymph.startTransaction(transaction);
757
- this.$nymph = tnymph;
758
- tilmeld = this.$nymph.tilmeld as Tilmeld;
759
-
760
- // Delete descendants.
761
- const descendants = await this.$getDescendants();
762
- if (descendants.length) {
763
- for (let curGroup of descendants) {
764
- if (!(await curGroup.$delete())) {
1072
+ this.$setNymph(tnymph);
1073
+ tilmeld = enforceTilmeld(this);
1074
+ const User = tnymph.getEntityClass(UserClass);
1075
+
1076
+ try {
1077
+ for (let callback of (this.constructor as typeof Group)
1078
+ .beforeDeleteCallbacks) {
1079
+ if (callback) {
1080
+ await callback(this);
1081
+ }
1082
+ }
1083
+
1084
+ // Delete descendants.
1085
+ const descendants = await this.$getDescendants();
1086
+ if (descendants.length) {
1087
+ for (let curGroup of descendants) {
1088
+ if (
1089
+ this.$skipAcWhenDeleting
1090
+ ? !(await curGroup.$deleteSkipAC())
1091
+ : !(await curGroup.$delete())
1092
+ ) {
1093
+ await tnymph.rollback(transaction);
1094
+ this.$setNymph(nymph);
1095
+ return false;
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ // Remove users from this primary group.
1101
+ const primaryUsers = await tnymph.getEntities(
1102
+ {
1103
+ class: User,
1104
+ skipAc: true,
1105
+ },
1106
+ {
1107
+ type: '&',
1108
+ ref: ['group', this],
1109
+ },
1110
+ );
1111
+ for (let user of primaryUsers) {
1112
+ delete user.group;
1113
+ if (
1114
+ this.$skipAcWhenDeleting
1115
+ ? !(await user.$saveSkipAC())
1116
+ : !(await user.$save())
1117
+ ) {
765
1118
  await tnymph.rollback(transaction);
766
- this.$nymph = nymph;
1119
+ this.$setNymph(nymph);
767
1120
  return false;
768
1121
  }
769
1122
  }
770
- }
771
1123
 
772
- // Remove users from this primary group.
773
- const primaryUsers = await tnymph.getEntities(
774
- {
775
- class: tilmeld.User,
776
- skipAc: true,
777
- },
778
- {
779
- type: '&',
780
- ref: ['group', this],
1124
+ // Remove users from this secondary group.
1125
+ const secondaryUsers = await tnymph.getEntities(
1126
+ {
1127
+ class: User,
1128
+ skipAc: true,
1129
+ },
1130
+ {
1131
+ type: '&',
1132
+ ref: ['groups', this],
1133
+ },
1134
+ );
1135
+ for (let user of secondaryUsers) {
1136
+ user.$delGroup(this);
1137
+ if (
1138
+ this.$skipAcWhenDeleting
1139
+ ? !(await user.$saveSkipAC())
1140
+ : !(await user.$save())
1141
+ ) {
1142
+ await tnymph.rollback(transaction);
1143
+ this.$setNymph(nymph);
1144
+ return false;
1145
+ }
781
1146
  }
782
- );
783
- for (let user of primaryUsers) {
784
- delete user.group;
785
- if (!(await user.$save())) {
1147
+
1148
+ // Delete the group.
1149
+ let success = await super.$delete();
1150
+ if (success) {
1151
+ for (let callback of (this.constructor as typeof Group)
1152
+ .afterDeleteCallbacks) {
1153
+ if (callback) {
1154
+ await callback(this);
1155
+ }
1156
+ }
1157
+
1158
+ success = await tnymph.commit(transaction);
1159
+ } else {
786
1160
  await tnymph.rollback(transaction);
787
- this.$nymph = nymph;
788
- return false;
789
1161
  }
1162
+ this.$setNymph(nymph);
1163
+ return success;
1164
+ } catch (e: any) {
1165
+ await tnymph.rollback(transaction);
1166
+ this.$setNymph(nymph);
1167
+ throw e;
790
1168
  }
1169
+ }
791
1170
 
792
- // Remove users from this secondary group.
793
- const secondaryUsers = await tnymph.getEntities(
794
- {
795
- class: tilmeld.User,
796
- skipAc: true,
797
- },
798
- {
799
- type: '&',
800
- ref: ['groups', this],
801
- }
802
- );
803
- for (let user of secondaryUsers) {
804
- user.$delGroup(this);
805
- if (!(await user.$save())) {
806
- await tnymph.rollback(transaction);
807
- return false;
808
- }
1171
+ /*
1172
+ * This should *never* be accessible on the client.
1173
+ */
1174
+ async $deleteSkipAC() {
1175
+ this.$skipAcWhenDeleting = true;
1176
+ return await this.$delete();
1177
+ }
1178
+
1179
+ $tilmeldDeleteSkipAC() {
1180
+ if (this.$skipAcWhenDeleting) {
1181
+ this.$skipAcWhenDeleting = false;
1182
+ return true;
809
1183
  }
1184
+ return false;
1185
+ }
810
1186
 
811
- // Delete the group.
812
- const success = await super.$delete();
813
- if (success) {
814
- await tnymph.commit(transaction);
815
- } else {
816
- await tnymph.rollback(transaction);
1187
+ public static on<T extends GroupEventType>(
1188
+ event: T,
1189
+ callback: T extends 'checkGroupname'
1190
+ ? TilmeldCheckGroupnameCallback
1191
+ : T extends 'beforeSave'
1192
+ ? TilmeldBeforeGroupSaveCallback
1193
+ : T extends 'afterSave'
1194
+ ? TilmeldAfterGroupSaveCallback
1195
+ : T extends 'beforeDelete'
1196
+ ? TilmeldBeforeGroupDeleteCallback
1197
+ : T extends 'afterDelete'
1198
+ ? TilmeldAfterGroupDeleteCallback
1199
+ : never,
1200
+ ) {
1201
+ const prop = (event + 'Callbacks') as T extends 'checkGroupname'
1202
+ ? 'checkGroupnameCallbacks'
1203
+ : T extends 'beforeSave'
1204
+ ? 'beforeSaveCallbacks'
1205
+ : T extends 'afterSave'
1206
+ ? 'afterSaveCallbacks'
1207
+ : T extends 'beforeDelete'
1208
+ ? 'beforeDeleteCallbacks'
1209
+ : T extends 'afterDelete'
1210
+ ? 'afterDeleteCallbacks'
1211
+ : never;
1212
+ if (!(prop in this)) {
1213
+ throw new Error('Invalid event type.');
817
1214
  }
818
- this.$nymph = nymph;
819
- return success;
1215
+ // @ts-ignore: The callback should always be the right type here.
1216
+ this[prop].push(callback);
1217
+ return () => this.off(event, callback);
1218
+ }
1219
+
1220
+ public static off<T extends GroupEventType>(
1221
+ event: T,
1222
+ callback: T extends 'checkGroupname'
1223
+ ? TilmeldCheckGroupnameCallback
1224
+ : T extends 'beforeSave'
1225
+ ? TilmeldBeforeGroupSaveCallback
1226
+ : T extends 'afterSave'
1227
+ ? TilmeldAfterGroupSaveCallback
1228
+ : T extends 'beforeDelete'
1229
+ ? TilmeldBeforeGroupDeleteCallback
1230
+ : T extends 'afterDelete'
1231
+ ? TilmeldAfterGroupDeleteCallback
1232
+ : never,
1233
+ ) {
1234
+ const prop = (event + 'Callbacks') as T extends 'checkGroupname'
1235
+ ? 'checkGroupnameCallbacks'
1236
+ : T extends 'beforeSave'
1237
+ ? 'beforeSaveCallbacks'
1238
+ : T extends 'afterSave'
1239
+ ? 'afterSaveCallbacks'
1240
+ : T extends 'beforeDelete'
1241
+ ? 'beforeDeleteCallbacks'
1242
+ : T extends 'afterDelete'
1243
+ ? 'afterDeleteCallbacks'
1244
+ : never;
1245
+ if (!(prop in this)) {
1246
+ return false;
1247
+ }
1248
+ // @ts-ignore: The callback should always be the right type here.
1249
+ const i = this[prop].indexOf(callback);
1250
+ if (i > -1) {
1251
+ // @ts-ignore: The callback should always be the right type here.
1252
+ this[prop].splice(i, 1);
1253
+ }
1254
+ return true;
820
1255
  }
821
1256
  }