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

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