@nymphjs/tilmeld 1.0.0-alpha.8 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Tilmeld.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  TilmeldAccessLevels,
8
8
  } from '@nymphjs/nymph';
9
9
  import { Request, Response } from 'express';
10
+ import { xor } from 'lodash';
10
11
 
11
12
  import { Config, ConfigDefaults as defaults } from './conf';
12
13
  import { AccessControlError } from './errors';
@@ -201,7 +202,7 @@ export default class Tilmeld implements TilmeldInterface {
201
202
  return;
202
203
  }
203
204
  // Test for permissions.
204
- if (tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)) {
205
+ if (!tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)) {
205
206
  throw new AccessControlError('No permission to delete.');
206
207
  }
207
208
  };
@@ -245,15 +246,15 @@ export default class Tilmeld implements TilmeldInterface {
245
246
  // allowing a change to ac properties.
246
247
 
247
248
  const originalAc = entity.$getOriginalAcValues();
248
- const newAc = {
249
- user: entity.user ?? null,
250
- group: entity.group ?? null,
251
- acUser: entity.acUser ?? null,
252
- acGroup: entity.acGroup ?? null,
253
- acOther: entity.acOther ?? null,
254
- acRead: entity.acRead ?? null,
255
- acWrite: entity.acWrite ?? null,
256
- acFull: entity.acFull ?? null,
249
+ const newAc: AccessControlData = {
250
+ user: entity.user ?? undefined,
251
+ group: entity.group ?? undefined,
252
+ acUser: entity.acUser ?? undefined,
253
+ acGroup: entity.acGroup ?? undefined,
254
+ acOther: entity.acOther ?? undefined,
255
+ acRead: entity.acRead ?? undefined,
256
+ acWrite: entity.acWrite ?? undefined,
257
+ acFull: entity.acFull ?? undefined,
257
258
  };
258
259
 
259
260
  const setAcProperties = (acValues: { [k: string]: any }) => {
@@ -269,10 +270,102 @@ export default class Tilmeld implements TilmeldInterface {
269
270
 
270
271
  // Restore original AC properties and check permissions.
271
272
  setAcProperties(originalAc);
272
- if (tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)) {
273
- // Only allow changes to AC properties if the user has full access.
274
- // TODO: only allow changes to `user` and `group` if tilmeld admin or
275
- // group is user's group.
273
+
274
+ function areAcPropertiesChanged(
275
+ a: AccessControlData,
276
+ b: AccessControlData
277
+ ) {
278
+ for (const name of ['user', 'group'] as ('user' | 'group')[]) {
279
+ const aVal = a[name];
280
+ const bVal = b[name];
281
+ if (aVal == null && bVal == null) {
282
+ continue;
283
+ } else if (aVal == null || bVal == null) {
284
+ return true;
285
+ }
286
+ if (!aVal.$is(bVal)) {
287
+ return true;
288
+ }
289
+ }
290
+ for (const name of ['acUser', 'acGroup', 'acOther'] as (
291
+ | 'acUser'
292
+ | 'acGroup'
293
+ | 'acOther'
294
+ )[]) {
295
+ const aVal = a[name];
296
+ const bVal = b[name];
297
+ if (aVal == null && bVal == null) {
298
+ continue;
299
+ } else if (aVal == null || bVal == null) {
300
+ return true;
301
+ }
302
+ if (aVal !== bVal) {
303
+ return true;
304
+ }
305
+ }
306
+ for (const name of ['acRead', 'acWrite', 'acFull'] as (
307
+ | 'acRead'
308
+ | 'acWrite'
309
+ | 'acFull'
310
+ )[]) {
311
+ const aVal = a[name];
312
+ const bVal = b[name];
313
+ if (aVal == null && bVal == null) {
314
+ continue;
315
+ } else if (aVal == null || bVal == null) {
316
+ return true;
317
+ }
318
+ if (aVal.length !== bVal.length) {
319
+ return true;
320
+ }
321
+ if (
322
+ xor(
323
+ aVal.map((entity) => entity.guid),
324
+ bVal.map((entity) => entity.guid)
325
+ ).length
326
+ ) {
327
+ return true;
328
+ }
329
+ }
330
+ return false;
331
+ }
332
+
333
+ if (areAcPropertiesChanged(originalAc, newAc)) {
334
+ if (
335
+ !tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)
336
+ ) {
337
+ // Only allow changes to AC properties if the user has full access.
338
+ throw new AccessControlError(
339
+ 'No permission to change access control properties.'
340
+ );
341
+ }
342
+
343
+ if (
344
+ ((originalAc.user && !originalAc.user.$is(newAc.user)) ||
345
+ (!originalAc.user &&
346
+ newAc.user &&
347
+ !newAc.user.$is(tilmeld.currentUser))) &&
348
+ !tilmeld.gatekeeper('tilmeld/admin')
349
+ ) {
350
+ throw new AccessControlError(
351
+ 'No permission to assign to another user.'
352
+ );
353
+ }
354
+
355
+ if (
356
+ newAc.group &&
357
+ !newAc.group.$is(originalAc.group) &&
358
+ !(
359
+ newAc.group.$is(tilmeld.currentUser?.group) ||
360
+ newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
361
+ ) &&
362
+ !tilmeld.gatekeeper('tilmeld/admin')
363
+ ) {
364
+ throw new AccessControlError(
365
+ 'No permission to assign to another group.'
366
+ );
367
+ }
368
+
276
369
  setAcProperties(newAc);
277
370
  }
278
371
  }
@@ -455,7 +548,7 @@ export default class Tilmeld implements TilmeldInterface {
455
548
  return;
456
549
  }
457
550
 
458
- if (user === null) {
551
+ if (user == null || user.guid == null) {
459
552
  selectors.push({
460
553
  type: '|',
461
554
  // Other access control is sufficient.
@@ -464,7 +557,7 @@ export default class Tilmeld implements TilmeldInterface {
464
557
  selector: [
465
558
  {
466
559
  type: '&',
467
- '!defined': ['user', 'group'],
560
+ '!defined': [['user', 'group']],
468
561
  },
469
562
  ],
470
563
  });
@@ -472,56 +565,43 @@ export default class Tilmeld implements TilmeldInterface {
472
565
  const subSelectors: FormattedSelector[] = [
473
566
  {
474
567
  type: '&',
475
- '!defined': ['user', 'group'],
568
+ '!defined': [['user', 'group']],
476
569
  },
477
570
  // It is owned by the user.
478
571
  {
479
572
  type: '&',
480
- ref: [['user', user]],
573
+ ref: [['user', user.guid]],
481
574
  gte: [['acUser', TilmeldAccessLevels.READ_ACCESS]],
482
575
  },
483
576
  ];
484
- const selector: FormattedSelector = {
485
- type: '|',
486
- // Other access control is sufficient.
487
- gte: [['acOther', TilmeldAccessLevels.READ_ACCESS]],
488
- // The user and group are not set.
489
- selector: subSelectors,
490
- // The user is listed in acRead, acWrite, or acFull.
491
- ref: [
492
- ['acRead', user],
493
- ['acWrite', user],
494
- ['acFull', user],
495
- ],
496
- };
497
577
  const groupRefs: FormattedSelector['ref'] = [];
498
578
  const acRefs: FormattedSelector['ref'] = [];
499
579
  if (user.group != null && user.group.guid != null) {
500
580
  // It belongs to the user's primary group.
501
- groupRefs.push(['group', user.group]);
581
+ groupRefs.push(['group', user.group.guid]);
502
582
  // User's primary group is listed in acRead, acWrite, or acFull.
503
- acRefs.push(['acRead', user.group]);
504
- acRefs.push(['acWrite', user.group]);
505
- acRefs.push(['acFull', user.group]);
583
+ acRefs.push(['acRead', user.group.guid]);
584
+ acRefs.push(['acWrite', user.group.guid]);
585
+ acRefs.push(['acFull', user.group.guid]);
506
586
  }
507
587
  for (let curSecondaryGroup of user.groups ?? []) {
508
588
  if (curSecondaryGroup != null && curSecondaryGroup.guid != null) {
509
589
  // It belongs to the user's secondary group.
510
- groupRefs.push(['group', curSecondaryGroup]);
590
+ groupRefs.push(['group', curSecondaryGroup.guid]);
511
591
  // User's secondary group is listed in acRead, acWrite, or acFull.
512
- acRefs.push(['acRead', curSecondaryGroup]);
513
- acRefs.push(['acWrite', curSecondaryGroup]);
514
- acRefs.push(['acFull', curSecondaryGroup]);
592
+ acRefs.push(['acRead', curSecondaryGroup.guid]);
593
+ acRefs.push(['acWrite', curSecondaryGroup.guid]);
594
+ acRefs.push(['acFull', curSecondaryGroup.guid]);
515
595
  }
516
596
  }
517
597
  for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
518
598
  if (curDescendantGroup != null && curDescendantGroup.guid != null) {
519
599
  // It belongs to the user's secondary group.
520
- groupRefs.push(['group', curDescendantGroup]);
600
+ groupRefs.push(['group', curDescendantGroup.guid]);
521
601
  // User's secondary group is listed in acRead, acWrite, or acFull.
522
- acRefs.push(['acRead', curDescendantGroup]);
523
- acRefs.push(['acWrite', curDescendantGroup]);
524
- acRefs.push(['acFull', curDescendantGroup]);
602
+ acRefs.push(['acRead', curDescendantGroup.guid]);
603
+ acRefs.push(['acWrite', curDescendantGroup.guid]);
604
+ acRefs.push(['acFull', curDescendantGroup.guid]);
525
605
  }
526
606
  }
527
607
  // All the group refs.
@@ -544,6 +624,19 @@ export default class Tilmeld implements TilmeldInterface {
544
624
  ref: acRefs,
545
625
  });
546
626
  }
627
+ const selector: FormattedSelector = {
628
+ type: '|',
629
+ // Other access control is sufficient.
630
+ gte: [['acOther', TilmeldAccessLevels.READ_ACCESS]],
631
+ // The user and group are not set.
632
+ selector: subSelectors,
633
+ // The user is listed in acRead, acWrite, or acFull.
634
+ ref: [
635
+ ['acRead', user.guid],
636
+ ['acWrite', user.guid],
637
+ ['acFull', user.guid],
638
+ ],
639
+ };
547
640
  selectors.push(selector);
548
641
  }
549
642
  }
package/src/User.ts CHANGED
@@ -7,12 +7,11 @@ import {
7
7
  } from '@nymphjs/nymph';
8
8
  import { EmailOptions } from 'email-templates';
9
9
  import strtotime from 'locutus/php/datetime/strtotime';
10
- import { nanoid, customAlphabet } from 'nanoid';
11
- import { nolookalikesSafe } from 'nanoid-dictionary';
12
10
  import Base64 from 'crypto-js/enc-base64';
13
11
  import sha256 from 'crypto-js/sha256';
14
12
  import md5 from 'crypto-js/md5';
15
13
  import { difference } from 'lodash';
14
+ import { humanSecret, nanoid } from '@nymphjs/guid';
16
15
 
17
16
  import type Tilmeld from './Tilmeld';
18
17
  import AbleObject from './AbleObject';
@@ -24,6 +23,44 @@ import {
24
23
  EmailChangeRateLimitExceededError,
25
24
  } from './errors';
26
25
 
26
+ export type EventType =
27
+ | 'beforeRegister'
28
+ | 'afterRegister'
29
+ | 'beforeLogin'
30
+ | 'afterLogin'
31
+ | 'beforeLogout'
32
+ | 'afterLogout';
33
+ /**
34
+ * Theses are run before the user data checks, so the only checks before are
35
+ * whether registration is allowed and whether the user is already registered.
36
+ */
37
+ export type TilmeldBeforeRegisterCallback = (
38
+ user: User & UserData,
39
+ data: { password: string; additionalData?: { [k: string]: any } }
40
+ ) => Promise<void> | void;
41
+ export type TilmeldAfterRegisterCallback = (
42
+ user: User & UserData,
43
+ result: { loggedin: boolean; message: string }
44
+ ) => Promise<void> | void;
45
+ /**
46
+ * These are run after the authentication checks, but before the login action.
47
+ */
48
+ export type TilmeldBeforeLoginCallback = (
49
+ user: User & UserData,
50
+ data: {
51
+ username: string;
52
+ password: string;
53
+ additionalData?: { [k: string]: any };
54
+ }
55
+ ) => void;
56
+ /**
57
+ * This is run before the transaction is committed, and you can perform
58
+ * additional functions on the transaction, which is available in `user.$nymph`.
59
+ */
60
+ export type TilmeldAfterLoginCallback = (user: User & UserData) => void;
61
+ export type TilmeldBeforeLogoutCallback = (user: User & UserData) => void;
62
+ export type TilmeldAfterLogoutCallback = (user: User & UserData) => void;
63
+
27
64
  export type UserData = {
28
65
  /**
29
66
  * The abilities granted to the user.
@@ -142,6 +179,12 @@ export default class User extends AbleObject<UserData> {
142
179
  static tilmeld: Tilmeld;
143
180
  static ETYPE = 'tilmeld_user';
144
181
  static class = 'User';
182
+ private static beforeRegisterCallbacks: TilmeldBeforeRegisterCallback[] = [];
183
+ private static afterRegisterCallbacks: TilmeldAfterRegisterCallback[] = [];
184
+ private static beforeLoginCallbacks: TilmeldBeforeLoginCallback[] = [];
185
+ private static afterLoginCallbacks: TilmeldAfterLoginCallback[] = [];
186
+ private static beforeLogoutCallbacks: TilmeldBeforeLogoutCallback[] = [];
187
+ private static afterLogoutCallbacks: TilmeldAfterLogoutCallback[] = [];
145
188
 
146
189
  private static DEFAULT_CLIENT_ENABLED_METHODS = [
147
190
  '$checkUsername',
@@ -171,7 +214,7 @@ export default class User extends AbleObject<UserData> {
171
214
  ];
172
215
  private static DEFAULT_ALLOWLIST_DATA: string[] = [];
173
216
 
174
- protected $clientEnabledMethods = User.DEFAULT_CLIENT_ENABLED_METHODS;
217
+ protected $clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
175
218
  public static clientEnabledStaticMethods = [
176
219
  'current',
177
220
  'loginUser',
@@ -179,9 +222,9 @@ export default class User extends AbleObject<UserData> {
179
222
  'recover',
180
223
  'getClientConfig',
181
224
  ];
182
- protected $privateData = User.DEFAULT_PRIVATE_DATA;
183
- public static searchRestrictedData = User.DEFAULT_PRIVATE_DATA;
184
- protected $allowlistData? = User.DEFAULT_ALLOWLIST_DATA;
225
+ protected $privateData = [...User.DEFAULT_PRIVATE_DATA];
226
+ public static searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
227
+ protected $allowlistData? = [...User.DEFAULT_ALLOWLIST_DATA];
185
228
  protected $allowlistTags?: string[] = [];
186
229
 
187
230
  /**
@@ -325,7 +368,7 @@ export default class User extends AbleObject<UserData> {
325
368
  }
326
369
 
327
370
  // Create a unique secret.
328
- getUser.recoverSecret = customAlphabet(nolookalikesSafe, 10)();
371
+ getUser.recoverSecret = humanSecret();
329
372
  getUser.recoverSecretDate = Date.now();
330
373
  if (!(await getUser.$saveSkipAC())) {
331
374
  return { result: false, message: "Couldn't save recovery secret." };
@@ -429,7 +472,11 @@ export default class User extends AbleObject<UserData> {
429
472
  };
430
473
  }
431
474
 
432
- public static async loginUser(data: { username: string; password: string }) {
475
+ public static async loginUser(data: {
476
+ username: string;
477
+ password: string;
478
+ additionalData?: { [k: string]: any };
479
+ }) {
433
480
  if (!('username' in data) || !data.username.length) {
434
481
  return { result: false, message: 'Incorrect login/password.' };
435
482
  }
@@ -443,7 +490,11 @@ export default class User extends AbleObject<UserData> {
443
490
  return result;
444
491
  }
445
492
 
446
- public $login(data: { username: string; password: string }) {
493
+ public $login(data: {
494
+ username: string;
495
+ password: string;
496
+ additionalData?: { [k: string]: any };
497
+ }) {
447
498
  const tilmeld = this.$nymph.tilmeld as Tilmeld;
448
499
  if (this.guid == null) {
449
500
  return { result: false, message: 'Incorrect login/password.' };
@@ -458,11 +509,32 @@ export default class User extends AbleObject<UserData> {
458
509
  return { result: false, message: 'Incorrect login/password.' };
459
510
  }
460
511
 
512
+ try {
513
+ for (let callback of (this.constructor as typeof User)
514
+ .beforeLoginCallbacks) {
515
+ if (callback) {
516
+ callback(this, data);
517
+ }
518
+ }
519
+ } catch (e: any) {
520
+ return {
521
+ result: false,
522
+ message: e.message,
523
+ };
524
+ }
525
+
461
526
  // Authentication was successful, attempt to login.
462
527
  if (!tilmeld.login(this, true)) {
463
528
  return { result: false, message: 'Incorrect login/password.' };
464
529
  }
465
530
 
531
+ for (let callback of (this.constructor as typeof User)
532
+ .afterLoginCallbacks) {
533
+ if (callback) {
534
+ callback(this);
535
+ }
536
+ }
537
+
466
538
  // Login was successful.
467
539
  return { result: true, message: 'You are logged in.' };
468
540
  }
@@ -473,7 +545,30 @@ export default class User extends AbleObject<UserData> {
473
545
  */
474
546
  public $logout() {
475
547
  const tilmeld = this.$nymph.tilmeld as Tilmeld;
548
+
549
+ try {
550
+ for (let callback of (this.constructor as typeof User)
551
+ .beforeLogoutCallbacks) {
552
+ if (callback) {
553
+ callback(this);
554
+ }
555
+ }
556
+ } catch (e: any) {
557
+ return {
558
+ result: false,
559
+ message: e.message,
560
+ };
561
+ }
562
+
476
563
  tilmeld.logout();
564
+
565
+ for (let callback of (this.constructor as typeof User)
566
+ .afterLogoutCallbacks) {
567
+ if (callback) {
568
+ callback(this);
569
+ }
570
+ }
571
+
477
572
  return { result: true, message: 'You have been logged out.' };
478
573
  }
479
574
 
@@ -579,9 +674,9 @@ export default class User extends AbleObject<UserData> {
579
674
  const tilmeld = this.$nymph.tilmeld as Tilmeld;
580
675
  let user = givenUser ?? tilmeld.User.current();
581
676
 
582
- this.$clientEnabledMethods = User.DEFAULT_CLIENT_ENABLED_METHODS;
583
- this.$privateData = User.DEFAULT_PRIVATE_DATA;
584
- this.$allowlistData = User.DEFAULT_ALLOWLIST_DATA;
677
+ this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
678
+ this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
679
+ this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
585
680
 
586
681
  if (tilmeld.config.emailUsernames) {
587
682
  this.$privateData.push('username');
@@ -1123,8 +1218,9 @@ export default class User extends AbleObject<UserData> {
1123
1218
 
1124
1219
  public async $register(data: {
1125
1220
  password: string;
1221
+ additionalData?: { [k: string]: any };
1126
1222
  }): Promise<{ result: boolean; loggedin: boolean; message: string }> {
1127
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1223
+ let tilmeld = this.$nymph.tilmeld as Tilmeld;
1128
1224
  if (!tilmeld.config.allowRegistration) {
1129
1225
  return {
1130
1226
  result: false,
@@ -1139,6 +1235,22 @@ export default class User extends AbleObject<UserData> {
1139
1235
  message: 'This is already a registered user.',
1140
1236
  };
1141
1237
  }
1238
+
1239
+ try {
1240
+ for (let callback of (this.constructor as typeof User)
1241
+ .beforeRegisterCallbacks) {
1242
+ if (callback) {
1243
+ await callback(this, data);
1244
+ }
1245
+ }
1246
+ } catch (e: any) {
1247
+ return {
1248
+ result: false,
1249
+ loggedin: false,
1250
+ message: e.message,
1251
+ };
1252
+ }
1253
+
1142
1254
  if (!('password' in data) || !data.password.length) {
1143
1255
  return {
1144
1256
  result: false,
@@ -1170,6 +1282,7 @@ export default class User extends AbleObject<UserData> {
1170
1282
  const nymph = this.$nymph;
1171
1283
  const tnymph = await this.$nymph.startTransaction(transaction);
1172
1284
  this.$nymph = tnymph;
1285
+ tilmeld = this.$nymph.tilmeld as Tilmeld;
1173
1286
 
1174
1287
  try {
1175
1288
  // Add primary group.
@@ -1318,19 +1431,40 @@ export default class User extends AbleObject<UserData> {
1318
1431
  tilmeld.config.unverifiedAccess &&
1319
1432
  !madeAdmin
1320
1433
  ) {
1321
- tilmeld.login(this, true);
1434
+ if (!tilmeld.login(this, true)) {
1435
+ throw new Error('An error occurred trying to log you in.');
1436
+ }
1322
1437
  message +=
1323
1438
  "You're now logged in! An email has been sent to " +
1324
1439
  `${this.$data.email} with a verification link for you to finish ` +
1325
1440
  'registration.';
1326
1441
  loggedin = true;
1327
1442
  } else {
1328
- tilmeld.login(this, true);
1443
+ if (!tilmeld.login(this, true)) {
1444
+ throw new Error('An error occurred trying to log you in.');
1445
+ }
1329
1446
  message += "You're now registered and logged in!";
1330
1447
  loggedin = true;
1331
1448
  }
1332
- await tnymph.commit(transaction);
1333
- this.$nymph = nymph;
1449
+
1450
+ for (let callback of (this.constructor as typeof User)
1451
+ .afterRegisterCallbacks) {
1452
+ if (callback) {
1453
+ await callback(this, {
1454
+ loggedin,
1455
+ message,
1456
+ });
1457
+ }
1458
+ }
1459
+
1460
+ try {
1461
+ await tnymph.commit(transaction);
1462
+ this.$nymph = nymph;
1463
+ this.$nymph.tilmeld?.fillSession(this);
1464
+ } catch (e: any) {
1465
+ throw e;
1466
+ }
1467
+
1334
1468
  return {
1335
1469
  result: true,
1336
1470
  loggedin,
@@ -1353,7 +1487,7 @@ export default class User extends AbleObject<UserData> {
1353
1487
  }
1354
1488
 
1355
1489
  public async $save() {
1356
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
1490
+ let tilmeld = this.$nymph.tilmeld as Tilmeld;
1357
1491
  if (this.$data.username == null || !this.$data.username.trim().length) {
1358
1492
  return false;
1359
1493
  }
@@ -1551,6 +1685,7 @@ export default class User extends AbleObject<UserData> {
1551
1685
  const nymph = this.$nymph;
1552
1686
  const tnymph = await this.$nymph.startTransaction(transaction);
1553
1687
  this.$nymph = tnymph;
1688
+ tilmeld = this.$nymph.tilmeld as Tilmeld;
1554
1689
 
1555
1690
  if (
1556
1691
  this.$data.group != null &&
@@ -1597,14 +1732,14 @@ export default class User extends AbleObject<UserData> {
1597
1732
  }
1598
1733
  }
1599
1734
 
1600
- if (tilmeld.User.current(true).$is(this)) {
1601
- // Update the user in the session cache.
1602
- tilmeld.fillSession(this);
1603
- }
1604
-
1605
1735
  this.$descendantGroups = undefined;
1606
1736
  this.$gatekeeperCache = undefined;
1607
1737
  await tnymph.commit(transaction);
1738
+
1739
+ if ((nymph.tilmeld as Tilmeld).User.current(true).$is(this)) {
1740
+ // Update the user in the session cache.
1741
+ (nymph.tilmeld as Tilmeld).fillSession(this);
1742
+ }
1608
1743
  } else {
1609
1744
  await tnymph.rollback(transaction);
1610
1745
  }
@@ -1646,4 +1781,82 @@ export default class User extends AbleObject<UserData> {
1646
1781
  }
1647
1782
  return await super.$delete();
1648
1783
  }
1784
+
1785
+ public static on<T extends EventType>(
1786
+ event: T,
1787
+ callback: T extends 'beforeRegister'
1788
+ ? TilmeldBeforeRegisterCallback
1789
+ : T extends 'afterRegister'
1790
+ ? TilmeldAfterRegisterCallback
1791
+ : T extends 'beforeLogin'
1792
+ ? TilmeldBeforeLoginCallback
1793
+ : T extends 'afterLogin'
1794
+ ? TilmeldAfterLoginCallback
1795
+ : T extends 'beforeLogout'
1796
+ ? TilmeldBeforeLogoutCallback
1797
+ : T extends 'afterLogout'
1798
+ ? TilmeldAfterLogoutCallback
1799
+ : never
1800
+ ) {
1801
+ const prop = (event + 'Callbacks') as T extends 'beforeRegister'
1802
+ ? 'beforeRegisterCallbacks'
1803
+ : T extends 'afterRegister'
1804
+ ? 'afterRegisterCallbacks'
1805
+ : T extends 'beforeLogin'
1806
+ ? 'beforeLoginCallbacks'
1807
+ : T extends 'afterLogin'
1808
+ ? 'afterLoginCallbacks'
1809
+ : T extends 'beforeLogout'
1810
+ ? 'beforeLogoutCallbacks'
1811
+ : T extends 'afterLogout'
1812
+ ? 'afterLogoutCallbacks'
1813
+ : never;
1814
+ if (!(prop in this)) {
1815
+ throw new Error('Invalid event type.');
1816
+ }
1817
+ // @ts-ignore: The callback should always be the right type here.
1818
+ this[prop].push(callback);
1819
+ return () => this.off(event, callback);
1820
+ }
1821
+
1822
+ public static off<T extends EventType>(
1823
+ event: T,
1824
+ callback: T extends 'beforeRegister'
1825
+ ? TilmeldBeforeRegisterCallback
1826
+ : T extends 'afterRegister'
1827
+ ? TilmeldAfterRegisterCallback
1828
+ : T extends 'beforeLogin'
1829
+ ? TilmeldBeforeLoginCallback
1830
+ : T extends 'afterLogin'
1831
+ ? TilmeldAfterLoginCallback
1832
+ : T extends 'beforeLogout'
1833
+ ? TilmeldBeforeLogoutCallback
1834
+ : T extends 'afterLogout'
1835
+ ? TilmeldAfterLogoutCallback
1836
+ : never
1837
+ ) {
1838
+ const prop = (event + 'Callbacks') as T extends 'beforeRegister'
1839
+ ? 'beforeRegisterCallbacks'
1840
+ : T extends 'afterRegister'
1841
+ ? 'afterRegisterCallbacks'
1842
+ : T extends 'beforeLogin'
1843
+ ? 'beforeLoginCallbacks'
1844
+ : T extends 'afterLogin'
1845
+ ? 'afterLoginCallbacks'
1846
+ : T extends 'beforeLogout'
1847
+ ? 'beforeLogoutCallbacks'
1848
+ : T extends 'afterLogout'
1849
+ ? 'afterLogoutCallbacks'
1850
+ : never;
1851
+ if (!(prop in this)) {
1852
+ return false;
1853
+ }
1854
+ // @ts-ignore: The callback should always be the right type here.
1855
+ const i = this[prop].indexOf(callback);
1856
+ if (i > -1) {
1857
+ // @ts-ignore: The callback should always be the right type here.
1858
+ this[prop].splice(i, 1);
1859
+ }
1860
+ return true;
1861
+ }
1649
1862
  }
package/src/conf/d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EmailOptions } from 'email-templates';
1
+ import { EmailOptions, EmailConfig } from 'email-templates';
2
2
 
3
3
  import type Tilmeld from '../Tilmeld';
4
4
  import Group, { GroupData } from '../Group';
@@ -245,6 +245,18 @@ export interface Config {
245
245
  * email sender.
246
246
  */
247
247
  emailTemplateDir: string;
248
+ /**
249
+ * Configure an email object to prepare for sending.
250
+ *
251
+ * You can use this, for example, for DKIM signing.
252
+ *
253
+ * See: https://github.com/forwardemail/email-templates/blob/v8.0.9/README.md#Options
254
+ */
255
+ configEmail: (
256
+ tilmeld: Tilmeld,
257
+ options: EmailOptions,
258
+ user: User & UserData
259
+ ) => Promise<EmailConfig>;
248
260
  /**
249
261
  * Send an email to a user. Uses `email-templates` by default.
250
262
  *