@nymphjs/tilmeld 1.0.0-beta.55 → 1.0.0-beta.56

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 (39) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/AbleObject.d.ts +15 -0
  3. package/dist/AbleObject.js +15 -0
  4. package/dist/AbleObject.js.map +1 -1
  5. package/dist/Group.d.ts +116 -0
  6. package/dist/Group.js +98 -0
  7. package/dist/Group.js.map +1 -1
  8. package/dist/Group.test.js +1 -0
  9. package/dist/Group.test.js.map +1 -1
  10. package/dist/Tilmeld.d.ts +226 -1
  11. package/dist/Tilmeld.js +304 -13
  12. package/dist/Tilmeld.js.map +1 -1
  13. package/dist/User.d.ts +265 -0
  14. package/dist/User.js +232 -0
  15. package/dist/User.js.map +1 -1
  16. package/dist/User.test.js +1 -0
  17. package/dist/User.test.js.map +1 -1
  18. package/dist/conf/d.d.ts +244 -0
  19. package/dist/conf/defaults.js +6 -3
  20. package/dist/conf/defaults.js.map +1 -1
  21. package/dist/errors/AccessControlError.d.ts +2 -1
  22. package/dist/errors/AccessControlError.js +3 -2
  23. package/dist/errors/AccessControlError.js.map +1 -1
  24. package/dist/errors/BadDataError.d.ts +2 -1
  25. package/dist/errors/BadDataError.js +3 -2
  26. package/dist/errors/BadDataError.js.map +1 -1
  27. package/dist/errors/BadEmailError.d.ts +2 -1
  28. package/dist/errors/BadEmailError.js +3 -2
  29. package/dist/errors/BadEmailError.js.map +1 -1
  30. package/dist/errors/BadUsernameError.d.ts +2 -1
  31. package/dist/errors/BadUsernameError.js +3 -2
  32. package/dist/errors/BadUsernameError.js.map +1 -1
  33. package/package.json +8 -7
  34. package/src/Tilmeld.ts +7 -3
  35. package/src/errors/AccessControlError.ts +4 -2
  36. package/src/errors/BadDataError.ts +4 -2
  37. package/src/errors/BadEmailError.ts +4 -2
  38. package/src/errors/BadUsernameError.ts +4 -2
  39. package/tsconfig.json +1 -1
package/dist/Tilmeld.js CHANGED
@@ -11,15 +11,64 @@ const errors_1 = require("./errors");
11
11
  const Group_1 = __importDefault(require("./Group"));
12
12
  const User_1 = __importDefault(require("./User"));
13
13
  const enforceTilmeld_1 = require("./enforceTilmeld");
14
+ /**
15
+ * A user and group system for Nymph.js.
16
+ *
17
+ * Written by Hunter Perrin for SciActive.
18
+ *
19
+ * @author Hunter Perrin <hperrin@gmail.com>
20
+ * @copyright SciActive Inc
21
+ * @see http://nymph.io/
22
+ */
14
23
  class Tilmeld {
24
+ /**
25
+ * Create a new instance of Tilmeld.
26
+ *
27
+ * @param config The Tilmeld configuration.
28
+ */
15
29
  constructor(config) {
30
+ /**
31
+ * The Nymph instance.
32
+ */
16
33
  this.nymph = {};
34
+ /**
35
+ * The currently logged in user.
36
+ */
17
37
  this.currentUser = null;
38
+ /**
39
+ * The user class for this instance of Tilmeld.
40
+ */
18
41
  this.User = User_1.default;
42
+ /**
43
+ * The group class for this instance of Tilmeld.
44
+ */
19
45
  this.Group = Group_1.default;
46
+ /**
47
+ * If you will be performing authentication functions (logging in/out), you
48
+ * should set these so Tilmeld can read and write cookies and headers.
49
+ *
50
+ * If you want the user to be authenticated with the cookie and/or header they
51
+ * provide, you should set at least the request. It's better to set both, so
52
+ * the JWT can be updated if needed.
53
+ *
54
+ * After you set these, call `authenticate()` to read user authentication data
55
+ * from them and fill the user's session.
56
+ *
57
+ * If you want to support cookie based authentication (which still requires an
58
+ * XSRF token for security), you should enable the cookie parser middleware.
59
+ */
20
60
  this.request = null;
21
61
  this.response = null;
62
+ /**
63
+ * Used to avoid infinite loop.
64
+ */
22
65
  this.alreadyLoggedOutSwitch = false;
66
+ /**
67
+ * Gatekeeper ability cache.
68
+ *
69
+ * Gatekeeper will cache the user's abilities that it calculates, so it can
70
+ * check faster if that user has been checked before.
71
+ */
23
72
  this.gatekeeperCache = null;
24
73
  this.config = { ...conf_1.ConfigDefaults, ...config };
25
74
  if (this.config.emailUsernames &&
@@ -33,6 +82,11 @@ class Tilmeld {
33
82
  }
34
83
  }
35
84
  }
85
+ /**
86
+ * This is used internally by Nymph. Don't call it yourself.
87
+ *
88
+ * @returns A clone of this instance.
89
+ */
36
90
  clone() {
37
91
  const tilmeld = new Tilmeld(this.config);
38
92
  tilmeld.request = this.request;
@@ -41,19 +95,30 @@ class Tilmeld {
41
95
  tilmeld.gatekeeperCache = this.gatekeeperCache;
42
96
  return tilmeld;
43
97
  }
98
+ /**
99
+ * Initialize Tilmeld.
100
+ *
101
+ * This is meant to be called internally by Nymph. Don't call this directly.
102
+ *
103
+ * @param nymph The Nymph instance.
104
+ */
44
105
  init(nymph) {
45
106
  this.nymph = nymph;
46
107
  if (this.nymph.driver == null) {
47
108
  throw new Error("Tilmeld can't be configured before Nymph.");
48
109
  }
110
+ // Configure the classes.
49
111
  if (nymph.parent) {
112
+ // This is a cloned Nymph, it should have classes already.
50
113
  this.User = this.nymph.getEntityClass(User_1.default);
51
114
  this.Group = this.nymph.getEntityClass(Group_1.default);
52
115
  }
53
116
  else {
117
+ // This is a new Nymph, add the classes.
54
118
  this.User = this.nymph.addEntityClass(User_1.default);
55
119
  this.Group = this.nymph.addEntityClass(Group_1.default);
56
120
  }
121
+ // Clone the current user so its Nymph instance is correct.
57
122
  if (this.currentUser) {
58
123
  const currentUserGUID = this.currentUser.guid;
59
124
  const currentUserCDate = this.currentUser.cdate;
@@ -71,8 +136,18 @@ class Tilmeld {
71
136
  this.currentUser.$updateDataProtection();
72
137
  this.currentUser.$updateGroupDataProtection();
73
138
  }
139
+ // Set up access control hooks when Nymph is called.
74
140
  this.initAccessControl();
75
141
  }
142
+ /**
143
+ * Check to see if the current user has an ability.
144
+ *
145
+ * If `ability` is undefined, it will check to see if a user is currently
146
+ * logged in.
147
+ *
148
+ * @param ability The ability.
149
+ * @returns Whether the user has the given ability.
150
+ */
76
151
  gatekeeper(ability) {
77
152
  if (this.currentUser == null || this.gatekeeperCache == null) {
78
153
  return false;
@@ -84,6 +159,7 @@ class Tilmeld {
84
159
  !!this.currentUser.abilities?.includes('system/admin'));
85
160
  }
86
161
  initAccessControl() {
162
+ // Check for the skip access control option and add AC selectors.
87
163
  const handleQuery = function (nymph, options, selectors) {
88
164
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
89
165
  if (!options ||
@@ -93,38 +169,45 @@ class Tilmeld {
93
169
  if (options?.source === 'client') {
94
170
  if ((!tilmeld.currentUser ||
95
171
  (!tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
96
- !tilmeld.currentUser.abilities?.includes('system/admin'))) &&
172
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) && // The user is not an admin.
97
173
  ((!tilmeld.config.enableUserSearch &&
98
- options.class?.factorySync() instanceof User_1.default) ||
174
+ options.class?.factorySync() instanceof User_1.default) || // The use is searching for a user and searching is disabled.
99
175
  (!tilmeld.config.enableGroupSearch &&
100
- options.class?.factorySync() instanceof Group_1.default)) &&
101
- (selectors[0] == null ||
102
- selectors[0].type !== '&' ||
103
- (!('guid' in selectors[0]) && !('equal' in selectors[0])) ||
176
+ options.class?.factorySync() instanceof Group_1.default)) && // Or the same, but for a group.
177
+ (selectors[0] == null || // No selector specified.
178
+ selectors[0].type !== '&' || // Or the type is not and.
179
+ (!('guid' in selectors[0]) && !('equal' in selectors[0])) || // Or they're searching for something other than guid or equal.
104
180
  ('guid' in selectors[0] &&
105
181
  (!selectors[0].guid?.length ||
106
- !selectors[0].guid?.[0]?.length)) ||
182
+ !selectors[0].guid?.[0]?.length)) || // Or they're not searching for a specific guid.
107
183
  ('equal' in selectors[0] &&
108
184
  selectors[0].equal?.[0]?.[0] !== 'username' &&
109
- selectors[0].equal?.[0]?.[0] !== 'groupname'))) {
185
+ selectors[0].equal?.[0]?.[0] !== 'groupname')) // Or they're not searching for a specific username/groupname.
186
+ ) {
187
+ // If the user is not specifically searching for a GUID or username,
188
+ // and they're not allowed to search, it should fail.
110
189
  throw new errors_1.AccessControlError('No permission to search.');
111
190
  }
112
191
  }
192
+ // Add access control selectors
113
193
  tilmeld.addAccessControlSelectors(options, selectors);
114
194
  }
115
195
  };
116
196
  handleQuery.skipOnClone = true;
197
+ // Filter entities being deleted for user permissions.
117
198
  const checkPermissionsDelete = async function (nymph, entity) {
118
199
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
119
200
  if (typeof entity.$tilmeldDeleteSkipAC === 'function' &&
120
201
  entity.$tilmeldDeleteSkipAC()) {
121
202
  return;
122
203
  }
204
+ // Test for permissions.
123
205
  if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.FULL_ACCESS)) {
124
206
  throw new errors_1.AccessControlError('No permission to delete.');
125
207
  }
126
208
  };
127
209
  checkPermissionsDelete.skipOnClone = true;
210
+ // Filter entities being deleted for user permissions.
128
211
  const checkPermissionsDeleteByID = async function (nymph, guid, className) {
129
212
  const entity = await nymph.getEntity({ class: nymph.getEntityClass(className ?? 'Entity') }, { type: '&', guid: guid });
130
213
  if (entity != null) {
@@ -132,6 +215,8 @@ class Tilmeld {
132
215
  }
133
216
  };
134
217
  checkPermissionsDeleteByID.skipOnClone = true;
218
+ // Filter entities being saved for user permissions, and filter any
219
+ // disallowed changes to AC properties.
135
220
  const checkPermissionsSaveAndFilterAcChanges = async function (nymph, entity) {
136
221
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
137
222
  if (!entity) {
@@ -142,6 +227,8 @@ class Tilmeld {
142
227
  return;
143
228
  }
144
229
  if (entity.guid != null) {
230
+ // If the entity is not new, check that the user has full access before
231
+ // allowing a change to ac properties.
145
232
  const originalAc = entity.$getOriginalAcValues();
146
233
  const newAc = entity.$getCurrentAcValues();
147
234
  function areAcPropertiesChanged(a, b) {
@@ -191,6 +278,7 @@ class Tilmeld {
191
278
  }
192
279
  if (areAcPropertiesChanged(originalAc, newAc)) {
193
280
  if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.FULL_ACCESS, undefined, originalAc)) {
281
+ // Only allow changes to AC properties if the user has full access.
194
282
  throw new errors_1.AccessControlError('No permission to change access control properties.');
195
283
  }
196
284
  if ((originalAc.user != null && originalAc.user !== newAc.user) ||
@@ -207,11 +295,28 @@ class Tilmeld {
207
295
  }
208
296
  }
209
297
  }
298
+ // Test for permissions.
210
299
  if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.WRITE_ACCESS)) {
211
300
  throw new errors_1.AccessControlError('No permission to write.');
212
301
  }
213
302
  };
214
303
  checkPermissionsSaveAndFilterAcChanges.skipOnClone = true;
304
+ /*
305
+ * Add the current user's "user", "group", and access control to new entity.
306
+ *
307
+ * This occurs right before an entity is saved. It only alters the entity
308
+ * if:
309
+ *
310
+ * - There is a user logged in.
311
+ * - The entity is new (doesn't have a GUID.)
312
+ * - The entity is not a user or group.
313
+ *
314
+ * Default access control is
315
+ *
316
+ * - acUser = TilmeldAccessLevels.FULL_ACCESS
317
+ * - acGroup = TilmeldAccessLevels.READ_ACCESS
318
+ * - acOther = TilmeldAccessLevels.NO_ACCESS
319
+ */
215
320
  const addAccess = async function (nymph, entity) {
216
321
  const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
217
322
  const user = tilmeld.currentUser;
@@ -306,6 +411,12 @@ class Tilmeld {
306
411
  this.nymph.on('beforeDeleteEntity', checkPermissionsDelete);
307
412
  this.nymph.on('beforeDeleteEntityByID', checkPermissionsDeleteByID);
308
413
  }
414
+ /**
415
+ * Add selectors to a list of options and selectors which will limit results
416
+ * to only entities the current user has access to.
417
+ *
418
+ * @param optionsAndSelectors The options and selectors of the query.
419
+ */
309
420
  addAccessControlSelectors(options, selectors) {
310
421
  const user = this.currentUser;
311
422
  if (options == null) {
@@ -314,12 +425,15 @@ class Tilmeld {
314
425
  else if ('class' in options &&
315
426
  (options.class?.factorySync() instanceof User_1.default ||
316
427
  options.class?.factorySync() instanceof Group_1.default)) {
428
+ // They are requesting a user/group. Always accessible for reading.
317
429
  return;
318
430
  }
319
431
  if (user == null || user.guid == null) {
320
432
  selectors.push({
321
433
  type: '|',
434
+ // Other access control is sufficient.
322
435
  gte: [['acOther', nymph_1.TilmeldAccessLevels.READ_ACCESS]],
436
+ // The user and group are not set.
323
437
  selector: [
324
438
  {
325
439
  type: '&',
@@ -334,6 +448,7 @@ class Tilmeld {
334
448
  type: '&',
335
449
  '!defined': [['user', 'group']],
336
450
  },
451
+ // It is owned by the user.
337
452
  {
338
453
  type: '&',
339
454
  ref: [['user', user.guid]],
@@ -344,18 +459,23 @@ class Tilmeld {
344
459
  const acRefs = [];
345
460
  const gid = user.$getGid();
346
461
  if (gid != null) {
462
+ // It belongs to the user's primary group.
347
463
  groupRefs.push(['group', gid]);
464
+ // User's primary group is listed in acRead, acWrite, or acFull.
348
465
  acRefs.push(['acRead', gid]);
349
466
  acRefs.push(['acWrite', gid]);
350
467
  acRefs.push(['acFull', gid]);
351
468
  }
352
469
  const gids = user.$getGids();
353
470
  for (let curGid of gids ?? []) {
471
+ // It belongs to the user's secondary group.
354
472
  groupRefs.push(['group', curGid]);
473
+ // User's secondary group is listed in acRead, acWrite, or acFull.
355
474
  acRefs.push(['acRead', curGid]);
356
475
  acRefs.push(['acWrite', curGid]);
357
476
  acRefs.push(['acFull', curGid]);
358
477
  }
478
+ // All the group refs.
359
479
  if (groupRefs.length) {
360
480
  subSelectors.push({
361
481
  type: '&',
@@ -368,6 +488,7 @@ class Tilmeld {
368
488
  ],
369
489
  });
370
490
  }
491
+ // All the acRead, acWrite, and acFull refs.
371
492
  if (acRefs.length) {
372
493
  subSelectors.push({
373
494
  type: '|',
@@ -376,8 +497,11 @@ class Tilmeld {
376
497
  }
377
498
  const selector = {
378
499
  type: '|',
500
+ // Other access control is sufficient.
379
501
  gte: [['acOther', nymph_1.TilmeldAccessLevels.READ_ACCESS]],
502
+ // The user and group are not set.
380
503
  selector: subSelectors,
504
+ // The user is listed in acRead, acWrite, or acFull.
381
505
  ref: [
382
506
  ['acRead', user.guid],
383
507
  ['acWrite', user.guid],
@@ -387,12 +511,76 @@ class Tilmeld {
387
511
  selectors.push(selector);
388
512
  }
389
513
  }
514
+ /**
515
+ * Check an entity's permissions for a user.
516
+ *
517
+ * This will check the AC (Access Control) properties of the entity. These
518
+ * include the following properties:
519
+ *
520
+ * - acUser
521
+ * - acGroup
522
+ * - acOther
523
+ * - acRead
524
+ * - acWrite
525
+ * - acFull
526
+ *
527
+ * "acUser" refers to the entity's owner, "acGroup" refers to all users in the
528
+ * entity's group and all ancestor groups, and "acOther" refers to any user
529
+ * who doesn't fit these descriptions.
530
+ *
531
+ * Each of these properties should be either NO_ACCESS, READ_ACCESS,
532
+ * WRITE_ACCESS, or FULL_ACCESS.
533
+ *
534
+ * - NO_ACCESS - the user has no access to the entity.
535
+ * - READ_ACCESS, the user has read access to the entity.
536
+ * - WRITE_ACCESS, the user has read and write access to the entity, but can't
537
+ * delete it, change its access controls, or change its ownership.
538
+ * - FULL_ACCESS, the user has read, write, and delete access to the entity,
539
+ * as well as being able to manage its access controls and ownership.
540
+ *
541
+ * These properties default to:
542
+ *
543
+ * - acUser = TilmeldAccessLevels.FULL_ACCESS
544
+ * - acGroup = TilmeldAccessLevels.READ_ACCESS
545
+ * - acOther = TilmeldAccessLevels.NO_ACCESS
546
+ *
547
+ * "acRead", "acWrite", and "acFull" are arrays of users and/or groups that
548
+ * also have those permissions.
549
+ *
550
+ * Only users with FULL_ACCESS have the ability to change any of the ac*,
551
+ * user, and group properties.
552
+ *
553
+ * The following conditions will result in different checks, which determine
554
+ * whether the check passes:
555
+ *
556
+ * - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
557
+ * - No user is logged in. (Check other AC.)
558
+ * - The entity is the user. (Always true.)
559
+ * - It is the user's primary group. (True for READ_ACCESS.)
560
+ * - The user or its groups are listed in "acRead". (True for READ_ACCESS.)
561
+ * - The user or its groups are listed in "acWrite". (True for READ_ACCESS and
562
+ * WRITE_ACCESS.)
563
+ * - The user or its groups are listed in "acFull". (Always true.)
564
+ * - Its "user" is the user. (It is owned by the user.) (Check user AC.)
565
+ * - Its "group" is the user's primary group. (Check group AC.)
566
+ * - Its "group" is one of the user's secondary groups. (Check group AC.)
567
+ * - Its "group" is a descendant of one of the user's groups. (Check group
568
+ * AC.)
569
+ * - None of the above. (Check other AC.)
570
+ *
571
+ * @param entity The entity to check.
572
+ * @param type The lowest level of permission to consider a pass.
573
+ * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
574
+ * @param acProperties The acProperties to use instead of getting them from the entity.
575
+ * @returns Whether the current user has at least `type` permission for the entity.
576
+ */
390
577
  checkPermissions(entity, type = nymph_1.TilmeldAccessLevels.READ_ACCESS, user, acProperties) {
391
578
  if (!acProperties) {
392
579
  acProperties = entity.$getCurrentAcValues();
393
580
  }
394
581
  let userOrNull = null;
395
582
  let userOrEmpty = this.User.factorySync();
583
+ // Calculate the user.
396
584
  if (user == null) {
397
585
  userOrNull = this.currentUser;
398
586
  userOrEmpty = this.User.current(true);
@@ -401,29 +589,35 @@ class Tilmeld {
401
589
  userOrNull = user;
402
590
  userOrEmpty = user;
403
591
  }
592
+ // Users and groups are always readable. Editable by Tilmeld admins.
404
593
  if ((entity instanceof User_1.default || entity instanceof Group_1.default) &&
405
594
  (type === nymph_1.TilmeldAccessLevels.READ_ACCESS ||
406
595
  userOrEmpty.abilities?.includes('tilmeld/admin') ||
407
596
  userOrEmpty.abilities?.includes('system/admin'))) {
408
597
  return true;
409
598
  }
599
+ // Load access control, since we need it now...
410
600
  const acUser = acProperties.acUser ?? nymph_1.TilmeldAccessLevels.FULL_ACCESS;
411
601
  const acGroup = acProperties.acGroup ?? nymph_1.TilmeldAccessLevels.READ_ACCESS;
412
602
  const acOther = acProperties.acOther ?? nymph_1.TilmeldAccessLevels.NO_ACCESS;
413
603
  if (userOrNull == null) {
414
604
  return acOther >= type;
415
605
  }
606
+ // Check if the entity is the user.
416
607
  if (userOrEmpty.$is(entity)) {
417
608
  return true;
418
609
  }
610
+ // Check if the entity is the user's group. Always readable.
419
611
  const gid = userOrEmpty.$getGid();
420
612
  if (gid != null &&
421
613
  entity.guid === gid &&
422
614
  type === nymph_1.TilmeldAccessLevels.READ_ACCESS) {
423
615
  return true;
424
616
  }
617
+ // Calculate all the groups the user belongs to.
425
618
  let allGids = gid == null ? [] : [gid];
426
619
  allGids = allGids.concat(userOrEmpty.$getGids() ?? []);
620
+ // Check access ac properties.
427
621
  const checks = [
428
622
  {
429
623
  type: nymph_1.TilmeldAccessLevels.FULL_ACCESS,
@@ -450,6 +644,7 @@ class Tilmeld {
450
644
  }
451
645
  }
452
646
  }
647
+ // Check ownership ac properties.
453
648
  if (acProperties.user != null && acProperties.user === userOrNull.guid) {
454
649
  return acUser >= type;
455
650
  }
@@ -458,8 +653,33 @@ class Tilmeld {
458
653
  }
459
654
  return acOther >= type;
460
655
  }
656
+ /**
657
+ * Check a UID's permissions for a user.
658
+ *
659
+ * ## THIS ONLY CHECKS AUTOMATICALLY FOR CLIENT REQUESTS.
660
+ *
661
+ * UID functions on the Node.js side are not checked automatically. This
662
+ * function is only run automatically for UID functions run from the client.
663
+ * You should call this function manually if you're running a UID function on
664
+ * the Node.js side and you want it gated.
665
+ *
666
+ * This will check the Tilmeld config and the user's abilities.
667
+ *
668
+ * - READ_ACCESS, the UID is listed in clientReadableUIDs or the user has the
669
+ * "uid/get/nameofuid" ability.
670
+ * - WRITE_ACCESS, the UID is listed in clientEnabledUIDs or the user has the
671
+ * "uid/new/nameofuid" ability.
672
+ * - FULL_ACCESS, the UID is listed in clientSetabledUIDs or the user has the
673
+ * "uid/set/nameofuid" ability.
674
+ *
675
+ * @param name The UID to check.
676
+ * @param type The lowest level of permission to consider a pass.
677
+ * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
678
+ * @returns Whether the current user has at least `type` permission for the UID.
679
+ */
461
680
  async checkClientUIDPermissions(name, type = nymph_1.TilmeldAccessLevels.READ_ACCESS, user) {
462
681
  let userOrEmpty = this.User.factorySync();
682
+ // Calculate the user.
463
683
  if (user == null) {
464
684
  userOrEmpty = this.User.current(true);
465
685
  }
@@ -489,12 +709,21 @@ class Tilmeld {
489
709
  }
490
710
  return false;
491
711
  }
712
+ /**
713
+ * Fill session user data.
714
+ *
715
+ * @param user The user.
716
+ */
492
717
  async fillSession(user) {
493
718
  this.currentUser = user;
494
719
  this.gatekeeperCache = await user.$getGatekeeperCache();
720
+ // Now update the data protection on the user and all the groups.
495
721
  this.currentUser.$updateDataProtection();
496
722
  this.currentUser.$updateGroupDataProtection();
497
723
  }
724
+ /**
725
+ * Clear session user data.
726
+ */
498
727
  clearSession() {
499
728
  const user = this.currentUser;
500
729
  this.currentUser = null;
@@ -504,6 +733,12 @@ class Tilmeld {
504
733
  user.$updateGroupDataProtection();
505
734
  }
506
735
  }
736
+ /**
737
+ * Validate and extract the user from a token.
738
+ *
739
+ * @param token The authentication token.
740
+ * @returns The user on success, null on failure.
741
+ */
507
742
  async extractToken(token) {
508
743
  const extract = this.config.jwtExtract(this.config, token);
509
744
  if (extract == null) {
@@ -519,18 +754,38 @@ class Tilmeld {
519
754
  }
520
755
  return user;
521
756
  }
522
- async authenticate(skipXsrfToken = false) {
757
+ /**
758
+ * Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
759
+ * from it/them.
760
+ *
761
+ * You can also call this function after setting `response.locals.user` to the
762
+ * user you want to authenticate. You *should* check for `user.enabled` before
763
+ * setting this variable, unless you explicitly want to log in as a disabled
764
+ * user. (The user must be an instance of the User class for this Tilmeld
765
+ * instance.)
766
+ *
767
+ * This function will set `response.locals.user` to the logged in user on
768
+ * successful authentication.
769
+ *
770
+ * @param skipXsrfToken Skip the XSRF token check.
771
+ * @param skipRenew Skip the token renewal step, even if the token is close to expiration.
772
+ * @returns True if a user was authenticated, false on any failure.
773
+ */
774
+ async authenticate(skipXsrfToken = false, skipRenew = false) {
523
775
  if (this.request == null) {
524
776
  return false;
525
777
  }
526
778
  if (this.response &&
527
779
  this.response.locals.user &&
528
780
  this.response.locals.user instanceof this.User) {
781
+ // The user has already been authenticated through some other means.
529
782
  const user = this.response.locals.user;
530
783
  await this.fillSession(user);
531
784
  return true;
532
785
  }
533
786
  const cookies = this.request.cookies ?? {};
787
+ // If a client does't support cookies, they can use the X-TILMELDAUTH header
788
+ // to provide the auth token.
534
789
  let fromAuthHeader = false;
535
790
  let authToken;
536
791
  let switchToken;
@@ -555,12 +810,16 @@ class Tilmeld {
555
810
  let switchExtract;
556
811
  if (skipXsrfToken ||
557
812
  this.request.originalUrl.startsWith(this.config.setupPath)) {
813
+ // The request is for the setup app, or we were told to skip the XSRF
814
+ // check, so don't check for the XSRF token.
558
815
  authExtract = this.config.jwtExtract(this.config, authToken);
559
816
  switchExtract = switchToken
560
817
  ? this.config.jwtExtract(this.config, switchToken)
561
818
  : null;
562
819
  }
563
820
  else {
821
+ // The request is for something else, so check for a valid XSRF token,
822
+ // unless the auth token is provided by a header (instead of a cookie).
564
823
  const xsrfToken = this.request.header('x-xsrf-token');
565
824
  if (xsrfToken == null && !fromAuthHeader) {
566
825
  return false;
@@ -589,6 +848,7 @@ class Tilmeld {
589
848
  return false;
590
849
  }
591
850
  if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
851
+ // Load the switch user.
592
852
  const switchUser = await this.User.factory(switchGuid);
593
853
  if (switchUser.guid == null) {
594
854
  await this.logoutSwitch();
@@ -601,7 +861,10 @@ class Tilmeld {
601
861
  return true;
602
862
  }
603
863
  }
604
- if (expire.getTime() < Date.now() + this.config.jwtRenew * 1000) {
864
+ if (!skipRenew &&
865
+ expire.getTime() < Date.now() + this.config.jwtRenew * 1000) {
866
+ // If the user is less than renew time from needing a new token, give them
867
+ // a new one.
605
868
  await this.login(user, fromAuthHeader);
606
869
  }
607
870
  else {
@@ -612,6 +875,14 @@ class Tilmeld {
612
875
  }
613
876
  return true;
614
877
  }
878
+ /**
879
+ * Logs the given user into the system.
880
+ *
881
+ * @param user The user.
882
+ * @param sendAuthHeader Send the auth token as a custom header.
883
+ * @param sendCookie Send the auth token as a cookie.
884
+ * @returns True on success, false on failure.
885
+ */
615
886
  async login(user, sendAuthHeader, sendCookie = true) {
616
887
  if (user.guid != null && user.enabled) {
617
888
  if (this.response) {
@@ -624,7 +895,7 @@ class Tilmeld {
624
895
  path: this.config.cookiePath,
625
896
  maxAge: this.config.jwtExpire * 1000,
626
897
  secure: appUrl.protocol === 'https:',
627
- httpOnly: false,
898
+ httpOnly: false, // Allow JS access (for CSRF protection).
628
899
  sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
629
900
  });
630
901
  }
@@ -639,6 +910,16 @@ class Tilmeld {
639
910
  }
640
911
  return false;
641
912
  }
913
+ /**
914
+ * Adds a switch auth token for the given user.
915
+ *
916
+ * This effectively logs the current user in to the system as the given user.
917
+ *
918
+ * @param user The user.
919
+ * @param sendAuthHeader Send the auth token as a custom header.
920
+ * @param sendCookie Send the auth token as a cookie.
921
+ * @returns True on success, false on failure.
922
+ */
642
923
  async loginSwitch(user, sendAuthHeader, sendCookie = true) {
643
924
  if (user.guid != null) {
644
925
  if (this.response) {
@@ -651,7 +932,7 @@ class Tilmeld {
651
932
  path: this.config.cookiePath,
652
933
  maxAge: this.config.jwtSwitchExpire * 1000,
653
934
  secure: appUrl.protocol === 'https:',
654
- httpOnly: false,
935
+ httpOnly: false, // Allow JS access (for PubSub auth token).
655
936
  sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
656
937
  });
657
938
  }
@@ -666,6 +947,11 @@ class Tilmeld {
666
947
  }
667
948
  return false;
668
949
  }
950
+ /**
951
+ * Logs the current user out of the system.
952
+ *
953
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
954
+ */
669
955
  async logout(clearCookie = true) {
670
956
  if (this.request && !this.alreadyLoggedOutSwitch) {
671
957
  const cookies = this.request.cookies ?? {};
@@ -686,6 +972,11 @@ class Tilmeld {
686
972
  this.response.locals.user = null;
687
973
  }
688
974
  }
975
+ /**
976
+ * Clears the switch user out of the system.
977
+ *
978
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
979
+ */
689
980
  async logoutSwitch(clearCookie = true) {
690
981
  this.clearSession();
691
982
  if (this.response) {
@@ -697,7 +988,7 @@ class Tilmeld {
697
988
  this.response.set('X-TILMELDSWITCH', '');
698
989
  }
699
990
  this.response.locals.user = null;
700
- await this.authenticate();
991
+ await this.authenticate(false, true);
701
992
  }
702
993
  }
703
994
  }