@nymphjs/tilmeld 1.0.0-beta.6 → 1.0.0-beta.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +284 -0
  2. package/README.md +14 -1
  3. package/dist/AbleObject.d.ts +15 -0
  4. package/dist/AbleObject.js +16 -1
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +119 -8
  7. package/dist/Group.js +230 -97
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.test.js +3 -2
  10. package/dist/Group.test.js.map +1 -1
  11. package/dist/Tilmeld.d.ts +237 -9
  12. package/dist/Tilmeld.js +559 -195
  13. package/dist/Tilmeld.js.map +1 -1
  14. package/dist/User.d.ts +333 -14
  15. package/dist/User.js +797 -204
  16. package/dist/User.js.map +1 -1
  17. package/dist/User.test.js +3 -2
  18. package/dist/User.test.js.map +1 -1
  19. package/dist/conf/d.d.ts +250 -3
  20. package/dist/conf/defaults.js +146 -98
  21. package/dist/conf/defaults.js.map +1 -1
  22. package/dist/enforceTilmeld.d.ts +3 -0
  23. package/dist/enforceTilmeld.js +22 -0
  24. package/dist/enforceTilmeld.js.map +1 -0
  25. package/dist/errors/AccessControlError.d.ts +2 -1
  26. package/dist/errors/AccessControlError.js +3 -2
  27. package/dist/errors/AccessControlError.js.map +1 -1
  28. package/dist/errors/BadDataError.d.ts +2 -1
  29. package/dist/errors/BadDataError.js +3 -2
  30. package/dist/errors/BadDataError.js.map +1 -1
  31. package/dist/errors/BadEmailError.d.ts +2 -1
  32. package/dist/errors/BadEmailError.js +3 -2
  33. package/dist/errors/BadEmailError.js.map +1 -1
  34. package/dist/errors/BadUsernameError.d.ts +2 -1
  35. package/dist/errors/BadUsernameError.js +3 -2
  36. package/dist/errors/BadUsernameError.js.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +1 -0
  39. package/dist/index.js.map +1 -1
  40. package/package.json +30 -26
  41. package/src/AbleObject.ts +2 -2
  42. package/src/Group.test.ts +3 -3
  43. package/src/Group.ts +194 -143
  44. package/src/Tilmeld.ts +384 -249
  45. package/src/User.test.ts +3 -3
  46. package/src/User.ts +876 -266
  47. package/src/conf/d.ts +26 -13
  48. package/src/conf/defaults.ts +151 -104
  49. package/src/enforceTilmeld.ts +20 -0
  50. package/src/errors/AccessControlError.ts +4 -2
  51. package/src/errors/BadDataError.ts +4 -2
  52. package/src/errors/BadEmailError.ts +4 -2
  53. package/src/errors/BadUsernameError.ts +4 -2
  54. package/src/index.ts +2 -0
  55. package/src/tilmeldJoiProps.ts +9 -9
  56. package/tsconfig.json +1 -1
  57. package/typedoc.json +4 -0
package/dist/Tilmeld.js CHANGED
@@ -10,104 +10,216 @@ const conf_1 = require("./conf");
10
10
  const errors_1 = require("./errors");
11
11
  const Group_1 = __importDefault(require("./Group"));
12
12
  const User_1 = __importDefault(require("./User"));
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
+ */
13
23
  class Tilmeld {
24
+ /**
25
+ * Create a new instance of Tilmeld.
26
+ *
27
+ * @param config The Tilmeld configuration.
28
+ */
14
29
  constructor(config) {
30
+ /**
31
+ * The Nymph instance.
32
+ */
15
33
  this.nymph = {};
34
+ /**
35
+ * The currently logged in user.
36
+ */
16
37
  this.currentUser = null;
38
+ /**
39
+ * The user class for this instance of Tilmeld.
40
+ */
17
41
  this.User = User_1.default;
42
+ /**
43
+ * The group class for this instance of Tilmeld.
44
+ */
18
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
+ */
19
60
  this.request = null;
20
61
  this.response = null;
21
- this.skipAuthenticate = false;
62
+ /**
63
+ * Used to avoid infinite loop.
64
+ */
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
+ */
72
+ this.gatekeeperCache = null;
22
73
  this.config = { ...conf_1.ConfigDefaults, ...config };
74
+ if (this.config.emailUsernames &&
75
+ (!this.config.userFields.includes('email') ||
76
+ !this.config.regFields.includes('email'))) {
77
+ throw new Error('You must include email in userFields and regFields if you use emailUsernames.');
78
+ }
79
+ for (let field of this.config.regFields) {
80
+ if (!this.config.userFields.includes(field)) {
81
+ throw new Error(`All fields in regFields must also be in userFields: ${field}`);
82
+ }
83
+ }
23
84
  }
85
+ /**
86
+ * This is used internally by Nymph. Don't call it yourself.
87
+ *
88
+ * @returns A clone of this instance.
89
+ */
24
90
  clone() {
25
91
  const tilmeld = new Tilmeld(this.config);
26
92
  tilmeld.request = this.request;
27
93
  tilmeld.response = this.response;
28
94
  tilmeld.currentUser = this.currentUser;
29
- tilmeld.skipAuthenticate = true;
95
+ tilmeld.gatekeeperCache = this.gatekeeperCache;
30
96
  return tilmeld;
31
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
+ */
32
105
  init(nymph) {
33
106
  this.nymph = nymph;
34
- const tilmeld = this;
35
107
  if (this.nymph.driver == null) {
36
108
  throw new Error("Tilmeld can't be configured before Nymph.");
37
109
  }
38
- class NymphUser extends User_1.default {
110
+ // Configure the classes.
111
+ if (nymph.parent) {
112
+ // This is a cloned Nymph, it should have classes already.
113
+ this.User = this.nymph.getEntityClass(User_1.default);
114
+ this.Group = this.nymph.getEntityClass(Group_1.default);
39
115
  }
40
- NymphUser.nymph = nymph;
41
- NymphUser.tilmeld = tilmeld;
42
- NymphUser.skipOnClone = true;
43
- this.User = this.nymph.addEntityClass(NymphUser);
44
- class NymphGroup extends Group_1.default {
45
- }
46
- NymphGroup.nymph = nymph;
47
- NymphGroup.tilmeld = tilmeld;
48
- NymphUser.skipOnClone = true;
49
- this.Group = this.nymph.addEntityClass(NymphGroup);
116
+ else {
117
+ // This is a new Nymph, add the classes.
118
+ this.User = this.nymph.addEntityClass(User_1.default);
119
+ this.Group = this.nymph.addEntityClass(Group_1.default);
120
+ }
121
+ // Clone the current user so its Nymph instance is correct.
122
+ if (this.currentUser) {
123
+ const currentUserGUID = this.currentUser.guid;
124
+ const currentUserCDate = this.currentUser.cdate;
125
+ const currentUserMDate = this.currentUser.mdate;
126
+ const currentUserTags = this.currentUser.tags;
127
+ const currentUserData = this.currentUser.$getData();
128
+ const currentUserSData = this.currentUser.$getSData();
129
+ const user = this.User.factorySync();
130
+ user.guid = currentUserGUID;
131
+ user.cdate = currentUserCDate;
132
+ user.mdate = currentUserMDate;
133
+ user.tags = currentUserTags;
134
+ user.$putData(currentUserData, currentUserSData);
135
+ this.currentUser = user;
136
+ this.currentUser.$updateDataProtection();
137
+ this.currentUser.$updateGroupDataProtection();
138
+ }
139
+ // Set up access control hooks when Nymph is called.
140
+ this.nymph.config.debugLog('tilmeld', 'Initializing access controls.');
50
141
  this.initAccessControl();
51
- if (this.request != null && !this.skipAuthenticate) {
52
- this.authenticate();
53
- }
54
- this.skipAuthenticate = false;
55
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
+ */
56
152
  gatekeeper(ability) {
57
- if (this.currentUser == null) {
153
+ if (this.currentUser == null || this.gatekeeperCache == null) {
58
154
  return false;
59
155
  }
60
- return this.currentUser.$gatekeeper(ability);
156
+ if (ability == null) {
157
+ return true;
158
+ }
159
+ return ((ability in this.gatekeeperCache && !!this.gatekeeperCache[ability]) ||
160
+ !!this.currentUser.abilities?.includes('system/admin'));
61
161
  }
62
162
  initAccessControl() {
163
+ // Check for the skip access control option and add AC selectors.
63
164
  const handleQuery = function (nymph, options, selectors) {
64
- const tilmeld = nymph.tilmeld;
165
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
65
166
  if (!options ||
66
167
  !('skipAc' in options) ||
67
168
  !options.skipAc ||
68
169
  options.source === 'client') {
69
170
  if (options?.source === 'client') {
70
- if (!tilmeld.gatekeeper('tilmeld/admin') &&
171
+ if ((!tilmeld.currentUser ||
172
+ (!tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
173
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) && // The user is not an admin.
71
174
  ((!tilmeld.config.enableUserSearch &&
72
- 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.
73
176
  (!tilmeld.config.enableGroupSearch &&
74
- options.class?.factorySync() instanceof Group_1.default)) &&
75
- (selectors[0] == null ||
76
- selectors[0].type !== '&' ||
77
- (!('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.
78
181
  ('guid' in selectors[0] &&
79
182
  (!selectors[0].guid?.length ||
80
- !selectors[0].guid?.[0]?.length)) ||
183
+ !selectors[0].guid?.[0]?.length)) || // Or they're not searching for a specific guid.
81
184
  ('equal' in selectors[0] &&
82
185
  selectors[0].equal?.[0]?.[0] !== 'username' &&
83
- 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.
84
190
  throw new errors_1.AccessControlError('No permission to search.');
85
191
  }
86
192
  }
193
+ // Add access control selectors
87
194
  tilmeld.addAccessControlSelectors(options, selectors);
88
195
  }
89
196
  };
90
197
  handleQuery.skipOnClone = true;
198
+ // Filter entities being deleted for user permissions.
91
199
  const checkPermissionsDelete = async function (nymph, entity) {
92
- const tilmeld = nymph.tilmeld;
200
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
93
201
  if (typeof entity.$tilmeldDeleteSkipAC === 'function' &&
94
202
  entity.$tilmeldDeleteSkipAC()) {
95
203
  return;
96
204
  }
205
+ // Test for permissions.
97
206
  if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.FULL_ACCESS)) {
98
207
  throw new errors_1.AccessControlError('No permission to delete.');
99
208
  }
100
209
  };
101
210
  checkPermissionsDelete.skipOnClone = true;
211
+ // Filter entities being deleted for user permissions.
102
212
  const checkPermissionsDeleteByID = async function (nymph, guid, className) {
103
- const entity = nymph.driver.getEntitySync({ class: nymph.getEntityClass(className ?? 'Entity') }, { type: '&', guid: guid });
213
+ const entity = await nymph.getEntity({ class: nymph.getEntityClass(className ?? 'Entity') }, { type: '&', guid: guid });
104
214
  if (entity != null) {
105
215
  checkPermissionsDelete(nymph, entity);
106
216
  }
107
217
  };
108
218
  checkPermissionsDeleteByID.skipOnClone = true;
219
+ // Filter entities being saved for user permissions, and filter any
220
+ // disallowed changes to AC properties.
109
221
  const checkPermissionsSaveAndFilterAcChanges = async function (nymph, entity) {
110
- const tilmeld = nymph.tilmeld;
222
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
111
223
  if (!entity) {
112
224
  return;
113
225
  }
@@ -116,29 +228,10 @@ class Tilmeld {
116
228
  return;
117
229
  }
118
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.
119
233
  const originalAc = entity.$getOriginalAcValues();
120
- const newAc = {
121
- user: entity.user ?? undefined,
122
- group: entity.group ?? undefined,
123
- acUser: entity.acUser ?? undefined,
124
- acGroup: entity.acGroup ?? undefined,
125
- acOther: entity.acOther ?? undefined,
126
- acRead: entity.acRead ?? undefined,
127
- acWrite: entity.acWrite ?? undefined,
128
- acFull: entity.acFull ?? undefined,
129
- };
130
- const setAcProperties = (acValues) => {
131
- for (let name in acValues) {
132
- const value = acValues[name];
133
- if (value == null) {
134
- delete entity[name];
135
- }
136
- else {
137
- entity[name] = value;
138
- }
139
- }
140
- };
141
- setAcProperties(originalAc);
234
+ const newAc = entity.$getCurrentAcValues();
142
235
  function areAcPropertiesChanged(a, b) {
143
236
  for (const name of ['user', 'group']) {
144
237
  const aVal = a[name];
@@ -149,7 +242,7 @@ class Tilmeld {
149
242
  else if (aVal == null || bVal == null) {
150
243
  return true;
151
244
  }
152
- if (!aVal.$is(bVal)) {
245
+ if (aVal !== bVal) {
153
246
  return true;
154
247
  }
155
248
  }
@@ -178,40 +271,55 @@ class Tilmeld {
178
271
  if (aVal.length !== bVal.length) {
179
272
  return true;
180
273
  }
181
- if ((0, lodash_1.xor)(aVal.map((entity) => entity.guid), bVal.map((entity) => entity.guid)).length) {
274
+ if ((0, lodash_1.xor)(aVal, bVal).length) {
182
275
  return true;
183
276
  }
184
277
  }
185
278
  return false;
186
279
  }
187
280
  if (areAcPropertiesChanged(originalAc, newAc)) {
188
- if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.FULL_ACCESS)) {
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.
189
283
  throw new errors_1.AccessControlError('No permission to change access control properties.');
190
284
  }
191
- if (((originalAc.user && !originalAc.user.$is(newAc.user)) ||
192
- (!originalAc.user &&
193
- newAc.user &&
194
- !newAc.user.$is(tilmeld.currentUser))) &&
195
- !tilmeld.gatekeeper('tilmeld/admin')) {
285
+ if ((originalAc.user != null && originalAc.user !== newAc.user) ||
286
+ (originalAc.user == null &&
287
+ newAc.user != null &&
288
+ newAc.user !== tilmeld.currentUser?.guid)) {
196
289
  throw new errors_1.AccessControlError('No permission to assign to another user.');
197
290
  }
198
- if (newAc.group &&
199
- !newAc.group.$is(originalAc.group) &&
200
- !(newAc.group.$is(tilmeld.currentUser?.group) ||
201
- newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])) &&
202
- !tilmeld.gatekeeper('tilmeld/admin')) {
291
+ if (newAc.group != null &&
292
+ newAc.group !== originalAc.group &&
293
+ !(newAc.group === tilmeld.currentUser?.$getGid() ||
294
+ (tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group))) {
203
295
  throw new errors_1.AccessControlError('No permission to assign to another group.');
204
296
  }
205
- setAcProperties(newAc);
206
297
  }
207
298
  }
299
+ // Test for permissions.
208
300
  if (!tilmeld.checkPermissions(entity, nymph_1.TilmeldAccessLevels.WRITE_ACCESS)) {
209
301
  throw new errors_1.AccessControlError('No permission to write.');
210
302
  }
211
303
  };
212
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
+ */
213
321
  const addAccess = async function (nymph, entity) {
214
- const tilmeld = nymph.tilmeld;
322
+ const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
215
323
  const user = tilmeld.currentUser;
216
324
  if (user != null &&
217
325
  entity.guid == null &&
@@ -220,10 +328,11 @@ class Tilmeld {
220
328
  if (entity.user == null) {
221
329
  entity.user = user;
222
330
  }
223
- if (entity.group == null &&
224
- user.group != null &&
225
- user.group.guid != null) {
226
- entity.group = user.group;
331
+ if (entity.group == null) {
332
+ const group = user.group;
333
+ if (group != null && group.guid != null) {
334
+ entity.group = group;
335
+ }
227
336
  }
228
337
  if (entity.acUser == null) {
229
338
  entity.acUser = nymph_1.TilmeldAccessLevels.FULL_ACCESS;
@@ -303,24 +412,29 @@ class Tilmeld {
303
412
  this.nymph.on('beforeDeleteEntity', checkPermissionsDelete);
304
413
  this.nymph.on('beforeDeleteEntityByID', checkPermissionsDeleteByID);
305
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
+ */
306
421
  addAccessControlSelectors(options, selectors) {
307
422
  const user = this.currentUser;
308
- if (user != null &&
309
- (user.abilities?.indexOf('system/admin') ?? -1) !== -1) {
310
- return;
311
- }
312
423
  if (options == null) {
313
424
  throw new Error('No options in argument.');
314
425
  }
315
426
  else if ('class' in options &&
316
427
  (options.class?.factorySync() instanceof User_1.default ||
317
428
  options.class?.factorySync() instanceof Group_1.default)) {
429
+ // They are requesting a user/group. Always accessible for reading.
318
430
  return;
319
431
  }
320
432
  if (user == null || user.guid == null) {
321
433
  selectors.push({
322
434
  type: '|',
435
+ // Other access control is sufficient.
323
436
  gte: [['acOther', nymph_1.TilmeldAccessLevels.READ_ACCESS]],
437
+ // The user and group are not set.
324
438
  selector: [
325
439
  {
326
440
  type: '&',
@@ -335,6 +449,7 @@ class Tilmeld {
335
449
  type: '&',
336
450
  '!defined': [['user', 'group']],
337
451
  },
452
+ // It is owned by the user.
338
453
  {
339
454
  type: '&',
340
455
  ref: [['user', user.guid]],
@@ -343,28 +458,25 @@ class Tilmeld {
343
458
  ];
344
459
  const groupRefs = [];
345
460
  const acRefs = [];
346
- if (user.group != null && user.group.guid != null) {
347
- groupRefs.push(['group', user.group.guid]);
348
- acRefs.push(['acRead', user.group.guid]);
349
- acRefs.push(['acWrite', user.group.guid]);
350
- acRefs.push(['acFull', user.group.guid]);
461
+ const gid = user.$getGid();
462
+ if (gid != null) {
463
+ // It belongs to the user's primary group.
464
+ groupRefs.push(['group', gid]);
465
+ // User's primary group is listed in acRead, acWrite, or acFull.
466
+ acRefs.push(['acRead', gid]);
467
+ acRefs.push(['acWrite', gid]);
468
+ acRefs.push(['acFull', gid]);
351
469
  }
352
- for (let curSecondaryGroup of user.groups ?? []) {
353
- if (curSecondaryGroup != null && curSecondaryGroup.guid != null) {
354
- groupRefs.push(['group', curSecondaryGroup.guid]);
355
- acRefs.push(['acRead', curSecondaryGroup.guid]);
356
- acRefs.push(['acWrite', curSecondaryGroup.guid]);
357
- acRefs.push(['acFull', curSecondaryGroup.guid]);
358
- }
359
- }
360
- for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
361
- if (curDescendantGroup != null && curDescendantGroup.guid != null) {
362
- groupRefs.push(['group', curDescendantGroup.guid]);
363
- acRefs.push(['acRead', curDescendantGroup.guid]);
364
- acRefs.push(['acWrite', curDescendantGroup.guid]);
365
- acRefs.push(['acFull', curDescendantGroup.guid]);
366
- }
470
+ const gids = user.$getGids();
471
+ for (let curGid of gids ?? []) {
472
+ // It belongs to the user's secondary group.
473
+ groupRefs.push(['group', curGid]);
474
+ // User's secondary group is listed in acRead, acWrite, or acFull.
475
+ acRefs.push(['acRead', curGid]);
476
+ acRefs.push(['acWrite', curGid]);
477
+ acRefs.push(['acFull', curGid]);
367
478
  }
479
+ // All the group refs.
368
480
  if (groupRefs.length) {
369
481
  subSelectors.push({
370
482
  type: '&',
@@ -377,6 +489,7 @@ class Tilmeld {
377
489
  ],
378
490
  });
379
491
  }
492
+ // All the acRead, acWrite, and acFull refs.
380
493
  if (acRefs.length) {
381
494
  subSelectors.push({
382
495
  type: '|',
@@ -385,8 +498,11 @@ class Tilmeld {
385
498
  }
386
499
  const selector = {
387
500
  type: '|',
501
+ // Other access control is sufficient.
388
502
  gte: [['acOther', nymph_1.TilmeldAccessLevels.READ_ACCESS]],
503
+ // The user and group are not set.
389
504
  selector: subSelectors,
505
+ // The user is listed in acRead, acWrite, or acFull.
390
506
  ref: [
391
507
  ['acRead', user.guid],
392
508
  ['acWrite', user.guid],
@@ -396,12 +512,76 @@ class Tilmeld {
396
512
  selectors.push(selector);
397
513
  }
398
514
  }
399
- checkPermissions(entity, type = nymph_1.TilmeldAccessLevels.READ_ACCESS, user) {
400
- if (typeof entity !== 'object' || typeof entity.$is !== 'function') {
401
- return false;
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
+ */
578
+ checkPermissions(entity, type = nymph_1.TilmeldAccessLevels.READ_ACCESS, user, acProperties) {
579
+ if (!acProperties) {
580
+ acProperties = entity.$getCurrentAcValues();
402
581
  }
403
582
  let userOrNull = null;
404
583
  let userOrEmpty = this.User.factorySync();
584
+ // Calculate the user.
405
585
  if (user == null) {
406
586
  userOrNull = this.currentUser;
407
587
  userOrEmpty = this.User.current(true);
@@ -410,131 +590,158 @@ class Tilmeld {
410
590
  userOrNull = user;
411
591
  userOrEmpty = user;
412
592
  }
413
- if (userOrEmpty.$gatekeeper('system/admin')) {
414
- return true;
415
- }
593
+ // Users and groups are always readable. Editable by Tilmeld admins.
416
594
  if ((entity instanceof User_1.default || entity instanceof Group_1.default) &&
417
595
  (type === nymph_1.TilmeldAccessLevels.READ_ACCESS ||
418
- userOrEmpty.$gatekeeper('tilmeld/admin'))) {
419
- return true;
420
- }
421
- if (entity.user == null && entity.group == null) {
596
+ userOrEmpty.abilities?.includes('tilmeld/admin') ||
597
+ userOrEmpty.abilities?.includes('system/admin'))) {
422
598
  return true;
423
599
  }
424
- const acUser = entity.acUser ?? nymph_1.TilmeldAccessLevels.FULL_ACCESS;
425
- const acGroup = entity.acGroup ?? nymph_1.TilmeldAccessLevels.READ_ACCESS;
426
- const acOther = entity.acOther ?? nymph_1.TilmeldAccessLevels.NO_ACCESS;
427
- if (userOrNull === null) {
600
+ // Load access control, since we need it now...
601
+ const acUser = acProperties.acUser ?? nymph_1.TilmeldAccessLevels.FULL_ACCESS;
602
+ const acGroup = acProperties.acGroup ?? nymph_1.TilmeldAccessLevels.READ_ACCESS;
603
+ const acOther = acProperties.acOther ?? nymph_1.TilmeldAccessLevels.NO_ACCESS;
604
+ if (userOrNull == null) {
428
605
  return acOther >= type;
429
606
  }
607
+ // Check if the entity is the user.
430
608
  if (userOrEmpty.$is(entity)) {
431
609
  return true;
432
610
  }
433
- if (userOrEmpty.group != null &&
434
- typeof userOrEmpty.group.$is === 'function' &&
435
- userOrEmpty.group.$is(entity) &&
611
+ // Check if the entity is the user's group. Always readable.
612
+ const gid = userOrEmpty.$getGid();
613
+ if (gid != null &&
614
+ entity.guid === gid &&
436
615
  type === nymph_1.TilmeldAccessLevels.READ_ACCESS) {
437
616
  return true;
438
617
  }
439
- let allGroups = userOrEmpty.group == null ? [] : [userOrEmpty.group];
440
- allGroups = allGroups.concat(userOrEmpty.groups ?? []);
441
- allGroups = allGroups.concat(userOrEmpty.$getDescendantGroupsSync());
618
+ // Calculate all the groups the user belongs to.
619
+ let allGids = gid == null ? [] : [gid];
620
+ allGids = allGids.concat(userOrEmpty.$getGids() ?? []);
621
+ // Check access ac properties.
442
622
  const checks = [
443
623
  {
444
624
  type: nymph_1.TilmeldAccessLevels.FULL_ACCESS,
445
- array: [...(entity.acFull ?? [])],
625
+ array: [...(acProperties.acFull ?? [])],
446
626
  },
447
627
  {
448
628
  type: nymph_1.TilmeldAccessLevels.WRITE_ACCESS,
449
- array: [...(entity.acWrite ?? [])],
629
+ array: [...(acProperties.acWrite ?? [])],
450
630
  },
451
631
  {
452
632
  type: nymph_1.TilmeldAccessLevels.READ_ACCESS,
453
- array: [...(entity.acRead ?? [])],
633
+ array: [...(acProperties.acRead ?? [])],
454
634
  },
455
635
  ];
456
636
  for (let curCheck of checks) {
457
637
  if (type <= curCheck.type) {
458
- if (userOrEmpty.$inArray(curCheck.array)) {
638
+ if (curCheck.array.includes(userOrEmpty.guid)) {
459
639
  return true;
460
640
  }
461
- for (let curGroup of allGroups) {
462
- if (typeof curGroup.$inArray === 'function' &&
463
- curGroup.$inArray(curCheck.array)) {
641
+ for (let curGid of allGids) {
642
+ if (curCheck.array.includes(curGid)) {
464
643
  return true;
465
644
  }
466
645
  }
467
646
  }
468
647
  }
469
- if (entity.user != null &&
470
- typeof entity.user.$is === 'function' &&
471
- entity.user.$is(userOrNull)) {
648
+ // Check ownership ac properties.
649
+ if (acProperties.user != null && acProperties.user === userOrNull.guid) {
472
650
  return acUser >= type;
473
651
  }
474
- if (entity.group != null &&
475
- typeof entity.group.$inArray === 'function' &&
476
- entity.group.$inArray(allGroups)) {
652
+ if (acProperties.group != null && allGids.includes(acProperties.group)) {
477
653
  return acGroup >= type;
478
654
  }
479
655
  return acOther >= type;
480
656
  }
481
- checkClientUIDPermissions(name, type = nymph_1.TilmeldAccessLevels.READ_ACCESS, user) {
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
+ */
681
+ async checkClientUIDPermissions(name, type = nymph_1.TilmeldAccessLevels.READ_ACCESS, user) {
482
682
  let userOrEmpty = this.User.factorySync();
683
+ // Calculate the user.
483
684
  if (user == null) {
484
685
  userOrEmpty = this.User.current(true);
485
686
  }
486
687
  else if (user !== false) {
487
688
  userOrEmpty = user;
488
689
  }
489
- if (userOrEmpty.$gatekeeper('system/admin')) {
690
+ if (userOrEmpty.abilities?.includes('system/admin')) {
490
691
  return true;
491
692
  }
492
693
  if (type === nymph_1.TilmeldAccessLevels.FULL_ACCESS) {
493
- return (this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
494
- userOrEmpty.$gatekeeper(`uid/set/${name}`));
694
+ return (this.config.clientSetabledUIDs.includes(name) ||
695
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`)));
495
696
  }
496
697
  else if (type === nymph_1.TilmeldAccessLevels.WRITE_ACCESS) {
497
- return (this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
498
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
499
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
500
- userOrEmpty.$gatekeeper(`uid/set/${name}`));
698
+ return (this.config.clientEnabledUIDs.includes(name) ||
699
+ this.config.clientSetabledUIDs.includes(name) ||
700
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
701
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`)));
501
702
  }
502
703
  else if (type === nymph_1.TilmeldAccessLevels.READ_ACCESS) {
503
- return (this.config.clientReadableUIDs.indexOf(name) !== -1 ||
504
- this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
505
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
506
- userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
507
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
508
- userOrEmpty.$gatekeeper(`uid/set/${name}`));
704
+ return (this.config.clientReadableUIDs.includes(name) ||
705
+ this.config.clientEnabledUIDs.includes(name) ||
706
+ this.config.clientSetabledUIDs.includes(name) ||
707
+ (await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
708
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
709
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`)));
509
710
  }
510
711
  return false;
511
712
  }
512
- fillSession(user) {
513
- const _group = user.group;
514
- const _groups = user.groups;
713
+ /**
714
+ * Fill session user data.
715
+ *
716
+ * @param user The user.
717
+ */
718
+ async fillSession(user) {
515
719
  this.currentUser = user;
720
+ this.gatekeeperCache = await user.$getGatekeeperCache();
721
+ // Now update the data protection on the user and all the groups.
516
722
  this.currentUser.$updateDataProtection();
517
- if (this.currentUser.group != null) {
518
- this.currentUser.group.$updateDataProtection();
519
- }
520
- for (let group of this.currentUser.groups ?? []) {
521
- group.$updateDataProtection();
522
- }
723
+ this.currentUser.$updateGroupDataProtection();
523
724
  }
725
+ /**
726
+ * Clear session user data.
727
+ */
524
728
  clearSession() {
525
729
  const user = this.currentUser;
526
730
  this.currentUser = null;
731
+ this.gatekeeperCache = null;
527
732
  if (user) {
528
733
  user.$updateDataProtection();
529
- if (user.group != null) {
530
- user.group.$updateDataProtection();
531
- }
532
- for (let group of user.groups ?? []) {
533
- group.$updateDataProtection();
534
- }
734
+ user.$updateGroupDataProtection();
535
735
  }
536
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
+ */
537
743
  async extractToken(token) {
744
+ this.nymph.config.debugLog('tilmeld', 'Extracting and validating token.');
538
745
  const extract = this.config.jwtExtract(this.config, token);
539
746
  if (extract == null) {
540
747
  return null;
@@ -544,89 +751,246 @@ class Tilmeld {
544
751
  type: '&',
545
752
  guid: guid,
546
753
  });
547
- if (!user || !user.guid || !user.enabled) {
754
+ if (!user || !user.guid) {
548
755
  return null;
549
756
  }
550
757
  return user;
551
758
  }
552
- 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) {
553
777
  if (this.request == null) {
554
778
  return false;
555
779
  }
780
+ if (this.response &&
781
+ this.response.locals.user &&
782
+ this.response.locals.user instanceof this.User) {
783
+ // The user has already been authenticated through some other means.
784
+ const user = this.response.locals.user;
785
+ await this.fillSession(user);
786
+ return true;
787
+ }
556
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.
557
791
  let fromAuthHeader = false;
558
792
  let authToken;
793
+ let switchToken;
559
794
  if (this.request.header('x-tilmeldauth') != null) {
560
795
  fromAuthHeader = true;
561
796
  authToken = this.request.header('x-tilmeldauth');
797
+ switchToken =
798
+ this.request.header('x-tilmeldswitch') ||
799
+ 'TILMELDSWITCH' in cookies
800
+ ? cookies.TILMELDSWITCH
801
+ : null;
562
802
  }
563
803
  else if ('TILMELDAUTH' in cookies) {
564
804
  fromAuthHeader = false;
565
805
  authToken = cookies.TILMELDAUTH;
806
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
566
807
  }
567
808
  else {
568
809
  return false;
569
810
  }
570
- let extract;
811
+ let authExtract;
812
+ let switchExtract;
571
813
  if (skipXsrfToken ||
572
814
  this.request.originalUrl.startsWith(this.config.setupPath)) {
573
- extract = this.config.jwtExtract(this.config, authToken);
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.
817
+ authExtract = this.config.jwtExtract(this.config, authToken);
818
+ switchExtract = switchToken
819
+ ? this.config.jwtExtract(this.config, switchToken)
820
+ : null;
574
821
  }
575
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).
576
825
  const xsrfToken = this.request.header('x-xsrf-token');
577
826
  if (xsrfToken == null && !fromAuthHeader) {
578
827
  return false;
579
828
  }
580
- extract = this.config.jwtExtract(this.config, authToken, xsrfToken);
829
+ authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
830
+ switchExtract = switchToken
831
+ ? this.config.jwtExtract(this.config, switchToken)
832
+ : null;
581
833
  }
582
- if (extract == null) {
583
- this.logout();
834
+ if (authExtract == null) {
835
+ await this.logout();
584
836
  return false;
585
837
  }
586
- const { guid, expire } = extract;
587
- const user = this.User.factorySync(guid);
588
- if (user.guid == null || !user.enabled) {
589
- this.logout();
838
+ const { guid, issued, expire } = authExtract;
839
+ const { guid: switchGuid, expire: switchExpire } = switchExtract || {
840
+ guid: null,
841
+ expire: null,
842
+ };
843
+ const user = await this.User.factory(guid);
844
+ if (user.guid == null ||
845
+ !user.enabled ||
846
+ expire.getTime() <= Date.now() ||
847
+ issued.getTime() > Date.now() ||
848
+ (user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)) {
849
+ await this.logout();
590
850
  return false;
591
851
  }
592
- if (expire.valueOf() < Date.now() + this.config.jwtRenew * 1000) {
593
- this.login(user, fromAuthHeader);
852
+ if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
853
+ // Load the switch user.
854
+ const switchUser = await this.User.factory(switchGuid);
855
+ if (switchUser.guid == null) {
856
+ await this.logoutSwitch();
857
+ }
858
+ else {
859
+ await this.fillSession(switchUser);
860
+ if (this.response) {
861
+ this.response.locals.user = switchUser;
862
+ }
863
+ return true;
864
+ }
865
+ }
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.
870
+ await this.login(user, fromAuthHeader);
594
871
  }
595
872
  else {
596
- this.fillSession(user);
873
+ await this.fillSession(user);
874
+ }
875
+ if (this.response) {
876
+ this.response.locals.user = user;
597
877
  }
598
878
  return true;
599
879
  }
600
- login(user, sendAuthHeader) {
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
+ */
888
+ async login(user, sendAuthHeader, sendCookie = true) {
601
889
  if (user.guid != null && user.enabled) {
602
- if (this.response && !this.response.headersSent) {
603
- const token = this.config.jwtBuilder(this.config, user);
604
- const appUrl = new url_1.URL(this.config.appUrl);
605
- this.response.cookie('TILMELDAUTH', token, {
606
- domain: this.config.cookieDomain,
607
- path: this.config.cookiePath,
608
- maxAge: this.config.jwtExpire * 1000,
609
- secure: appUrl.protocol === 'https',
610
- httpOnly: false,
611
- sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
612
- });
613
- if (sendAuthHeader) {
614
- this.response.set('X-TILMELDAUTH', token);
890
+ if (this.response) {
891
+ if (!this.response.headersSent) {
892
+ const token = this.config.jwtBuilder(this.config, user);
893
+ if (sendCookie) {
894
+ const appUrl = new url_1.URL(this.config.appUrl);
895
+ this.response.cookie('TILMELDAUTH', token, {
896
+ domain: this.config.cookieDomain,
897
+ path: this.config.cookiePath,
898
+ maxAge: this.config.jwtExpire * 1000,
899
+ secure: appUrl.protocol === 'https:',
900
+ httpOnly: false, // Allow JS access (for CSRF protection).
901
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
902
+ });
903
+ }
904
+ if (sendAuthHeader) {
905
+ this.response.set('X-TILMELDAUTH', token);
906
+ }
907
+ }
908
+ this.response.locals.user = user;
909
+ }
910
+ await this.fillSession(user);
911
+ return true;
912
+ }
913
+ return false;
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
+ */
925
+ async loginSwitch(user, sendAuthHeader, sendCookie = true) {
926
+ if (user.guid != null) {
927
+ if (this.response) {
928
+ if (!this.response.headersSent) {
929
+ const token = this.config.jwtBuilder(this.config, user, true);
930
+ if (sendCookie) {
931
+ const appUrl = new url_1.URL(this.config.appUrl);
932
+ this.response.cookie('TILMELDSWITCH', token, {
933
+ domain: this.config.cookieDomain,
934
+ path: this.config.cookiePath,
935
+ maxAge: this.config.jwtSwitchExpire * 1000,
936
+ secure: appUrl.protocol === 'https:',
937
+ httpOnly: false, // Allow JS access (for PubSub auth token).
938
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
939
+ });
940
+ }
941
+ if (sendAuthHeader) {
942
+ this.response.set('X-TILMELDSWITCH', token);
943
+ }
615
944
  }
945
+ this.response.locals.user = user;
616
946
  }
617
- this.fillSession(user);
947
+ await this.fillSession(user);
618
948
  return true;
619
949
  }
620
950
  return false;
621
951
  }
622
- logout() {
952
+ /**
953
+ * Logs the current user out of the system.
954
+ *
955
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
956
+ */
957
+ async logout(clearCookie = true) {
958
+ if (this.request && !this.alreadyLoggedOutSwitch) {
959
+ const cookies = this.request.cookies ?? {};
960
+ if ('TILMELDSWITCH' in cookies) {
961
+ this.alreadyLoggedOutSwitch = true;
962
+ return await this.logoutSwitch(clearCookie);
963
+ }
964
+ }
623
965
  this.clearSession();
624
- if (this.response && !this.response.headersSent) {
625
- this.response.clearCookie('TILMELDAUTH', {
626
- domain: this.config.cookieDomain,
627
- path: this.config.cookiePath,
628
- });
629
- this.response.set('X-TILMELDAUTH', '');
966
+ if (this.response) {
967
+ if (clearCookie && !this.response.headersSent) {
968
+ this.response.clearCookie('TILMELDAUTH', {
969
+ domain: this.config.cookieDomain,
970
+ path: this.config.cookiePath,
971
+ });
972
+ this.response.set('X-TILMELDAUTH', '');
973
+ }
974
+ this.response.locals.user = null;
975
+ }
976
+ }
977
+ /**
978
+ * Clears the switch user out of the system.
979
+ *
980
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
981
+ */
982
+ async logoutSwitch(clearCookie = true) {
983
+ this.clearSession();
984
+ if (this.response) {
985
+ if (clearCookie && !this.response.headersSent) {
986
+ this.response.clearCookie('TILMELDSWITCH', {
987
+ domain: this.config.cookieDomain,
988
+ path: this.config.cookiePath,
989
+ });
990
+ this.response.set('X-TILMELDSWITCH', '');
991
+ }
992
+ this.response.locals.user = null;
993
+ await this.authenticate(false, true);
630
994
  }
631
995
  }
632
996
  }