@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.
- package/lib/db/UsersCollection.d.ts.map +1 -1
- package/lib/db/UsersCollection.js +0 -8
- package/lib/db/UsersCollection.js.map +1 -1
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts +8 -0
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts.map +1 -0
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js +13 -0
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js.map +1 -0
- package/lib/module/configureUsersModule.d.ts +88 -2
- package/lib/module/configureUsersModule.d.ts.map +1 -1
- package/lib/module/configureUsersModule.js +277 -47
- package/lib/module/configureUsersModule.js.map +1 -1
- package/lib/module/configureUsersWebAuthnModule.d.ts +25 -0
- package/lib/module/configureUsersWebAuthnModule.d.ts.map +1 -0
- package/lib/module/configureUsersWebAuthnModule.js +148 -0
- package/lib/module/configureUsersWebAuthnModule.js.map +1 -0
- package/lib/module/pbkdf2.d.ts +4 -0
- package/lib/module/pbkdf2.d.ts.map +1 -0
- package/lib/module/pbkdf2.js +27 -0
- package/lib/module/pbkdf2.js.map +1 -0
- package/lib/module/sha256.d.ts +2 -0
- package/lib/module/sha256.d.ts.map +1 -0
- package/lib/module/sha256.js +6 -0
- package/lib/module/sha256.js.map +1 -0
- package/lib/services/migrateUserDataService.d.ts +3 -0
- package/lib/services/migrateUserDataService.d.ts.map +1 -0
- package/lib/services/migrateUserDataService.js +18 -0
- package/lib/services/migrateUserDataService.js.map +1 -0
- package/lib/services/userServices.d.ts +6 -2
- package/lib/services/userServices.d.ts.map +1 -1
- package/lib/services/userServices.js +2 -0
- package/lib/services/userServices.js.map +1 -1
- package/lib/users-index.d.ts +1 -0
- package/lib/users-index.d.ts.map +1 -1
- package/lib/users-index.js +1 -0
- package/lib/users-index.js.map +1 -1
- package/lib/users-settings.d.ts +3 -0
- package/lib/users-settings.d.ts.map +1 -0
- package/lib/users-settings.js +51 -0
- package/lib/users-settings.js.map +1 -0
- package/package.json +9 -7
- package/src/db/UsersCollection.ts +0 -9
- package/src/db/WebAuthnCredentialsCreationRequestsCollection.ts +20 -0
- package/src/module/configureUsersModule.ts +411 -62
- package/src/module/configureUsersWebAuthnModule.ts +201 -0
- package/src/module/pbkdf2.ts +39 -0
- package/src/module/sha256.ts +5 -0
- package/src/services/migrateUserDataService.ts +31 -0
- package/src/services/userServices.ts +3 -2
- package/src/users-index.ts +1 -0
- package/src/users-settings.ts +71 -0
- 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 {
|
|
4
|
-
|
|
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 {
|
|
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<
|
|
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
|
-
|
|
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
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
75
|
-
}
|
|
99
|
+
async findUserByUsername(username: string): Promise<User> {
|
|
100
|
+
if (!username) return null;
|
|
101
|
+
return Users.findOne({ username }, {});
|
|
102
|
+
},
|
|
76
103
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
228
|
+
return userCount === 1;
|
|
127
229
|
},
|
|
128
230
|
|
|
129
231
|
// Transformations
|
|
130
|
-
primaryEmail:
|
|
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:
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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}@${
|
|
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}-${
|
|
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
|
-
|
|
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 (
|
|
363
|
-
|
|
364
|
-
|
|
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 (
|
|
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
|
-
|
|
745
|
+
|
|
746
|
+
removePushSubscription: async (userId: string, p256dh: string): Promise<void> => {
|
|
398
747
|
await Users.updateOne(
|
|
399
748
|
{ _id: userId },
|
|
400
749
|
{
|