@unchainedshop/core-users 2.13.0 → 3.0.0-alpha2

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 (51) hide show
  1. package/lib/db/UsersCollection.d.ts.map +1 -1
  2. package/lib/db/UsersCollection.js +0 -8
  3. package/lib/db/UsersCollection.js.map +1 -1
  4. package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts +8 -0
  5. package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts.map +1 -0
  6. package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js +13 -0
  7. package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js.map +1 -0
  8. package/lib/module/configureUsersModule.d.ts +88 -2
  9. package/lib/module/configureUsersModule.d.ts.map +1 -1
  10. package/lib/module/configureUsersModule.js +277 -47
  11. package/lib/module/configureUsersModule.js.map +1 -1
  12. package/lib/module/configureUsersWebAuthnModule.d.ts +25 -0
  13. package/lib/module/configureUsersWebAuthnModule.d.ts.map +1 -0
  14. package/lib/module/configureUsersWebAuthnModule.js +148 -0
  15. package/lib/module/configureUsersWebAuthnModule.js.map +1 -0
  16. package/lib/module/pbkdf2.d.ts +4 -0
  17. package/lib/module/pbkdf2.d.ts.map +1 -0
  18. package/lib/module/pbkdf2.js +27 -0
  19. package/lib/module/pbkdf2.js.map +1 -0
  20. package/lib/module/sha256.d.ts +2 -0
  21. package/lib/module/sha256.d.ts.map +1 -0
  22. package/lib/module/sha256.js +6 -0
  23. package/lib/module/sha256.js.map +1 -0
  24. package/lib/services/migrateUserDataService.d.ts +3 -0
  25. package/lib/services/migrateUserDataService.d.ts.map +1 -0
  26. package/lib/services/migrateUserDataService.js +18 -0
  27. package/lib/services/migrateUserDataService.js.map +1 -0
  28. package/lib/services/userServices.d.ts +6 -2
  29. package/lib/services/userServices.d.ts.map +1 -1
  30. package/lib/services/userServices.js +2 -0
  31. package/lib/services/userServices.js.map +1 -1
  32. package/lib/users-index.d.ts +1 -0
  33. package/lib/users-index.d.ts.map +1 -1
  34. package/lib/users-index.js +1 -0
  35. package/lib/users-index.js.map +1 -1
  36. package/lib/users-settings.d.ts +3 -0
  37. package/lib/users-settings.d.ts.map +1 -0
  38. package/lib/users-settings.js +51 -0
  39. package/lib/users-settings.js.map +1 -0
  40. package/package.json +9 -7
  41. package/src/db/UsersCollection.ts +0 -9
  42. package/src/db/WebAuthnCredentialsCreationRequestsCollection.ts +20 -0
  43. package/src/module/configureUsersModule.ts +411 -62
  44. package/src/module/configureUsersWebAuthnModule.ts +201 -0
  45. package/src/module/pbkdf2.ts +39 -0
  46. package/src/module/sha256.ts +5 -0
  47. package/src/services/migrateUserDataService.ts +31 -0
  48. package/src/services/userServices.ts +3 -2
  49. package/src/users-index.ts +1 -0
  50. package/src/users-settings.ts +71 -0
  51. package/tests/mock/user-mock.ts +0 -8
@@ -1,25 +1,47 @@
1
1
  import localePkg from 'locale';
2
+ import bcrypt from 'bcryptjs';
3
+ import { Address, Contact } from '@unchainedshop/types/common.js';
2
4
  import { ModuleInput, UnchainedCore } from '@unchainedshop/types/core.js';
3
- import { User, UserQuery, UsersModule } from '@unchainedshop/types/user.js';
4
- import { log, LogLevel } from '@unchainedshop/logger';
5
+ import {
6
+ User,
7
+ UserQuery,
8
+ Email,
9
+ UserLastLogin,
10
+ UserProfile,
11
+ UserSettingsOptions,
12
+ UserData,
13
+ } from '@unchainedshop/types/user.js';
5
14
  import { emit, registerEvents } from '@unchainedshop/events';
6
- import { generateDbFilterById, buildSortOptions, mongodb } from '@unchainedshop/mongodb';
15
+ import {
16
+ generateDbFilterById,
17
+ buildSortOptions,
18
+ mongodb,
19
+ generateDbObjectId,
20
+ } from '@unchainedshop/mongodb';
7
21
  import { systemLocale } from '@unchainedshop/utils';
8
22
  import { FileDirector } from '@unchainedshop/file-upload';
9
23
  import { SortDirection, SortOption } from '@unchainedshop/types/api.js';
10
- import { v4 as uuidv4 } from 'uuid';
11
24
  import { UsersCollection } from '../db/UsersCollection.js';
12
25
  import addMigrations from './addMigrations.js';
26
+ import { userSettings } from '../users-settings.js';
27
+ import { configureUsersWebAuthnModule } from './configureUsersWebAuthnModule.js';
28
+ import * as pbkdf2 from './pbkdf2.js';
29
+ import * as sha256 from './sha256.js';
13
30
 
14
31
  const { Locale } = localePkg;
15
32
 
16
33
  const USER_EVENTS = [
34
+ 'USER_ACCOUNT_ACTION',
35
+ 'USER_CREATE',
36
+ 'USER_ADD_ROLES',
17
37
  'USER_UPDATE',
18
38
  'USER_UPDATE_PROFILE',
19
- 'USER_ADD_ROLES',
20
39
  'USER_UPDATE_ROLE',
21
40
  'USER_UPDATE_TAGS',
22
41
  'USER_UPDATE_AVATAR',
42
+ 'USER_UPDATE_GUEST',
43
+ 'USER_UPDATE_USERNAME',
44
+ 'USER_UPDATE_PASSWORD',
23
45
  'USER_UPDATE_HEARTBEAT',
24
46
  'USER_UPDATE_BILLING_ADDRESS',
25
47
  'USER_UPDATE_LAST_CONTACT',
@@ -42,64 +64,144 @@ export const buildFindSelector = ({ includeGuests, queryString, ...rest }: UserQ
42
64
 
43
65
  FileDirector.registerFileUploadCallback('user-avatars', async (file, context: UnchainedCore) => {
44
66
  const { services } = context;
67
+
45
68
  return services.users.updateUserAvatarAfterUpload({ file }, context);
46
69
  });
47
70
 
48
71
  export const configureUsersModule = async ({
49
72
  db,
73
+ options,
50
74
  migrationRepository,
51
- }: ModuleInput<Record<string, never>>): Promise<UsersModule> => {
75
+ }: ModuleInput<UserSettingsOptions>) => {
76
+ userSettings.configureSettings(options || {}, db);
77
+
52
78
  registerEvents(USER_EVENTS);
53
79
  const Users = await UsersCollection(db);
54
80
 
55
81
  // Migration
56
82
  addMigrations(migrationRepository);
57
83
 
84
+ const webAuthn = await configureUsersWebAuthnModule({ db, options });
85
+
58
86
  return {
59
87
  // Queries
60
- count: async (query) => {
88
+ webAuthn,
89
+ async count(query: UserQuery): Promise<number> {
61
90
  const userCount = await Users.countDocuments(buildFindSelector(query));
62
91
  return userCount;
63
92
  },
64
93
 
65
- async findUserById(userId) {
94
+ async findUserById(userId: string): Promise<User> {
66
95
  if (!userId) return null;
67
96
  return Users.findOne(generateDbFilterById(userId), {});
68
97
  },
69
98
 
70
- async findUserByToken({ resetToken, hashedToken, verifyEmailToken }) {
71
- if (hashedToken) {
72
- return Users.findOne({
73
- 'services.resume.loginTokens.hashedToken': hashedToken,
74
- });
75
- }
99
+ async findUserByUsername(username: string): Promise<User> {
100
+ if (!username) return null;
101
+ return Users.findOne({ username }, {});
102
+ },
76
103
 
77
- if (resetToken) {
78
- return Users.findOne({
79
- 'services.password.reset.token': resetToken,
80
- });
81
- }
104
+ async findUserByEmail(email: string): Promise<User> {
105
+ if (!email) return null;
106
+ return Users.findOne({ 'emails.address': { $regex: email, $options: 'i' } }, {});
107
+ },
82
108
 
83
- if (verifyEmailToken) {
84
- return Users.findOne({
109
+ async findUnverifiedEmailToken(plainToken: string): Promise<{
110
+ userId: string;
111
+ address: string;
112
+ when: Date;
113
+ }> {
114
+ if (!plainToken) return null;
115
+ const token = await sha256.hash(plainToken);
116
+ const user = await Users.findOne(
117
+ {
85
118
  'services.email.verificationTokens': {
86
119
  $elemMatch: {
87
- token: verifyEmailToken,
120
+ token,
121
+ when: { $gt: new Date().getTime() },
122
+ },
123
+ },
124
+ },
125
+ {},
126
+ );
127
+ if (!user) return null;
128
+ const verificationToken = user.services.email.verificationTokens.find((v) => v.token === token);
129
+ return {
130
+ userId: user._id,
131
+ ...verificationToken,
132
+ };
133
+ },
134
+
135
+ async verifyEmail(userId: string, address: string): Promise<void> {
136
+ await Users.updateOne(
137
+ { _id: userId },
138
+ {
139
+ $set: {
140
+ 'emails.$[email].verified': true,
141
+ },
142
+ $pull: {
143
+ 'services.email.verificationTokens': {
144
+ address,
145
+ },
146
+ },
147
+ },
148
+ {
149
+ arrayFilters: [{ 'email.address': address }],
150
+ },
151
+ );
152
+
153
+ await emit('USER_ACCOUNT_ACTION', {
154
+ action: 'email-verified',
155
+ address,
156
+ userId,
157
+ });
158
+ },
159
+
160
+ async findUserByResetToken(plainToken: string): Promise<User> {
161
+ const token = await sha256.hash(plainToken);
162
+ const user = await Users.findOne(
163
+ {
164
+ 'services.password.reset': {
165
+ $elemMatch: {
166
+ token,
167
+ when: { $gt: new Date().getTime() },
88
168
  },
89
169
  },
170
+ },
171
+ {},
172
+ );
173
+ return user;
174
+ },
175
+
176
+ async findUserByToken(plainToken?: string): Promise<User> {
177
+ const token = await sha256.hash(plainToken);
178
+
179
+ if (token) {
180
+ return Users.findOne({
181
+ 'services.token.secret': token,
90
182
  });
91
183
  }
92
184
 
93
185
  return null;
94
186
  },
95
187
 
96
- findUser: async (query, options) => {
188
+ async findUser(
189
+ query: UserQuery & { sort?: Array<SortOption> },
190
+ findOptions?: mongodb.FindOptions,
191
+ ): Promise<User> {
97
192
  const selector = buildFindSelector(query);
98
-
99
- return Users.findOne(selector, options);
193
+ return Users.findOne(selector, findOptions);
100
194
  },
101
195
 
102
- findUsers: async ({ limit, offset, ...query }) => {
196
+ async findUsers({
197
+ limit,
198
+ offset,
199
+ ...query
200
+ }: UserQuery & {
201
+ sort?: Array<SortOption>;
202
+ limit?: number;
203
+ offset?: number;
204
+ }): Promise<Array<User>> {
103
205
  const defaultSort = [{ key: 'created', value: SortDirection.ASC }] as SortOption[];
104
206
  const selector = buildFindSelector(query);
105
207
 
@@ -119,29 +221,265 @@ export const configureUsersModule = async ({
119
221
  }).toArray();
120
222
  },
121
223
 
122
- userExists: async ({ userId }) => {
224
+ async userExists({ userId }: { userId: string }): Promise<boolean> {
123
225
  const selector = generateDbFilterById<User>(userId);
124
226
  selector.deleted = null; // skip deleted users when checked for existance!
125
227
  const userCount = await Users.countDocuments(selector, { limit: 1 });
126
- return !!userCount;
228
+ return userCount === 1;
127
229
  },
128
230
 
129
231
  // Transformations
130
- primaryEmail: (user) => {
232
+ primaryEmail(user: User): Email {
131
233
  return (user.emails || []).sort(
132
234
  (left, right) => Number(right.verified) - Number(left.verified),
133
235
  )?.[0];
134
236
  },
135
237
 
136
- userLocale: (user) => {
238
+ userLocale(user: User): localePkg.Locale {
137
239
  if (!user?.lastLogin?.locale) return systemLocale;
138
240
  return new Locale(user.lastLogin.locale);
139
241
  },
140
242
 
141
- updateAvatar: async (_id, fileId) => {
142
- const userFilter = generateDbFilterById(_id);
143
- log('Update Avatar', { userId: _id });
243
+ // Mutations
244
+ async createUser(
245
+ {
246
+ password,
247
+ email,
248
+ username,
249
+ initialPassword,
250
+ roles,
251
+ webAuthnPublicKeyCredentials,
252
+ ...userData
253
+ }: UserData,
254
+ {
255
+ skipMessaging,
256
+ skipPasswordEnrollment,
257
+ }: { skipMessaging?: boolean; skipPasswordEnrollment?: boolean } = {},
258
+ ): Promise<string> {
259
+ const normalizedUserData = await userSettings.validateNewUser(userData);
260
+
261
+ const webAuthnService =
262
+ webAuthnPublicKeyCredentials &&
263
+ (await this.webAuthn.verifyCredentialCreation(username, webAuthnPublicKeyCredentials));
264
+
265
+ const services: Record<string, any> = {};
266
+
267
+ if (email) {
268
+ if (!(await userSettings.validateEmail(email))) {
269
+ throw new Error(`E-Mail address ${email} is invalid`, { cause: 'EMAIL_INVALID' });
270
+ }
271
+ }
272
+
273
+ if (password) {
274
+ if (!(await userSettings.validatePassword(password))) {
275
+ throw new Error(`Provided password is invalid`, { cause: 'PASSWORD_INVALID' });
276
+ }
277
+ services.password = await this.hashPassword(password);
278
+ }
144
279
 
280
+ if (webAuthnService) {
281
+ services.webAuthn = [webAuthnService];
282
+ }
283
+
284
+ const doc = {
285
+ ...normalizedUserData,
286
+ _id: normalizedUserData._id || generateDbObjectId(),
287
+ roles: roles || [],
288
+ initialPassword: Boolean(initialPassword),
289
+ emails: email ? [{ address: email, verified: false }] : [],
290
+ created: new Date(),
291
+ };
292
+
293
+ if (username) {
294
+ if (!(await userSettings.validateUsername(username))) {
295
+ throw new Error(`Username ${username} is invalid`, { cause: 'USERNAME_INVALID' });
296
+ }
297
+ doc.username = username;
298
+ }
299
+
300
+ const { insertedId: userId } = await Users.insertOne(doc);
301
+
302
+ try {
303
+ const autoMessagingEnabled = skipMessaging
304
+ ? false
305
+ : userSettings.autoMessagingAfterUserCreation && !!email && !!userId;
306
+
307
+ if (autoMessagingEnabled) {
308
+ if (password === undefined) {
309
+ if (!skipPasswordEnrollment && !webAuthnService) {
310
+ await this.sendResetPasswordEmail(userId, email, true);
311
+ }
312
+ } else {
313
+ await this.sendVerificationEmail(userId, email);
314
+ }
315
+ }
316
+ } catch (e) {
317
+ /* */
318
+ }
319
+
320
+ const user = await Users.findOne({ _id: userId }, {});
321
+ await emit('USER_CREATE', {
322
+ user: removeConfidentialServiceHashes(user),
323
+ });
324
+
325
+ return userId;
326
+ },
327
+
328
+ async hashPassword(password) {
329
+ const salt = pbkdf2.generateSalt();
330
+ const hashedPassword = await pbkdf2.getDerivedKey(salt, password);
331
+ return { pbkdf2: `${salt}:${hashedPassword}` };
332
+ },
333
+
334
+ async verifyPassword(
335
+ { bcrypt: bcryptHash, pbkdf2: pbkdf2SaltAndHash }: { bcrypt?: string; pbkdf2?: string },
336
+ plainPassword: string,
337
+ ): Promise<boolean> {
338
+ if (bcryptHash) {
339
+ const password = await sha256.hash(plainPassword);
340
+ return bcrypt.compare(password, bcryptHash);
341
+ }
342
+ if (pbkdf2SaltAndHash) {
343
+ const [pbkdf2Salt, pbkdf2Hash] = pbkdf2SaltAndHash.split(':');
344
+ return pbkdf2.compare(plainPassword, pbkdf2Hash, pbkdf2Salt);
345
+ }
346
+ return false;
347
+ },
348
+
349
+ async addEmail(userId: string, address: string): Promise<void> {
350
+ if (!(await userSettings.validateEmail(address))) {
351
+ throw new Error(`E-Mail address ${address} is invalid`, { cause: 'EMAIL_INVALID' });
352
+ }
353
+ await this.updateUser(
354
+ { _id: userId, 'emails.address': { $not: { $regex: address, $options: 'i' } } },
355
+ {
356
+ $push: {
357
+ emails: {
358
+ address: address.trim(),
359
+ verified: false,
360
+ },
361
+ },
362
+ },
363
+ );
364
+ },
365
+
366
+ async removeEmail(userId: string, address: string): Promise<void> {
367
+ await this.updateUser(
368
+ { _id: userId, 'emails.address': { $regex: address, $options: 'i' } },
369
+ {
370
+ $pull: {
371
+ emails: { address: { $regex: address, $options: 'i' } },
372
+ },
373
+ },
374
+ );
375
+ },
376
+
377
+ async sendResetPasswordEmail(userId: string, email: string, isEnrollment?: boolean): Promise<void> {
378
+ const plainToken = crypto.randomUUID();
379
+ const resetToken = {
380
+ token: await sha256.hash(plainToken),
381
+ address: email,
382
+ when: new Date().getTime() + 1000 * 60 * 60, // 1 hour
383
+ };
384
+
385
+ await Users.updateOne(
386
+ { _id: userId },
387
+ {
388
+ $push: {
389
+ 'services.password.reset': resetToken,
390
+ },
391
+ },
392
+ );
393
+
394
+ await emit('USER_ACCOUNT_ACTION', {
395
+ action: isEnrollment ? 'enroll-account' : 'reset-password',
396
+ userId,
397
+ ...resetToken,
398
+ token: plainToken,
399
+ });
400
+ },
401
+
402
+ async sendVerificationEmail(userId: string, email: string): Promise<void> {
403
+ const plainToken = crypto.randomUUID();
404
+ const verificationToken = {
405
+ token: await sha256.hash(plainToken),
406
+ address: email,
407
+ when: new Date().getTime() + 1000 * 60 * 60, // 1 hour
408
+ };
409
+
410
+ await Users.updateOne(
411
+ { _id: userId },
412
+ {
413
+ $push: {
414
+ 'services.email.verificationTokens': verificationToken,
415
+ },
416
+ },
417
+ );
418
+
419
+ await emit('USER_ACCOUNT_ACTION', {
420
+ action: 'verify-email',
421
+ userId,
422
+ ...verificationToken,
423
+ token: plainToken,
424
+ });
425
+ },
426
+
427
+ addRoles: async (userId: string, roles: Array<string>): Promise<number> => {
428
+ const selector = generateDbFilterById(userId);
429
+ const updateResult = await Users.updateOne(selector, {
430
+ $addToSet: { roles: { $each: roles } },
431
+ });
432
+
433
+ const user = await Users.findOne(selector, {});
434
+ await emit('USER_ADD_ROLES', {
435
+ user: removeConfidentialServiceHashes(user),
436
+ });
437
+
438
+ return updateResult.modifiedCount;
439
+ },
440
+
441
+ async setUsername(userId: string, username: string) {
442
+ if (!(await userSettings.validateUsername(username))) {
443
+ throw new Error(`Username ${username} is invalid`, { cause: 'USERNAME_INVALID' });
444
+ }
445
+ await Users.updateOne(
446
+ { _id: userId },
447
+ {
448
+ $set: {
449
+ username: username.trim(),
450
+ },
451
+ },
452
+ );
453
+ const user = await Users.findOne({ _id: userId }, {});
454
+ await emit('USER_UPDATE_USERNAME', {
455
+ user: removeConfidentialServiceHashes(user),
456
+ });
457
+ },
458
+
459
+ async setPassword(userId: string, plainPassword: string) {
460
+ if (!(await userSettings.validatePassword(plainPassword))) {
461
+ throw new Error(`Password ***** is invalid`, { cause: 'PASSWORD_INVALID' });
462
+ }
463
+ const password = plainPassword || crypto.randomUUID().split('-').pop();
464
+ await Users.updateOne(
465
+ { _id: userId },
466
+ {
467
+ $set: {
468
+ initialPassword: false,
469
+ 'services.password': await this.hashPassword(
470
+ password || crypto.randomUUID().split('-').pop(),
471
+ ),
472
+ },
473
+ },
474
+ );
475
+ const user = await Users.findOne({ _id: userId }, {});
476
+ await emit('USER_UPDATE_PASSWORD', {
477
+ user: removeConfidentialServiceHashes(user),
478
+ });
479
+ },
480
+
481
+ updateAvatar: async (_id: string, fileId: string): Promise<User> => {
482
+ const userFilter = generateDbFilterById(_id);
145
483
  const modifier = {
146
484
  $set: {
147
485
  avatarId: fileId,
@@ -159,14 +497,18 @@ export const configureUsersModule = async ({
159
497
  return user;
160
498
  },
161
499
 
162
- updateGuest: async (user, guest) => {
163
- log('Update guest', { userId: user._id });
164
-
500
+ updateGuest: async (user: User, guest: boolean): Promise<void> => {
165
501
  const modifier = { $set: { guest } };
166
502
  await Users.updateOne(generateDbFilterById(user._id), modifier);
503
+ await emit('USER_UPDATE_GUEST', {
504
+ user: removeConfidentialServiceHashes({
505
+ ...user,
506
+ guest,
507
+ }),
508
+ });
167
509
  },
168
510
 
169
- updateHeartbeat: async (userId, lastLogin) => {
511
+ updateHeartbeat: async (userId: string, lastLogin: UserLastLogin): Promise<User> => {
170
512
  const modifier = {
171
513
  $set: {
172
514
  lastLogin: {
@@ -186,17 +528,7 @@ export const configureUsersModule = async ({
186
528
  return user;
187
529
  },
188
530
 
189
- updateInitialPassword: async (user, initialPassword) => {
190
- log(`Update initial password flag to ${initialPassword}`, {
191
- userId: user._id,
192
- level: LogLevel.Verbose,
193
- });
194
-
195
- const modifier = { $set: { initialPassword } };
196
- await Users.updateOne(generateDbFilterById(user._id), modifier);
197
- },
198
-
199
- delete: async (userId) => {
531
+ delete: async (userId: string): Promise<User> => {
200
532
  const userFilter = generateDbFilterById(userId);
201
533
 
202
534
  const existingUser = await Users.findOne(userFilter, {
@@ -204,17 +536,18 @@ export const configureUsersModule = async ({
204
536
  });
205
537
  if (!existingUser) return null;
206
538
 
539
+ const uuid = crypto.randomUUID();
207
540
  const obfuscatedEmails = existingUser.emails?.flatMap(({ address, verified }) => {
208
541
  if (!verified) return [];
209
542
  return [
210
543
  {
211
- address: `${address}@${uuidv4()}.unchained.local`,
544
+ address: `${address}@${uuid}.unchained.local`,
212
545
  verified: true,
213
546
  },
214
547
  ];
215
548
  });
216
549
 
217
- const obfuscatedUsername = existingUser.username ? `${existingUser.username}-${uuidv4()}` : null;
550
+ const obfuscatedUsername = existingUser.username ? `${existingUser.username}-${uuid}` : null;
218
551
 
219
552
  Users.updateOne(userFilter, {
220
553
  $set: {
@@ -230,7 +563,11 @@ export const configureUsersModule = async ({
230
563
  });
231
564
  return user;
232
565
  },
233
- updateProfile: async (userId, updatedData) => {
566
+
567
+ updateProfile: async (
568
+ userId: string,
569
+ updatedData: { profile?: UserProfile; meta?: any },
570
+ ): Promise<User> => {
234
571
  const userFilter = generateDbFilterById(userId);
235
572
  const { meta, profile } = updatedData;
236
573
 
@@ -265,7 +602,7 @@ export const configureUsersModule = async ({
265
602
  return user;
266
603
  },
267
604
 
268
- updateLastBillingAddress: async (_id, lastBillingAddress) => {
605
+ updateLastBillingAddress: async (_id: string, lastBillingAddress: Address): Promise<User> => {
269
606
  const userFilter = generateDbFilterById(_id);
270
607
  const user = await Users.findOne(userFilter, {});
271
608
 
@@ -297,7 +634,7 @@ export const configureUsersModule = async ({
297
634
  return updatedUser;
298
635
  },
299
636
 
300
- updateLastContact: async (_id, lastContact) => {
637
+ updateLastContact: async (_id: string, lastContact: Contact): Promise<User> => {
301
638
  const userFilter = generateDbFilterById(_id);
302
639
  const user = await Users.findOne(userFilter, {});
303
640
  const profile = user.profile || {};
@@ -325,7 +662,7 @@ export const configureUsersModule = async ({
325
662
  return updatedUser;
326
663
  },
327
664
 
328
- updateRoles: async (_id, roles) => {
665
+ updateRoles: async (_id: string, roles: Array<string>): Promise<User> => {
329
666
  const modifier = {
330
667
  $set: {
331
668
  updated: new Date(),
@@ -343,7 +680,7 @@ export const configureUsersModule = async ({
343
680
  return user;
344
681
  },
345
682
 
346
- updateTags: async (_id, tags) => {
683
+ updateTags: async (_id: string, tags: Array<string>): Promise<User> => {
347
684
  const modifier = {
348
685
  $set: {
349
686
  updated: new Date(),
@@ -359,9 +696,13 @@ export const configureUsersModule = async ({
359
696
  return user;
360
697
  },
361
698
 
362
- updateUser: async (query, modifier, options) => {
363
- const user = await Users.findOneAndUpdate(query, modifier, {
364
- ...options,
699
+ updateUser: async (
700
+ selector: mongodb.Filter<User>,
701
+ modifier: mongodb.UpdateFilter<User>,
702
+ updateOptions?: mongodb.FindOneAndUpdateOptions,
703
+ ): Promise<User> => {
704
+ const user = await Users.findOneAndUpdate(selector, modifier, {
705
+ ...updateOptions,
365
706
  returnDocument: 'after',
366
707
  });
367
708
  await emit('USER_UPDATE', {
@@ -370,20 +711,27 @@ export const configureUsersModule = async ({
370
711
  return user;
371
712
  },
372
713
 
373
- addPushSubscription: async (userId, subscription, { userAgent, unsubscribeFromOtherUsers }) => {
714
+ addPushSubscription: async (
715
+ userId: string,
716
+ subscription: any,
717
+ subscriptionOptions?: {
718
+ userAgent: string;
719
+ unsubscribeFromOtherUsers: boolean;
720
+ },
721
+ ): Promise<void> => {
374
722
  const updateResult = await Users.updateOne(
375
723
  { _id: userId, 'pushSubscriptions.keys.p256dh': { $ne: subscription?.keys?.p256dh } },
376
724
  {
377
725
  $push: {
378
726
  pushSubscriptions: {
379
- userAgent,
727
+ userAgent: subscriptionOptions?.userAgent,
380
728
  ...subscription,
381
729
  },
382
730
  },
383
731
  },
384
732
  {},
385
733
  );
386
- if (updateResult.modifiedCount === 1 && unsubscribeFromOtherUsers) {
734
+ if (updateResult.modifiedCount === 1 && subscriptionOptions?.unsubscribeFromOtherUsers) {
387
735
  await Users.updateMany(
388
736
  { _id: { $ne: userId }, 'pushSubscriptions.keys.p256dh': subscription?.keys?.p256dh },
389
737
  {
@@ -394,7 +742,8 @@ export const configureUsersModule = async ({
394
742
  );
395
743
  }
396
744
  },
397
- removePushSubscription: async (userId, p256dh) => {
745
+
746
+ removePushSubscription: async (userId: string, p256dh: string): Promise<void> => {
398
747
  await Users.updateOne(
399
748
  { _id: userId },
400
749
  {