@nymphjs/tilmeld-client 1.0.0-beta.13 → 1.0.0-beta.131

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,28 +196,36 @@ 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]);
207
209
  }
208
210
 
209
- public async $checkUsername(): Promise<{
210
- result: boolean;
211
- message: string;
212
- }> {
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;
218
+ }
219
+
220
+ public async $checkUsername(): Promise<{ result: boolean; message: string }> {
213
221
  return await this.$serverCall('$checkUsername', [], true);
214
222
  }
215
223
 
216
- public async $checkEmail(): Promise<{
217
- result: boolean;
218
- message: string;
219
- }> {
224
+ public async $checkEmail(): Promise<{ result: boolean; message: string }> {
220
225
  return await this.$serverCall('$checkEmail', [], true);
221
226
  }
222
227
 
223
- public async $checkPhone(): Promise<{
224
- result: boolean;
225
- message: string;
226
- }> {
228
+ public async $checkPhone(): Promise<{ result: boolean; message: string }> {
227
229
  return await this.$serverCall('$checkPhone', [], true);
228
230
  }
229
231
 
@@ -238,7 +240,7 @@ export default class User extends Entity<UserData> {
238
240
  const store = User.stores.get(this.$nymph);
239
241
  if (store == null) {
240
242
  throw new Error(
241
- 'This user class was never initialized with an instance of Nymph'
243
+ 'This user class was never initialized with an instance of Nymph',
242
244
  );
243
245
  }
244
246
 
@@ -257,11 +259,31 @@ export default class User extends Entity<UserData> {
257
259
  return response;
258
260
  }
259
261
 
262
+ public async $switchUser(data?: {
263
+ additionalData?: { [k: string]: any };
264
+ }): Promise<{ result: boolean; message: string }> {
265
+ const store = User.stores.get(this.$nymph);
266
+ if (store == null) {
267
+ throw new Error(
268
+ 'This user class was never initialized with an instance of Nymph',
269
+ );
270
+ }
271
+
272
+ const response = await this.$serverCall('$switchUser', [data]);
273
+ if (response.result) {
274
+ (this.constructor as typeof User).handleToken();
275
+ for (let i = 0; i < store.loginCallbacks.length; i++) {
276
+ store.loginCallbacks[i] && store.loginCallbacks[i](this);
277
+ }
278
+ }
279
+ return response;
280
+ }
281
+
260
282
  public async $logout(): Promise<{ result: boolean; message: string }> {
261
283
  const store = User.stores.get(this.$nymph);
262
284
  if (store == null) {
263
285
  throw new Error(
264
- 'This user class was never initialized with an instance of Nymph'
286
+ 'This user class was never initialized with an instance of Nymph',
265
287
  );
266
288
  }
267
289
 
@@ -282,18 +304,54 @@ export default class User extends Entity<UserData> {
282
304
  public async $changePassword(data: {
283
305
  newPassword: string;
284
306
  currentPassword: string;
307
+ revokeCurrentTokens?: boolean;
285
308
  }): Promise<{ result: boolean; message: string }> {
286
309
  return await this.$serverCall('$changePassword', [data]);
287
310
  }
288
311
 
312
+ public async $revokeCurrentTokens(data: {
313
+ password: string;
314
+ }): Promise<{ result: boolean; message: string }> {
315
+ return await this.$serverCall('$revokeCurrentTokens', [data]);
316
+ }
317
+
318
+ public async $hasTOTPSecret(): Promise<boolean> {
319
+ return await this.$serverCall('$hasTOTPSecret', [], true);
320
+ }
321
+
322
+ public async $getNewTOTPSecret(): Promise<{
323
+ uri: string;
324
+ qrcode: string;
325
+ secret: string;
326
+ }> {
327
+ return await this.$serverCall('$getNewTOTPSecret', [], true);
328
+ }
329
+
330
+ public async $saveTOTPSecret(data: {
331
+ password: string;
332
+ secret: string;
333
+ code: string;
334
+ }): Promise<{ result: boolean; message: string }> {
335
+ return await this.$serverCall('$saveTOTPSecret', [data]);
336
+ }
337
+
338
+ public async $removeTOTPSecret(data?: {
339
+ password: string;
340
+ code: string;
341
+ }): Promise<{ result: boolean; message: string }> {
342
+ return await this.$serverCall('$removeTOTPSecret', [
343
+ ...(data ? [data] : []),
344
+ ]);
345
+ }
346
+
289
347
  public static async current(
290
- returnObjectIfNotExist: true
348
+ returnObjectIfNotExist: true,
291
349
  ): Promise<User & CurrentUserData>;
292
350
  public static async current(
293
- returnObjectIfNotExist?: false
351
+ returnObjectIfNotExist?: false,
294
352
  ): Promise<(User & CurrentUserData) | null>;
295
353
  public static async current(
296
- returnObjectIfNotExist?: boolean
354
+ returnObjectIfNotExist?: boolean,
297
355
  ): Promise<(User & CurrentUserData) | null> {
298
356
  const currentUser = await this.serverCallStatic('current', [false]);
299
357
  if (currentUser == null) {
@@ -305,16 +363,18 @@ export default class User extends Entity<UserData> {
305
363
  public static async loginUser(data: {
306
364
  username: string;
307
365
  password: string;
366
+ code?: string;
308
367
  additionalData?: { [k: string]: any };
309
368
  }): Promise<{
310
369
  result: boolean;
311
370
  message: string;
371
+ needTOTP?: true;
312
372
  user?: User & CurrentUserData;
313
373
  }> {
314
374
  const store = User.stores.get(this.nymph);
315
375
  if (store == null) {
316
376
  throw new Error(
317
- 'This user class was never initialized with an instance of Nymph'
377
+ 'This user class was never initialized with an instance of Nymph',
318
378
  );
319
379
  }
320
380
 
@@ -347,7 +407,7 @@ export default class User extends Entity<UserData> {
347
407
  const store = User.stores.get(this.nymph);
348
408
  if (store == null) {
349
409
  throw new Error(
350
- 'This user class was never initialized with an instance of Nymph'
410
+ 'This user class was never initialized with an instance of Nymph',
351
411
  );
352
412
  }
353
413
 
@@ -357,7 +417,7 @@ export default class User extends Entity<UserData> {
357
417
  if (!store.clientConfigPromise) {
358
418
  store.clientConfigPromise = this.serverCallStatic(
359
419
  'getClientConfig',
360
- []
420
+ [],
361
421
  ).then((config) => {
362
422
  store.clientConfig = config;
363
423
  store.clientConfigPromise = undefined;
@@ -368,27 +428,44 @@ export default class User extends Entity<UserData> {
368
428
  }
369
429
 
370
430
  private static handleToken(response?: Response) {
431
+ const store = User.stores.get(this.nymph);
432
+
433
+ if (store == null) {
434
+ throw new Error(
435
+ 'This user class was never initialized with an instance of Nymph',
436
+ );
437
+ }
438
+
371
439
  let token: string | null = null;
440
+ let switchToken: string | null = null;
441
+ let hasNoCookie =
442
+ typeof document === 'undefined' || typeof document.cookie === 'undefined';
372
443
  const authCookiePattern =
373
444
  /(?:(?:^|.*;\s*)TILMELDAUTH\s*=\s*([^;]*).*$)|^.*$/;
445
+ const switchCookiePattern =
446
+ /(?:(?:^|.*;\s*)TILMELDSWITCH\s*=\s*([^;]*).*$)|^.*$/;
374
447
  if (response && response.headers.has('X-TILMELDAUTH')) {
375
448
  token = response.headers.get('X-TILMELDAUTH');
449
+ switchToken = response.headers.get('X-TILMELDSWITCH');
376
450
  } else if (
377
451
  typeof document !== 'undefined' &&
378
452
  document.cookie.match(authCookiePattern)
379
453
  ) {
380
454
  token = document.cookie.replace(authCookiePattern, '$1');
455
+ switchToken = document.cookie.replace(switchCookiePattern, '$1');
381
456
  } else {
382
457
  return;
383
458
  }
384
- if (currentToken !== token) {
459
+ if (store.currentToken != token) {
385
460
  if (token == null || token === '') {
386
- if (currentToken != null) {
387
- this.nymph.setXsrfToken(null);
461
+ if (store.currentToken != null) {
462
+ delete this.nymph.headers['X-Xsrf-Token'];
463
+ delete this.nymph.headers['X-TILMELDAUTH'];
464
+ delete this.nymph.headers['X-TILMELDSWITCH'];
388
465
  if (this.nymph.pubsub) {
389
- this.nymph.pubsub.setToken(null);
466
+ this.nymph.pubsub.authenticate(null);
390
467
  }
391
- currentToken = null;
468
+ delete store.currentToken;
392
469
  }
393
470
  } else {
394
471
  const base64Url = token.split('.')[1];
@@ -398,11 +475,18 @@ export default class User extends Entity<UserData> {
398
475
  ? Buffer.from(base64, 'base64').toString('binary') // node
399
476
  : atob(base64); // browser
400
477
  const jwt = JSON.parse(json);
401
- this.nymph.setXsrfToken(jwt.xsrfToken);
478
+ this.nymph.headers['X-Xsrf-Token'] = jwt.xsrfToken;
479
+ if (hasNoCookie) {
480
+ this.nymph.headers['X-TILMELDAUTH'] = token;
481
+ delete this.nymph.headers['X-TILMELDSWITCH'];
482
+ if (switchToken != null) {
483
+ this.nymph.headers['X-TILMELDSWITCH'] = switchToken;
484
+ }
485
+ }
402
486
  if (this.nymph.pubsub) {
403
- this.nymph.pubsub.setToken(token);
487
+ this.nymph.pubsub.authenticate(token, switchToken);
404
488
  }
405
- currentToken = token;
489
+ store.currentToken = token;
406
490
  }
407
491
  }
408
492
  }
@@ -412,25 +496,25 @@ export default class User extends Entity<UserData> {
412
496
  callback: T extends 'register'
413
497
  ? RegisterCallback
414
498
  : T extends 'login'
415
- ? LoginCallback
416
- : T extends 'logout'
417
- ? LogoutCallback
418
- : never
499
+ ? LoginCallback
500
+ : T extends 'logout'
501
+ ? LogoutCallback
502
+ : never,
419
503
  ) {
420
504
  const store = User.stores.get(this.nymph);
421
505
  if (store == null) {
422
506
  throw new Error(
423
- 'This user class was never initialized with an instance of Nymph'
507
+ 'This user class was never initialized with an instance of Nymph',
424
508
  );
425
509
  }
426
510
 
427
511
  const prop = (event + 'Callbacks') as T extends 'register'
428
512
  ? 'registerCallbacks'
429
513
  : T extends 'login'
430
- ? 'loginCallbacks'
431
- : T extends 'logout'
432
- ? 'logoutCallbacks'
433
- : never;
514
+ ? 'loginCallbacks'
515
+ : T extends 'logout'
516
+ ? 'logoutCallbacks'
517
+ : never;
434
518
  if (!(prop in store)) {
435
519
  throw new Error('Invalid event type.');
436
520
  }
@@ -444,25 +528,25 @@ export default class User extends Entity<UserData> {
444
528
  callback: T extends 'register'
445
529
  ? RegisterCallback
446
530
  : T extends 'login'
447
- ? LoginCallback
448
- : T extends 'logout'
449
- ? LogoutCallback
450
- : never
531
+ ? LoginCallback
532
+ : T extends 'logout'
533
+ ? LogoutCallback
534
+ : never,
451
535
  ) {
452
536
  const store = User.stores.get(this.nymph);
453
537
  if (store == null) {
454
538
  throw new Error(
455
- 'This user class was never initialized with an instance of Nymph'
539
+ 'This user class was never initialized with an instance of Nymph',
456
540
  );
457
541
  }
458
542
 
459
543
  const prop = (event + 'Callbacks') as T extends 'register'
460
544
  ? 'registerCallbacks'
461
545
  : T extends 'login'
462
- ? 'loginCallbacks'
463
- : T extends 'logout'
464
- ? 'logoutCallbacks'
465
- : never;
546
+ ? 'loginCallbacks'
547
+ : T extends 'logout'
548
+ ? 'logoutCallbacks'
549
+ : never;
466
550
  if (!(prop in store)) {
467
551
  return false;
468
552
  }
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;