@nymphjs/tilmeld-client 1.0.0-beta.11 → 1.0.0-beta.111

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/User.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Nymph, Entity } from '@nymphjs/client';
2
2
 
3
- import type Group from './Group';
4
- import type { AdminGroupData, CurrentGroupData } from './Group';
3
+ import type Group from './Group.js';
4
+ import type { AdminGroupData, CurrentGroupData } from './Group.js';
5
5
 
6
6
  export type EventType = 'register' | 'login' | 'logout';
7
7
  export type RegisterCallback = (user: User & CurrentUserData) => void;
@@ -76,6 +76,11 @@ export type CurrentUserData = UserData & {
76
76
  * Whether the user should inherit the abilities of his groups.
77
77
  */
78
78
  inheritAbilities?: boolean;
79
+ /**
80
+ * If the user has changed their email address, this is the new one, awaiting
81
+ * verification.
82
+ */
83
+ newEmailAddress?: string;
79
84
  };
80
85
 
81
86
  export type AdminUserData = CurrentUserData & {
@@ -123,10 +128,13 @@ export type AdminUserData = CurrentUserData & {
123
128
  * Used by admins to change a user's password. Not saved to the database.
124
129
  */
125
130
  passwordTemp?: string;
131
+ /**
132
+ * If set, this timestamp is the cutoff point for JWT issue dates. Any token
133
+ * issued before this date will not authenticate the user.
134
+ */
135
+ revokeTokenDate?: number;
126
136
  };
127
137
 
128
- let currentToken: string | null = null;
129
-
130
138
  type InstanceStore = {
131
139
  registerCallbacks: RegisterCallback[];
132
140
  loginCallbacks: LoginCallback[];
@@ -134,6 +142,7 @@ type InstanceStore = {
134
142
  clientConfig?: ClientConfig;
135
143
  clientConfigPromise?: Promise<ClientConfig>;
136
144
  removeNymphResponseListener?: () => void;
145
+ currentToken?: string;
137
146
  };
138
147
 
139
148
  export default class User extends Entity<UserData> {
@@ -163,26 +172,11 @@ export default class User extends Entity<UserData> {
163
172
  store.removeNymphResponseListener();
164
173
  }
165
174
  store.removeNymphResponseListener = nymph.on('response', (response) =>
166
- this.handleToken(response)
175
+ this.handleToken(response),
167
176
  );
168
177
  this.handleToken();
169
178
  }
170
179
 
171
- constructor(guid?: string) {
172
- super(guid);
173
-
174
- if (guid == null) {
175
- this.$data.enabled = true;
176
- (this.$data as CurrentUserData).abilities = [];
177
- (this.$data as CurrentUserData).groups = [];
178
- (this.$data as CurrentUserData).inheritAbilities = true;
179
- }
180
- }
181
-
182
- static async factory(guid?: string): Promise<User & UserData> {
183
- return (await super.factory(guid)) as User & UserData;
184
- }
185
-
186
180
  static async factoryUsername(username?: string): Promise<User & UserData> {
187
181
  const entity = new this();
188
182
  if (username != null) {
@@ -193,7 +187,7 @@ export default class User extends Entity<UserData> {
193
187
  {
194
188
  type: '&',
195
189
  ilike: ['username', username.replace(/([\\%_])/g, (s) => `\\${s}`)],
196
- }
190
+ },
197
191
  );
198
192
  if (entity != null) {
199
193
  return entity;
@@ -202,8 +196,25 @@ export default class User extends Entity<UserData> {
202
196
  return entity;
203
197
  }
204
198
 
205
- static factorySync(guid?: string): User & UserData {
206
- return super.factorySync(guid) as User & UserData;
199
+ static async getDomainUsers(
200
+ domain: string,
201
+ options?: {
202
+ limit?: number;
203
+ offset?: number;
204
+ sort?: string;
205
+ reverse?: boolean;
206
+ },
207
+ ): Promise<(User & UserData)[]> {
208
+ return await this.serverCallStatic('getDomainUsers', [domain, options]);
209
+ }
210
+
211
+ constructor() {
212
+ super();
213
+
214
+ this.$data.enabled = true;
215
+ (this.$data as CurrentUserData).abilities = [];
216
+ (this.$data as CurrentUserData).groups = [];
217
+ (this.$data as CurrentUserData).inheritAbilities = true;
207
218
  }
208
219
 
209
220
  public async $checkUsername(): Promise<{
@@ -238,7 +249,7 @@ export default class User extends Entity<UserData> {
238
249
  const store = User.stores.get(this.$nymph);
239
250
  if (store == null) {
240
251
  throw new Error(
241
- 'This user class was never initialized with an instance of Nymph'
252
+ 'This user class was never initialized with an instance of Nymph',
242
253
  );
243
254
  }
244
255
 
@@ -257,11 +268,34 @@ export default class User extends Entity<UserData> {
257
268
  return response;
258
269
  }
259
270
 
271
+ public async $switchUser(data?: {
272
+ additionalData?: { [k: string]: any };
273
+ }): Promise<{
274
+ result: boolean;
275
+ message: string;
276
+ }> {
277
+ const store = User.stores.get(this.$nymph);
278
+ if (store == null) {
279
+ throw new Error(
280
+ 'This user class was never initialized with an instance of Nymph',
281
+ );
282
+ }
283
+
284
+ const response = await this.$serverCall('$switchUser', [data]);
285
+ if (response.result) {
286
+ (this.constructor as typeof User).handleToken();
287
+ for (let i = 0; i < store.loginCallbacks.length; i++) {
288
+ store.loginCallbacks[i] && store.loginCallbacks[i](this);
289
+ }
290
+ }
291
+ return response;
292
+ }
293
+
260
294
  public async $logout(): Promise<{ result: boolean; message: string }> {
261
295
  const store = User.stores.get(this.$nymph);
262
296
  if (store == null) {
263
297
  throw new Error(
264
- 'This user class was never initialized with an instance of Nymph'
298
+ 'This user class was never initialized with an instance of Nymph',
265
299
  );
266
300
  }
267
301
 
@@ -282,18 +316,54 @@ export default class User extends Entity<UserData> {
282
316
  public async $changePassword(data: {
283
317
  newPassword: string;
284
318
  currentPassword: string;
319
+ revokeCurrentTokens?: boolean;
285
320
  }): Promise<{ result: boolean; message: string }> {
286
321
  return await this.$serverCall('$changePassword', [data]);
287
322
  }
288
323
 
324
+ public async $revokeCurrentTokens(data: {
325
+ password: string;
326
+ }): Promise<{ result: boolean; message: string }> {
327
+ return await this.$serverCall('$revokeCurrentTokens', [data]);
328
+ }
329
+
330
+ public async $hasTOTPSecret(): Promise<boolean> {
331
+ return await this.$serverCall('$hasTOTPSecret', [], true);
332
+ }
333
+
334
+ public async $getNewTOTPSecret(): Promise<{
335
+ uri: string;
336
+ qrcode: string;
337
+ secret: string;
338
+ }> {
339
+ return await this.$serverCall('$getNewTOTPSecret', [], true);
340
+ }
341
+
342
+ public async $saveTOTPSecret(data: {
343
+ password: string;
344
+ secret: string;
345
+ code: string;
346
+ }): Promise<{ result: boolean; message: string }> {
347
+ return await this.$serverCall('$saveTOTPSecret', [data]);
348
+ }
349
+
350
+ public async $removeTOTPSecret(data?: {
351
+ password: string;
352
+ code: string;
353
+ }): Promise<{ result: boolean; message: string }> {
354
+ return await this.$serverCall('$removeTOTPSecret', [
355
+ ...(data ? [data] : []),
356
+ ]);
357
+ }
358
+
289
359
  public static async current(
290
- returnObjectIfNotExist: true
360
+ returnObjectIfNotExist: true,
291
361
  ): Promise<User & CurrentUserData>;
292
362
  public static async current(
293
- returnObjectIfNotExist?: false
363
+ returnObjectIfNotExist?: false,
294
364
  ): Promise<(User & CurrentUserData) | null>;
295
365
  public static async current(
296
- returnObjectIfNotExist?: boolean
366
+ returnObjectIfNotExist?: boolean,
297
367
  ): Promise<(User & CurrentUserData) | null> {
298
368
  const currentUser = await this.serverCallStatic('current', [false]);
299
369
  if (currentUser == null) {
@@ -305,16 +375,18 @@ export default class User extends Entity<UserData> {
305
375
  public static async loginUser(data: {
306
376
  username: string;
307
377
  password: string;
378
+ code?: string;
308
379
  additionalData?: { [k: string]: any };
309
380
  }): Promise<{
310
381
  result: boolean;
311
382
  message: string;
383
+ needTOTP?: true;
312
384
  user?: User & CurrentUserData;
313
385
  }> {
314
386
  const store = User.stores.get(this.nymph);
315
387
  if (store == null) {
316
388
  throw new Error(
317
- 'This user class was never initialized with an instance of Nymph'
389
+ 'This user class was never initialized with an instance of Nymph',
318
390
  );
319
391
  }
320
392
 
@@ -347,7 +419,7 @@ export default class User extends Entity<UserData> {
347
419
  const store = User.stores.get(this.nymph);
348
420
  if (store == null) {
349
421
  throw new Error(
350
- 'This user class was never initialized with an instance of Nymph'
422
+ 'This user class was never initialized with an instance of Nymph',
351
423
  );
352
424
  }
353
425
 
@@ -357,7 +429,7 @@ export default class User extends Entity<UserData> {
357
429
  if (!store.clientConfigPromise) {
358
430
  store.clientConfigPromise = this.serverCallStatic(
359
431
  'getClientConfig',
360
- []
432
+ [],
361
433
  ).then((config) => {
362
434
  store.clientConfig = config;
363
435
  store.clientConfigPromise = undefined;
@@ -368,27 +440,44 @@ export default class User extends Entity<UserData> {
368
440
  }
369
441
 
370
442
  private static handleToken(response?: Response) {
443
+ const store = User.stores.get(this.nymph);
444
+
445
+ if (store == null) {
446
+ throw new Error(
447
+ 'This user class was never initialized with an instance of Nymph',
448
+ );
449
+ }
450
+
371
451
  let token: string | null = null;
452
+ let switchToken: string | null = null;
453
+ let hasNoCookie =
454
+ typeof document === 'undefined' || typeof document.cookie === 'undefined';
372
455
  const authCookiePattern =
373
456
  /(?:(?:^|.*;\s*)TILMELDAUTH\s*=\s*([^;]*).*$)|^.*$/;
457
+ const switchCookiePattern =
458
+ /(?:(?:^|.*;\s*)TILMELDSWITCH\s*=\s*([^;]*).*$)|^.*$/;
374
459
  if (response && response.headers.has('X-TILMELDAUTH')) {
375
460
  token = response.headers.get('X-TILMELDAUTH');
461
+ switchToken = response.headers.get('X-TILMELDSWITCH');
376
462
  } else if (
377
463
  typeof document !== 'undefined' &&
378
464
  document.cookie.match(authCookiePattern)
379
465
  ) {
380
466
  token = document.cookie.replace(authCookiePattern, '$1');
467
+ switchToken = document.cookie.replace(switchCookiePattern, '$1');
381
468
  } else {
382
469
  return;
383
470
  }
384
- if (currentToken !== token) {
471
+ if (store.currentToken != token) {
385
472
  if (token == null || token === '') {
386
- if (currentToken != null) {
387
- this.nymph.setXsrfToken(null);
473
+ if (store.currentToken != null) {
474
+ delete this.nymph.headers['X-Xsrf-Token'];
475
+ delete this.nymph.headers['X-TILMELDAUTH'];
476
+ delete this.nymph.headers['X-TILMELDSWITCH'];
388
477
  if (this.nymph.pubsub) {
389
- this.nymph.pubsub.setToken(null);
478
+ this.nymph.pubsub.authenticate(null);
390
479
  }
391
- currentToken = null;
480
+ delete store.currentToken;
392
481
  }
393
482
  } else {
394
483
  const base64Url = token.split('.')[1];
@@ -398,11 +487,18 @@ export default class User extends Entity<UserData> {
398
487
  ? Buffer.from(base64, 'base64').toString('binary') // node
399
488
  : atob(base64); // browser
400
489
  const jwt = JSON.parse(json);
401
- this.nymph.setXsrfToken(jwt.xsrfToken);
490
+ this.nymph.headers['X-Xsrf-Token'] = jwt.xsrfToken;
491
+ if (hasNoCookie) {
492
+ this.nymph.headers['X-TILMELDAUTH'] = token;
493
+ delete this.nymph.headers['X-TILMELDSWITCH'];
494
+ if (switchToken != null) {
495
+ this.nymph.headers['X-TILMELDSWITCH'] = switchToken;
496
+ }
497
+ }
402
498
  if (this.nymph.pubsub) {
403
- this.nymph.pubsub.setToken(token);
499
+ this.nymph.pubsub.authenticate(token, switchToken);
404
500
  }
405
- currentToken = token;
501
+ store.currentToken = token;
406
502
  }
407
503
  }
408
504
  }
@@ -412,25 +508,25 @@ export default class User extends Entity<UserData> {
412
508
  callback: T extends 'register'
413
509
  ? RegisterCallback
414
510
  : T extends 'login'
415
- ? LoginCallback
416
- : T extends 'logout'
417
- ? LogoutCallback
418
- : never
511
+ ? LoginCallback
512
+ : T extends 'logout'
513
+ ? LogoutCallback
514
+ : never,
419
515
  ) {
420
516
  const store = User.stores.get(this.nymph);
421
517
  if (store == null) {
422
518
  throw new Error(
423
- 'This user class was never initialized with an instance of Nymph'
519
+ 'This user class was never initialized with an instance of Nymph',
424
520
  );
425
521
  }
426
522
 
427
523
  const prop = (event + 'Callbacks') as T extends 'register'
428
524
  ? 'registerCallbacks'
429
525
  : T extends 'login'
430
- ? 'loginCallbacks'
431
- : T extends 'logout'
432
- ? 'logoutCallbacks'
433
- : never;
526
+ ? 'loginCallbacks'
527
+ : T extends 'logout'
528
+ ? 'logoutCallbacks'
529
+ : never;
434
530
  if (!(prop in store)) {
435
531
  throw new Error('Invalid event type.');
436
532
  }
@@ -444,25 +540,25 @@ export default class User extends Entity<UserData> {
444
540
  callback: T extends 'register'
445
541
  ? RegisterCallback
446
542
  : T extends 'login'
447
- ? LoginCallback
448
- : T extends 'logout'
449
- ? LogoutCallback
450
- : never
543
+ ? LoginCallback
544
+ : T extends 'logout'
545
+ ? LogoutCallback
546
+ : never,
451
547
  ) {
452
548
  const store = User.stores.get(this.nymph);
453
549
  if (store == null) {
454
550
  throw new Error(
455
- 'This user class was never initialized with an instance of Nymph'
551
+ 'This user class was never initialized with an instance of Nymph',
456
552
  );
457
553
  }
458
554
 
459
555
  const prop = (event + 'Callbacks') as T extends 'register'
460
556
  ? 'registerCallbacks'
461
557
  : T extends 'login'
462
- ? 'loginCallbacks'
463
- : T extends 'logout'
464
- ? 'logoutCallbacks'
465
- : never;
558
+ ? 'loginCallbacks'
559
+ : T extends 'logout'
560
+ ? 'logoutCallbacks'
561
+ : never;
466
562
  if (!(prop in store)) {
467
563
  return false;
468
564
  }
package/src/helpers.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type UserClass from './User';
2
- import type { ClientConfig, CurrentUserData } from './User';
1
+ import type UserClass from './User.js';
2
+ import type { ClientConfig, CurrentUserData } from './User.js';
3
3
 
4
4
  export type RegistrationDetails = {
5
5
  /**
@@ -40,11 +40,14 @@ export type RegistrationDetails = {
40
40
  additionalData?: { [k: string]: any };
41
41
  };
42
42
 
43
+ export class NeedTOTPError extends Error {}
44
+
43
45
  export async function login(
44
46
  User: typeof UserClass,
45
47
  username: string,
46
48
  password: string,
47
- additionalData?: { [k: string]: any }
49
+ code?: string,
50
+ additionalData?: { [k: string]: any },
48
51
  ) {
49
52
  if (username === '') {
50
53
  throw new Error('You need to enter a username.');
@@ -54,16 +57,24 @@ export async function login(
54
57
  }
55
58
 
56
59
  try {
57
- const { result, ...response } = await User.loginUser({
60
+ const { result, needTOTP, ...response } = await User.loginUser({
58
61
  username,
59
62
  password,
63
+ code,
60
64
  ...(additionalData ? { additionalData } : {}),
61
65
  });
62
66
  if (!result) {
67
+ if (needTOTP) {
68
+ throw new NeedTOTPError(response.message);
69
+ }
70
+
63
71
  throw new Error(response.message);
64
72
  }
65
73
  return response;
66
74
  } catch (e: any) {
75
+ if (e instanceof NeedTOTPError) {
76
+ throw e;
77
+ }
67
78
  throw new Error(e?.message ?? 'An error occurred.');
68
79
  }
69
80
  }
@@ -71,7 +82,7 @@ export async function login(
71
82
  export async function register(
72
83
  User: typeof UserClass,
73
84
  userDetails: RegistrationDetails,
74
- clientConfig?: ClientConfig
85
+ clientConfig?: ClientConfig,
75
86
  ): Promise<{
76
87
  loggedin: boolean;
77
88
  message: string;
@@ -95,16 +106,18 @@ export async function register(
95
106
  user.username = userDetails.username;
96
107
  const config = clientConfig || (await User.getClientConfig());
97
108
 
98
- if (config.emailUsernames) {
99
- user.email = userDetails.username;
100
- } else if (config.regFields.indexOf('email') !== -1) {
101
- user.email = userDetails.email;
109
+ if (config.regFields.includes('email')) {
110
+ if (config.emailUsernames) {
111
+ user.email = userDetails.username;
112
+ } else {
113
+ user.email = userDetails.email;
114
+ }
102
115
  }
103
- if (config.regFields.indexOf('name') !== -1) {
116
+ if (config.regFields.includes('name')) {
104
117
  user.nameFirst = userDetails.nameFirst;
105
118
  user.nameLast = userDetails.nameLast;
106
119
  }
107
- if (config.regFields.indexOf('phone') !== -1) {
120
+ if (config.regFields.includes('phone')) {
108
121
  user.phone = userDetails.phone;
109
122
  }
110
123
 
@@ -127,7 +140,7 @@ export async function register(
127
140
  export async function checkUsername(
128
141
  User: typeof UserClass,
129
142
  username: string,
130
- clientConfig?: ClientConfig
143
+ clientConfig?: ClientConfig,
131
144
  ) {
132
145
  let user = User.factorySync() as UserClass & CurrentUserData;
133
146
  user.username = username;