@unchainedshop/core-users 2.14.2 → 3.0.0-alpha3

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