@nymphjs/tilmeld 1.0.0-beta.10 → 1.0.0-beta.100

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 +455 -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 +652 -253
  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 +1355 -368
  18. package/dist/User.js.map +1 -1
  19. package/dist/User.test.js +130 -13
  20. package/dist/User.test.js.map +1 -1
  21. package/dist/conf/d.d.ts +306 -7
  22. package/dist/conf/d.js +1 -2
  23. package/dist/conf/defaults.d.ts +1 -1
  24. package/dist/conf/defaults.js +196 -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 +441 -254
  62. package/src/Tilmeld.types.ts +2 -2
  63. package/src/User.test.ts +160 -7
  64. package/src/User.ts +1596 -457
  65. package/src/conf/d.ts +83 -17
  66. package/src/conf/defaults.ts +172 -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,14 @@ 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
+ if (className === 'User' || className === 'Group') {
279
+ throw new AccessControlError("You can't delete users or groups by ID.");
280
+ }
281
+ const entity = await nymph.getEntity(
221
282
  { class: nymph.getEntityClass(className ?? 'Entity') },
222
- { type: '&', guid: guid }
283
+ { type: '&', guid: guid },
223
284
  );
224
285
  if (entity != null) {
225
286
  checkPermissionsDelete(nymph, entity);
@@ -231,9 +292,9 @@ export default class Tilmeld implements TilmeldInterface {
231
292
  // disallowed changes to AC properties.
232
293
  const checkPermissionsSaveAndFilterAcChanges = async function (
233
294
  nymph: Nymph,
234
- entity: EntityInterface & AccessControlData
295
+ entity: EntityInterface & AccessControlData,
235
296
  ) {
236
- const tilmeld = nymph.tilmeld as Tilmeld;
297
+ const tilmeld = enforceTilmeld(nymph);
237
298
  if (!entity) {
238
299
  return;
239
300
  }
@@ -249,35 +310,9 @@ export default class Tilmeld implements TilmeldInterface {
249
310
  // allowing a change to ac properties.
250
311
 
251
312
  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
- };
262
-
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);
313
+ const newAc = entity.$getCurrentAcValues();
276
314
 
277
- function areAcPropertiesChanged(
278
- a: AccessControlData,
279
- b: AccessControlData
280
- ) {
315
+ function areAcPropertiesChanged(a: ACProperties, b: ACProperties) {
281
316
  for (const name of ['user', 'group'] as ('user' | 'group')[]) {
282
317
  const aVal = a[name];
283
318
  const bVal = b[name];
@@ -286,7 +321,7 @@ export default class Tilmeld implements TilmeldInterface {
286
321
  } else if (aVal == null || bVal == null) {
287
322
  return true;
288
323
  }
289
- if (!aVal.$is(bVal)) {
324
+ if (aVal !== bVal) {
290
325
  return true;
291
326
  }
292
327
  }
@@ -321,12 +356,7 @@ export default class Tilmeld implements TilmeldInterface {
321
356
  if (aVal.length !== bVal.length) {
322
357
  return true;
323
358
  }
324
- if (
325
- xor(
326
- aVal.map((entity) => entity.guid),
327
- bVal.map((entity) => entity.guid)
328
- ).length
329
- ) {
359
+ if (xor(aVal, bVal).length) {
330
360
  return true;
331
361
  }
332
362
  }
@@ -335,41 +365,42 @@ export default class Tilmeld implements TilmeldInterface {
335
365
 
336
366
  if (areAcPropertiesChanged(originalAc, newAc)) {
337
367
  if (
338
- !tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)
368
+ !tilmeld.checkPermissions(
369
+ entity,
370
+ TilmeldAccessLevels.FULL_ACCESS,
371
+ undefined,
372
+ originalAc,
373
+ )
339
374
  ) {
340
375
  // Only allow changes to AC properties if the user has full access.
341
376
  throw new AccessControlError(
342
- 'No permission to change access control properties.'
377
+ 'No permission to change access control properties.',
343
378
  );
344
379
  }
345
380
 
346
381
  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')
382
+ (originalAc.user != null && originalAc.user !== newAc.user) ||
383
+ (originalAc.user == null &&
384
+ newAc.user != null &&
385
+ newAc.user !== tilmeld.currentUser?.guid)
352
386
  ) {
353
387
  throw new AccessControlError(
354
- 'No permission to assign to another user.'
388
+ 'No permission to assign to another user.',
355
389
  );
356
390
  }
357
391
 
358
392
  if (
359
- newAc.group &&
360
- !newAc.group.$is(originalAc.group) &&
393
+ newAc.group != null &&
394
+ newAc.group !== originalAc.group &&
361
395
  !(
362
- newAc.group.$is(tilmeld.currentUser?.group) ||
363
- newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
364
- ) &&
365
- !tilmeld.gatekeeper('tilmeld/admin')
396
+ newAc.group === tilmeld.currentUser?.$getGid() ||
397
+ (tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group)
398
+ )
366
399
  ) {
367
400
  throw new AccessControlError(
368
- 'No permission to assign to another group.'
401
+ 'No permission to assign to another group.',
369
402
  );
370
403
  }
371
-
372
- setAcProperties(newAc);
373
404
  }
374
405
  }
375
406
 
@@ -398,9 +429,9 @@ export default class Tilmeld implements TilmeldInterface {
398
429
  */
399
430
  const addAccess = async function (
400
431
  nymph: Nymph,
401
- entity: EntityInterface & AccessControlData
432
+ entity: EntityInterface & AccessControlData,
402
433
  ) {
403
- const tilmeld = nymph.tilmeld as Tilmeld;
434
+ const tilmeld = enforceTilmeld(nymph);
404
435
  const user = tilmeld.currentUser;
405
436
  if (
406
437
  user != null &&
@@ -411,12 +442,11 @@ export default class Tilmeld implements TilmeldInterface {
411
442
  if (entity.user == null) {
412
443
  entity.user = user;
413
444
  }
414
- if (
415
- entity.group == null &&
416
- user.group != null &&
417
- user.group.guid != null
418
- ) {
419
- entity.group = user.group;
445
+ if (entity.group == null) {
446
+ const group = user.group;
447
+ if (group != null && group.guid != null) {
448
+ entity.group = group;
449
+ }
420
450
  }
421
451
  if (entity.acUser == null) {
422
452
  entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
@@ -442,7 +472,7 @@ export default class Tilmeld implements TilmeldInterface {
442
472
 
443
473
  const validate = async function (
444
474
  _nymph: Nymph,
445
- entity: EntityInterface & AccessControlData
475
+ entity: EntityInterface & AccessControlData,
446
476
  ) {
447
477
  if (!(entity instanceof User) && !(entity instanceof Group)) {
448
478
  const ownershipAcPropertyValidator = (prop: any) => {
@@ -452,7 +482,7 @@ export default class Tilmeld implements TilmeldInterface {
452
482
  prop > TilmeldAccessLevels.FULL_ACCESS
453
483
  ) {
454
484
  throw new AccessControlError(
455
- 'Invalid access control property: ' + prop
485
+ 'Invalid access control property: ' + prop,
456
486
  );
457
487
  }
458
488
  };
@@ -460,13 +490,13 @@ export default class Tilmeld implements TilmeldInterface {
460
490
  const accessAcPropertyValidator = (prop: any) => {
461
491
  if (!Array.isArray(prop)) {
462
492
  throw new AccessControlError(
463
- 'Invalid access control property: ' + prop
493
+ 'Invalid access control property: ' + prop,
464
494
  );
465
495
  }
466
496
  prop.forEach((value) => {
467
497
  if (!(value instanceof User || value instanceof Group)) {
468
498
  throw new AccessControlError(
469
- 'Invalid access control property: ' + prop
499
+ 'Invalid access control property: ' + prop,
470
500
  );
471
501
  }
472
502
  });
@@ -475,7 +505,7 @@ export default class Tilmeld implements TilmeldInterface {
475
505
  if ('user' in entity) {
476
506
  if (!(entity.user instanceof User)) {
477
507
  throw new AccessControlError(
478
- 'Invalid access control property: user'
508
+ 'Invalid access control property: user',
479
509
  );
480
510
  }
481
511
  }
@@ -483,7 +513,7 @@ export default class Tilmeld implements TilmeldInterface {
483
513
  if ('group' in entity) {
484
514
  if (!(entity.group instanceof Group)) {
485
515
  throw new AccessControlError(
486
- 'Invalid access control property: group'
516
+ 'Invalid access control property: group',
487
517
  );
488
518
  }
489
519
  }
@@ -528,18 +558,10 @@ export default class Tilmeld implements TilmeldInterface {
528
558
  */
529
559
  public addAccessControlSelectors(
530
560
  options: Options,
531
- selectors: FormattedSelector[]
561
+ selectors: FormattedSelector[],
532
562
  ) {
533
563
  const user = this.currentUser;
534
564
 
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
565
  if (options == null) {
544
566
  throw new Error('No options in argument.');
545
567
  } else if (
@@ -579,33 +601,23 @@ export default class Tilmeld implements TilmeldInterface {
579
601
  ];
580
602
  const groupRefs: FormattedSelector['ref'] = [];
581
603
  const acRefs: FormattedSelector['ref'] = [];
582
- if (user.group != null && user.group.guid != null) {
604
+ const gid = user.$getGid();
605
+ if (gid != null) {
583
606
  // It belongs to the user's primary group.
584
- groupRefs.push(['group', user.group.guid]);
607
+ groupRefs.push(['group', gid]);
585
608
  // 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]);
609
+ acRefs.push(['acRead', gid]);
610
+ acRefs.push(['acWrite', gid]);
611
+ acRefs.push(['acFull', gid]);
589
612
  }
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
- }
613
+ const gids = user.$getGids();
614
+ for (let curGid of gids ?? []) {
615
+ // It belongs to the user's secondary group.
616
+ groupRefs.push(['group', curGid]);
617
+ // User's secondary group is listed in acRead, acWrite, or acFull.
618
+ acRefs.push(['acRead', curGid]);
619
+ acRefs.push(['acWrite', curGid]);
620
+ acRefs.push(['acFull', curGid]);
609
621
  }
610
622
  // All the group refs.
611
623
  if (groupRefs.length) {
@@ -671,7 +683,7 @@ export default class Tilmeld implements TilmeldInterface {
671
683
  * - FULL_ACCESS, the user has read, write, and delete access to the entity,
672
684
  * as well as being able to manage its access controls and ownership.
673
685
  *
674
- * These properties defaults to:
686
+ * These properties default to:
675
687
  *
676
688
  * - acUser = TilmeldAccessLevels.FULL_ACCESS
677
689
  * - acGroup = TilmeldAccessLevels.READ_ACCESS
@@ -686,9 +698,9 @@ export default class Tilmeld implements TilmeldInterface {
686
698
  * The following conditions will result in different checks, which determine
687
699
  * whether the check passes:
688
700
  *
689
- * - The user has the "system/admin" ability. (Always true.)
690
701
  * - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
691
- * - The entity has no "user" and no "group". (Always true.)
702
+ * - It is a user or group that belongs to a domain the current user is an
703
+ * admin for, and domain support is enabled. (Always true.)
692
704
  * - No user is logged in. (Check other AC.)
693
705
  * - The entity is the user. (Always true.)
694
706
  * - It is the user's primary group. (True for READ_ACCESS.)
@@ -706,16 +718,17 @@ export default class Tilmeld implements TilmeldInterface {
706
718
  * @param entity The entity to check.
707
719
  * @param type The lowest level of permission to consider a pass.
708
720
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
721
+ * @param acProperties The acProperties to use instead of getting them from the entity.
709
722
  * @returns Whether the current user has at least `type` permission for the entity.
710
723
  */
711
724
  public checkPermissions(
712
- entity: EntityInterface & AccessControlData,
725
+ entity: EntityInterface,
713
726
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
714
- user?: (User & UserData) | false
727
+ user?: (User & UserData) | false,
728
+ acProperties?: ACProperties,
715
729
  ) {
716
- // Only works for entities.
717
- if (typeof entity !== 'object' || typeof entity.$is !== 'function') {
718
- return false;
730
+ if (!acProperties) {
731
+ acProperties = entity.$getCurrentAcValues() as ACProperties;
719
732
  }
720
733
 
721
734
  let userOrNull: (User & UserData) | null = null;
@@ -729,30 +742,43 @@ export default class Tilmeld implements TilmeldInterface {
729
742
  userOrEmpty = user;
730
743
  }
731
744
 
732
- if (userOrEmpty.$gatekeeper('system/admin')) {
733
- return true;
734
- }
735
-
736
745
  // Users and groups are always readable. Editable by Tilmeld admins.
737
746
  if (
738
747
  (entity instanceof User || entity instanceof Group) &&
739
748
  (type === TilmeldAccessLevels.READ_ACCESS ||
740
- userOrEmpty.$gatekeeper('tilmeld/admin'))
749
+ userOrEmpty.abilities?.includes('tilmeld/admin') ||
750
+ userOrEmpty.abilities?.includes('system/admin'))
741
751
  ) {
742
752
  return true;
743
753
  }
744
754
 
745
- // Entities with no owners are always editable.
746
- if (entity.user == null && entity.group == null) {
747
- return true;
755
+ // Users and groups that belong to a domain are readable and editable by
756
+ // that domain's admins.
757
+ if (
758
+ this.config.domainSupport &&
759
+ (entity instanceof User || entity instanceof Group)
760
+ ) {
761
+ const [_local, domain] = splitn(
762
+ (entity instanceof User
763
+ ? (entity as User & UserData).username
764
+ : (entity as Group & GroupData).groupname) ?? '',
765
+ '@',
766
+ 2,
767
+ );
768
+ if (
769
+ domain &&
770
+ userOrEmpty.abilities?.includes(`tilmeld/domain/${domain}/admin`)
771
+ ) {
772
+ return true;
773
+ }
748
774
  }
749
775
 
750
776
  // 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;
777
+ const acUser = acProperties.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
778
+ const acGroup = acProperties.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
779
+ const acOther = acProperties.acOther ?? TilmeldAccessLevels.NO_ACCESS;
754
780
 
755
- if (userOrNull === null) {
781
+ if (userOrNull == null) {
756
782
  return acOther >= type;
757
783
  }
758
784
 
@@ -762,45 +788,59 @@ export default class Tilmeld implements TilmeldInterface {
762
788
  }
763
789
 
764
790
  // Check if the entity is the user's group. Always readable.
791
+ const gid = userOrEmpty.$getGid();
792
+ if (
793
+ gid != null &&
794
+ entity.guid === gid &&
795
+ type === TilmeldAccessLevels.READ_ACCESS
796
+ ) {
797
+ return true;
798
+ }
799
+
800
+ // Check if the entity is one of the user's secondary groups. Always
801
+ // readable.
802
+ const gids = userOrEmpty.$getGids();
765
803
  if (
766
- userOrEmpty.group != null &&
767
- typeof userOrEmpty.group.$is === 'function' &&
768
- userOrEmpty.group.$is(entity) &&
804
+ gids != null &&
805
+ entity.guid != null &&
806
+ gids.indexOf(entity.guid) !== -1 &&
769
807
  type === TilmeldAccessLevels.READ_ACCESS
770
808
  ) {
771
809
  return true;
772
810
  }
773
811
 
812
+ // At this point, if the entity is a user or group, it should not be
813
+ // accessible.
814
+ if (entity instanceof User || entity instanceof Group) {
815
+ return false;
816
+ }
817
+
774
818
  // 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());
819
+ let allGids = gid == null ? [] : [gid];
820
+ allGids = allGids.concat(gids ?? []);
778
821
 
779
822
  // Check access ac properties.
780
823
  const checks = [
781
824
  {
782
825
  type: TilmeldAccessLevels.FULL_ACCESS,
783
- array: [...(entity.acFull ?? [])],
826
+ array: [...(acProperties.acFull ?? [])],
784
827
  },
785
828
  {
786
829
  type: TilmeldAccessLevels.WRITE_ACCESS,
787
- array: [...(entity.acWrite ?? [])],
830
+ array: [...(acProperties.acWrite ?? [])],
788
831
  },
789
832
  {
790
833
  type: TilmeldAccessLevels.READ_ACCESS,
791
- array: [...(entity.acRead ?? [])],
834
+ array: [...(acProperties.acRead ?? [])],
792
835
  },
793
836
  ];
794
837
  for (let curCheck of checks) {
795
838
  if (type <= curCheck.type) {
796
- if (userOrEmpty.$inArray(curCheck.array)) {
839
+ if (curCheck.array.includes(userOrEmpty.guid)) {
797
840
  return true;
798
841
  }
799
- for (let curGroup of allGroups) {
800
- if (
801
- typeof curGroup.$inArray === 'function' &&
802
- curGroup.$inArray(curCheck.array)
803
- ) {
842
+ for (let curGid of allGids) {
843
+ if (curCheck.array.includes(curGid)) {
804
844
  return true;
805
845
  }
806
846
  }
@@ -808,18 +848,10 @@ export default class Tilmeld implements TilmeldInterface {
808
848
  }
809
849
 
810
850
  // Check ownership ac properties.
811
- if (
812
- entity.user != null &&
813
- typeof entity.user.$is === 'function' &&
814
- entity.user.$is(userOrNull)
815
- ) {
851
+ if (acProperties.user != null && acProperties.user === userOrNull.guid) {
816
852
  return acUser >= type;
817
853
  }
818
- if (
819
- entity.group != null &&
820
- typeof entity.group.$inArray === 'function' &&
821
- entity.group.$inArray(allGroups)
822
- ) {
854
+ if (acProperties.group != null && allGids.includes(acProperties.group)) {
823
855
  return acGroup >= type;
824
856
  }
825
857
  return acOther >= type;
@@ -849,10 +881,10 @@ export default class Tilmeld implements TilmeldInterface {
849
881
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
850
882
  * @returns Whether the current user has at least `type` permission for the UID.
851
883
  */
852
- public checkClientUIDPermissions(
884
+ public async checkClientUIDPermissions(
853
885
  name: string,
854
886
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
855
- user?: (User & UserData) | false
887
+ user?: (User & UserData) | false,
856
888
  ) {
857
889
  let userOrEmpty: User & UserData = this.User.factorySync();
858
890
  // Calculate the user.
@@ -862,30 +894,30 @@ export default class Tilmeld implements TilmeldInterface {
862
894
  userOrEmpty = user;
863
895
  }
864
896
 
865
- if (userOrEmpty.$gatekeeper('system/admin')) {
897
+ if (userOrEmpty.abilities?.includes('system/admin')) {
866
898
  return true;
867
899
  }
868
900
 
869
901
  if (type === TilmeldAccessLevels.FULL_ACCESS) {
870
902
  return (
871
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
872
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
903
+ this.config.clientSetabledUIDs.includes(name) ||
904
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
873
905
  );
874
906
  } else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
875
907
  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}`)
908
+ this.config.clientEnabledUIDs.includes(name) ||
909
+ this.config.clientSetabledUIDs.includes(name) ||
910
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
911
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
880
912
  );
881
913
  } else if (type === TilmeldAccessLevels.READ_ACCESS) {
882
914
  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}`)
915
+ this.config.clientReadableUIDs.includes(name) ||
916
+ this.config.clientEnabledUIDs.includes(name) ||
917
+ this.config.clientSetabledUIDs.includes(name) ||
918
+ (await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
919
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
920
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
889
921
  );
890
922
  }
891
923
 
@@ -897,20 +929,13 @@ export default class Tilmeld implements TilmeldInterface {
897
929
  *
898
930
  * @param user The user.
899
931
  */
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;
932
+ public async fillSession(user: User & UserData) {
905
933
  this.currentUser = user;
934
+ this.gatekeeperCache = await user.$getGatekeeperCache();
935
+
906
936
  // Now update the data protection on the user and all the groups.
907
937
  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
- }
938
+ this.currentUser.$updateGroupDataProtection();
914
939
  }
915
940
 
916
941
  /**
@@ -919,14 +944,10 @@ export default class Tilmeld implements TilmeldInterface {
919
944
  public clearSession() {
920
945
  const user = this.currentUser;
921
946
  this.currentUser = null;
947
+ this.gatekeeperCache = null;
922
948
  if (user) {
923
949
  user.$updateDataProtection();
924
- if (user.group != null) {
925
- user.group.$updateDataProtection();
926
- }
927
- for (let group of user.groups ?? []) {
928
- group.$updateDataProtection();
929
- }
950
+ user.$updateGroupDataProtection();
930
951
  }
931
952
  }
932
953
 
@@ -937,6 +958,7 @@ export default class Tilmeld implements TilmeldInterface {
937
958
  * @returns The user on success, null on failure.
938
959
  */
939
960
  public async extractToken(token: string) {
961
+ this.nymph.config.debugInfo('tilmeld', 'Extracting and validating token.');
940
962
  const extract = this.config.jwtExtract(this.config, token);
941
963
  if (extract == null) {
942
964
  return null;
@@ -948,9 +970,9 @@ export default class Tilmeld implements TilmeldInterface {
948
970
  {
949
971
  type: '&',
950
972
  guid: guid,
951
- }
973
+ },
952
974
  );
953
- if (!user || !user.guid || !user.enabled) {
975
+ if (!user || !user.guid) {
954
976
  return null;
955
977
  }
956
978
 
@@ -958,40 +980,73 @@ export default class Tilmeld implements TilmeldInterface {
958
980
  }
959
981
 
960
982
  /**
961
- * Check for a TILMELDAUTH token, and, if set, authenticate from it.
983
+ * Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
984
+ * from it/them.
985
+ *
986
+ * You can also call this function after setting `response.locals.user` to the
987
+ * user you want to authenticate. You *should* check for `user.enabled` before
988
+ * setting this variable, unless you explicitly want to log in as a disabled
989
+ * user. (The user must be an instance of the User class for this Tilmeld
990
+ * instance.)
991
+ *
992
+ * This function will set `response.locals.user` to the logged in user on
993
+ * successful authentication.
962
994
  *
963
995
  * @param skipXsrfToken Skip the XSRF token check.
996
+ * @param skipRenew Skip the token renewal step, even if the token is close to expiration.
964
997
  * @returns True if a user was authenticated, false on any failure.
965
998
  */
966
- public authenticate(skipXsrfToken = false) {
999
+ public async authenticate(skipXsrfToken = false, skipRenew = false) {
967
1000
  if (this.request == null) {
968
1001
  return false;
969
1002
  }
970
1003
 
1004
+ if (
1005
+ this.response &&
1006
+ this.response.locals.user &&
1007
+ this.response.locals.user instanceof this.User
1008
+ ) {
1009
+ // The user has already been authenticated through some other means.
1010
+ const user = this.response.locals.user;
1011
+ await this.fillSession(user);
1012
+ return true;
1013
+ }
1014
+
971
1015
  const cookies = this.request.cookies ?? {};
972
1016
 
973
1017
  // If a client does't support cookies, they can use the X-TILMELDAUTH header
974
1018
  // to provide the auth token.
975
1019
  let fromAuthHeader = false;
976
1020
  let authToken: string;
1021
+ let switchToken: string | null;
977
1022
  if (this.request.header('x-tilmeldauth') != null) {
978
1023
  fromAuthHeader = true;
979
1024
  authToken = this.request.header('x-tilmeldauth') as string;
1025
+ switchToken =
1026
+ (this.request.header('x-tilmeldswitch') as string) ||
1027
+ 'TILMELDSWITCH' in cookies
1028
+ ? cookies.TILMELDSWITCH
1029
+ : null;
980
1030
  } else if ('TILMELDAUTH' in cookies) {
981
1031
  fromAuthHeader = false;
982
1032
  authToken = cookies.TILMELDAUTH;
1033
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
983
1034
  } else {
984
1035
  return false;
985
1036
  }
986
1037
 
987
- let extract: { guid: string; expire: Date } | null;
1038
+ let authExtract: { guid: string; issued: Date; expire: Date } | null;
1039
+ let switchExtract: { guid: string; expire: Date } | null;
988
1040
  if (
989
1041
  skipXsrfToken ||
990
1042
  this.request.originalUrl.startsWith(this.config.setupPath)
991
1043
  ) {
992
1044
  // The request is for the setup app, or we were told to skip the XSRF
993
1045
  // check, so don't check for the XSRF token.
994
- extract = this.config.jwtExtract(this.config, authToken);
1046
+ authExtract = this.config.jwtExtract(this.config, authToken);
1047
+ switchExtract = switchToken
1048
+ ? this.config.jwtExtract(this.config, switchToken)
1049
+ : null;
995
1050
  } else {
996
1051
  // The request is for something else, so check for a valid XSRF token,
997
1052
  // unless the auth token is provided by a header (instead of a cookie).
@@ -1000,28 +1055,65 @@ export default class Tilmeld implements TilmeldInterface {
1000
1055
  return false;
1001
1056
  }
1002
1057
 
1003
- extract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1058
+ authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1059
+ switchExtract = switchToken
1060
+ ? this.config.jwtExtract(this.config, switchToken)
1061
+ : null;
1004
1062
  }
1005
1063
 
1006
- if (extract == null) {
1007
- this.logout();
1064
+ if (authExtract == null) {
1065
+ await this.logout();
1008
1066
  return false;
1009
1067
  }
1010
- const { guid, expire } = extract;
1068
+ const { guid, issued, expire } = authExtract;
1069
+ const { guid: switchGuid, expire: switchExpire } = switchExtract || {
1070
+ guid: null,
1071
+ expire: null,
1072
+ };
1011
1073
 
1012
- const user = this.User.factorySync(guid);
1013
- if (user.guid == null || !user.enabled) {
1014
- this.logout();
1074
+ const user = await this.User.factory(guid);
1075
+ if (
1076
+ user.guid == null ||
1077
+ !user.enabled ||
1078
+ expire.getTime() <= Date.now() ||
1079
+ issued.getTime() > Date.now() ||
1080
+ (user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)
1081
+ ) {
1082
+ await this.logout();
1015
1083
  return false;
1016
1084
  }
1017
1085
 
1018
- if (expire.valueOf() < Date.now() + this.config.jwtRenew * 1000) {
1086
+ if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
1087
+ // Load the switch user.
1088
+ const switchUser = await this.User.factory(switchGuid);
1089
+ if (switchUser.guid == null) {
1090
+ await this.logoutSwitch();
1091
+ } else {
1092
+ await this.fillSession(switchUser);
1093
+
1094
+ if (this.response) {
1095
+ this.response.locals.user = switchUser;
1096
+ }
1097
+
1098
+ return true;
1099
+ }
1100
+ }
1101
+
1102
+ if (
1103
+ !skipRenew &&
1104
+ expire.getTime() < Date.now() + this.config.jwtRenew * 1000
1105
+ ) {
1019
1106
  // If the user is less than renew time from needing a new token, give them
1020
1107
  // a new one.
1021
- this.login(user, fromAuthHeader);
1108
+ await this.login(user, fromAuthHeader);
1022
1109
  } else {
1023
- this.fillSession(user);
1110
+ await this.fillSession(user);
1111
+ }
1112
+
1113
+ if (this.response) {
1114
+ this.response.locals.user = user;
1024
1115
  }
1116
+
1025
1117
  return true;
1026
1118
  }
1027
1119
 
@@ -1029,27 +1121,87 @@ export default class Tilmeld implements TilmeldInterface {
1029
1121
  * Logs the given user into the system.
1030
1122
  *
1031
1123
  * @param user The user.
1032
- * @param sendAuthHeader When true, a custom header with the auth token will be sent.
1124
+ * @param sendAuthHeader Send the auth token as a custom header.
1125
+ * @param sendCookie Send the auth token as a cookie.
1033
1126
  * @returns True on success, false on failure.
1034
1127
  */
1035
- public login(user: User & UserData, sendAuthHeader: boolean) {
1128
+ public async login(
1129
+ user: User & UserData,
1130
+ sendAuthHeader: boolean,
1131
+ sendCookie = true,
1132
+ ) {
1036
1133
  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);
1134
+ if (this.response) {
1135
+ if (!this.response.headersSent) {
1136
+ const token = this.config.jwtBuilder(this.config, user);
1137
+
1138
+ if (sendCookie) {
1139
+ const appUrl = new URL(this.config.appUrl);
1140
+ this.response.cookie('TILMELDAUTH', token, {
1141
+ domain: this.config.cookieDomain,
1142
+ path: this.config.cookiePath,
1143
+ maxAge: this.config.jwtExpire * 1000,
1144
+ secure: appUrl.protocol === 'https:',
1145
+ httpOnly: false, // Allow JS access (for CSRF protection).
1146
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1147
+ });
1148
+ }
1149
+
1150
+ if (sendAuthHeader) {
1151
+ this.response.set('X-TILMELDAUTH', token);
1152
+ }
1050
1153
  }
1154
+
1155
+ this.response.locals.user = user;
1051
1156
  }
1052
- this.fillSession(user);
1157
+
1158
+ await this.fillSession(user);
1159
+ return true;
1160
+ }
1161
+ return false;
1162
+ }
1163
+
1164
+ /**
1165
+ * Adds a switch auth token for the given user.
1166
+ *
1167
+ * This effectively logs the current user in to the system as the given user.
1168
+ *
1169
+ * @param user The user.
1170
+ * @param sendAuthHeader Send the auth token as a custom header.
1171
+ * @param sendCookie Send the auth token as a cookie.
1172
+ * @returns True on success, false on failure.
1173
+ */
1174
+ public async loginSwitch(
1175
+ user: User & UserData,
1176
+ sendAuthHeader: boolean,
1177
+ sendCookie = true,
1178
+ ) {
1179
+ if (user.guid != null) {
1180
+ if (this.response) {
1181
+ if (!this.response.headersSent) {
1182
+ const token = this.config.jwtBuilder(this.config, user, true);
1183
+
1184
+ if (sendCookie) {
1185
+ const appUrl = new URL(this.config.appUrl);
1186
+ this.response.cookie('TILMELDSWITCH', token, {
1187
+ domain: this.config.cookieDomain,
1188
+ path: this.config.cookiePath,
1189
+ maxAge: this.config.jwtSwitchExpire * 1000,
1190
+ secure: appUrl.protocol === 'https:',
1191
+ httpOnly: false, // Allow JS access (for PubSub auth token).
1192
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1193
+ });
1194
+ }
1195
+
1196
+ if (sendAuthHeader) {
1197
+ this.response.set('X-TILMELDSWITCH', token);
1198
+ }
1199
+ }
1200
+
1201
+ this.response.locals.user = user;
1202
+ }
1203
+
1204
+ await this.fillSession(user);
1053
1205
  return true;
1054
1206
  }
1055
1207
  return false;
@@ -1057,15 +1209,50 @@ export default class Tilmeld implements TilmeldInterface {
1057
1209
 
1058
1210
  /**
1059
1211
  * Logs the current user out of the system.
1212
+ *
1213
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1214
+ */
1215
+ public async logout(clearCookie = true) {
1216
+ if (this.request && !this.alreadyLoggedOutSwitch) {
1217
+ const cookies = this.request.cookies ?? {};
1218
+
1219
+ if ('TILMELDSWITCH' in cookies) {
1220
+ this.alreadyLoggedOutSwitch = true;
1221
+ return await this.logoutSwitch(clearCookie);
1222
+ }
1223
+ }
1224
+
1225
+ this.clearSession();
1226
+ if (this.response) {
1227
+ if (clearCookie && !this.response.headersSent) {
1228
+ this.response.clearCookie('TILMELDAUTH', {
1229
+ domain: this.config.cookieDomain,
1230
+ path: this.config.cookiePath,
1231
+ });
1232
+ this.response.set('X-TILMELDAUTH', '');
1233
+ }
1234
+ this.response.locals.user = null;
1235
+ }
1236
+ }
1237
+
1238
+ /**
1239
+ * Clears the switch user out of the system.
1240
+ *
1241
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1060
1242
  */
1061
- public logout() {
1243
+ public async logoutSwitch(clearCookie = true) {
1062
1244
  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', '');
1245
+ if (this.response) {
1246
+ if (clearCookie && !this.response.headersSent) {
1247
+ this.response.clearCookie('TILMELDSWITCH', {
1248
+ domain: this.config.cookieDomain,
1249
+ path: this.config.cookiePath,
1250
+ });
1251
+ this.response.set('X-TILMELDSWITCH', '');
1252
+ }
1253
+ this.response.locals.user = null;
1254
+
1255
+ await this.authenticate(false, true);
1069
1256
  }
1070
1257
  }
1071
1258