@nymphjs/tilmeld 1.0.0-beta.1 → 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 +505 -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 +657 -259
  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 +405 -32
  17. package/dist/User.js +1361 -372
  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 +449 -260
  62. package/src/Tilmeld.types.ts +2 -2
  63. package/src/User.test.ts +160 -7
  64. package/src/User.ts +1615 -468
  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,10 +91,35 @@ 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
  /**
86
121
  * This is used internally by Nymph. Don't call it yourself.
122
+ *
87
123
  * @returns A clone of this instance.
88
124
  */
89
125
  public clone() {
@@ -91,7 +127,7 @@ export default class Tilmeld implements TilmeldInterface {
91
127
  tilmeld.request = this.request;
92
128
  tilmeld.response = this.response;
93
129
  tilmeld.currentUser = this.currentUser;
94
- tilmeld.skipAuthenticate = true;
130
+ tilmeld.gatekeeperCache = this.gatekeeperCache;
95
131
 
96
132
  return tilmeld;
97
133
  }
@@ -106,30 +142,46 @@ export default class Tilmeld implements TilmeldInterface {
106
142
  public init(nymph: Nymph) {
107
143
  this.nymph = nymph;
108
144
 
109
- // Set up access control hooks when Nymph is called.
110
145
  if (this.nymph.driver == null) {
111
146
  throw new Error("Tilmeld can't be configured before Nymph.");
112
147
  }
113
148
 
114
149
  // Configure the classes.
115
- class NymphUser extends User {}
116
- NymphUser.nymph = this.nymph;
117
- NymphUser.tilmeld = this;
118
- (NymphUser as any).skipOnClone = true;
119
- this.User = NymphUser;
120
- this.nymph.addEntityClass(NymphUser);
121
- class NymphGroup extends Group {}
122
- NymphGroup.nymph = this.nymph;
123
- NymphGroup.tilmeld = this;
124
- (NymphUser as any).skipOnClone = true;
125
- this.Group = NymphGroup;
126
- this.nymph.addEntityClass(NymphGroup);
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);
158
+ }
127
159
 
128
- this.initAccessControl();
129
- if (this.request != null && !this.skipAuthenticate) {
130
- this.authenticate();
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();
131
180
  }
132
- this.skipAuthenticate = false;
181
+
182
+ // Set up access control hooks when Nymph is called.
183
+ this.nymph.config.debugInfo('tilmeld', 'Initializing access controls.');
184
+ this.initAccessControl();
133
185
  }
134
186
 
135
187
  /**
@@ -141,11 +193,17 @@ export default class Tilmeld implements TilmeldInterface {
141
193
  * @param ability The ability.
142
194
  * @returns Whether the user has the given ability.
143
195
  */
144
- public gatekeeper(ability?: string): boolean {
145
- if (this.currentUser == null) {
196
+ public gatekeeper(ability?: string) {
197
+ if (this.currentUser == null || this.gatekeeperCache == null) {
146
198
  return false;
147
199
  }
148
- 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
+ );
149
207
  }
150
208
 
151
209
  private initAccessControl() {
@@ -153,9 +211,9 @@ export default class Tilmeld implements TilmeldInterface {
153
211
  const handleQuery = function (
154
212
  nymph: Nymph,
155
213
  options: Options,
156
- selectors: FormattedSelector[]
214
+ selectors: FormattedSelector[],
157
215
  ) {
158
- const tilmeld = nymph.tilmeld as Tilmeld;
216
+ const tilmeld = enforceTilmeld(nymph);
159
217
  if (
160
218
  !options ||
161
219
  !('skipAc' in options) ||
@@ -164,7 +222,9 @@ export default class Tilmeld implements TilmeldInterface {
164
222
  ) {
165
223
  if (options?.source === 'client') {
166
224
  if (
167
- !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.
168
228
  ((!tilmeld.config.enableUserSearch &&
169
229
  options.class?.factorySync() instanceof User) || // The use is searching for a user and searching is disabled.
170
230
  (!tilmeld.config.enableGroupSearch &&
@@ -191,11 +251,11 @@ export default class Tilmeld implements TilmeldInterface {
191
251
  handleQuery.skipOnClone = true;
192
252
 
193
253
  // Filter entities being deleted for user permissions.
194
- const checkPermissionsDelete = function (
254
+ const checkPermissionsDelete = async function (
195
255
  nymph: Nymph,
196
- entity: EntityInterface
256
+ entity: EntityInterface,
197
257
  ) {
198
- const tilmeld = nymph.tilmeld as Tilmeld;
258
+ const tilmeld = enforceTilmeld(nymph);
199
259
  if (
200
260
  typeof entity.$tilmeldDeleteSkipAC === 'function' &&
201
261
  entity.$tilmeldDeleteSkipAC()
@@ -210,14 +270,17 @@ export default class Tilmeld implements TilmeldInterface {
210
270
  checkPermissionsDelete.skipOnClone = true;
211
271
 
212
272
  // Filter entities being deleted for user permissions.
213
- const checkPermissionsDeleteByID = function (
273
+ const checkPermissionsDeleteByID = async function (
214
274
  nymph: Nymph,
215
275
  guid: string,
216
- className?: string
276
+ className?: string,
217
277
  ) {
218
- 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(
219
282
  { class: nymph.getEntityClass(className ?? 'Entity') },
220
- { type: '&', guid: guid }
283
+ { type: '&', guid: guid },
221
284
  );
222
285
  if (entity != null) {
223
286
  checkPermissionsDelete(nymph, entity);
@@ -227,11 +290,11 @@ export default class Tilmeld implements TilmeldInterface {
227
290
 
228
291
  // Filter entities being saved for user permissions, and filter any
229
292
  // disallowed changes to AC properties.
230
- const checkPermissionsSaveAndFilterAcChanges = function (
293
+ const checkPermissionsSaveAndFilterAcChanges = async function (
231
294
  nymph: Nymph,
232
- entity: EntityInterface & AccessControlData
295
+ entity: EntityInterface & AccessControlData,
233
296
  ) {
234
- const tilmeld = nymph.tilmeld as Tilmeld;
297
+ const tilmeld = enforceTilmeld(nymph);
235
298
  if (!entity) {
236
299
  return;
237
300
  }
@@ -247,35 +310,9 @@ export default class Tilmeld implements TilmeldInterface {
247
310
  // allowing a change to ac properties.
248
311
 
249
312
  const originalAc = entity.$getOriginalAcValues();
250
- const newAc: AccessControlData = {
251
- user: entity.user ?? undefined,
252
- group: entity.group ?? undefined,
253
- acUser: entity.acUser ?? undefined,
254
- acGroup: entity.acGroup ?? undefined,
255
- acOther: entity.acOther ?? undefined,
256
- acRead: entity.acRead ?? undefined,
257
- acWrite: entity.acWrite ?? undefined,
258
- acFull: entity.acFull ?? undefined,
259
- };
313
+ const newAc = entity.$getCurrentAcValues();
260
314
 
261
- const setAcProperties = (acValues: { [k: string]: any }) => {
262
- for (let name in acValues) {
263
- const value = acValues[name];
264
- if (value == null) {
265
- delete entity[name];
266
- } else {
267
- entity[name] = value;
268
- }
269
- }
270
- };
271
-
272
- // Restore original AC properties and check permissions.
273
- setAcProperties(originalAc);
274
-
275
- function areAcPropertiesChanged(
276
- a: AccessControlData,
277
- b: AccessControlData
278
- ) {
315
+ function areAcPropertiesChanged(a: ACProperties, b: ACProperties) {
279
316
  for (const name of ['user', 'group'] as ('user' | 'group')[]) {
280
317
  const aVal = a[name];
281
318
  const bVal = b[name];
@@ -284,7 +321,7 @@ export default class Tilmeld implements TilmeldInterface {
284
321
  } else if (aVal == null || bVal == null) {
285
322
  return true;
286
323
  }
287
- if (!aVal.$is(bVal)) {
324
+ if (aVal !== bVal) {
288
325
  return true;
289
326
  }
290
327
  }
@@ -319,12 +356,7 @@ export default class Tilmeld implements TilmeldInterface {
319
356
  if (aVal.length !== bVal.length) {
320
357
  return true;
321
358
  }
322
- if (
323
- xor(
324
- aVal.map((entity) => entity.guid),
325
- bVal.map((entity) => entity.guid)
326
- ).length
327
- ) {
359
+ if (xor(aVal, bVal).length) {
328
360
  return true;
329
361
  }
330
362
  }
@@ -333,41 +365,42 @@ export default class Tilmeld implements TilmeldInterface {
333
365
 
334
366
  if (areAcPropertiesChanged(originalAc, newAc)) {
335
367
  if (
336
- !tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)
368
+ !tilmeld.checkPermissions(
369
+ entity,
370
+ TilmeldAccessLevels.FULL_ACCESS,
371
+ undefined,
372
+ originalAc,
373
+ )
337
374
  ) {
338
375
  // Only allow changes to AC properties if the user has full access.
339
376
  throw new AccessControlError(
340
- 'No permission to change access control properties.'
377
+ 'No permission to change access control properties.',
341
378
  );
342
379
  }
343
380
 
344
381
  if (
345
- ((originalAc.user && !originalAc.user.$is(newAc.user)) ||
346
- (!originalAc.user &&
347
- newAc.user &&
348
- !newAc.user.$is(tilmeld.currentUser))) &&
349
- !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)
350
386
  ) {
351
387
  throw new AccessControlError(
352
- 'No permission to assign to another user.'
388
+ 'No permission to assign to another user.',
353
389
  );
354
390
  }
355
391
 
356
392
  if (
357
- newAc.group &&
358
- !newAc.group.$is(originalAc.group) &&
393
+ newAc.group != null &&
394
+ newAc.group !== originalAc.group &&
359
395
  !(
360
- newAc.group.$is(tilmeld.currentUser?.group) ||
361
- newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
362
- ) &&
363
- !tilmeld.gatekeeper('tilmeld/admin')
396
+ newAc.group === tilmeld.currentUser?.$getGid() ||
397
+ (tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group)
398
+ )
364
399
  ) {
365
400
  throw new AccessControlError(
366
- 'No permission to assign to another group.'
401
+ 'No permission to assign to another group.',
367
402
  );
368
403
  }
369
-
370
- setAcProperties(newAc);
371
404
  }
372
405
  }
373
406
 
@@ -394,11 +427,11 @@ export default class Tilmeld implements TilmeldInterface {
394
427
  * - acGroup = TilmeldAccessLevels.READ_ACCESS
395
428
  * - acOther = TilmeldAccessLevels.NO_ACCESS
396
429
  */
397
- const addAccess = function (
430
+ const addAccess = async function (
398
431
  nymph: Nymph,
399
- entity: EntityInterface & AccessControlData
432
+ entity: EntityInterface & AccessControlData,
400
433
  ) {
401
- const tilmeld = nymph.tilmeld as Tilmeld;
434
+ const tilmeld = enforceTilmeld(nymph);
402
435
  const user = tilmeld.currentUser;
403
436
  if (
404
437
  user != null &&
@@ -409,12 +442,11 @@ export default class Tilmeld implements TilmeldInterface {
409
442
  if (entity.user == null) {
410
443
  entity.user = user;
411
444
  }
412
- if (
413
- entity.group == null &&
414
- user.group != null &&
415
- user.group.guid != null
416
- ) {
417
- 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
+ }
418
450
  }
419
451
  if (entity.acUser == null) {
420
452
  entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
@@ -438,9 +470,9 @@ export default class Tilmeld implements TilmeldInterface {
438
470
  };
439
471
  addAccess.skipOnClone = true;
440
472
 
441
- const validate = function (
473
+ const validate = async function (
442
474
  _nymph: Nymph,
443
- entity: EntityInterface & AccessControlData
475
+ entity: EntityInterface & AccessControlData,
444
476
  ) {
445
477
  if (!(entity instanceof User) && !(entity instanceof Group)) {
446
478
  const ownershipAcPropertyValidator = (prop: any) => {
@@ -450,7 +482,7 @@ export default class Tilmeld implements TilmeldInterface {
450
482
  prop > TilmeldAccessLevels.FULL_ACCESS
451
483
  ) {
452
484
  throw new AccessControlError(
453
- 'Invalid access control property: ' + prop
485
+ 'Invalid access control property: ' + prop,
454
486
  );
455
487
  }
456
488
  };
@@ -458,13 +490,13 @@ export default class Tilmeld implements TilmeldInterface {
458
490
  const accessAcPropertyValidator = (prop: any) => {
459
491
  if (!Array.isArray(prop)) {
460
492
  throw new AccessControlError(
461
- 'Invalid access control property: ' + prop
493
+ 'Invalid access control property: ' + prop,
462
494
  );
463
495
  }
464
496
  prop.forEach((value) => {
465
497
  if (!(value instanceof User || value instanceof Group)) {
466
498
  throw new AccessControlError(
467
- 'Invalid access control property: ' + prop
499
+ 'Invalid access control property: ' + prop,
468
500
  );
469
501
  }
470
502
  });
@@ -473,7 +505,7 @@ export default class Tilmeld implements TilmeldInterface {
473
505
  if ('user' in entity) {
474
506
  if (!(entity.user instanceof User)) {
475
507
  throw new AccessControlError(
476
- 'Invalid access control property: user'
508
+ 'Invalid access control property: user',
477
509
  );
478
510
  }
479
511
  }
@@ -481,7 +513,7 @@ export default class Tilmeld implements TilmeldInterface {
481
513
  if ('group' in entity) {
482
514
  if (!(entity.group instanceof Group)) {
483
515
  throw new AccessControlError(
484
- 'Invalid access control property: group'
516
+ 'Invalid access control property: group',
485
517
  );
486
518
  }
487
519
  }
@@ -526,18 +558,10 @@ export default class Tilmeld implements TilmeldInterface {
526
558
  */
527
559
  public addAccessControlSelectors(
528
560
  options: Options,
529
- selectors: FormattedSelector[]
561
+ selectors: FormattedSelector[],
530
562
  ) {
531
563
  const user = this.currentUser;
532
564
 
533
- if (
534
- user != null &&
535
- (user.abilities?.indexOf('system/admin') ?? -1) !== -1
536
- ) {
537
- // The user is a system admin, so they can see everything.
538
- return;
539
- }
540
-
541
565
  if (options == null) {
542
566
  throw new Error('No options in argument.');
543
567
  } else if (
@@ -577,33 +601,23 @@ export default class Tilmeld implements TilmeldInterface {
577
601
  ];
578
602
  const groupRefs: FormattedSelector['ref'] = [];
579
603
  const acRefs: FormattedSelector['ref'] = [];
580
- if (user.group != null && user.group.guid != null) {
604
+ const gid = user.$getGid();
605
+ if (gid != null) {
581
606
  // It belongs to the user's primary group.
582
- groupRefs.push(['group', user.group.guid]);
607
+ groupRefs.push(['group', gid]);
583
608
  // User's primary group is listed in acRead, acWrite, or acFull.
584
- acRefs.push(['acRead', user.group.guid]);
585
- acRefs.push(['acWrite', user.group.guid]);
586
- acRefs.push(['acFull', user.group.guid]);
587
- }
588
- for (let curSecondaryGroup of user.groups ?? []) {
589
- if (curSecondaryGroup != null && curSecondaryGroup.guid != null) {
590
- // It belongs to the user's secondary group.
591
- groupRefs.push(['group', curSecondaryGroup.guid]);
592
- // User's secondary group is listed in acRead, acWrite, or acFull.
593
- acRefs.push(['acRead', curSecondaryGroup.guid]);
594
- acRefs.push(['acWrite', curSecondaryGroup.guid]);
595
- acRefs.push(['acFull', curSecondaryGroup.guid]);
596
- }
609
+ acRefs.push(['acRead', gid]);
610
+ acRefs.push(['acWrite', gid]);
611
+ acRefs.push(['acFull', gid]);
597
612
  }
598
- for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
599
- if (curDescendantGroup != null && curDescendantGroup.guid != null) {
600
- // It belongs to the user's secondary group.
601
- groupRefs.push(['group', curDescendantGroup.guid]);
602
- // User's secondary group is listed in acRead, acWrite, or acFull.
603
- acRefs.push(['acRead', curDescendantGroup.guid]);
604
- acRefs.push(['acWrite', curDescendantGroup.guid]);
605
- acRefs.push(['acFull', curDescendantGroup.guid]);
606
- }
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]);
607
621
  }
608
622
  // All the group refs.
609
623
  if (groupRefs.length) {
@@ -669,7 +683,7 @@ export default class Tilmeld implements TilmeldInterface {
669
683
  * - FULL_ACCESS, the user has read, write, and delete access to the entity,
670
684
  * as well as being able to manage its access controls and ownership.
671
685
  *
672
- * These properties defaults to:
686
+ * These properties default to:
673
687
  *
674
688
  * - acUser = TilmeldAccessLevels.FULL_ACCESS
675
689
  * - acGroup = TilmeldAccessLevels.READ_ACCESS
@@ -684,9 +698,9 @@ export default class Tilmeld implements TilmeldInterface {
684
698
  * The following conditions will result in different checks, which determine
685
699
  * whether the check passes:
686
700
  *
687
- * - The user has the "system/admin" ability. (Always true.)
688
701
  * - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
689
- * - 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.)
690
704
  * - No user is logged in. (Check other AC.)
691
705
  * - The entity is the user. (Always true.)
692
706
  * - It is the user's primary group. (True for READ_ACCESS.)
@@ -704,16 +718,17 @@ export default class Tilmeld implements TilmeldInterface {
704
718
  * @param entity The entity to check.
705
719
  * @param type The lowest level of permission to consider a pass.
706
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.
707
722
  * @returns Whether the current user has at least `type` permission for the entity.
708
723
  */
709
724
  public checkPermissions(
710
- entity: EntityInterface & AccessControlData,
725
+ entity: EntityInterface,
711
726
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
712
- user?: (User & UserData) | false
727
+ user?: (User & UserData) | false,
728
+ acProperties?: ACProperties,
713
729
  ) {
714
- // Only works for entities.
715
- if (typeof entity !== 'object' || typeof entity.$is !== 'function') {
716
- return false;
730
+ if (!acProperties) {
731
+ acProperties = entity.$getCurrentAcValues() as ACProperties;
717
732
  }
718
733
 
719
734
  let userOrNull: (User & UserData) | null = null;
@@ -727,30 +742,43 @@ export default class Tilmeld implements TilmeldInterface {
727
742
  userOrEmpty = user;
728
743
  }
729
744
 
730
- if (userOrEmpty.$gatekeeper('system/admin')) {
731
- return true;
732
- }
733
-
734
745
  // Users and groups are always readable. Editable by Tilmeld admins.
735
746
  if (
736
747
  (entity instanceof User || entity instanceof Group) &&
737
748
  (type === TilmeldAccessLevels.READ_ACCESS ||
738
- userOrEmpty.$gatekeeper('tilmeld/admin'))
749
+ userOrEmpty.abilities?.includes('tilmeld/admin') ||
750
+ userOrEmpty.abilities?.includes('system/admin'))
739
751
  ) {
740
752
  return true;
741
753
  }
742
754
 
743
- // Entities with no owners are always editable.
744
- if (entity.user == null && entity.group == null) {
745
- 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
+ }
746
774
  }
747
775
 
748
776
  // Load access control, since we need it now...
749
- const acUser = entity.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
750
- const acGroup = entity.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
751
- 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;
752
780
 
753
- if (userOrNull === null) {
781
+ if (userOrNull == null) {
754
782
  return acOther >= type;
755
783
  }
756
784
 
@@ -760,45 +788,59 @@ export default class Tilmeld implements TilmeldInterface {
760
788
  }
761
789
 
762
790
  // Check if the entity is the user's group. Always readable.
791
+ const gid = userOrEmpty.$getGid();
763
792
  if (
764
- userOrEmpty.group != null &&
765
- typeof userOrEmpty.group.$is === 'function' &&
766
- userOrEmpty.group.$is(entity) &&
793
+ gid != null &&
794
+ entity.guid === gid &&
767
795
  type === TilmeldAccessLevels.READ_ACCESS
768
796
  ) {
769
797
  return true;
770
798
  }
771
799
 
800
+ // Check if the entity is one of the user's secondary groups. Always
801
+ // readable.
802
+ const gids = userOrEmpty.$getGids();
803
+ if (
804
+ gids != null &&
805
+ entity.guid != null &&
806
+ gids.indexOf(entity.guid) !== -1 &&
807
+ type === TilmeldAccessLevels.READ_ACCESS
808
+ ) {
809
+ return true;
810
+ }
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
+
772
818
  // Calculate all the groups the user belongs to.
773
- let allGroups = userOrEmpty.group == null ? [] : [userOrEmpty.group];
774
- allGroups = allGroups.concat(userOrEmpty.groups ?? []);
775
- allGroups = allGroups.concat(userOrEmpty.$getDescendantGroupsSync());
819
+ let allGids = gid == null ? [] : [gid];
820
+ allGids = allGids.concat(gids ?? []);
776
821
 
777
822
  // Check access ac properties.
778
823
  const checks = [
779
824
  {
780
825
  type: TilmeldAccessLevels.FULL_ACCESS,
781
- array: [...(entity.acFull ?? [])],
826
+ array: [...(acProperties.acFull ?? [])],
782
827
  },
783
828
  {
784
829
  type: TilmeldAccessLevels.WRITE_ACCESS,
785
- array: [...(entity.acWrite ?? [])],
830
+ array: [...(acProperties.acWrite ?? [])],
786
831
  },
787
832
  {
788
833
  type: TilmeldAccessLevels.READ_ACCESS,
789
- array: [...(entity.acRead ?? [])],
834
+ array: [...(acProperties.acRead ?? [])],
790
835
  },
791
836
  ];
792
837
  for (let curCheck of checks) {
793
838
  if (type <= curCheck.type) {
794
- if (userOrEmpty.$inArray(curCheck.array)) {
839
+ if (curCheck.array.includes(userOrEmpty.guid)) {
795
840
  return true;
796
841
  }
797
- for (let curGroup of allGroups) {
798
- if (
799
- typeof curGroup.$inArray === 'function' &&
800
- curGroup.$inArray(curCheck.array)
801
- ) {
842
+ for (let curGid of allGids) {
843
+ if (curCheck.array.includes(curGid)) {
802
844
  return true;
803
845
  }
804
846
  }
@@ -806,18 +848,10 @@ export default class Tilmeld implements TilmeldInterface {
806
848
  }
807
849
 
808
850
  // Check ownership ac properties.
809
- if (
810
- entity.user != null &&
811
- typeof entity.user.$is === 'function' &&
812
- entity.user.$is(userOrNull)
813
- ) {
851
+ if (acProperties.user != null && acProperties.user === userOrNull.guid) {
814
852
  return acUser >= type;
815
853
  }
816
- if (
817
- entity.group != null &&
818
- typeof entity.group.$inArray === 'function' &&
819
- entity.group.$inArray(allGroups)
820
- ) {
854
+ if (acProperties.group != null && allGids.includes(acProperties.group)) {
821
855
  return acGroup >= type;
822
856
  }
823
857
  return acOther >= type;
@@ -847,10 +881,10 @@ export default class Tilmeld implements TilmeldInterface {
847
881
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
848
882
  * @returns Whether the current user has at least `type` permission for the UID.
849
883
  */
850
- public checkClientUIDPermissions(
884
+ public async checkClientUIDPermissions(
851
885
  name: string,
852
886
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
853
- user?: (User & UserData) | false
887
+ user?: (User & UserData) | false,
854
888
  ) {
855
889
  let userOrEmpty: User & UserData = this.User.factorySync();
856
890
  // Calculate the user.
@@ -860,30 +894,30 @@ export default class Tilmeld implements TilmeldInterface {
860
894
  userOrEmpty = user;
861
895
  }
862
896
 
863
- if (userOrEmpty.$gatekeeper('system/admin')) {
897
+ if (userOrEmpty.abilities?.includes('system/admin')) {
864
898
  return true;
865
899
  }
866
900
 
867
901
  if (type === TilmeldAccessLevels.FULL_ACCESS) {
868
902
  return (
869
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
870
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
903
+ this.config.clientSetabledUIDs.includes(name) ||
904
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
871
905
  );
872
906
  } else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
873
907
  return (
874
- this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
875
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
876
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
877
- 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}`))
878
912
  );
879
913
  } else if (type === TilmeldAccessLevels.READ_ACCESS) {
880
914
  return (
881
- this.config.clientReadableUIDs.indexOf(name) !== -1 ||
882
- this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
883
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
884
- userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
885
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
886
- 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}`))
887
921
  );
888
922
  }
889
923
 
@@ -895,20 +929,13 @@ export default class Tilmeld implements TilmeldInterface {
895
929
  *
896
930
  * @param user The user.
897
931
  */
898
- public fillSession(user: User & UserData) {
899
- // Read groups right now, since gatekeeper needs them, so
900
- // $udpateDataProtection will fail to read them (since it runs gatekeeper).
901
- const _group = user.group;
902
- const _groups = user.groups;
932
+ public async fillSession(user: User & UserData) {
903
933
  this.currentUser = user;
934
+ this.gatekeeperCache = await user.$getGatekeeperCache();
935
+
904
936
  // Now update the data protection on the user and all the groups.
905
937
  this.currentUser.$updateDataProtection();
906
- if (this.currentUser.group != null) {
907
- this.currentUser.group.$updateDataProtection();
908
- }
909
- for (let group of this.currentUser.groups ?? []) {
910
- group.$updateDataProtection();
911
- }
938
+ this.currentUser.$updateGroupDataProtection();
912
939
  }
913
940
 
914
941
  /**
@@ -917,14 +944,10 @@ export default class Tilmeld implements TilmeldInterface {
917
944
  public clearSession() {
918
945
  const user = this.currentUser;
919
946
  this.currentUser = null;
947
+ this.gatekeeperCache = null;
920
948
  if (user) {
921
949
  user.$updateDataProtection();
922
- if (user.group != null) {
923
- user.group.$updateDataProtection();
924
- }
925
- for (let group of user.groups ?? []) {
926
- group.$updateDataProtection();
927
- }
950
+ user.$updateGroupDataProtection();
928
951
  }
929
952
  }
930
953
 
@@ -935,6 +958,7 @@ export default class Tilmeld implements TilmeldInterface {
935
958
  * @returns The user on success, null on failure.
936
959
  */
937
960
  public async extractToken(token: string) {
961
+ this.nymph.config.debugInfo('tilmeld', 'Extracting and validating token.');
938
962
  const extract = this.config.jwtExtract(this.config, token);
939
963
  if (extract == null) {
940
964
  return null;
@@ -946,9 +970,9 @@ export default class Tilmeld implements TilmeldInterface {
946
970
  {
947
971
  type: '&',
948
972
  guid: guid,
949
- }
973
+ },
950
974
  );
951
- if (!user || !user.guid || !user.enabled) {
975
+ if (!user || !user.guid) {
952
976
  return null;
953
977
  }
954
978
 
@@ -956,40 +980,73 @@ export default class Tilmeld implements TilmeldInterface {
956
980
  }
957
981
 
958
982
  /**
959
- * 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.
960
994
  *
961
995
  * @param skipXsrfToken Skip the XSRF token check.
996
+ * @param skipRenew Skip the token renewal step, even if the token is close to expiration.
962
997
  * @returns True if a user was authenticated, false on any failure.
963
998
  */
964
- public authenticate(skipXsrfToken = false) {
999
+ public async authenticate(skipXsrfToken = false, skipRenew = false) {
965
1000
  if (this.request == null) {
966
1001
  return false;
967
1002
  }
968
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
+
969
1015
  const cookies = this.request.cookies ?? {};
970
1016
 
971
1017
  // If a client does't support cookies, they can use the X-TILMELDAUTH header
972
1018
  // to provide the auth token.
973
1019
  let fromAuthHeader = false;
974
1020
  let authToken: string;
1021
+ let switchToken: string | null;
975
1022
  if (this.request.header('x-tilmeldauth') != null) {
976
1023
  fromAuthHeader = true;
977
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;
978
1030
  } else if ('TILMELDAUTH' in cookies) {
979
1031
  fromAuthHeader = false;
980
1032
  authToken = cookies.TILMELDAUTH;
1033
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
981
1034
  } else {
982
1035
  return false;
983
1036
  }
984
1037
 
985
- 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;
986
1040
  if (
987
1041
  skipXsrfToken ||
988
1042
  this.request.originalUrl.startsWith(this.config.setupPath)
989
1043
  ) {
990
1044
  // The request is for the setup app, or we were told to skip the XSRF
991
1045
  // check, so don't check for the XSRF token.
992
- 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;
993
1050
  } else {
994
1051
  // The request is for something else, so check for a valid XSRF token,
995
1052
  // unless the auth token is provided by a header (instead of a cookie).
@@ -998,28 +1055,65 @@ export default class Tilmeld implements TilmeldInterface {
998
1055
  return false;
999
1056
  }
1000
1057
 
1001
- 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;
1002
1062
  }
1003
1063
 
1004
- if (extract == null) {
1005
- this.logout();
1064
+ if (authExtract == null) {
1065
+ await this.logout();
1006
1066
  return false;
1007
1067
  }
1008
- const { guid, expire } = extract;
1068
+ const { guid, issued, expire } = authExtract;
1069
+ const { guid: switchGuid, expire: switchExpire } = switchExtract || {
1070
+ guid: null,
1071
+ expire: null,
1072
+ };
1009
1073
 
1010
- const user = this.User.factorySync(guid);
1011
- if (user.guid == null || !user.enabled) {
1012
- 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();
1013
1083
  return false;
1014
1084
  }
1015
1085
 
1016
- 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
+ ) {
1017
1106
  // If the user is less than renew time from needing a new token, give them
1018
1107
  // a new one.
1019
- this.login(user, fromAuthHeader);
1108
+ await this.login(user, fromAuthHeader);
1020
1109
  } else {
1021
- this.fillSession(user);
1110
+ await this.fillSession(user);
1022
1111
  }
1112
+
1113
+ if (this.response) {
1114
+ this.response.locals.user = user;
1115
+ }
1116
+
1023
1117
  return true;
1024
1118
  }
1025
1119
 
@@ -1027,27 +1121,87 @@ export default class Tilmeld implements TilmeldInterface {
1027
1121
  * Logs the given user into the system.
1028
1122
  *
1029
1123
  * @param user The user.
1030
- * @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.
1031
1126
  * @returns True on success, false on failure.
1032
1127
  */
1033
- public login(user: User & UserData, sendAuthHeader: boolean) {
1128
+ public async login(
1129
+ user: User & UserData,
1130
+ sendAuthHeader: boolean,
1131
+ sendCookie = true,
1132
+ ) {
1034
1133
  if (user.guid != null && user.enabled) {
1035
- if (this.response && !this.response.headersSent) {
1036
- const token = this.config.jwtBuilder(this.config, user);
1037
- const appUrl = new URL(this.config.appUrl);
1038
- this.response.cookie('TILMELDAUTH', token, {
1039
- domain: this.config.cookieDomain,
1040
- path: this.config.cookiePath,
1041
- maxAge: this.config.jwtExpire * 1000,
1042
- secure: appUrl.protocol === 'https',
1043
- httpOnly: false, // Allow JS access (for CSRF protection).
1044
- sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1045
- });
1046
- if (sendAuthHeader) {
1047
- 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
+ }
1153
+ }
1154
+
1155
+ this.response.locals.user = user;
1156
+ }
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
+ }
1048
1199
  }
1200
+
1201
+ this.response.locals.user = user;
1049
1202
  }
1050
- this.fillSession(user);
1203
+
1204
+ await this.fillSession(user);
1051
1205
  return true;
1052
1206
  }
1053
1207
  return false;
@@ -1055,15 +1209,50 @@ export default class Tilmeld implements TilmeldInterface {
1055
1209
 
1056
1210
  /**
1057
1211
  * Logs the current user out of the system.
1212
+ *
1213
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1058
1214
  */
1059
- public logout() {
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
+
1060
1225
  this.clearSession();
1061
- if (this.response && !this.response.headersSent) {
1062
- this.response.clearCookie('TILMELDAUTH', {
1063
- domain: this.config.cookieDomain,
1064
- path: this.config.cookiePath,
1065
- });
1066
- this.response.set('X-TILMELDAUTH', '');
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.)
1242
+ */
1243
+ public async logoutSwitch(clearCookie = true) {
1244
+ this.clearSession();
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);
1067
1256
  }
1068
1257
  }
1069
1258