@nymphjs/tilmeld 1.0.0-beta.4 → 1.0.0-beta.41

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.
package/src/Tilmeld.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  Options,
6
6
  TilmeldInterface,
7
7
  TilmeldAccessLevels,
8
+ ACProperties,
8
9
  } from '@nymphjs/nymph';
9
10
  import { URL } from 'url';
10
11
  import { Request, Response } from 'express';
@@ -15,6 +16,7 @@ import { AccessControlError } from './errors';
15
16
  import Group from './Group';
16
17
  import User, { UserData } from './User';
17
18
  import { AccessControlData } from './Tilmeld.types';
19
+ import { enforceTilmeld } from './enforceTilmeld';
18
20
 
19
21
  /**
20
22
  * A user and group system for Nymph.js.
@@ -69,9 +71,9 @@ export default class Tilmeld implements TilmeldInterface {
69
71
  public response: Response | null = null;
70
72
 
71
73
  /**
72
- * Skip the authenticate step of initialization.
74
+ * Used to avoid infinite loop.
73
75
  */
74
- private skipAuthenticate = false;
76
+ private alreadyLoggedOutSwitch = false;
75
77
 
76
78
  /**
77
79
  * Create a new instance of Tilmeld.
@@ -84,6 +86,7 @@ export default class Tilmeld implements TilmeldInterface {
84
86
 
85
87
  /**
86
88
  * This is used internally by Nymph. Don't call it yourself.
89
+ *
87
90
  * @returns A clone of this instance.
88
91
  */
89
92
  public clone() {
@@ -91,7 +94,6 @@ export default class Tilmeld implements TilmeldInterface {
91
94
  tilmeld.request = this.request;
92
95
  tilmeld.response = this.response;
93
96
  tilmeld.currentUser = this.currentUser;
94
- tilmeld.skipAuthenticate = true;
95
97
 
96
98
  return tilmeld;
97
99
  }
@@ -106,30 +108,41 @@ export default class Tilmeld implements TilmeldInterface {
106
108
  public init(nymph: Nymph) {
107
109
  this.nymph = nymph;
108
110
 
109
- // Set up access control hooks when Nymph is called.
110
111
  if (this.nymph.driver == null) {
111
112
  throw new Error("Tilmeld can't be configured before Nymph.");
112
113
  }
113
114
 
114
115
  // 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);
116
+ if (nymph.parent) {
117
+ // This is a cloned Nymph, it should have classes already.
118
+ this.User = this.nymph.getEntityClass(User);
119
+ this.Group = this.nymph.getEntityClass(Group);
120
+ } else {
121
+ // This is a new Nymph, add the classes.
122
+ this.User = this.nymph.addEntityClass(User);
123
+ this.Group = this.nymph.addEntityClass(Group);
124
+ }
127
125
 
128
- this.initAccessControl();
129
- if (this.request != null && !this.skipAuthenticate) {
130
- this.authenticate();
126
+ // Clone the current user so its Nymph instance is correct.
127
+ if (this.currentUser) {
128
+ const currentUserGUID = this.currentUser.guid;
129
+ const currentUserCDate = this.currentUser.cdate;
130
+ const currentUserMDate = this.currentUser.mdate;
131
+ const currentUserTags = this.currentUser.tags;
132
+ const currentUserData = this.currentUser.$getData();
133
+ const currentUserSData = this.currentUser.$getSData();
134
+
135
+ const user = this.User.factorySync();
136
+ user.guid = currentUserGUID;
137
+ user.cdate = currentUserCDate;
138
+ user.mdate = currentUserMDate;
139
+ user.tags = currentUserTags;
140
+ user.$putData(currentUserData, currentUserSData);
141
+ this.fillSession(user);
131
142
  }
132
- this.skipAuthenticate = false;
143
+
144
+ // Set up access control hooks when Nymph is called.
145
+ this.initAccessControl();
133
146
  }
134
147
 
135
148
  /**
@@ -141,11 +154,11 @@ export default class Tilmeld implements TilmeldInterface {
141
154
  * @param ability The ability.
142
155
  * @returns Whether the user has the given ability.
143
156
  */
144
- public gatekeeper(ability?: string): boolean {
157
+ public async gatekeeper(ability?: string): Promise<boolean> {
145
158
  if (this.currentUser == null) {
146
159
  return false;
147
160
  }
148
- return this.currentUser.$gatekeeper(ability);
161
+ return await this.currentUser.$gatekeeper(ability);
149
162
  }
150
163
 
151
164
  private initAccessControl() {
@@ -155,7 +168,7 @@ export default class Tilmeld implements TilmeldInterface {
155
168
  options: Options,
156
169
  selectors: FormattedSelector[]
157
170
  ) {
158
- const tilmeld = nymph.tilmeld as Tilmeld;
171
+ const tilmeld = enforceTilmeld(nymph);
159
172
  if (
160
173
  !options ||
161
174
  !('skipAc' in options) ||
@@ -164,7 +177,9 @@ export default class Tilmeld implements TilmeldInterface {
164
177
  ) {
165
178
  if (options?.source === 'client') {
166
179
  if (
167
- !tilmeld.gatekeeper('tilmeld/admin') && // The user is not an admin.
180
+ (!tilmeld.currentUser ||
181
+ (!tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
182
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) && // The user is not an admin.
168
183
  ((!tilmeld.config.enableUserSearch &&
169
184
  options.class?.factorySync() instanceof User) || // The use is searching for a user and searching is disabled.
170
185
  (!tilmeld.config.enableGroupSearch &&
@@ -195,7 +210,7 @@ export default class Tilmeld implements TilmeldInterface {
195
210
  nymph: Nymph,
196
211
  entity: EntityInterface
197
212
  ) {
198
- const tilmeld = nymph.tilmeld as Tilmeld;
213
+ const tilmeld = enforceTilmeld(nymph);
199
214
  if (
200
215
  typeof entity.$tilmeldDeleteSkipAC === 'function' &&
201
216
  entity.$tilmeldDeleteSkipAC()
@@ -215,7 +230,7 @@ export default class Tilmeld implements TilmeldInterface {
215
230
  guid: string,
216
231
  className?: string
217
232
  ) {
218
- const entity = nymph.driver.getEntitySync(
233
+ const entity = await nymph.getEntity(
219
234
  { class: nymph.getEntityClass(className ?? 'Entity') },
220
235
  { type: '&', guid: guid }
221
236
  );
@@ -231,7 +246,7 @@ export default class Tilmeld implements TilmeldInterface {
231
246
  nymph: Nymph,
232
247
  entity: EntityInterface & AccessControlData
233
248
  ) {
234
- const tilmeld = nymph.tilmeld as Tilmeld;
249
+ const tilmeld = enforceTilmeld(nymph);
235
250
  if (!entity) {
236
251
  return;
237
252
  }
@@ -247,35 +262,9 @@ export default class Tilmeld implements TilmeldInterface {
247
262
  // allowing a change to ac properties.
248
263
 
249
264
  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
- };
260
-
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);
265
+ const newAc = entity.$getCurrentAcValues();
274
266
 
275
- function areAcPropertiesChanged(
276
- a: AccessControlData,
277
- b: AccessControlData
278
- ) {
267
+ function areAcPropertiesChanged(a: ACProperties, b: ACProperties) {
279
268
  for (const name of ['user', 'group'] as ('user' | 'group')[]) {
280
269
  const aVal = a[name];
281
270
  const bVal = b[name];
@@ -284,7 +273,7 @@ export default class Tilmeld implements TilmeldInterface {
284
273
  } else if (aVal == null || bVal == null) {
285
274
  return true;
286
275
  }
287
- if (!aVal.$is(bVal)) {
276
+ if (aVal !== bVal) {
288
277
  return true;
289
278
  }
290
279
  }
@@ -319,12 +308,7 @@ export default class Tilmeld implements TilmeldInterface {
319
308
  if (aVal.length !== bVal.length) {
320
309
  return true;
321
310
  }
322
- if (
323
- xor(
324
- aVal.map((entity) => entity.guid),
325
- bVal.map((entity) => entity.guid)
326
- ).length
327
- ) {
311
+ if (xor(aVal, bVal).length) {
328
312
  return true;
329
313
  }
330
314
  }
@@ -333,7 +317,12 @@ export default class Tilmeld implements TilmeldInterface {
333
317
 
334
318
  if (areAcPropertiesChanged(originalAc, newAc)) {
335
319
  if (
336
- !tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)
320
+ !tilmeld.checkPermissions(
321
+ entity,
322
+ TilmeldAccessLevels.FULL_ACCESS,
323
+ undefined,
324
+ originalAc
325
+ )
337
326
  ) {
338
327
  // Only allow changes to AC properties if the user has full access.
339
328
  throw new AccessControlError(
@@ -342,11 +331,10 @@ export default class Tilmeld implements TilmeldInterface {
342
331
  }
343
332
 
344
333
  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')
334
+ (originalAc.user && originalAc.user !== newAc.user) ||
335
+ (!originalAc.user &&
336
+ newAc.user &&
337
+ newAc.user !== tilmeld.currentUser)
350
338
  ) {
351
339
  throw new AccessControlError(
352
340
  'No permission to assign to another user.'
@@ -355,19 +343,15 @@ export default class Tilmeld implements TilmeldInterface {
355
343
 
356
344
  if (
357
345
  newAc.group &&
358
- !newAc.group.$is(originalAc.group) &&
359
- !(
360
- newAc.group.$is(tilmeld.currentUser?.group) ||
361
- newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
362
- ) &&
363
- !tilmeld.gatekeeper('tilmeld/admin')
346
+ newAc.group !== originalAc.group &&
347
+ !(newAc.group =
348
+ tilmeld.currentUser?.$getGid() ||
349
+ (tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group))
364
350
  ) {
365
351
  throw new AccessControlError(
366
352
  'No permission to assign to another group.'
367
353
  );
368
354
  }
369
-
370
- setAcProperties(newAc);
371
355
  }
372
356
  }
373
357
 
@@ -398,7 +382,7 @@ export default class Tilmeld implements TilmeldInterface {
398
382
  nymph: Nymph,
399
383
  entity: EntityInterface & AccessControlData
400
384
  ) {
401
- const tilmeld = nymph.tilmeld as Tilmeld;
385
+ const tilmeld = enforceTilmeld(nymph);
402
386
  const user = tilmeld.currentUser;
403
387
  if (
404
388
  user != null &&
@@ -409,12 +393,11 @@ export default class Tilmeld implements TilmeldInterface {
409
393
  if (entity.user == null) {
410
394
  entity.user = user;
411
395
  }
412
- if (
413
- entity.group == null &&
414
- user.group != null &&
415
- user.group.guid != null
416
- ) {
417
- entity.group = user.group;
396
+ if (entity.group == null) {
397
+ const group = user.group;
398
+ if (group != null && group.guid != null) {
399
+ entity.group = group;
400
+ }
418
401
  }
419
402
  if (entity.acUser == null) {
420
403
  entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
@@ -530,14 +513,6 @@ export default class Tilmeld implements TilmeldInterface {
530
513
  ) {
531
514
  const user = this.currentUser;
532
515
 
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
516
  if (options == null) {
542
517
  throw new Error('No options in argument.');
543
518
  } else if (
@@ -577,33 +552,23 @@ export default class Tilmeld implements TilmeldInterface {
577
552
  ];
578
553
  const groupRefs: FormattedSelector['ref'] = [];
579
554
  const acRefs: FormattedSelector['ref'] = [];
580
- if (user.group != null && user.group.guid != null) {
555
+ const gid = user.$getGid();
556
+ if (gid != null) {
581
557
  // It belongs to the user's primary group.
582
- groupRefs.push(['group', user.group.guid]);
558
+ groupRefs.push(['group', gid]);
583
559
  // 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
- }
560
+ acRefs.push(['acRead', gid]);
561
+ acRefs.push(['acWrite', gid]);
562
+ acRefs.push(['acFull', gid]);
597
563
  }
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
- }
564
+ const gids = user.$getGids();
565
+ for (let curGid of gids ?? []) {
566
+ // It belongs to the user's secondary group.
567
+ groupRefs.push(['group', curGid]);
568
+ // User's secondary group is listed in acRead, acWrite, or acFull.
569
+ acRefs.push(['acRead', curGid]);
570
+ acRefs.push(['acWrite', curGid]);
571
+ acRefs.push(['acFull', curGid]);
607
572
  }
608
573
  // All the group refs.
609
574
  if (groupRefs.length) {
@@ -669,7 +634,7 @@ export default class Tilmeld implements TilmeldInterface {
669
634
  * - FULL_ACCESS, the user has read, write, and delete access to the entity,
670
635
  * as well as being able to manage its access controls and ownership.
671
636
  *
672
- * These properties defaults to:
637
+ * These properties default to:
673
638
  *
674
639
  * - acUser = TilmeldAccessLevels.FULL_ACCESS
675
640
  * - acGroup = TilmeldAccessLevels.READ_ACCESS
@@ -684,9 +649,7 @@ export default class Tilmeld implements TilmeldInterface {
684
649
  * The following conditions will result in different checks, which determine
685
650
  * whether the check passes:
686
651
  *
687
- * - The user has the "system/admin" ability. (Always true.)
688
652
  * - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
689
- * - The entity has no "user" and no "group". (Always true.)
690
653
  * - No user is logged in. (Check other AC.)
691
654
  * - The entity is the user. (Always true.)
692
655
  * - It is the user's primary group. (True for READ_ACCESS.)
@@ -704,16 +667,17 @@ export default class Tilmeld implements TilmeldInterface {
704
667
  * @param entity The entity to check.
705
668
  * @param type The lowest level of permission to consider a pass.
706
669
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
670
+ * @param acProperties The acProperties to use instead of getting them from the entity.
707
671
  * @returns Whether the current user has at least `type` permission for the entity.
708
672
  */
709
673
  public checkPermissions(
710
- entity: EntityInterface & AccessControlData,
674
+ entity: EntityInterface,
711
675
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
712
- user?: (User & UserData) | false
676
+ user?: (User & UserData) | false,
677
+ acProperties?: ACProperties
713
678
  ) {
714
- // Only works for entities.
715
- if (typeof entity !== 'object' || typeof entity.$is !== 'function') {
716
- return false;
679
+ if (!acProperties) {
680
+ acProperties = entity.$getCurrentAcValues() as ACProperties;
717
681
  }
718
682
 
719
683
  let userOrNull: (User & UserData) | null = null;
@@ -727,30 +691,22 @@ export default class Tilmeld implements TilmeldInterface {
727
691
  userOrEmpty = user;
728
692
  }
729
693
 
730
- if (userOrEmpty.$gatekeeper('system/admin')) {
731
- return true;
732
- }
733
-
734
694
  // Users and groups are always readable. Editable by Tilmeld admins.
735
695
  if (
736
696
  (entity instanceof User || entity instanceof Group) &&
737
697
  (type === TilmeldAccessLevels.READ_ACCESS ||
738
- userOrEmpty.$gatekeeper('tilmeld/admin'))
698
+ userOrEmpty.abilities?.includes('tilmeld/admin') ||
699
+ userOrEmpty.abilities?.includes('system/admin'))
739
700
  ) {
740
701
  return true;
741
702
  }
742
703
 
743
- // Entities with no owners are always editable.
744
- if (entity.user == null && entity.group == null) {
745
- return true;
746
- }
747
-
748
704
  // 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;
705
+ const acUser = acProperties.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
706
+ const acGroup = acProperties.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
707
+ const acOther = acProperties.acOther ?? TilmeldAccessLevels.NO_ACCESS;
752
708
 
753
- if (userOrNull === null) {
709
+ if (userOrNull == null) {
754
710
  return acOther >= type;
755
711
  }
756
712
 
@@ -760,45 +716,41 @@ export default class Tilmeld implements TilmeldInterface {
760
716
  }
761
717
 
762
718
  // Check if the entity is the user's group. Always readable.
719
+ const gid = userOrEmpty.$getGid();
763
720
  if (
764
- userOrEmpty.group != null &&
765
- typeof userOrEmpty.group.$is === 'function' &&
766
- userOrEmpty.group.$is(entity) &&
721
+ gid != null &&
722
+ entity.guid === gid &&
767
723
  type === TilmeldAccessLevels.READ_ACCESS
768
724
  ) {
769
725
  return true;
770
726
  }
771
727
 
772
728
  // 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());
729
+ let allGids = gid == null ? [] : [gid];
730
+ allGids = allGids.concat(userOrEmpty.$getGids() ?? []);
776
731
 
777
732
  // Check access ac properties.
778
733
  const checks = [
779
734
  {
780
735
  type: TilmeldAccessLevels.FULL_ACCESS,
781
- array: [...(entity.acFull ?? [])],
736
+ array: [...(acProperties.acFull ?? [])],
782
737
  },
783
738
  {
784
739
  type: TilmeldAccessLevels.WRITE_ACCESS,
785
- array: [...(entity.acWrite ?? [])],
740
+ array: [...(acProperties.acWrite ?? [])],
786
741
  },
787
742
  {
788
743
  type: TilmeldAccessLevels.READ_ACCESS,
789
- array: [...(entity.acRead ?? [])],
744
+ array: [...(acProperties.acRead ?? [])],
790
745
  },
791
746
  ];
792
747
  for (let curCheck of checks) {
793
748
  if (type <= curCheck.type) {
794
- if (userOrEmpty.$inArray(curCheck.array)) {
749
+ if (curCheck.array.includes(userOrEmpty.guid)) {
795
750
  return true;
796
751
  }
797
- for (let curGroup of allGroups) {
798
- if (
799
- typeof curGroup.$inArray === 'function' &&
800
- curGroup.$inArray(curCheck.array)
801
- ) {
752
+ for (let curGid of allGids) {
753
+ if (curCheck.array.includes(curGid)) {
802
754
  return true;
803
755
  }
804
756
  }
@@ -806,18 +758,10 @@ export default class Tilmeld implements TilmeldInterface {
806
758
  }
807
759
 
808
760
  // Check ownership ac properties.
809
- if (
810
- entity.user != null &&
811
- typeof entity.user.$is === 'function' &&
812
- entity.user.$is(userOrNull)
813
- ) {
761
+ if (acProperties.user != null && acProperties.user === userOrNull.guid) {
814
762
  return acUser >= type;
815
763
  }
816
- if (
817
- entity.group != null &&
818
- typeof entity.group.$inArray === 'function' &&
819
- entity.group.$inArray(allGroups)
820
- ) {
764
+ if (acProperties.group != null && allGids.includes(acProperties.group)) {
821
765
  return acGroup >= type;
822
766
  }
823
767
  return acOther >= type;
@@ -847,7 +791,7 @@ export default class Tilmeld implements TilmeldInterface {
847
791
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
848
792
  * @returns Whether the current user has at least `type` permission for the UID.
849
793
  */
850
- public checkClientUIDPermissions(
794
+ public async checkClientUIDPermissions(
851
795
  name: string,
852
796
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
853
797
  user?: (User & UserData) | false
@@ -860,30 +804,30 @@ export default class Tilmeld implements TilmeldInterface {
860
804
  userOrEmpty = user;
861
805
  }
862
806
 
863
- if (userOrEmpty.$gatekeeper('system/admin')) {
807
+ if (userOrEmpty.abilities?.includes('system/admin')) {
864
808
  return true;
865
809
  }
866
810
 
867
811
  if (type === TilmeldAccessLevels.FULL_ACCESS) {
868
812
  return (
869
813
  this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
870
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
814
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
871
815
  );
872
816
  } else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
873
817
  return (
874
818
  this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
875
819
  this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
876
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
877
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
820
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
821
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
878
822
  );
879
823
  } else if (type === TilmeldAccessLevels.READ_ACCESS) {
880
824
  return (
881
825
  this.config.clientReadableUIDs.indexOf(name) !== -1 ||
882
826
  this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
883
827
  this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
884
- userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
885
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
886
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
828
+ (await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
829
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
830
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
887
831
  );
888
832
  }
889
833
 
@@ -896,19 +840,10 @@ export default class Tilmeld implements TilmeldInterface {
896
840
  * @param user The user.
897
841
  */
898
842
  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;
903
843
  this.currentUser = user;
904
844
  // Now update the data protection on the user and all the groups.
905
845
  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
- }
846
+ this.currentUser.$updateGroupDataProtection();
912
847
  }
913
848
 
914
849
  /**
@@ -919,12 +854,7 @@ export default class Tilmeld implements TilmeldInterface {
919
854
  this.currentUser = null;
920
855
  if (user) {
921
856
  user.$updateDataProtection();
922
- if (user.group != null) {
923
- user.group.$updateDataProtection();
924
- }
925
- for (let group of user.groups ?? []) {
926
- group.$updateDataProtection();
927
- }
857
+ user.$updateGroupDataProtection();
928
858
  }
929
859
  }
930
860
 
@@ -948,7 +878,7 @@ export default class Tilmeld implements TilmeldInterface {
948
878
  guid: guid,
949
879
  }
950
880
  );
951
- if (!user || !user.guid || !user.enabled) {
881
+ if (!user || !user.guid) {
952
882
  return null;
953
883
  }
954
884
 
@@ -956,40 +886,72 @@ export default class Tilmeld implements TilmeldInterface {
956
886
  }
957
887
 
958
888
  /**
959
- * Check for a TILMELDAUTH token, and, if set, authenticate from it.
889
+ * Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
890
+ * from it/them.
891
+ *
892
+ * You can also call this function after setting `response.locals.user` to the
893
+ * user you want to authenticate. You *should* check for `user.enabled` before
894
+ * setting this variable, unless you explicitly want to log in as a disabled
895
+ * user. (The user must be an instance of the User class for this Tilmeld
896
+ * instance.)
897
+ *
898
+ * This function will set `response.locals.user` to the logged in user on
899
+ * successful authentication.
960
900
  *
961
901
  * @param skipXsrfToken Skip the XSRF token check.
962
902
  * @returns True if a user was authenticated, false on any failure.
963
903
  */
964
- public authenticate(skipXsrfToken = false) {
904
+ public async authenticate(skipXsrfToken = false) {
965
905
  if (this.request == null) {
966
906
  return false;
967
907
  }
968
908
 
909
+ if (
910
+ this.response &&
911
+ this.response.locals.user &&
912
+ this.response.locals.user instanceof this.User
913
+ ) {
914
+ // The user has already been authenticated through some other means.
915
+ const user = this.response.locals.user;
916
+ this.fillSession(user);
917
+ return true;
918
+ }
919
+
969
920
  const cookies = this.request.cookies ?? {};
970
921
 
971
922
  // If a client does't support cookies, they can use the X-TILMELDAUTH header
972
923
  // to provide the auth token.
973
924
  let fromAuthHeader = false;
974
925
  let authToken: string;
926
+ let switchToken: string | null;
975
927
  if (this.request.header('x-tilmeldauth') != null) {
976
928
  fromAuthHeader = true;
977
929
  authToken = this.request.header('x-tilmeldauth') as string;
930
+ switchToken =
931
+ (this.request.header('x-tilmeldswitch') as string) ||
932
+ 'TILMELDSWITCH' in cookies
933
+ ? cookies.TILMELDSWITCH
934
+ : null;
978
935
  } else if ('TILMELDAUTH' in cookies) {
979
936
  fromAuthHeader = false;
980
937
  authToken = cookies.TILMELDAUTH;
938
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
981
939
  } else {
982
940
  return false;
983
941
  }
984
942
 
985
- let extract: { guid: string; expire: Date } | null;
943
+ let authExtract: { guid: string; issued: Date; expire: Date } | null;
944
+ let switchExtract: { guid: string; expire: Date } | null;
986
945
  if (
987
946
  skipXsrfToken ||
988
947
  this.request.originalUrl.startsWith(this.config.setupPath)
989
948
  ) {
990
949
  // The request is for the setup app, or we were told to skip the XSRF
991
950
  // check, so don't check for the XSRF token.
992
- extract = this.config.jwtExtract(this.config, authToken);
951
+ authExtract = this.config.jwtExtract(this.config, authToken);
952
+ switchExtract = switchToken
953
+ ? this.config.jwtExtract(this.config, switchToken)
954
+ : null;
993
955
  } else {
994
956
  // The request is for something else, so check for a valid XSRF token,
995
957
  // unless the auth token is provided by a header (instead of a cookie).
@@ -998,28 +960,62 @@ export default class Tilmeld implements TilmeldInterface {
998
960
  return false;
999
961
  }
1000
962
 
1001
- extract = this.config.jwtExtract(this.config, authToken, xsrfToken);
963
+ authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
964
+ switchExtract = switchToken
965
+ ? this.config.jwtExtract(this.config, switchToken)
966
+ : null;
1002
967
  }
1003
968
 
1004
- if (extract == null) {
1005
- this.logout();
969
+ if (authExtract == null) {
970
+ await this.logout();
1006
971
  return false;
1007
972
  }
1008
- const { guid, expire } = extract;
973
+ const { guid, issued, expire } = authExtract;
974
+ const { guid: switchGuid, expire: switchExpire } = switchExtract || {
975
+ guid: null,
976
+ expire: null,
977
+ };
1009
978
 
1010
- const user = this.User.factorySync(guid);
1011
- if (user.guid == null || !user.enabled) {
1012
- this.logout();
979
+ const user = await this.User.factory(guid);
980
+ if (
981
+ user.guid == null ||
982
+ !user.enabled ||
983
+ expire.getTime() <= Date.now() ||
984
+ issued.getTime() > Date.now() ||
985
+ (user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)
986
+ ) {
987
+ await this.logout();
1013
988
  return false;
1014
989
  }
1015
990
 
1016
- if (expire.valueOf() < Date.now() + this.config.jwtRenew * 1000) {
991
+ if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
992
+ // Load the switch user.
993
+ const switchUser = await this.User.factory(switchGuid);
994
+ if (switchUser.guid == null) {
995
+ await this.logoutSwitch();
996
+ } else {
997
+ this.fillSession(switchUser);
998
+
999
+ if (this.response) {
1000
+ this.response.locals.user = switchUser;
1001
+ }
1002
+
1003
+ return true;
1004
+ }
1005
+ }
1006
+
1007
+ if (expire.getTime() < Date.now() + this.config.jwtRenew * 1000) {
1017
1008
  // If the user is less than renew time from needing a new token, give them
1018
1009
  // a new one.
1019
1010
  this.login(user, fromAuthHeader);
1020
1011
  } else {
1021
1012
  this.fillSession(user);
1022
1013
  }
1014
+
1015
+ if (this.response) {
1016
+ this.response.locals.user = user;
1017
+ }
1018
+
1023
1019
  return true;
1024
1020
  }
1025
1021
 
@@ -1027,26 +1023,86 @@ export default class Tilmeld implements TilmeldInterface {
1027
1023
  * Logs the given user into the system.
1028
1024
  *
1029
1025
  * @param user The user.
1030
- * @param sendAuthHeader When true, a custom header with the auth token will be sent.
1026
+ * @param sendAuthHeader Send the auth token as a custom header.
1027
+ * @param sendCookie Send the auth token as a cookie.
1031
1028
  * @returns True on success, false on failure.
1032
1029
  */
1033
- public login(user: User & UserData, sendAuthHeader: boolean) {
1030
+ public login(
1031
+ user: User & UserData,
1032
+ sendAuthHeader: boolean,
1033
+ sendCookie = true
1034
+ ) {
1034
1035
  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);
1036
+ if (this.response) {
1037
+ if (!this.response.headersSent) {
1038
+ const token = this.config.jwtBuilder(this.config, user);
1039
+
1040
+ if (sendCookie) {
1041
+ const appUrl = new URL(this.config.appUrl);
1042
+ this.response.cookie('TILMELDAUTH', token, {
1043
+ domain: this.config.cookieDomain,
1044
+ path: this.config.cookiePath,
1045
+ maxAge: this.config.jwtExpire * 1000,
1046
+ secure: appUrl.protocol === 'https',
1047
+ httpOnly: false, // Allow JS access (for CSRF protection).
1048
+ sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1049
+ });
1050
+ }
1051
+
1052
+ if (sendAuthHeader) {
1053
+ this.response.set('X-TILMELDAUTH', token);
1054
+ }
1055
+ }
1056
+
1057
+ this.response.locals.user = user;
1058
+ }
1059
+
1060
+ this.fillSession(user);
1061
+ return true;
1062
+ }
1063
+ return false;
1064
+ }
1065
+
1066
+ /**
1067
+ * Adds a switch auth token for the given user.
1068
+ *
1069
+ * This effectively logs the current user in to the system as the given user.
1070
+ *
1071
+ * @param user The user.
1072
+ * @param sendAuthHeader Send the auth token as a custom header.
1073
+ * @param sendCookie Send the auth token as a cookie.
1074
+ * @returns True on success, false on failure.
1075
+ */
1076
+ public loginSwitch(
1077
+ user: User & UserData,
1078
+ sendAuthHeader: boolean,
1079
+ sendCookie = true
1080
+ ) {
1081
+ if (user.guid != null) {
1082
+ if (this.response) {
1083
+ if (!this.response.headersSent) {
1084
+ const token = this.config.jwtBuilder(this.config, user, true);
1085
+
1086
+ if (sendCookie) {
1087
+ const appUrl = new URL(this.config.appUrl);
1088
+ this.response.cookie('TILMELDSWITCH', token, {
1089
+ domain: this.config.cookieDomain,
1090
+ path: this.config.cookiePath,
1091
+ maxAge: this.config.jwtSwitchExpire * 1000,
1092
+ secure: appUrl.protocol === 'https',
1093
+ httpOnly: false, // Allow JS access (for PubSub auth token).
1094
+ sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1095
+ });
1096
+ }
1097
+
1098
+ if (sendAuthHeader) {
1099
+ this.response.set('X-TILMELDSWITCH', token);
1100
+ }
1048
1101
  }
1102
+
1103
+ this.response.locals.user = user;
1049
1104
  }
1105
+
1050
1106
  this.fillSession(user);
1051
1107
  return true;
1052
1108
  }
@@ -1055,15 +1111,50 @@ export default class Tilmeld implements TilmeldInterface {
1055
1111
 
1056
1112
  /**
1057
1113
  * Logs the current user out of the system.
1114
+ *
1115
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1058
1116
  */
1059
- public logout() {
1117
+ public async logout(clearCookie = true) {
1118
+ if (this.request && !this.alreadyLoggedOutSwitch) {
1119
+ const cookies = this.request.cookies ?? {};
1120
+
1121
+ if ('TILMELDSWITCH' in cookies) {
1122
+ this.alreadyLoggedOutSwitch = true;
1123
+ return await this.logoutSwitch(clearCookie);
1124
+ }
1125
+ }
1126
+
1060
1127
  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', '');
1128
+ if (this.response) {
1129
+ if (clearCookie && !this.response.headersSent) {
1130
+ this.response.clearCookie('TILMELDAUTH', {
1131
+ domain: this.config.cookieDomain,
1132
+ path: this.config.cookiePath,
1133
+ });
1134
+ this.response.set('X-TILMELDAUTH', '');
1135
+ }
1136
+ this.response.locals.user = null;
1137
+ }
1138
+ }
1139
+
1140
+ /**
1141
+ * Clears the switch user out of the system.
1142
+ *
1143
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1144
+ */
1145
+ public async logoutSwitch(clearCookie = true) {
1146
+ this.clearSession();
1147
+ if (this.response) {
1148
+ if (clearCookie && !this.response.headersSent) {
1149
+ this.response.clearCookie('TILMELDSWITCH', {
1150
+ domain: this.config.cookieDomain,
1151
+ path: this.config.cookiePath,
1152
+ });
1153
+ this.response.set('X-TILMELDSWITCH', '');
1154
+ }
1155
+ this.response.locals.user = null;
1156
+
1157
+ await this.authenticate();
1067
1158
  }
1068
1159
  }
1069
1160