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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +414 -0
  2. package/README.md +15 -2
  3. package/dist/AbleObject.d.ts +15 -0
  4. package/dist/AbleObject.js +18 -6
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +181 -15
  7. package/dist/Group.js +551 -192
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.test.js +83 -13
  10. package/dist/Group.test.js.map +1 -1
  11. package/dist/Tilmeld.d.ts +241 -12
  12. package/dist/Tilmeld.js +636 -254
  13. package/dist/Tilmeld.js.map +1 -1
  14. package/dist/Tilmeld.types.d.ts +2 -2
  15. package/dist/Tilmeld.types.js +1 -2
  16. package/dist/User.d.ts +395 -22
  17. package/dist/User.js +1316 -365
  18. package/dist/User.js.map +1 -1
  19. package/dist/User.test.js +132 -13
  20. package/dist/User.test.js.map +1 -1
  21. package/dist/conf/d.d.ts +287 -7
  22. package/dist/conf/d.js +1 -2
  23. package/dist/conf/defaults.d.ts +1 -1
  24. package/dist/conf/defaults.js +188 -141
  25. package/dist/conf/defaults.js.map +1 -1
  26. package/dist/conf/index.d.ts +2 -2
  27. package/dist/conf/index.js +2 -8
  28. package/dist/conf/index.js.map +1 -1
  29. package/dist/enforceTilmeld.d.ts +3 -0
  30. package/dist/enforceTilmeld.js +18 -0
  31. package/dist/enforceTilmeld.js.map +1 -0
  32. package/dist/errors/AccessControlError.d.ts +2 -1
  33. package/dist/errors/AccessControlError.js +3 -6
  34. package/dist/errors/AccessControlError.js.map +1 -1
  35. package/dist/errors/BadDataError.d.ts +2 -1
  36. package/dist/errors/BadDataError.js +3 -6
  37. package/dist/errors/BadDataError.js.map +1 -1
  38. package/dist/errors/BadEmailError.d.ts +2 -1
  39. package/dist/errors/BadEmailError.js +3 -6
  40. package/dist/errors/BadEmailError.js.map +1 -1
  41. package/dist/errors/BadUsernameError.d.ts +2 -1
  42. package/dist/errors/BadUsernameError.js +3 -6
  43. package/dist/errors/BadUsernameError.js.map +1 -1
  44. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js +1 -5
  45. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js.map +1 -1
  46. package/dist/errors/EmailChangeRateLimitExceededError.js +1 -5
  47. package/dist/errors/EmailChangeRateLimitExceededError.js.map +1 -1
  48. package/dist/errors/index.d.ts +6 -6
  49. package/dist/errors/index.js +6 -22
  50. package/dist/errors/index.js.map +1 -1
  51. package/dist/index.d.ts +11 -10
  52. package/dist/index.js +16 -35
  53. package/dist/index.js.map +1 -1
  54. package/dist/tilmeldJoiProps.js +13 -18
  55. package/dist/tilmeldJoiProps.js.map +1 -1
  56. package/jest.config.js +11 -2
  57. package/package.json +34 -31
  58. package/src/AbleObject.ts +2 -2
  59. package/src/Group.test.ts +104 -6
  60. package/src/Group.ts +661 -226
  61. package/src/Tilmeld.ts +420 -254
  62. package/src/Tilmeld.types.ts +2 -2
  63. package/src/User.test.ts +163 -6
  64. package/src/User.ts +1557 -454
  65. package/src/conf/d.ts +63 -17
  66. package/src/conf/defaults.ts +162 -109
  67. package/src/conf/index.ts +2 -2
  68. package/src/enforceTilmeld.ts +20 -0
  69. package/src/errors/AccessControlError.ts +4 -2
  70. package/src/errors/BadDataError.ts +4 -2
  71. package/src/errors/BadEmailError.ts +4 -2
  72. package/src/errors/BadUsernameError.ts +4 -2
  73. package/src/errors/index.ts +6 -6
  74. package/src/index.ts +12 -10
  75. package/src/tilmeldJoiProps.ts +11 -11
  76. package/src/type/locutus.d.ts +3 -0
  77. package/tsconfig.json +5 -3
  78. package/typedoc.json +4 -0
package/src/Tilmeld.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { URL } from 'node:url';
1
2
  import {
2
3
  Nymph,
3
4
  EntityInterface,
@@ -5,16 +6,18 @@ import {
5
6
  Options,
6
7
  TilmeldInterface,
7
8
  TilmeldAccessLevels,
9
+ ACProperties,
8
10
  } from '@nymphjs/nymph';
9
- import { URL } from 'url';
10
11
  import { Request, Response } from 'express';
11
- import { xor } from 'lodash';
12
+ import { xor } from 'lodash-es';
13
+ import { splitn } from '@sciactive/splitn';
12
14
 
13
- import { Config, ConfigDefaults as defaults } from './conf';
14
- import { AccessControlError } from './errors';
15
- import Group from './Group';
16
- import User, { UserData } from './User';
17
- import { AccessControlData } from './Tilmeld.types';
15
+ import { Config, ConfigDefaults as defaults } from './conf/index.js';
16
+ import { AccessControlError } from './errors/index.js';
17
+ import Group, { GroupData } from './Group.js';
18
+ import User, { UserData } from './User.js';
19
+ import { AccessControlData } from './Tilmeld.types.js';
20
+ import { enforceTilmeld } from './enforceTilmeld.js';
18
21
 
19
22
  /**
20
23
  * A user and group system for Nymph.js.
@@ -69,9 +72,17 @@ export default class Tilmeld implements TilmeldInterface {
69
72
  public response: Response | null = null;
70
73
 
71
74
  /**
72
- * Skip the authenticate step of initialization.
75
+ * Used to avoid infinite loop.
73
76
  */
74
- private skipAuthenticate = false;
77
+ private alreadyLoggedOutSwitch = false;
78
+
79
+ /**
80
+ * Gatekeeper ability cache.
81
+ *
82
+ * Gatekeeper will cache the user's abilities that it calculates, so it can
83
+ * check faster if that user has been checked before.
84
+ */
85
+ private gatekeeperCache: { [k: string]: true } | null = null;
75
86
 
76
87
  /**
77
88
  * Create a new instance of Tilmeld.
@@ -80,6 +91,30 @@ export default class Tilmeld implements TilmeldInterface {
80
91
  */
81
92
  public constructor(config: Partial<Config> & { jwtSecret: string }) {
82
93
  this.config = { ...defaults, ...config };
94
+
95
+ if (
96
+ this.config.emailUsernames &&
97
+ (!this.config.userFields.includes('email') ||
98
+ !this.config.regFields.includes('email'))
99
+ ) {
100
+ throw new Error(
101
+ 'You must include email in userFields and regFields if you use emailUsernames.',
102
+ );
103
+ }
104
+
105
+ for (let field of this.config.regFields) {
106
+ if (!this.config.userFields.includes(field)) {
107
+ throw new Error(
108
+ `All fields in regFields must also be in userFields: ${field}`,
109
+ );
110
+ }
111
+ }
112
+
113
+ if (this.config.emailUsernames && this.config.domainSupport) {
114
+ throw new Error(
115
+ 'Email usernames and domain support cannot both be enabled.',
116
+ );
117
+ }
83
118
  }
84
119
 
85
120
  /**
@@ -92,7 +127,7 @@ export default class Tilmeld implements TilmeldInterface {
92
127
  tilmeld.request = this.request;
93
128
  tilmeld.response = this.response;
94
129
  tilmeld.currentUser = this.currentUser;
95
- tilmeld.skipAuthenticate = true;
130
+ tilmeld.gatekeeperCache = this.gatekeeperCache;
96
131
 
97
132
  return tilmeld;
98
133
  }
@@ -106,32 +141,47 @@ export default class Tilmeld implements TilmeldInterface {
106
141
  */
107
142
  public init(nymph: Nymph) {
108
143
  this.nymph = nymph;
109
- const tilmeld = this;
110
144
 
111
- // Set up access control hooks when Nymph is called.
112
145
  if (this.nymph.driver == null) {
113
146
  throw new Error("Tilmeld can't be configured before Nymph.");
114
147
  }
115
148
 
116
149
  // Configure the classes.
117
- class NymphUser extends User {
118
- static nymph: Nymph = nymph;
119
- static tilmeld: Tilmeld = tilmeld;
150
+ if (nymph.parent) {
151
+ // This is a cloned Nymph, it should have classes already.
152
+ this.User = this.nymph.getEntityClass(User);
153
+ this.Group = this.nymph.getEntityClass(Group);
154
+ } else {
155
+ // This is a new Nymph, add the classes.
156
+ this.User = this.nymph.addEntityClass(User);
157
+ this.Group = this.nymph.addEntityClass(Group);
120
158
  }
121
- (NymphUser as any).skipOnClone = true;
122
- this.User = this.nymph.addEntityClass(NymphUser);
123
- class NymphGroup extends Group {
124
- static nymph: Nymph = nymph;
125
- static tilmeld: Tilmeld = tilmeld;
159
+
160
+ // Clone the current user so its Nymph instance is correct.
161
+ if (this.currentUser) {
162
+ const currentUserGUID = this.currentUser.guid;
163
+ const currentUserCDate = this.currentUser.cdate;
164
+ const currentUserMDate = this.currentUser.mdate;
165
+ const currentUserTags = this.currentUser.tags;
166
+ const currentUserData = this.currentUser.$getData();
167
+ const currentUserSData = this.currentUser.$getSData();
168
+
169
+ const user = this.User.factorySync();
170
+ user.guid = currentUserGUID;
171
+ user.cdate = currentUserCDate;
172
+ user.mdate = currentUserMDate;
173
+ user.tags = currentUserTags;
174
+ user.$putData(currentUserData, currentUserSData);
175
+ user.$originalEmail = this.currentUser.$originalEmail;
176
+ user.$originalUsername = this.currentUser.$originalUsername;
177
+ this.currentUser = user;
178
+ this.currentUser.$updateDataProtection();
179
+ this.currentUser.$updateGroupDataProtection();
126
180
  }
127
- (NymphUser as any).skipOnClone = true;
128
- this.Group = this.nymph.addEntityClass(NymphGroup);
129
181
 
182
+ // Set up access control hooks when Nymph is called.
183
+ this.nymph.config.debugInfo('tilmeld', 'Initializing access controls.');
130
184
  this.initAccessControl();
131
- if (this.request != null && !this.skipAuthenticate) {
132
- this.authenticate();
133
- }
134
- this.skipAuthenticate = false;
135
185
  }
136
186
 
137
187
  /**
@@ -143,11 +193,17 @@ export default class Tilmeld implements TilmeldInterface {
143
193
  * @param ability The ability.
144
194
  * @returns Whether the user has the given ability.
145
195
  */
146
- public gatekeeper(ability?: string): boolean {
147
- if (this.currentUser == null) {
196
+ public gatekeeper(ability?: string) {
197
+ if (this.currentUser == null || this.gatekeeperCache == null) {
148
198
  return false;
149
199
  }
150
- return this.currentUser.$gatekeeper(ability);
200
+ if (ability == null) {
201
+ return true;
202
+ }
203
+ return (
204
+ (ability in this.gatekeeperCache && !!this.gatekeeperCache[ability]) ||
205
+ !!this.currentUser.abilities?.includes('system/admin')
206
+ );
151
207
  }
152
208
 
153
209
  private initAccessControl() {
@@ -155,9 +211,9 @@ export default class Tilmeld implements TilmeldInterface {
155
211
  const handleQuery = function (
156
212
  nymph: Nymph,
157
213
  options: Options,
158
- selectors: FormattedSelector[]
214
+ selectors: FormattedSelector[],
159
215
  ) {
160
- const tilmeld = nymph.tilmeld as Tilmeld;
216
+ const tilmeld = enforceTilmeld(nymph);
161
217
  if (
162
218
  !options ||
163
219
  !('skipAc' in options) ||
@@ -166,7 +222,9 @@ export default class Tilmeld implements TilmeldInterface {
166
222
  ) {
167
223
  if (options?.source === 'client') {
168
224
  if (
169
- !tilmeld.gatekeeper('tilmeld/admin') && // The user is not an admin.
225
+ (!tilmeld.currentUser ||
226
+ (!tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
227
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) && // The user is not an admin.
170
228
  ((!tilmeld.config.enableUserSearch &&
171
229
  options.class?.factorySync() instanceof User) || // The use is searching for a user and searching is disabled.
172
230
  (!tilmeld.config.enableGroupSearch &&
@@ -195,9 +253,9 @@ export default class Tilmeld implements TilmeldInterface {
195
253
  // Filter entities being deleted for user permissions.
196
254
  const checkPermissionsDelete = async function (
197
255
  nymph: Nymph,
198
- entity: EntityInterface
256
+ entity: EntityInterface,
199
257
  ) {
200
- const tilmeld = nymph.tilmeld as Tilmeld;
258
+ const tilmeld = enforceTilmeld(nymph);
201
259
  if (
202
260
  typeof entity.$tilmeldDeleteSkipAC === 'function' &&
203
261
  entity.$tilmeldDeleteSkipAC()
@@ -215,11 +273,11 @@ export default class Tilmeld implements TilmeldInterface {
215
273
  const checkPermissionsDeleteByID = async function (
216
274
  nymph: Nymph,
217
275
  guid: string,
218
- className?: string
276
+ className?: string,
219
277
  ) {
220
- const entity = nymph.driver.getEntitySync(
278
+ const entity = await nymph.getEntity(
221
279
  { class: nymph.getEntityClass(className ?? 'Entity') },
222
- { type: '&', guid: guid }
280
+ { type: '&', guid: guid },
223
281
  );
224
282
  if (entity != null) {
225
283
  checkPermissionsDelete(nymph, entity);
@@ -231,9 +289,9 @@ export default class Tilmeld implements TilmeldInterface {
231
289
  // disallowed changes to AC properties.
232
290
  const checkPermissionsSaveAndFilterAcChanges = async function (
233
291
  nymph: Nymph,
234
- entity: EntityInterface & AccessControlData
292
+ entity: EntityInterface & AccessControlData,
235
293
  ) {
236
- const tilmeld = nymph.tilmeld as Tilmeld;
294
+ const tilmeld = enforceTilmeld(nymph);
237
295
  if (!entity) {
238
296
  return;
239
297
  }
@@ -249,35 +307,9 @@ export default class Tilmeld implements TilmeldInterface {
249
307
  // allowing a change to ac properties.
250
308
 
251
309
  const originalAc = entity.$getOriginalAcValues();
252
- const newAc: AccessControlData = {
253
- user: entity.user ?? undefined,
254
- group: entity.group ?? undefined,
255
- acUser: entity.acUser ?? undefined,
256
- acGroup: entity.acGroup ?? undefined,
257
- acOther: entity.acOther ?? undefined,
258
- acRead: entity.acRead ?? undefined,
259
- acWrite: entity.acWrite ?? undefined,
260
- acFull: entity.acFull ?? undefined,
261
- };
310
+ const newAc = entity.$getCurrentAcValues();
262
311
 
263
- const setAcProperties = (acValues: { [k: string]: any }) => {
264
- for (let name in acValues) {
265
- const value = acValues[name];
266
- if (value == null) {
267
- delete entity[name];
268
- } else {
269
- entity[name] = value;
270
- }
271
- }
272
- };
273
-
274
- // Restore original AC properties and check permissions.
275
- setAcProperties(originalAc);
276
-
277
- function areAcPropertiesChanged(
278
- a: AccessControlData,
279
- b: AccessControlData
280
- ) {
312
+ function areAcPropertiesChanged(a: ACProperties, b: ACProperties) {
281
313
  for (const name of ['user', 'group'] as ('user' | 'group')[]) {
282
314
  const aVal = a[name];
283
315
  const bVal = b[name];
@@ -286,7 +318,7 @@ export default class Tilmeld implements TilmeldInterface {
286
318
  } else if (aVal == null || bVal == null) {
287
319
  return true;
288
320
  }
289
- if (!aVal.$is(bVal)) {
321
+ if (aVal !== bVal) {
290
322
  return true;
291
323
  }
292
324
  }
@@ -321,12 +353,7 @@ export default class Tilmeld implements TilmeldInterface {
321
353
  if (aVal.length !== bVal.length) {
322
354
  return true;
323
355
  }
324
- if (
325
- xor(
326
- aVal.map((entity) => entity.guid),
327
- bVal.map((entity) => entity.guid)
328
- ).length
329
- ) {
356
+ if (xor(aVal, bVal).length) {
330
357
  return true;
331
358
  }
332
359
  }
@@ -335,41 +362,42 @@ export default class Tilmeld implements TilmeldInterface {
335
362
 
336
363
  if (areAcPropertiesChanged(originalAc, newAc)) {
337
364
  if (
338
- !tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)
365
+ !tilmeld.checkPermissions(
366
+ entity,
367
+ TilmeldAccessLevels.FULL_ACCESS,
368
+ undefined,
369
+ originalAc,
370
+ )
339
371
  ) {
340
372
  // Only allow changes to AC properties if the user has full access.
341
373
  throw new AccessControlError(
342
- 'No permission to change access control properties.'
374
+ 'No permission to change access control properties.',
343
375
  );
344
376
  }
345
377
 
346
378
  if (
347
- ((originalAc.user && !originalAc.user.$is(newAc.user)) ||
348
- (!originalAc.user &&
349
- newAc.user &&
350
- !newAc.user.$is(tilmeld.currentUser))) &&
351
- !tilmeld.gatekeeper('tilmeld/admin')
379
+ (originalAc.user != null && originalAc.user !== newAc.user) ||
380
+ (originalAc.user == null &&
381
+ newAc.user != null &&
382
+ newAc.user !== tilmeld.currentUser?.guid)
352
383
  ) {
353
384
  throw new AccessControlError(
354
- 'No permission to assign to another user.'
385
+ 'No permission to assign to another user.',
355
386
  );
356
387
  }
357
388
 
358
389
  if (
359
- newAc.group &&
360
- !newAc.group.$is(originalAc.group) &&
390
+ newAc.group != null &&
391
+ newAc.group !== originalAc.group &&
361
392
  !(
362
- newAc.group.$is(tilmeld.currentUser?.group) ||
363
- newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
364
- ) &&
365
- !tilmeld.gatekeeper('tilmeld/admin')
393
+ newAc.group === tilmeld.currentUser?.$getGid() ||
394
+ (tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group)
395
+ )
366
396
  ) {
367
397
  throw new AccessControlError(
368
- 'No permission to assign to another group.'
398
+ 'No permission to assign to another group.',
369
399
  );
370
400
  }
371
-
372
- setAcProperties(newAc);
373
401
  }
374
402
  }
375
403
 
@@ -398,9 +426,9 @@ export default class Tilmeld implements TilmeldInterface {
398
426
  */
399
427
  const addAccess = async function (
400
428
  nymph: Nymph,
401
- entity: EntityInterface & AccessControlData
429
+ entity: EntityInterface & AccessControlData,
402
430
  ) {
403
- const tilmeld = nymph.tilmeld as Tilmeld;
431
+ const tilmeld = enforceTilmeld(nymph);
404
432
  const user = tilmeld.currentUser;
405
433
  if (
406
434
  user != null &&
@@ -411,12 +439,11 @@ export default class Tilmeld implements TilmeldInterface {
411
439
  if (entity.user == null) {
412
440
  entity.user = user;
413
441
  }
414
- if (
415
- entity.group == null &&
416
- user.group != null &&
417
- user.group.guid != null
418
- ) {
419
- entity.group = user.group;
442
+ if (entity.group == null) {
443
+ const group = user.group;
444
+ if (group != null && group.guid != null) {
445
+ entity.group = group;
446
+ }
420
447
  }
421
448
  if (entity.acUser == null) {
422
449
  entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
@@ -442,7 +469,7 @@ export default class Tilmeld implements TilmeldInterface {
442
469
 
443
470
  const validate = async function (
444
471
  _nymph: Nymph,
445
- entity: EntityInterface & AccessControlData
472
+ entity: EntityInterface & AccessControlData,
446
473
  ) {
447
474
  if (!(entity instanceof User) && !(entity instanceof Group)) {
448
475
  const ownershipAcPropertyValidator = (prop: any) => {
@@ -452,7 +479,7 @@ export default class Tilmeld implements TilmeldInterface {
452
479
  prop > TilmeldAccessLevels.FULL_ACCESS
453
480
  ) {
454
481
  throw new AccessControlError(
455
- 'Invalid access control property: ' + prop
482
+ 'Invalid access control property: ' + prop,
456
483
  );
457
484
  }
458
485
  };
@@ -460,13 +487,13 @@ export default class Tilmeld implements TilmeldInterface {
460
487
  const accessAcPropertyValidator = (prop: any) => {
461
488
  if (!Array.isArray(prop)) {
462
489
  throw new AccessControlError(
463
- 'Invalid access control property: ' + prop
490
+ 'Invalid access control property: ' + prop,
464
491
  );
465
492
  }
466
493
  prop.forEach((value) => {
467
494
  if (!(value instanceof User || value instanceof Group)) {
468
495
  throw new AccessControlError(
469
- 'Invalid access control property: ' + prop
496
+ 'Invalid access control property: ' + prop,
470
497
  );
471
498
  }
472
499
  });
@@ -475,7 +502,7 @@ export default class Tilmeld implements TilmeldInterface {
475
502
  if ('user' in entity) {
476
503
  if (!(entity.user instanceof User)) {
477
504
  throw new AccessControlError(
478
- 'Invalid access control property: user'
505
+ 'Invalid access control property: user',
479
506
  );
480
507
  }
481
508
  }
@@ -483,7 +510,7 @@ export default class Tilmeld implements TilmeldInterface {
483
510
  if ('group' in entity) {
484
511
  if (!(entity.group instanceof Group)) {
485
512
  throw new AccessControlError(
486
- 'Invalid access control property: group'
513
+ 'Invalid access control property: group',
487
514
  );
488
515
  }
489
516
  }
@@ -528,18 +555,10 @@ export default class Tilmeld implements TilmeldInterface {
528
555
  */
529
556
  public addAccessControlSelectors(
530
557
  options: Options,
531
- selectors: FormattedSelector[]
558
+ selectors: FormattedSelector[],
532
559
  ) {
533
560
  const user = this.currentUser;
534
561
 
535
- if (
536
- user != null &&
537
- (user.abilities?.indexOf('system/admin') ?? -1) !== -1
538
- ) {
539
- // The user is a system admin, so they can see everything.
540
- return;
541
- }
542
-
543
562
  if (options == null) {
544
563
  throw new Error('No options in argument.');
545
564
  } else if (
@@ -579,33 +598,23 @@ export default class Tilmeld implements TilmeldInterface {
579
598
  ];
580
599
  const groupRefs: FormattedSelector['ref'] = [];
581
600
  const acRefs: FormattedSelector['ref'] = [];
582
- if (user.group != null && user.group.guid != null) {
601
+ const gid = user.$getGid();
602
+ if (gid != null) {
583
603
  // It belongs to the user's primary group.
584
- groupRefs.push(['group', user.group.guid]);
604
+ groupRefs.push(['group', gid]);
585
605
  // User's primary group is listed in acRead, acWrite, or acFull.
586
- acRefs.push(['acRead', user.group.guid]);
587
- acRefs.push(['acWrite', user.group.guid]);
588
- acRefs.push(['acFull', user.group.guid]);
606
+ acRefs.push(['acRead', gid]);
607
+ acRefs.push(['acWrite', gid]);
608
+ acRefs.push(['acFull', gid]);
589
609
  }
590
- for (let curSecondaryGroup of user.groups ?? []) {
591
- if (curSecondaryGroup != null && curSecondaryGroup.guid != null) {
592
- // It belongs to the user's secondary group.
593
- groupRefs.push(['group', curSecondaryGroup.guid]);
594
- // User's secondary group is listed in acRead, acWrite, or acFull.
595
- acRefs.push(['acRead', curSecondaryGroup.guid]);
596
- acRefs.push(['acWrite', curSecondaryGroup.guid]);
597
- acRefs.push(['acFull', curSecondaryGroup.guid]);
598
- }
599
- }
600
- for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
601
- if (curDescendantGroup != null && curDescendantGroup.guid != null) {
602
- // It belongs to the user's secondary group.
603
- groupRefs.push(['group', curDescendantGroup.guid]);
604
- // User's secondary group is listed in acRead, acWrite, or acFull.
605
- acRefs.push(['acRead', curDescendantGroup.guid]);
606
- acRefs.push(['acWrite', curDescendantGroup.guid]);
607
- acRefs.push(['acFull', curDescendantGroup.guid]);
608
- }
610
+ const gids = user.$getGids();
611
+ for (let curGid of gids ?? []) {
612
+ // It belongs to the user's secondary group.
613
+ groupRefs.push(['group', curGid]);
614
+ // User's secondary group is listed in acRead, acWrite, or acFull.
615
+ acRefs.push(['acRead', curGid]);
616
+ acRefs.push(['acWrite', curGid]);
617
+ acRefs.push(['acFull', curGid]);
609
618
  }
610
619
  // All the group refs.
611
620
  if (groupRefs.length) {
@@ -671,7 +680,7 @@ export default class Tilmeld implements TilmeldInterface {
671
680
  * - FULL_ACCESS, the user has read, write, and delete access to the entity,
672
681
  * as well as being able to manage its access controls and ownership.
673
682
  *
674
- * These properties defaults to:
683
+ * These properties default to:
675
684
  *
676
685
  * - acUser = TilmeldAccessLevels.FULL_ACCESS
677
686
  * - acGroup = TilmeldAccessLevels.READ_ACCESS
@@ -686,9 +695,9 @@ export default class Tilmeld implements TilmeldInterface {
686
695
  * The following conditions will result in different checks, which determine
687
696
  * whether the check passes:
688
697
  *
689
- * - The user has the "system/admin" ability. (Always true.)
690
698
  * - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
691
- * - The entity has no "user" and no "group". (Always true.)
699
+ * - It is a user or group that belongs to a domain the current user is an
700
+ * admin for, and domain support is enabled. (Always true.)
692
701
  * - No user is logged in. (Check other AC.)
693
702
  * - The entity is the user. (Always true.)
694
703
  * - It is the user's primary group. (True for READ_ACCESS.)
@@ -706,16 +715,17 @@ export default class Tilmeld implements TilmeldInterface {
706
715
  * @param entity The entity to check.
707
716
  * @param type The lowest level of permission to consider a pass.
708
717
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
718
+ * @param acProperties The acProperties to use instead of getting them from the entity.
709
719
  * @returns Whether the current user has at least `type` permission for the entity.
710
720
  */
711
721
  public checkPermissions(
712
- entity: EntityInterface & AccessControlData,
722
+ entity: EntityInterface,
713
723
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
714
- user?: (User & UserData) | false
724
+ user?: (User & UserData) | false,
725
+ acProperties?: ACProperties,
715
726
  ) {
716
- // Only works for entities.
717
- if (typeof entity !== 'object' || typeof entity.$is !== 'function') {
718
- return false;
727
+ if (!acProperties) {
728
+ acProperties = entity.$getCurrentAcValues() as ACProperties;
719
729
  }
720
730
 
721
731
  let userOrNull: (User & UserData) | null = null;
@@ -729,30 +739,43 @@ export default class Tilmeld implements TilmeldInterface {
729
739
  userOrEmpty = user;
730
740
  }
731
741
 
732
- if (userOrEmpty.$gatekeeper('system/admin')) {
733
- return true;
734
- }
735
-
736
742
  // Users and groups are always readable. Editable by Tilmeld admins.
737
743
  if (
738
744
  (entity instanceof User || entity instanceof Group) &&
739
745
  (type === TilmeldAccessLevels.READ_ACCESS ||
740
- userOrEmpty.$gatekeeper('tilmeld/admin'))
746
+ userOrEmpty.abilities?.includes('tilmeld/admin') ||
747
+ userOrEmpty.abilities?.includes('system/admin'))
741
748
  ) {
742
749
  return true;
743
750
  }
744
751
 
745
- // Entities with no owners are always editable.
746
- if (entity.user == null && entity.group == null) {
747
- return true;
752
+ // Users and groups that belong to a domain are readable and editable by
753
+ // that domain's admins.
754
+ if (
755
+ this.config.domainSupport &&
756
+ (entity instanceof User || entity instanceof Group)
757
+ ) {
758
+ const [_local, domain] = splitn(
759
+ (entity instanceof User
760
+ ? (entity as User & UserData).username
761
+ : (entity as Group & GroupData).groupname) ?? '',
762
+ '@',
763
+ 2,
764
+ );
765
+ if (
766
+ domain &&
767
+ userOrEmpty.abilities?.includes(`tilmeld/domain/${domain}/admin`)
768
+ ) {
769
+ return true;
770
+ }
748
771
  }
749
772
 
750
773
  // Load access control, since we need it now...
751
- const acUser = entity.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
752
- const acGroup = entity.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
753
- const acOther = entity.acOther ?? TilmeldAccessLevels.NO_ACCESS;
774
+ const acUser = acProperties.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
775
+ const acGroup = acProperties.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
776
+ const acOther = acProperties.acOther ?? TilmeldAccessLevels.NO_ACCESS;
754
777
 
755
- if (userOrNull === null) {
778
+ if (userOrNull == null) {
756
779
  return acOther >= type;
757
780
  }
758
781
 
@@ -762,45 +785,41 @@ export default class Tilmeld implements TilmeldInterface {
762
785
  }
763
786
 
764
787
  // Check if the entity is the user's group. Always readable.
788
+ const gid = userOrEmpty.$getGid();
765
789
  if (
766
- userOrEmpty.group != null &&
767
- typeof userOrEmpty.group.$is === 'function' &&
768
- userOrEmpty.group.$is(entity) &&
790
+ gid != null &&
791
+ entity.guid === gid &&
769
792
  type === TilmeldAccessLevels.READ_ACCESS
770
793
  ) {
771
794
  return true;
772
795
  }
773
796
 
774
797
  // Calculate all the groups the user belongs to.
775
- let allGroups = userOrEmpty.group == null ? [] : [userOrEmpty.group];
776
- allGroups = allGroups.concat(userOrEmpty.groups ?? []);
777
- allGroups = allGroups.concat(userOrEmpty.$getDescendantGroupsSync());
798
+ let allGids = gid == null ? [] : [gid];
799
+ allGids = allGids.concat(userOrEmpty.$getGids() ?? []);
778
800
 
779
801
  // Check access ac properties.
780
802
  const checks = [
781
803
  {
782
804
  type: TilmeldAccessLevels.FULL_ACCESS,
783
- array: [...(entity.acFull ?? [])],
805
+ array: [...(acProperties.acFull ?? [])],
784
806
  },
785
807
  {
786
808
  type: TilmeldAccessLevels.WRITE_ACCESS,
787
- array: [...(entity.acWrite ?? [])],
809
+ array: [...(acProperties.acWrite ?? [])],
788
810
  },
789
811
  {
790
812
  type: TilmeldAccessLevels.READ_ACCESS,
791
- array: [...(entity.acRead ?? [])],
813
+ array: [...(acProperties.acRead ?? [])],
792
814
  },
793
815
  ];
794
816
  for (let curCheck of checks) {
795
817
  if (type <= curCheck.type) {
796
- if (userOrEmpty.$inArray(curCheck.array)) {
818
+ if (curCheck.array.includes(userOrEmpty.guid)) {
797
819
  return true;
798
820
  }
799
- for (let curGroup of allGroups) {
800
- if (
801
- typeof curGroup.$inArray === 'function' &&
802
- curGroup.$inArray(curCheck.array)
803
- ) {
821
+ for (let curGid of allGids) {
822
+ if (curCheck.array.includes(curGid)) {
804
823
  return true;
805
824
  }
806
825
  }
@@ -808,18 +827,10 @@ export default class Tilmeld implements TilmeldInterface {
808
827
  }
809
828
 
810
829
  // Check ownership ac properties.
811
- if (
812
- entity.user != null &&
813
- typeof entity.user.$is === 'function' &&
814
- entity.user.$is(userOrNull)
815
- ) {
830
+ if (acProperties.user != null && acProperties.user === userOrNull.guid) {
816
831
  return acUser >= type;
817
832
  }
818
- if (
819
- entity.group != null &&
820
- typeof entity.group.$inArray === 'function' &&
821
- entity.group.$inArray(allGroups)
822
- ) {
833
+ if (acProperties.group != null && allGids.includes(acProperties.group)) {
823
834
  return acGroup >= type;
824
835
  }
825
836
  return acOther >= type;
@@ -849,10 +860,10 @@ export default class Tilmeld implements TilmeldInterface {
849
860
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
850
861
  * @returns Whether the current user has at least `type` permission for the UID.
851
862
  */
852
- public checkClientUIDPermissions(
863
+ public async checkClientUIDPermissions(
853
864
  name: string,
854
865
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
855
- user?: (User & UserData) | false
866
+ user?: (User & UserData) | false,
856
867
  ) {
857
868
  let userOrEmpty: User & UserData = this.User.factorySync();
858
869
  // Calculate the user.
@@ -862,30 +873,30 @@ export default class Tilmeld implements TilmeldInterface {
862
873
  userOrEmpty = user;
863
874
  }
864
875
 
865
- if (userOrEmpty.$gatekeeper('system/admin')) {
876
+ if (userOrEmpty.abilities?.includes('system/admin')) {
866
877
  return true;
867
878
  }
868
879
 
869
880
  if (type === TilmeldAccessLevels.FULL_ACCESS) {
870
881
  return (
871
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
872
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
882
+ this.config.clientSetabledUIDs.includes(name) ||
883
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
873
884
  );
874
885
  } else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
875
886
  return (
876
- this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
877
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
878
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
879
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
887
+ this.config.clientEnabledUIDs.includes(name) ||
888
+ this.config.clientSetabledUIDs.includes(name) ||
889
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
890
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
880
891
  );
881
892
  } else if (type === TilmeldAccessLevels.READ_ACCESS) {
882
893
  return (
883
- this.config.clientReadableUIDs.indexOf(name) !== -1 ||
884
- this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
885
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
886
- userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
887
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
888
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
894
+ this.config.clientReadableUIDs.includes(name) ||
895
+ this.config.clientEnabledUIDs.includes(name) ||
896
+ this.config.clientSetabledUIDs.includes(name) ||
897
+ (await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
898
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
899
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
889
900
  );
890
901
  }
891
902
 
@@ -897,20 +908,13 @@ export default class Tilmeld implements TilmeldInterface {
897
908
  *
898
909
  * @param user The user.
899
910
  */
900
- public fillSession(user: User & UserData) {
901
- // Read groups right now, since gatekeeper needs them, so
902
- // $udpateDataProtection will fail to read them (since it runs gatekeeper).
903
- const _group = user.group;
904
- const _groups = user.groups;
911
+ public async fillSession(user: User & UserData) {
905
912
  this.currentUser = user;
913
+ this.gatekeeperCache = await user.$getGatekeeperCache();
914
+
906
915
  // Now update the data protection on the user and all the groups.
907
916
  this.currentUser.$updateDataProtection();
908
- if (this.currentUser.group != null) {
909
- this.currentUser.group.$updateDataProtection();
910
- }
911
- for (let group of this.currentUser.groups ?? []) {
912
- group.$updateDataProtection();
913
- }
917
+ this.currentUser.$updateGroupDataProtection();
914
918
  }
915
919
 
916
920
  /**
@@ -919,14 +923,10 @@ export default class Tilmeld implements TilmeldInterface {
919
923
  public clearSession() {
920
924
  const user = this.currentUser;
921
925
  this.currentUser = null;
926
+ this.gatekeeperCache = null;
922
927
  if (user) {
923
928
  user.$updateDataProtection();
924
- if (user.group != null) {
925
- user.group.$updateDataProtection();
926
- }
927
- for (let group of user.groups ?? []) {
928
- group.$updateDataProtection();
929
- }
929
+ user.$updateGroupDataProtection();
930
930
  }
931
931
  }
932
932
 
@@ -937,6 +937,7 @@ export default class Tilmeld implements TilmeldInterface {
937
937
  * @returns The user on success, null on failure.
938
938
  */
939
939
  public async extractToken(token: string) {
940
+ this.nymph.config.debugInfo('tilmeld', 'Extracting and validating token.');
940
941
  const extract = this.config.jwtExtract(this.config, token);
941
942
  if (extract == null) {
942
943
  return null;
@@ -948,9 +949,9 @@ export default class Tilmeld implements TilmeldInterface {
948
949
  {
949
950
  type: '&',
950
951
  guid: guid,
951
- }
952
+ },
952
953
  );
953
- if (!user || !user.guid || !user.enabled) {
954
+ if (!user || !user.guid) {
954
955
  return null;
955
956
  }
956
957
 
@@ -958,40 +959,73 @@ export default class Tilmeld implements TilmeldInterface {
958
959
  }
959
960
 
960
961
  /**
961
- * Check for a TILMELDAUTH token, and, if set, authenticate from it.
962
+ * Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
963
+ * from it/them.
964
+ *
965
+ * You can also call this function after setting `response.locals.user` to the
966
+ * user you want to authenticate. You *should* check for `user.enabled` before
967
+ * setting this variable, unless you explicitly want to log in as a disabled
968
+ * user. (The user must be an instance of the User class for this Tilmeld
969
+ * instance.)
970
+ *
971
+ * This function will set `response.locals.user` to the logged in user on
972
+ * successful authentication.
962
973
  *
963
974
  * @param skipXsrfToken Skip the XSRF token check.
975
+ * @param skipRenew Skip the token renewal step, even if the token is close to expiration.
964
976
  * @returns True if a user was authenticated, false on any failure.
965
977
  */
966
- public authenticate(skipXsrfToken = false) {
978
+ public async authenticate(skipXsrfToken = false, skipRenew = false) {
967
979
  if (this.request == null) {
968
980
  return false;
969
981
  }
970
982
 
983
+ if (
984
+ this.response &&
985
+ this.response.locals.user &&
986
+ this.response.locals.user instanceof this.User
987
+ ) {
988
+ // The user has already been authenticated through some other means.
989
+ const user = this.response.locals.user;
990
+ await this.fillSession(user);
991
+ return true;
992
+ }
993
+
971
994
  const cookies = this.request.cookies ?? {};
972
995
 
973
996
  // If a client does't support cookies, they can use the X-TILMELDAUTH header
974
997
  // to provide the auth token.
975
998
  let fromAuthHeader = false;
976
999
  let authToken: string;
1000
+ let switchToken: string | null;
977
1001
  if (this.request.header('x-tilmeldauth') != null) {
978
1002
  fromAuthHeader = true;
979
1003
  authToken = this.request.header('x-tilmeldauth') as string;
1004
+ switchToken =
1005
+ (this.request.header('x-tilmeldswitch') as string) ||
1006
+ 'TILMELDSWITCH' in cookies
1007
+ ? cookies.TILMELDSWITCH
1008
+ : null;
980
1009
  } else if ('TILMELDAUTH' in cookies) {
981
1010
  fromAuthHeader = false;
982
1011
  authToken = cookies.TILMELDAUTH;
1012
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
983
1013
  } else {
984
1014
  return false;
985
1015
  }
986
1016
 
987
- let extract: { guid: string; expire: Date } | null;
1017
+ let authExtract: { guid: string; issued: Date; expire: Date } | null;
1018
+ let switchExtract: { guid: string; expire: Date } | null;
988
1019
  if (
989
1020
  skipXsrfToken ||
990
1021
  this.request.originalUrl.startsWith(this.config.setupPath)
991
1022
  ) {
992
1023
  // The request is for the setup app, or we were told to skip the XSRF
993
1024
  // check, so don't check for the XSRF token.
994
- extract = this.config.jwtExtract(this.config, authToken);
1025
+ authExtract = this.config.jwtExtract(this.config, authToken);
1026
+ switchExtract = switchToken
1027
+ ? this.config.jwtExtract(this.config, switchToken)
1028
+ : null;
995
1029
  } else {
996
1030
  // The request is for something else, so check for a valid XSRF token,
997
1031
  // unless the auth token is provided by a header (instead of a cookie).
@@ -1000,28 +1034,65 @@ export default class Tilmeld implements TilmeldInterface {
1000
1034
  return false;
1001
1035
  }
1002
1036
 
1003
- extract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1037
+ authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1038
+ switchExtract = switchToken
1039
+ ? this.config.jwtExtract(this.config, switchToken)
1040
+ : null;
1004
1041
  }
1005
1042
 
1006
- if (extract == null) {
1007
- this.logout();
1043
+ if (authExtract == null) {
1044
+ await this.logout();
1008
1045
  return false;
1009
1046
  }
1010
- const { guid, expire } = extract;
1047
+ const { guid, issued, expire } = authExtract;
1048
+ const { guid: switchGuid, expire: switchExpire } = switchExtract || {
1049
+ guid: null,
1050
+ expire: null,
1051
+ };
1011
1052
 
1012
- const user = this.User.factorySync(guid);
1013
- if (user.guid == null || !user.enabled) {
1014
- this.logout();
1053
+ const user = await this.User.factory(guid);
1054
+ if (
1055
+ user.guid == null ||
1056
+ !user.enabled ||
1057
+ expire.getTime() <= Date.now() ||
1058
+ issued.getTime() > Date.now() ||
1059
+ (user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)
1060
+ ) {
1061
+ await this.logout();
1015
1062
  return false;
1016
1063
  }
1017
1064
 
1018
- if (expire.valueOf() < Date.now() + this.config.jwtRenew * 1000) {
1065
+ if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
1066
+ // Load the switch user.
1067
+ const switchUser = await this.User.factory(switchGuid);
1068
+ if (switchUser.guid == null) {
1069
+ await this.logoutSwitch();
1070
+ } else {
1071
+ await this.fillSession(switchUser);
1072
+
1073
+ if (this.response) {
1074
+ this.response.locals.user = switchUser;
1075
+ }
1076
+
1077
+ return true;
1078
+ }
1079
+ }
1080
+
1081
+ if (
1082
+ !skipRenew &&
1083
+ expire.getTime() < Date.now() + this.config.jwtRenew * 1000
1084
+ ) {
1019
1085
  // If the user is less than renew time from needing a new token, give them
1020
1086
  // a new one.
1021
- this.login(user, fromAuthHeader);
1087
+ await this.login(user, fromAuthHeader);
1022
1088
  } else {
1023
- this.fillSession(user);
1089
+ await this.fillSession(user);
1090
+ }
1091
+
1092
+ if (this.response) {
1093
+ this.response.locals.user = user;
1024
1094
  }
1095
+
1025
1096
  return true;
1026
1097
  }
1027
1098
 
@@ -1029,27 +1100,87 @@ export default class Tilmeld implements TilmeldInterface {
1029
1100
  * Logs the given user into the system.
1030
1101
  *
1031
1102
  * @param user The user.
1032
- * @param sendAuthHeader When true, a custom header with the auth token will be sent.
1103
+ * @param sendAuthHeader Send the auth token as a custom header.
1104
+ * @param sendCookie Send the auth token as a cookie.
1033
1105
  * @returns True on success, false on failure.
1034
1106
  */
1035
- public login(user: User & UserData, sendAuthHeader: boolean) {
1107
+ public async login(
1108
+ user: User & UserData,
1109
+ sendAuthHeader: boolean,
1110
+ sendCookie = true,
1111
+ ) {
1036
1112
  if (user.guid != null && user.enabled) {
1037
- if (this.response && !this.response.headersSent) {
1038
- const token = this.config.jwtBuilder(this.config, user);
1039
- const appUrl = new URL(this.config.appUrl);
1040
- this.response.cookie('TILMELDAUTH', token, {
1041
- domain: this.config.cookieDomain,
1042
- path: this.config.cookiePath,
1043
- maxAge: this.config.jwtExpire * 1000,
1044
- secure: appUrl.protocol === 'https',
1045
- httpOnly: false, // Allow JS access (for CSRF protection).
1046
- sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1047
- });
1048
- if (sendAuthHeader) {
1049
- this.response.set('X-TILMELDAUTH', token);
1113
+ if (this.response) {
1114
+ if (!this.response.headersSent) {
1115
+ const token = this.config.jwtBuilder(this.config, user);
1116
+
1117
+ if (sendCookie) {
1118
+ const appUrl = new URL(this.config.appUrl);
1119
+ this.response.cookie('TILMELDAUTH', token, {
1120
+ domain: this.config.cookieDomain,
1121
+ path: this.config.cookiePath,
1122
+ maxAge: this.config.jwtExpire * 1000,
1123
+ secure: appUrl.protocol === 'https:',
1124
+ httpOnly: false, // Allow JS access (for CSRF protection).
1125
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1126
+ });
1127
+ }
1128
+
1129
+ if (sendAuthHeader) {
1130
+ this.response.set('X-TILMELDAUTH', token);
1131
+ }
1132
+ }
1133
+
1134
+ this.response.locals.user = user;
1135
+ }
1136
+
1137
+ await this.fillSession(user);
1138
+ return true;
1139
+ }
1140
+ return false;
1141
+ }
1142
+
1143
+ /**
1144
+ * Adds a switch auth token for the given user.
1145
+ *
1146
+ * This effectively logs the current user in to the system as the given user.
1147
+ *
1148
+ * @param user The user.
1149
+ * @param sendAuthHeader Send the auth token as a custom header.
1150
+ * @param sendCookie Send the auth token as a cookie.
1151
+ * @returns True on success, false on failure.
1152
+ */
1153
+ public async loginSwitch(
1154
+ user: User & UserData,
1155
+ sendAuthHeader: boolean,
1156
+ sendCookie = true,
1157
+ ) {
1158
+ if (user.guid != null) {
1159
+ if (this.response) {
1160
+ if (!this.response.headersSent) {
1161
+ const token = this.config.jwtBuilder(this.config, user, true);
1162
+
1163
+ if (sendCookie) {
1164
+ const appUrl = new URL(this.config.appUrl);
1165
+ this.response.cookie('TILMELDSWITCH', token, {
1166
+ domain: this.config.cookieDomain,
1167
+ path: this.config.cookiePath,
1168
+ maxAge: this.config.jwtSwitchExpire * 1000,
1169
+ secure: appUrl.protocol === 'https:',
1170
+ httpOnly: false, // Allow JS access (for PubSub auth token).
1171
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1172
+ });
1173
+ }
1174
+
1175
+ if (sendAuthHeader) {
1176
+ this.response.set('X-TILMELDSWITCH', token);
1177
+ }
1050
1178
  }
1179
+
1180
+ this.response.locals.user = user;
1051
1181
  }
1052
- this.fillSession(user);
1182
+
1183
+ await this.fillSession(user);
1053
1184
  return true;
1054
1185
  }
1055
1186
  return false;
@@ -1057,15 +1188,50 @@ export default class Tilmeld implements TilmeldInterface {
1057
1188
 
1058
1189
  /**
1059
1190
  * Logs the current user out of the system.
1191
+ *
1192
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1193
+ */
1194
+ public async logout(clearCookie = true) {
1195
+ if (this.request && !this.alreadyLoggedOutSwitch) {
1196
+ const cookies = this.request.cookies ?? {};
1197
+
1198
+ if ('TILMELDSWITCH' in cookies) {
1199
+ this.alreadyLoggedOutSwitch = true;
1200
+ return await this.logoutSwitch(clearCookie);
1201
+ }
1202
+ }
1203
+
1204
+ this.clearSession();
1205
+ if (this.response) {
1206
+ if (clearCookie && !this.response.headersSent) {
1207
+ this.response.clearCookie('TILMELDAUTH', {
1208
+ domain: this.config.cookieDomain,
1209
+ path: this.config.cookiePath,
1210
+ });
1211
+ this.response.set('X-TILMELDAUTH', '');
1212
+ }
1213
+ this.response.locals.user = null;
1214
+ }
1215
+ }
1216
+
1217
+ /**
1218
+ * Clears the switch user out of the system.
1219
+ *
1220
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1060
1221
  */
1061
- public logout() {
1222
+ public async logoutSwitch(clearCookie = true) {
1062
1223
  this.clearSession();
1063
- if (this.response && !this.response.headersSent) {
1064
- this.response.clearCookie('TILMELDAUTH', {
1065
- domain: this.config.cookieDomain,
1066
- path: this.config.cookiePath,
1067
- });
1068
- this.response.set('X-TILMELDAUTH', '');
1224
+ if (this.response) {
1225
+ if (clearCookie && !this.response.headersSent) {
1226
+ this.response.clearCookie('TILMELDSWITCH', {
1227
+ domain: this.config.cookieDomain,
1228
+ path: this.config.cookiePath,
1229
+ });
1230
+ this.response.set('X-TILMELDSWITCH', '');
1231
+ }
1232
+ this.response.locals.user = null;
1233
+
1234
+ await this.authenticate(false, true);
1069
1235
  }
1070
1236
  }
1071
1237