@unchainedshop/core-users 4.0.0-rc.2 → 4.0.0-rc.21

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 (44) hide show
  1. package/lib/db/UsersCollection.d.ts +2 -2
  2. package/lib/db/UsersCollection.d.ts.map +1 -1
  3. package/lib/db/UsersCollection.js +30 -26
  4. package/lib/db/UsersCollection.js.map +1 -1
  5. package/lib/module/configureUsersModule.d.ts +32 -31
  6. package/lib/module/configureUsersModule.d.ts.map +1 -1
  7. package/lib/module/configureUsersModule.js +81 -41
  8. package/lib/module/configureUsersModule.js.map +1 -1
  9. package/lib/module/configureUsersWebAuthnModule.d.ts +5 -5
  10. package/lib/module/configureUsersWebAuthnModule.d.ts.map +1 -1
  11. package/lib/module/configureUsersWebAuthnModule.js +9 -3
  12. package/lib/module/configureUsersWebAuthnModule.js.map +1 -1
  13. package/lib/module/pbkdf2.d.ts.map +1 -1
  14. package/lib/module/pbkdf2.js +9 -2
  15. package/lib/module/pbkdf2.js.map +1 -1
  16. package/lib/services/getUserCountryService.d.ts +3 -0
  17. package/lib/services/getUserCountryService.d.ts.map +1 -0
  18. package/lib/services/getUserCountryService.js +7 -0
  19. package/lib/services/getUserCountryService.js.map +1 -0
  20. package/lib/services/getUserLanguageService.d.ts +3 -0
  21. package/lib/services/getUserLanguageService.d.ts.map +1 -0
  22. package/lib/services/getUserLanguageService.js +7 -0
  23. package/lib/services/getUserLanguageService.js.map +1 -0
  24. package/lib/services/updateUserAvatarAfterUploadService.d.ts +3 -0
  25. package/lib/services/updateUserAvatarAfterUploadService.d.ts.map +1 -0
  26. package/lib/services/updateUserAvatarAfterUploadService.js +24 -0
  27. package/lib/services/updateUserAvatarAfterUploadService.js.map +1 -0
  28. package/lib/services/userServices.d.ts +3 -0
  29. package/lib/services/userServices.d.ts.map +1 -0
  30. package/lib/services/userServices.js +9 -0
  31. package/lib/services/userServices.js.map +1 -0
  32. package/lib/users-settings.d.ts +8 -7
  33. package/lib/users-settings.d.ts.map +1 -1
  34. package/lib/users-settings.js +34 -20
  35. package/lib/users-settings.js.map +1 -1
  36. package/package.json +10 -10
  37. package/src/db/UsersCollection.ts +33 -29
  38. package/src/module/buildFindSelector.test.ts +5 -2
  39. package/src/module/configureUsersModule.ts +126 -85
  40. package/src/module/configureUsersWebAuthnModule.ts +8 -8
  41. package/src/module/pbkdf2.ts +9 -2
  42. package/src/module/removeConfidentialServiceHashes.test.ts +1 -2
  43. package/src/users-settings.ts +43 -30
  44. package/tsconfig.json +2 -1
@@ -27,7 +27,7 @@ export interface UserLastLogin {
27
27
  export interface PushSubscriptionObject {
28
28
  userAgent: string;
29
29
  endpoint: string;
30
- expirationTime?: number;
30
+ expirationTime: number;
31
31
  keys: {
32
32
  auth: string;
33
33
  p256dh: string;
@@ -40,7 +40,7 @@ export interface Email {
40
40
  }
41
41
 
42
42
  export type User = {
43
- _id?: string;
43
+ _id: string;
44
44
  deleted?: Date;
45
45
  avatarId?: string;
46
46
  emails: Email[];
@@ -69,6 +69,37 @@ export type UserQuery = mongodb.Filter<User> & {
69
69
  export const UsersCollection = async (db: mongodb.Db) => {
70
70
  const Users = db.collection<User>('users');
71
71
 
72
+ if (!isDocumentDBCompatModeEnabled()) {
73
+ await buildDbIndexes<User>(Users, [
74
+ {
75
+ index: {
76
+ _id: 'text',
77
+ username: 'text',
78
+ 'emails.address': 'text',
79
+ 'profile.displayName': 'text',
80
+ 'lastBillingAddress.firstName': 'text',
81
+ 'lastBillingAddress.lastName': 'text',
82
+ 'lastBillingAddress.company': 'text',
83
+ 'lastBillingAddress.addressLine': 'text',
84
+ 'lastBillingAddress.addressLine2': 'text',
85
+ } as any,
86
+ options: {
87
+ weights: {
88
+ _id: 9,
89
+ 'emails.address': 7,
90
+ 'profile.displayName': 5,
91
+ 'lastBillingAddress.firstName': 3,
92
+ 'lastBillingAddress.lastName': 3,
93
+ 'lastBillingAddress.company': 1,
94
+ 'lastBillingAddress.addressLine': 1,
95
+ 'lastBillingAddress.addressLine2': 1,
96
+ },
97
+ name: 'user_fulltext_search',
98
+ },
99
+ },
100
+ ]);
101
+ }
102
+
72
103
  await buildDbIndexes<User>(Users, [
73
104
  {
74
105
  index: {
@@ -137,33 +168,6 @@ export const UsersCollection = async (db: mongodb.Db) => {
137
168
  sparse: true,
138
169
  },
139
170
  },
140
-
141
- !isDocumentDBCompatModeEnabled() && {
142
- index: {
143
- _id: 'text',
144
- username: 'text',
145
- 'emails.address': 'text',
146
- 'profile.displayName': 'text',
147
- 'lastBillingAddress.firstName': 'text',
148
- 'lastBillingAddress.lastName': 'text',
149
- 'lastBillingAddress.company': 'text',
150
- 'lastBillingAddress.addressLine': 'text',
151
- 'lastBillingAddress.addressLine2': 'text',
152
- } as any,
153
- options: {
154
- weights: {
155
- _id: 9,
156
- 'emails.address': 7,
157
- 'profile.displayName': 5,
158
- 'lastBillingAddress.firstName': 3,
159
- 'lastBillingAddress.lastName': 3,
160
- 'lastBillingAddress.company': 1,
161
- 'lastBillingAddress.addressLine': 1,
162
- 'lastBillingAddress.addressLine2': 1,
163
- },
164
- name: 'user_fulltext_search',
165
- },
166
- },
167
171
  ]);
168
172
 
169
173
  return Users;
@@ -4,7 +4,10 @@ import { buildFindSelector } from './configureUsersModule.js';
4
4
 
5
5
  describe('buildFindSelector', () => {
6
6
  it('Return the correct filter when no parameter is passed', () => {
7
- assert.deepStrictEqual(buildFindSelector({}), { deleted: null, guest: { $in: [false, null] } });
7
+ assert.deepStrictEqual(buildFindSelector({}), {
8
+ deleted: null,
9
+ guest: { $ne: true },
10
+ });
8
11
  });
9
12
  it('Return the correct filter when no parameter is passed queryString and includeGuest: true', () => {
10
13
  assert.deepStrictEqual(buildFindSelector({ queryString: 'Hello world', includeGuests: true }), {
@@ -23,7 +26,7 @@ describe('buildFindSelector', () => {
23
26
  {
24
27
  'profile.displayName': 'mikael',
25
28
  deleted: null,
26
- guest: { $in: [false, null] },
29
+ guest: { $ne: true },
27
30
  $text: { $search: 'Hello world' },
28
31
  },
29
32
  );
@@ -1,4 +1,4 @@
1
- import * as bcrypt from '@node-rs/bcrypt';
1
+ import * as bcrypt from 'bcryptjs';
2
2
  import {
3
3
  ModuleInput,
4
4
  Address,
@@ -20,7 +20,12 @@ import {
20
20
  } from '../db/UsersCollection.js';
21
21
  import { emit, registerEvents } from '@unchainedshop/events';
22
22
  import { systemLocale, SortDirection, SortOption, sha256 } from '@unchainedshop/utils';
23
- import { UserRegistrationData, userSettings, UserSettingsOptions } from '../users-settings.js';
23
+ import {
24
+ UserAccountAction,
25
+ UserRegistrationData,
26
+ userSettings,
27
+ UserSettingsOptions,
28
+ } from '../users-settings.js';
24
29
  import { configureUsersWebAuthnModule } from './configureUsersWebAuthnModule.js';
25
30
  import * as pbkdf2 from './pbkdf2.js';
26
31
 
@@ -42,7 +47,7 @@ const USER_EVENTS = [
42
47
  'USER_REMOVE',
43
48
  ];
44
49
  export const removeConfidentialServiceHashes = (rawUser: User): User => {
45
- const user = rawUser;
50
+ const user = { ...rawUser };
46
51
  delete user?.services;
47
52
  return user;
48
53
  };
@@ -53,14 +58,18 @@ export const buildFindSelector = ({
53
58
  queryString,
54
59
  emailVerified,
55
60
  lastLogin,
61
+ tags,
56
62
  ...rest
57
63
  }: UserQuery) => {
58
64
  const selector: mongodb.Filter<User> = { ...rest };
59
- if (!includeDeleted) selector.deleted = null;
60
- if (!includeGuests) selector.guest = { $in: [false, null] };
65
+ if (!includeDeleted) selector.deleted = null as any;
66
+ if (!includeGuests) selector.guest = { $ne: true };
61
67
  if (emailVerified === true) {
62
68
  selector['emails.verified'] = true;
63
69
  }
70
+ if (Array.isArray(tags) && tags?.length) {
71
+ selector.tags = { $in: tags };
72
+ }
64
73
  if (emailVerified === false) {
65
74
  // We need to use $ne here else we'd also find users with many emails where one is
66
75
  // unverified
@@ -82,12 +91,13 @@ export const buildFindSelector = ({
82
91
  return selector;
83
92
  };
84
93
 
85
- export const configureUsersModule = async ({ db, options }: ModuleInput<UserSettingsOptions>) => {
94
+ export const configureUsersModule = async (moduleInput: ModuleInput<UserSettingsOptions>) => {
95
+ const { db, options } = moduleInput;
86
96
  userSettings.configureSettings(options || {}, db);
87
97
  registerEvents(USER_EVENTS);
88
98
  const Users = await UsersCollection(db);
89
99
 
90
- const webAuthn = await configureUsersWebAuthnModule({ db, options });
100
+ const webAuthn = await configureUsersWebAuthnModule(moduleInput);
91
101
 
92
102
  return {
93
103
  // Queries
@@ -97,17 +107,17 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
97
107
  return userCount;
98
108
  },
99
109
 
100
- async findUserById(userId: string): Promise<User> {
110
+ async findUserById(userId: string) {
101
111
  if (!userId) return null;
102
112
  return Users.findOne(generateDbFilterById(userId), {});
103
113
  },
104
114
 
105
- async findUserByUsername(username: string): Promise<User> {
115
+ async findUserByUsername(username: string) {
106
116
  if (!username) return null;
107
117
  return Users.findOne({ username: insensitiveTrimmedRegexOperator(username) }, {});
108
118
  },
109
119
 
110
- async findUserByEmail(email: string): Promise<User> {
120
+ async findUserByEmail(email: string) {
111
121
  if (!email) return null;
112
122
  return Users.findOne({ 'emails.address': insensitiveTrimmedRegexOperator(email) }, {});
113
123
  },
@@ -116,7 +126,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
116
126
  userId: string;
117
127
  address: string;
118
128
  when: Date;
119
- }> {
129
+ } | null> {
120
130
  if (!plainToken) return null;
121
131
  const token = await sha256(plainToken);
122
132
  const user = await Users.findOne(
@@ -124,7 +134,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
124
134
  'services.email.verificationTokens': {
125
135
  $elemMatch: {
126
136
  token,
127
- when: { $gt: new Date(new Date().getTime() - 1000 * 60 * 60) },
137
+ when: { $gt: userSettings.earliestValidTokenDate(UserAccountAction.VERIFY_EMAIL) },
128
138
  },
129
139
  },
130
140
  },
@@ -158,7 +168,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
158
168
 
159
169
  if (updated.modifiedCount > 0) {
160
170
  await emit('USER_ACCOUNT_ACTION', {
161
- action: 'email-verified',
171
+ action: UserAccountAction.EMAIL_VERIFIED,
162
172
  address,
163
173
  userId,
164
174
  });
@@ -169,14 +179,14 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
169
179
  userId: string;
170
180
  address: string;
171
181
  when: Date;
172
- }> {
182
+ } | null> {
173
183
  const token = await sha256(plainToken);
174
184
  const user = await Users.findOne(
175
185
  {
176
186
  'services.password.reset': {
177
187
  $elemMatch: {
178
188
  token,
179
- when: { $gt: new Date(new Date().getTime() - 1000 * 60 * 60) },
189
+ when: { $gt: userSettings.earliestValidTokenDate(UserAccountAction.RESET_PASSWORD) },
180
190
  },
181
191
  },
182
192
  },
@@ -190,7 +200,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
190
200
  };
191
201
  },
192
202
 
193
- async findUserByToken(plainToken: string): Promise<User> {
203
+ async findUserByToken(plainToken: string) {
194
204
  const token = await sha256(plainToken);
195
205
 
196
206
  if (token) {
@@ -202,10 +212,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
202
212
  return null;
203
213
  },
204
214
 
205
- async findUser(
206
- query: UserQuery & { sort?: SortOption[] },
207
- findOptions?: mongodb.FindOptions,
208
- ): Promise<User> {
215
+ async findUser(query: UserQuery & { sort?: SortOption[] }, findOptions?: mongodb.FindOptions) {
209
216
  const selector = buildFindSelector(query);
210
217
  return Users.findOne(selector, findOptions);
211
218
  },
@@ -239,7 +246,10 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
239
246
  },
240
247
 
241
248
  async userExists({ userId }: { userId: string }): Promise<boolean> {
242
- const userCount = await Users.countDocuments({ _id: userId, deleted: null }, { limit: 1 });
249
+ const userCount = await Users.countDocuments(
250
+ { _id: userId, deleted: { $exists: false } },
251
+ { limit: 1 },
252
+ );
243
253
  return userCount === 1;
244
254
  },
245
255
 
@@ -250,7 +260,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
250
260
  )?.[0];
251
261
  },
252
262
 
253
- userLocale(user: User): Intl.Locale {
263
+ userLocale(user: User | null): Intl.Locale {
254
264
  if (!user?.lastLogin?.locale) return systemLocale;
255
265
  try {
256
266
  return new Intl.Locale(user.lastLogin.locale);
@@ -339,7 +349,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
339
349
  /* */
340
350
  }
341
351
 
342
- const user = await Users.findOne({ _id: userId }, {});
352
+ const user = (await Users.findOne({ _id: userId }, {})) as User;
343
353
  await emit('USER_CREATE', {
344
354
  user: removeConfidentialServiceHashes(user),
345
355
  });
@@ -416,7 +426,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
416
426
  );
417
427
 
418
428
  await emit('USER_ACCOUNT_ACTION', {
419
- action: isEnrollment ? 'enroll-account' : 'reset-password',
429
+ action: isEnrollment ? UserAccountAction.ENROLL_ACCOUNT : UserAccountAction.RESET_PASSWORD,
420
430
  userId,
421
431
  ...resetToken,
422
432
  token: plainToken,
@@ -441,46 +451,53 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
441
451
  );
442
452
 
443
453
  await emit('USER_ACCOUNT_ACTION', {
444
- action: 'verify-email',
454
+ action: UserAccountAction.VERIFY_EMAIL,
445
455
  userId,
446
456
  ...verificationToken,
447
457
  token: plainToken,
448
458
  });
449
459
  },
450
460
 
451
- addRoles: async (userId: string, roles: string[]): Promise<number> => {
461
+ addRoles: async (userId: string, roles: string[]) => {
452
462
  const selector = generateDbFilterById(userId);
453
- const updateResult = await Users.updateOne(selector, {
454
- $addToSet: { roles: { $each: roles } },
455
- });
463
+ const user = await Users.findOneAndUpdate(
464
+ selector,
465
+ {
466
+ $addToSet: { roles: { $each: roles } },
467
+ },
468
+ { returnDocument: 'after' },
469
+ );
470
+
471
+ if (!user) return null;
456
472
 
457
- const user = await Users.findOne(selector, {});
458
473
  await emit('USER_ADD_ROLES', {
459
474
  user: removeConfidentialServiceHashes(user),
460
475
  });
461
476
 
462
- return updateResult.modifiedCount;
477
+ return user;
463
478
  },
464
479
 
465
480
  async setUsername(userId: string, username: string) {
466
481
  if (!(await userSettings.validateUsername(username))) {
467
482
  throw new Error(`Username ${username} is invalid`, { cause: 'USERNAME_INVALID' });
468
483
  }
469
- await Users.updateOne(
484
+ const user = await Users.findOneAndUpdate(
470
485
  { _id: userId },
471
486
  {
472
487
  $set: {
473
488
  username: username.trim(),
474
489
  },
475
490
  },
491
+ { returnDocument: 'after' },
476
492
  );
477
- const user = await Users.findOne({ _id: userId }, {});
493
+ if (!user) return null;
478
494
  await emit('USER_UPDATE_USERNAME', {
479
495
  user: removeConfidentialServiceHashes(user),
480
496
  });
497
+ return user;
481
498
  },
482
499
 
483
- async setPassword(userId: string, plainPassword: string): Promise<User> {
500
+ async setPassword(userId: string, plainPassword: string) {
484
501
  if (!(await userSettings.validatePassword(plainPassword))) {
485
502
  throw new Error(`Password ***** is invalid`, { cause: 'PASSWORD_INVALID' });
486
503
  }
@@ -497,13 +514,14 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
497
514
  },
498
515
  { returnDocument: 'after' },
499
516
  );
517
+ if (!user) return null;
500
518
  await emit('USER_UPDATE_PASSWORD', {
501
519
  user: removeConfidentialServiceHashes(user),
502
520
  });
503
521
  return user;
504
522
  },
505
523
 
506
- async resetPassword(token: string, newPassword: string): Promise<User> {
524
+ async resetPassword(token: string, newPassword: string) {
507
525
  const resetToken = await this.findResetToken(token);
508
526
  if (!resetToken) return null;
509
527
  const updatedUser = await this.setPassword(resetToken.userId, newPassword);
@@ -522,7 +540,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
522
540
  },
523
541
  );
524
542
  await emit('USER_ACCOUNT_ACTION', {
525
- action: 'password-resetted',
543
+ action: UserAccountAction.PASSWORD_RESETTED,
526
544
  userId: updatedUser._id,
527
545
  });
528
546
 
@@ -531,7 +549,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
531
549
  return updatedUser;
532
550
  },
533
551
 
534
- updateAvatar: async (_id: string, fileId: string): Promise<User> => {
552
+ updateAvatar: async (_id: string, fileId: string) => {
535
553
  const userFilter = generateDbFilterById(_id);
536
554
  const modifier = {
537
555
  $set: {
@@ -544,44 +562,54 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
544
562
  returnDocument: 'after',
545
563
  });
546
564
 
565
+ if (!user) return null;
547
566
  await emit('USER_UPDATE_AVATAR', {
548
567
  user: removeConfidentialServiceHashes(user),
549
568
  });
550
569
  return user;
551
570
  },
552
571
 
553
- updateGuest: async (user: User, guest: boolean): Promise<void> => {
554
- const modifier = { $set: { guest } };
555
- await Users.updateOne(generateDbFilterById(user._id), modifier);
572
+ updateGuest: async (user: User, guest: boolean) => {
573
+ const updatedUser = await Users.findOneAndUpdate(
574
+ generateDbFilterById(user._id),
575
+ {
576
+ $set: { guest },
577
+ },
578
+ { returnDocument: 'after' },
579
+ );
580
+ if (!updatedUser) return null;
556
581
  await emit('USER_UPDATE_GUEST', {
557
582
  user: removeConfidentialServiceHashes({
558
- ...user,
583
+ ...updatedUser,
559
584
  guest,
560
585
  }),
561
586
  });
587
+ return updatedUser;
562
588
  },
563
589
 
564
- updateHeartbeat: async (userId: string, lastLogin: UserLastLogin): Promise<User> => {
565
- const modifier = {
566
- $set: {
567
- lastLogin: {
568
- timestamp: new Date(),
569
- ...lastLogin,
590
+ updateHeartbeat: async (userId: string, lastLogin: UserLastLogin) => {
591
+ const user = await Users.findOneAndUpdate(
592
+ generateDbFilterById(userId),
593
+ {
594
+ $set: {
595
+ lastLogin: {
596
+ timestamp: new Date(),
597
+ ...lastLogin,
598
+ },
570
599
  },
571
600
  },
572
- };
573
-
574
- const user = await Users.findOneAndUpdate(generateDbFilterById(userId), modifier, {
575
- returnDocument: 'after',
576
- });
577
-
601
+ {
602
+ returnDocument: 'after',
603
+ },
604
+ );
605
+ if (!user) return null;
578
606
  await emit('USER_UPDATE_HEARTBEAT', {
579
607
  user: removeConfidentialServiceHashes(user),
580
608
  });
581
609
  return user;
582
610
  },
583
611
 
584
- markDeleted: async (userId: string): Promise<User> => {
612
+ markDeleted: async (userId: string) => {
585
613
  await db.collection('sessions').deleteMany({
586
614
  session: insensitiveTrimmedRegexOperator(`"user":"${userId}"`),
587
615
  });
@@ -593,18 +621,21 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
593
621
  deleted: new Date(),
594
622
  emails: [],
595
623
  roles: [],
596
- profile: null,
597
- lastBillingAddress: null,
598
624
  services: {},
599
625
  pushSubscriptions: [],
600
- avatarId: null,
601
626
  initialPassword: false,
602
- lastContact: null,
603
- lastLogin: null,
627
+ },
628
+ $unset: {
629
+ profile: 1,
630
+ lastBillingAddress: 1,
631
+ lastContact: 1,
632
+ lastLogin: 1,
633
+ avatarId: 1,
604
634
  },
605
635
  },
606
636
  { returnDocument: 'after' },
607
637
  );
638
+ if (!user) return null;
608
639
 
609
640
  await emit('USER_REMOVE', {
610
641
  user,
@@ -612,14 +643,11 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
612
643
  return user;
613
644
  },
614
645
 
615
- deletePermanently: async ({ userId }: { userId: string }): Promise<User> => {
646
+ deletePermanently: async ({ userId }: { userId: string }) => {
616
647
  return Users.findOneAndDelete({ _id: userId });
617
648
  },
618
649
 
619
- updateProfile: async (
620
- userId: string,
621
- updatedData: { profile?: UserProfile; meta?: any },
622
- ): Promise<User> => {
650
+ updateProfile: async (userId: string, updatedData: { profile?: UserProfile; meta?: any }) => {
623
651
  const userFilter = generateDbFilterById(userId);
624
652
  const { meta, profile } = updatedData;
625
653
 
@@ -648,16 +676,17 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
648
676
  returnDocument: 'after',
649
677
  });
650
678
 
679
+ if (!user) return null;
651
680
  await emit('USER_UPDATE_PROFILE', {
652
681
  user: removeConfidentialServiceHashes(user),
653
682
  });
654
683
  return user;
655
684
  },
656
685
 
657
- updateLastBillingAddress: async (_id: string, lastBillingAddress: Address): Promise<User> => {
686
+ updateLastBillingAddress: async (_id: string, lastBillingAddress: Address) => {
658
687
  const userFilter = generateDbFilterById(_id);
659
688
  const user = await Users.findOne(userFilter, {});
660
-
689
+ if (!user) return null;
661
690
  if (!lastBillingAddress) return user;
662
691
 
663
692
  const modifier = {
@@ -682,18 +711,17 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
682
711
  returnDocument: 'after',
683
712
  });
684
713
 
685
- if (updatedUser) {
686
- await emit('USER_UPDATE_BILLING_ADDRESS', {
687
- user: removeConfidentialServiceHashes(updatedUser),
688
- });
689
- }
690
-
714
+ if (!updatedUser) return null;
715
+ await emit('USER_UPDATE_BILLING_ADDRESS', {
716
+ user: removeConfidentialServiceHashes(updatedUser),
717
+ });
691
718
  return updatedUser;
692
719
  },
693
720
 
694
- updateLastContact: async (_id: string, lastContact: Contact): Promise<User> => {
721
+ updateLastContact: async (_id: string, lastContact: Contact) => {
695
722
  const userFilter = generateDbFilterById(_id);
696
723
  const user = await Users.findOne(userFilter, {});
724
+ if (!user) return null;
697
725
  const profile = user.profile || {};
698
726
  const isGuest = !!user.guest;
699
727
 
@@ -713,13 +741,14 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
713
741
  returnDocument: 'after',
714
742
  });
715
743
 
744
+ if (!updatedUser) return null;
716
745
  await emit('USER_UPDATE_LAST_CONTACT', {
717
746
  user: removeConfidentialServiceHashes(user),
718
747
  });
719
748
  return updatedUser;
720
749
  },
721
750
 
722
- updateRoles: async (_id: string, roles: string[]): Promise<User> => {
751
+ updateRoles: async (_id: string, roles: string[]) => {
723
752
  const modifier = {
724
753
  $set: {
725
754
  updated: new Date(),
@@ -730,23 +759,27 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
730
759
  const user = await Users.findOneAndUpdate(generateDbFilterById(_id), modifier, {
731
760
  returnDocument: 'after',
732
761
  });
733
-
762
+ if (!user) return null;
734
763
  await emit('USER_UPDATE_ROLE', {
735
764
  user: removeConfidentialServiceHashes(user),
736
765
  });
737
766
  return user;
738
767
  },
739
768
 
740
- updateTags: async (_id: string, tags: string[]): Promise<User> => {
741
- const modifier = {
742
- $set: {
743
- updated: new Date(),
744
- tags,
769
+ updateTags: async (_id: string, tags: string[]) => {
770
+ const user = await Users.findOneAndUpdate(
771
+ generateDbFilterById(_id),
772
+ {
773
+ $set: {
774
+ updated: new Date(),
775
+ tags,
776
+ },
745
777
  },
746
- };
747
- const user = await Users.findOneAndUpdate(generateDbFilterById(_id), modifier, {
748
- returnDocument: 'after',
749
- });
778
+ {
779
+ returnDocument: 'after',
780
+ },
781
+ );
782
+ if (!user) return null;
750
783
  await emit('USER_UPDATE_TAGS', {
751
784
  user: removeConfidentialServiceHashes(user),
752
785
  });
@@ -757,11 +790,12 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
757
790
  selector: mongodb.Filter<User>,
758
791
  modifier: mongodb.UpdateFilter<User>,
759
792
  updateOptions?: mongodb.FindOneAndUpdateOptions,
760
- ): Promise<User> => {
793
+ ) => {
761
794
  const user = await Users.findOneAndUpdate(selector, modifier, {
762
795
  ...updateOptions,
763
796
  returnDocument: 'after',
764
797
  });
798
+ if (!user) return null;
765
799
  await emit('USER_UPDATE', {
766
800
  user: removeConfidentialServiceHashes(user),
767
801
  });
@@ -811,6 +845,13 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
811
845
  {},
812
846
  );
813
847
  },
848
+ existingTags: async (): Promise<string[]> => {
849
+ const tags = (await Users.distinct('tags', {
850
+ tags: { $exists: true },
851
+ deleted: { $exists: false },
852
+ })) as string[];
853
+ return tags.filter(Boolean).toSorted();
854
+ },
814
855
  };
815
856
  };
816
857
 
@@ -6,7 +6,7 @@ import type { PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOpti
6
6
 
7
7
  const logger = createLogger('unchained:core-users');
8
8
 
9
- const { ROOT_URL, EMAIL_WEBSITE_NAME } = process.env;
9
+ const { ROOT_URL = 'http://localhost:4010', EMAIL_WEBSITE_NAME = 'Unchained' } = process.env;
10
10
 
11
11
  type SerializedOptions<T> = Omit<T, 'challenge' | 'requestId'> & {
12
12
  challenge: string;
@@ -86,7 +86,8 @@ export const configureUsersWebAuthnModule = async ({ db }: ModuleInput<any>) =>
86
86
  if (!f2l) return null;
87
87
 
88
88
  const registrationOptions = await f2l.attestationOptions(extensionOptions);
89
- const challenge = Buffer.from(registrationOptions.challenge).toString('base64');
89
+ // Convert challenge to base64 without using Buffer
90
+ const challenge = btoa(String.fromCharCode(...new Uint8Array(registrationOptions.challenge)));
90
91
  const { insertedId } = await WebAuthnCredentialsCreationRequests.insertOne({
91
92
  _id: new Date().getTime(),
92
93
  challenge,
@@ -102,15 +103,12 @@ export const configureUsersWebAuthnModule = async ({ db }: ModuleInput<any>) =>
102
103
  } as SerializedOptions<PublicKeyCredentialCreationOptions>;
103
104
  },
104
105
 
105
- createCredentialRequestOptions: async (
106
- origin: string,
107
- username?: string,
108
- extensionOptions?: any,
109
- ) => {
106
+ createCredentialRequestOptions: async (origin: string, username: string, extensionOptions?: any) => {
110
107
  if (!f2l) return null;
111
108
 
112
109
  const loginOptions = await f2l.assertionOptions(extensionOptions);
113
- const challenge = Buffer.from(loginOptions.challenge).toString('base64');
110
+ // Convert challenge to base64 without using Buffer
111
+ const challenge = btoa(String.fromCharCode(...new Uint8Array(loginOptions.challenge)));
114
112
  const { insertedId } = await WebAuthnCredentialsCreationRequests.insertOne({
115
113
  _id: new Date().getTime(),
116
114
  challenge,
@@ -135,6 +133,7 @@ export const configureUsersWebAuthnModule = async ({ db }: ModuleInput<any>) =>
135
133
  },
136
134
  { sort: { _id: -1 } },
137
135
  );
136
+ if (!request) return null;
138
137
 
139
138
  const attestationExpectations = {
140
139
  challenge: request.challenge,
@@ -184,6 +183,7 @@ export const configureUsersWebAuthnModule = async ({ db }: ModuleInput<any>) =>
184
183
  },
185
184
  { sort: { _id: -1 } },
186
185
  );
186
+ if (!request) return null;
187
187
 
188
188
  const id = Buffer.from(credentials.id, 'base64');
189
189
  const authenticatorData = Buffer.from(credentials.response.authenticatorData, 'base64');