@unchainedshop/core-users 4.3.5 → 4.5.0
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/README.md +124 -2
- package/lib/db/UsersCollection.d.ts +8 -5
- package/lib/db/UsersCollection.js +0 -1
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts +1 -2
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js +0 -1
- package/lib/migrations/20241218092300-convert-locale.d.ts +2 -0
- package/lib/migrations/20241218092300-convert-locale.js +36 -0
- package/lib/module/configureUsersModule.d.ts +53 -23
- package/lib/module/configureUsersModule.js +127 -18
- package/lib/module/configureUsersWebAuthnModule.d.ts +78 -20
- package/lib/module/configureUsersWebAuthnModule.js +126 -112
- package/lib/module/pbkdf2.d.ts +0 -1
- package/lib/module/pbkdf2.js +3 -7
- package/lib/users-index.d.ts +5 -6
- package/lib/users-index.js +5 -6
- package/lib/users-settings.d.ts +11 -11
- package/lib/users-settings.js +7 -10
- package/lib/utils/web3-verification.d.ts +1 -0
- package/lib/utils/web3-verification.js +81 -0
- package/package.json +26 -16
- package/lib/db/UsersCollection.d.ts.map +0 -1
- package/lib/db/UsersCollection.js.map +0 -1
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts.map +0 -1
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js.map +0 -1
- package/lib/module/configureUsersModule.d.ts.map +0 -1
- package/lib/module/configureUsersModule.js.map +0 -1
- package/lib/module/configureUsersWebAuthnModule.d.ts.map +0 -1
- package/lib/module/configureUsersWebAuthnModule.js.map +0 -1
- package/lib/module/pbkdf2.d.ts.map +0 -1
- package/lib/module/pbkdf2.js.map +0 -1
- package/lib/users-index.d.ts.map +0 -1
- package/lib/users-index.js.map +0 -1
- package/lib/users-settings.d.ts.map +0 -1
- package/lib/users-settings.js.map +0 -1
- package/src/db/UsersCollection.ts +0 -174
- package/src/db/WebAuthnCredentialsCreationRequestsCollection.ts +0 -26
- package/src/module/buildFindSelector.test.ts +0 -34
- package/src/module/configureUsersModule.ts +0 -857
- package/src/module/configureUsersWebAuthnModule.ts +0 -234
- package/src/module/pbkdf2.ts +0 -46
- package/src/module/removeConfidentialServiceHashes.test.ts +0 -12
- package/src/users-index.ts +0 -5
- package/src/users-settings.ts +0 -99
- package/tests/mock/user-mock.ts +0 -72
- package/tsconfig.json +0 -10
|
@@ -1,857 +0,0 @@
|
|
|
1
|
-
import * as bcrypt from 'bcryptjs';
|
|
2
|
-
import {
|
|
3
|
-
ModuleInput,
|
|
4
|
-
Address,
|
|
5
|
-
Contact,
|
|
6
|
-
generateDbFilterById,
|
|
7
|
-
buildSortOptions,
|
|
8
|
-
mongodb,
|
|
9
|
-
generateDbObjectId,
|
|
10
|
-
insensitiveTrimmedRegexOperator,
|
|
11
|
-
assertDocumentDBCompatMode,
|
|
12
|
-
} from '@unchainedshop/mongodb';
|
|
13
|
-
import {
|
|
14
|
-
User,
|
|
15
|
-
UserQuery,
|
|
16
|
-
Email,
|
|
17
|
-
UserLastLogin,
|
|
18
|
-
UserProfile,
|
|
19
|
-
UsersCollection,
|
|
20
|
-
} from '../db/UsersCollection.js';
|
|
21
|
-
import { emit, registerEvents } from '@unchainedshop/events';
|
|
22
|
-
import { systemLocale, SortDirection, SortOption, sha256 } from '@unchainedshop/utils';
|
|
23
|
-
import {
|
|
24
|
-
UserAccountAction,
|
|
25
|
-
UserRegistrationData,
|
|
26
|
-
userSettings,
|
|
27
|
-
UserSettingsOptions,
|
|
28
|
-
} from '../users-settings.js';
|
|
29
|
-
import { configureUsersWebAuthnModule } from './configureUsersWebAuthnModule.js';
|
|
30
|
-
import * as pbkdf2 from './pbkdf2.js';
|
|
31
|
-
|
|
32
|
-
const USER_EVENTS = [
|
|
33
|
-
'USER_ACCOUNT_ACTION',
|
|
34
|
-
'USER_CREATE',
|
|
35
|
-
'USER_ADD_ROLES',
|
|
36
|
-
'USER_UPDATE',
|
|
37
|
-
'USER_UPDATE_PROFILE',
|
|
38
|
-
'USER_UPDATE_ROLE',
|
|
39
|
-
'USER_UPDATE_TAGS',
|
|
40
|
-
'USER_UPDATE_AVATAR',
|
|
41
|
-
'USER_UPDATE_GUEST',
|
|
42
|
-
'USER_UPDATE_USERNAME',
|
|
43
|
-
'USER_UPDATE_PASSWORD',
|
|
44
|
-
'USER_UPDATE_HEARTBEAT',
|
|
45
|
-
'USER_UPDATE_BILLING_ADDRESS',
|
|
46
|
-
'USER_UPDATE_LAST_CONTACT',
|
|
47
|
-
'USER_REMOVE',
|
|
48
|
-
];
|
|
49
|
-
export const removeConfidentialServiceHashes = (rawUser: User): User => {
|
|
50
|
-
const user = { ...rawUser };
|
|
51
|
-
delete user?.services;
|
|
52
|
-
return user;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
export const buildFindSelector = ({
|
|
56
|
-
includeGuests,
|
|
57
|
-
includeDeleted,
|
|
58
|
-
queryString,
|
|
59
|
-
emailVerified,
|
|
60
|
-
lastLogin,
|
|
61
|
-
tags,
|
|
62
|
-
...rest
|
|
63
|
-
}: UserQuery) => {
|
|
64
|
-
const selector: mongodb.Filter<User> = { ...rest };
|
|
65
|
-
if (!includeDeleted) selector.deleted = null as any;
|
|
66
|
-
if (!includeGuests) selector.guest = { $ne: true };
|
|
67
|
-
if (emailVerified === true) {
|
|
68
|
-
selector['emails.verified'] = true;
|
|
69
|
-
}
|
|
70
|
-
if (Array.isArray(tags) && tags?.length) {
|
|
71
|
-
selector.tags = { $in: tags };
|
|
72
|
-
}
|
|
73
|
-
if (emailVerified === false) {
|
|
74
|
-
// We need to use $ne here else we'd also find users with many emails where one is
|
|
75
|
-
// unverified
|
|
76
|
-
selector['emails.verified'] = { $ne: true };
|
|
77
|
-
}
|
|
78
|
-
if (lastLogin?.start) {
|
|
79
|
-
selector['lastLogin.timestamp'] = { $exists: true };
|
|
80
|
-
}
|
|
81
|
-
if (lastLogin?.end) {
|
|
82
|
-
selector['lastLogin.timestamp'].$lte = new Date(lastLogin.end);
|
|
83
|
-
}
|
|
84
|
-
if (lastLogin?.start) {
|
|
85
|
-
selector['lastLogin.timestamp'].$gte = new Date(lastLogin.start);
|
|
86
|
-
}
|
|
87
|
-
if (queryString) {
|
|
88
|
-
assertDocumentDBCompatMode();
|
|
89
|
-
(selector as any).$text = { $search: queryString };
|
|
90
|
-
}
|
|
91
|
-
return selector;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
export const configureUsersModule = async (moduleInput: ModuleInput<UserSettingsOptions>) => {
|
|
95
|
-
const { db, options } = moduleInput;
|
|
96
|
-
userSettings.configureSettings(options || {}, db);
|
|
97
|
-
registerEvents(USER_EVENTS);
|
|
98
|
-
const Users = await UsersCollection(db);
|
|
99
|
-
const webAuthn = await configureUsersWebAuthnModule(moduleInput);
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
// Queries
|
|
103
|
-
webAuthn,
|
|
104
|
-
async count(query: UserQuery): Promise<number> {
|
|
105
|
-
const userCount = await Users.countDocuments(buildFindSelector(query));
|
|
106
|
-
return userCount;
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
async findUserById(userId: string) {
|
|
110
|
-
if (!userId) return null;
|
|
111
|
-
return Users.findOne(generateDbFilterById(userId), {});
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
async findUserByUsername(username: string) {
|
|
115
|
-
if (!username) return null;
|
|
116
|
-
return Users.findOne({ username: insensitiveTrimmedRegexOperator(username) }, {});
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
async findUserByEmail(email: string) {
|
|
120
|
-
if (!email) return null;
|
|
121
|
-
return Users.findOne({ 'emails.address': insensitiveTrimmedRegexOperator(email) }, {});
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
async findUnverifiedEmailToken(plainToken: string): Promise<{
|
|
125
|
-
userId: string;
|
|
126
|
-
address: string;
|
|
127
|
-
when: Date;
|
|
128
|
-
} | null> {
|
|
129
|
-
if (!plainToken) return null;
|
|
130
|
-
const token = await sha256(plainToken);
|
|
131
|
-
const user = await Users.findOne(
|
|
132
|
-
{
|
|
133
|
-
'services.email.verificationTokens': {
|
|
134
|
-
$elemMatch: {
|
|
135
|
-
token,
|
|
136
|
-
when: { $gt: userSettings.earliestValidTokenDate(UserAccountAction.VERIFY_EMAIL) },
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
{},
|
|
141
|
-
);
|
|
142
|
-
if (!user) return null;
|
|
143
|
-
const verificationToken = user.services.email.verificationTokens.find((v) => v.token === token);
|
|
144
|
-
return {
|
|
145
|
-
userId: user._id,
|
|
146
|
-
...verificationToken,
|
|
147
|
-
};
|
|
148
|
-
},
|
|
149
|
-
|
|
150
|
-
async verifyEmail(userId: string, address: string): Promise<void> {
|
|
151
|
-
const updated = await Users.updateOne(
|
|
152
|
-
{
|
|
153
|
-
_id: userId,
|
|
154
|
-
emails: { $elemMatch: { address: insensitiveTrimmedRegexOperator(address), verified: false } },
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
$set: {
|
|
158
|
-
'emails.$.verified': true,
|
|
159
|
-
},
|
|
160
|
-
$pull: {
|
|
161
|
-
'services.email.verificationTokens': {
|
|
162
|
-
address: insensitiveTrimmedRegexOperator(address),
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
if (updated.modifiedCount > 0) {
|
|
169
|
-
await emit('USER_ACCOUNT_ACTION', {
|
|
170
|
-
action: UserAccountAction.EMAIL_VERIFIED,
|
|
171
|
-
address,
|
|
172
|
-
userId,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
|
|
177
|
-
async findResetToken(plainToken: string): Promise<{
|
|
178
|
-
userId: string;
|
|
179
|
-
address: string;
|
|
180
|
-
when: Date;
|
|
181
|
-
} | null> {
|
|
182
|
-
const token = await sha256(plainToken);
|
|
183
|
-
const user = await Users.findOne(
|
|
184
|
-
{
|
|
185
|
-
'services.password.reset': {
|
|
186
|
-
$elemMatch: {
|
|
187
|
-
token,
|
|
188
|
-
when: { $gt: userSettings.earliestValidTokenDate(UserAccountAction.RESET_PASSWORD) },
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
{},
|
|
193
|
-
);
|
|
194
|
-
if (!user) return null;
|
|
195
|
-
const resetToken = user.services.password.reset.find((v) => v.token === token);
|
|
196
|
-
return {
|
|
197
|
-
userId: user._id,
|
|
198
|
-
...resetToken,
|
|
199
|
-
};
|
|
200
|
-
},
|
|
201
|
-
|
|
202
|
-
async findUserByToken(plainToken: string) {
|
|
203
|
-
const token = await sha256(plainToken);
|
|
204
|
-
|
|
205
|
-
if (token) {
|
|
206
|
-
return Users.findOne({
|
|
207
|
-
'services.token.secret': token,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return null;
|
|
212
|
-
},
|
|
213
|
-
|
|
214
|
-
async findUser(query: UserQuery & { sort?: SortOption[] }, findOptions?: mongodb.FindOptions) {
|
|
215
|
-
const selector = buildFindSelector(query);
|
|
216
|
-
return Users.findOne(selector, findOptions);
|
|
217
|
-
},
|
|
218
|
-
|
|
219
|
-
async findUsers({
|
|
220
|
-
limit,
|
|
221
|
-
offset,
|
|
222
|
-
...query
|
|
223
|
-
}: UserQuery & {
|
|
224
|
-
sort?: SortOption[];
|
|
225
|
-
limit?: number;
|
|
226
|
-
offset?: number;
|
|
227
|
-
}): Promise<User[]> {
|
|
228
|
-
const defaultSort = [{ key: 'created', value: SortDirection.ASC }] as SortOption[];
|
|
229
|
-
const selector = buildFindSelector({ ...query });
|
|
230
|
-
|
|
231
|
-
if (query.queryString) {
|
|
232
|
-
return Users.find(selector, {
|
|
233
|
-
skip: offset,
|
|
234
|
-
limit,
|
|
235
|
-
projection: { score: { $meta: 'textScore' } },
|
|
236
|
-
sort: { score: { $meta: 'textScore' } },
|
|
237
|
-
}).toArray();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return Users.find(selector, {
|
|
241
|
-
skip: offset,
|
|
242
|
-
limit,
|
|
243
|
-
sort: buildSortOptions(query.sort || defaultSort),
|
|
244
|
-
}).toArray();
|
|
245
|
-
},
|
|
246
|
-
|
|
247
|
-
async userExists({ userId }: { userId: string }): Promise<boolean> {
|
|
248
|
-
const userCount = await Users.countDocuments(
|
|
249
|
-
{ _id: userId, deleted: { $exists: false } },
|
|
250
|
-
{ limit: 1 },
|
|
251
|
-
);
|
|
252
|
-
return userCount === 1;
|
|
253
|
-
},
|
|
254
|
-
|
|
255
|
-
// Transformations
|
|
256
|
-
primaryEmail(user: User): Email {
|
|
257
|
-
return (user.emails || []).toSorted(
|
|
258
|
-
(left, right) => Number(right.verified) - Number(left.verified),
|
|
259
|
-
)?.[0];
|
|
260
|
-
},
|
|
261
|
-
|
|
262
|
-
userLocale(user: User | null): Intl.Locale {
|
|
263
|
-
if (!user?.lastLogin?.locale) return systemLocale;
|
|
264
|
-
try {
|
|
265
|
-
return new Intl.Locale(user.lastLogin.locale);
|
|
266
|
-
} catch {
|
|
267
|
-
return systemLocale;
|
|
268
|
-
}
|
|
269
|
-
},
|
|
270
|
-
|
|
271
|
-
// Mutations
|
|
272
|
-
async createUser(
|
|
273
|
-
rawUserData: UserRegistrationData,
|
|
274
|
-
{
|
|
275
|
-
skipMessaging,
|
|
276
|
-
skipPasswordEnrollment,
|
|
277
|
-
}: { skipMessaging?: boolean; skipPasswordEnrollment?: boolean } = {},
|
|
278
|
-
): Promise<string> {
|
|
279
|
-
const {
|
|
280
|
-
password,
|
|
281
|
-
email,
|
|
282
|
-
username,
|
|
283
|
-
initialPassword,
|
|
284
|
-
roles,
|
|
285
|
-
webAuthnPublicKeyCredentials,
|
|
286
|
-
...userData
|
|
287
|
-
} = await userSettings.validateNewUser(rawUserData);
|
|
288
|
-
|
|
289
|
-
const webAuthnService =
|
|
290
|
-
webAuthnPublicKeyCredentials &&
|
|
291
|
-
(await this.webAuthn.verifyCredentialCreation(username, webAuthnPublicKeyCredentials));
|
|
292
|
-
|
|
293
|
-
const services: Record<string, any> = {};
|
|
294
|
-
|
|
295
|
-
if (email) {
|
|
296
|
-
if (!(await userSettings.validateEmail(email))) {
|
|
297
|
-
throw new Error(`E-Mail address ${email} is invalid`, { cause: 'EMAIL_INVALID' });
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (password) {
|
|
302
|
-
if (!(await userSettings.validatePassword(password))) {
|
|
303
|
-
throw new Error(`Provided password is invalid`, { cause: 'PASSWORD_INVALID' });
|
|
304
|
-
}
|
|
305
|
-
services.password = await this.hashPassword(password);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (webAuthnService) {
|
|
309
|
-
services.webAuthn = [webAuthnService];
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const doc: User = {
|
|
313
|
-
...userData,
|
|
314
|
-
_id: userData._id || generateDbObjectId(),
|
|
315
|
-
roles: roles || [],
|
|
316
|
-
initialPassword: Boolean(initialPassword),
|
|
317
|
-
services,
|
|
318
|
-
guest: Boolean(userData.guest),
|
|
319
|
-
pushSubscriptions: userData.pushSubscriptions || [],
|
|
320
|
-
emails: email ? [{ address: email, verified: false }] : [],
|
|
321
|
-
created: new Date(),
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
if (username) {
|
|
325
|
-
if (!(await userSettings.validateUsername(username))) {
|
|
326
|
-
throw new Error(`Username ${username} is invalid`, { cause: 'USERNAME_INVALID' });
|
|
327
|
-
}
|
|
328
|
-
doc.username = username;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const { insertedId: userId } = await Users.insertOne(doc);
|
|
332
|
-
|
|
333
|
-
try {
|
|
334
|
-
const autoMessagingEnabled = skipMessaging
|
|
335
|
-
? false
|
|
336
|
-
: userSettings.autoMessagingAfterUserCreation && !!email && !!userId;
|
|
337
|
-
|
|
338
|
-
if (autoMessagingEnabled) {
|
|
339
|
-
if (password === undefined) {
|
|
340
|
-
if (!skipPasswordEnrollment && !webAuthnService) {
|
|
341
|
-
await this.sendResetPasswordEmail(userId, email, true);
|
|
342
|
-
}
|
|
343
|
-
} else {
|
|
344
|
-
await this.sendVerificationEmail(userId, email);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
} catch {
|
|
348
|
-
/* */
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const user = (await Users.findOne({ _id: userId }, {})) as User;
|
|
352
|
-
await emit('USER_CREATE', {
|
|
353
|
-
user: removeConfidentialServiceHashes(user),
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
return userId;
|
|
357
|
-
},
|
|
358
|
-
|
|
359
|
-
async hashPassword(password: string): Promise<{
|
|
360
|
-
pbkdf2: string;
|
|
361
|
-
}> {
|
|
362
|
-
const salt = pbkdf2.generateSalt();
|
|
363
|
-
const hashedPassword = await pbkdf2.getDerivedKey(salt, password);
|
|
364
|
-
return { pbkdf2: `${salt}:${hashedPassword}` };
|
|
365
|
-
},
|
|
366
|
-
|
|
367
|
-
async verifyPassword(
|
|
368
|
-
{ bcrypt: bcryptHash, pbkdf2: pbkdf2SaltAndHash }: { bcrypt?: string; pbkdf2?: string },
|
|
369
|
-
plainPassword: string,
|
|
370
|
-
): Promise<boolean> {
|
|
371
|
-
if (bcryptHash) {
|
|
372
|
-
const password = await sha256(plainPassword);
|
|
373
|
-
return bcrypt.compare(password, bcryptHash);
|
|
374
|
-
}
|
|
375
|
-
if (pbkdf2SaltAndHash) {
|
|
376
|
-
const [pbkdf2Salt, pbkdf2Hash] = pbkdf2SaltAndHash.split(':');
|
|
377
|
-
return pbkdf2.compare(plainPassword, pbkdf2Hash, pbkdf2Salt);
|
|
378
|
-
}
|
|
379
|
-
return false;
|
|
380
|
-
},
|
|
381
|
-
|
|
382
|
-
async addEmail(userId: string, address: string): Promise<void> {
|
|
383
|
-
if (!(await userSettings.validateEmail(address))) {
|
|
384
|
-
throw new Error(`E-Mail address ${address} is invalid`, { cause: 'EMAIL_INVALID' });
|
|
385
|
-
}
|
|
386
|
-
await this.updateUser(
|
|
387
|
-
{ _id: userId, 'emails.address': { $not: insensitiveTrimmedRegexOperator(address) } },
|
|
388
|
-
{
|
|
389
|
-
$push: {
|
|
390
|
-
emails: {
|
|
391
|
-
address: address.trim(),
|
|
392
|
-
verified: false,
|
|
393
|
-
},
|
|
394
|
-
},
|
|
395
|
-
},
|
|
396
|
-
);
|
|
397
|
-
},
|
|
398
|
-
|
|
399
|
-
async removeEmail(userId: string, address: string): Promise<void> {
|
|
400
|
-
await this.updateUser(
|
|
401
|
-
{ _id: userId, 'emails.address': insensitiveTrimmedRegexOperator(address) },
|
|
402
|
-
{
|
|
403
|
-
$pull: {
|
|
404
|
-
emails: { address: insensitiveTrimmedRegexOperator(address) },
|
|
405
|
-
},
|
|
406
|
-
},
|
|
407
|
-
);
|
|
408
|
-
},
|
|
409
|
-
|
|
410
|
-
async sendResetPasswordEmail(userId: string, email: string, isEnrollment?: boolean): Promise<void> {
|
|
411
|
-
const plainToken = crypto.randomUUID();
|
|
412
|
-
const resetToken = {
|
|
413
|
-
token: await sha256(plainToken),
|
|
414
|
-
address: email,
|
|
415
|
-
when: new Date(),
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
await Users.updateOne(
|
|
419
|
-
{ _id: userId },
|
|
420
|
-
{
|
|
421
|
-
$push: {
|
|
422
|
-
'services.password.reset': resetToken,
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
);
|
|
426
|
-
|
|
427
|
-
await emit('USER_ACCOUNT_ACTION', {
|
|
428
|
-
action: isEnrollment ? UserAccountAction.ENROLL_ACCOUNT : UserAccountAction.RESET_PASSWORD,
|
|
429
|
-
userId,
|
|
430
|
-
...resetToken,
|
|
431
|
-
token: plainToken,
|
|
432
|
-
});
|
|
433
|
-
},
|
|
434
|
-
|
|
435
|
-
async sendVerificationEmail(userId: string, email: string): Promise<void> {
|
|
436
|
-
const plainToken = crypto.randomUUID();
|
|
437
|
-
const verificationToken = {
|
|
438
|
-
token: await sha256(plainToken),
|
|
439
|
-
address: email,
|
|
440
|
-
when: new Date(),
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
await Users.updateOne(
|
|
444
|
-
{ _id: userId },
|
|
445
|
-
{
|
|
446
|
-
$push: {
|
|
447
|
-
'services.email.verificationTokens': verificationToken,
|
|
448
|
-
},
|
|
449
|
-
},
|
|
450
|
-
);
|
|
451
|
-
|
|
452
|
-
await emit('USER_ACCOUNT_ACTION', {
|
|
453
|
-
action: UserAccountAction.VERIFY_EMAIL,
|
|
454
|
-
userId,
|
|
455
|
-
...verificationToken,
|
|
456
|
-
token: plainToken,
|
|
457
|
-
});
|
|
458
|
-
},
|
|
459
|
-
|
|
460
|
-
addRoles: async (userId: string, roles: string[]) => {
|
|
461
|
-
const selector = generateDbFilterById(userId);
|
|
462
|
-
const user = await Users.findOneAndUpdate(
|
|
463
|
-
selector,
|
|
464
|
-
{
|
|
465
|
-
$addToSet: { roles: { $each: roles } },
|
|
466
|
-
},
|
|
467
|
-
{ returnDocument: 'after' },
|
|
468
|
-
);
|
|
469
|
-
|
|
470
|
-
if (!user) return null;
|
|
471
|
-
|
|
472
|
-
await emit('USER_ADD_ROLES', {
|
|
473
|
-
user: removeConfidentialServiceHashes(user),
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
return user;
|
|
477
|
-
},
|
|
478
|
-
|
|
479
|
-
async setUsername(userId: string, username: string) {
|
|
480
|
-
if (!(await userSettings.validateUsername(username))) {
|
|
481
|
-
throw new Error(`Username ${username} is invalid`, { cause: 'USERNAME_INVALID' });
|
|
482
|
-
}
|
|
483
|
-
const user = await Users.findOneAndUpdate(
|
|
484
|
-
{ _id: userId },
|
|
485
|
-
{
|
|
486
|
-
$set: {
|
|
487
|
-
username: username.trim(),
|
|
488
|
-
},
|
|
489
|
-
},
|
|
490
|
-
{ returnDocument: 'after' },
|
|
491
|
-
);
|
|
492
|
-
if (!user) return null;
|
|
493
|
-
await emit('USER_UPDATE_USERNAME', {
|
|
494
|
-
user: removeConfidentialServiceHashes(user),
|
|
495
|
-
});
|
|
496
|
-
return user;
|
|
497
|
-
},
|
|
498
|
-
|
|
499
|
-
async setPassword(userId: string, plainPassword: string) {
|
|
500
|
-
if (!(await userSettings.validatePassword(plainPassword))) {
|
|
501
|
-
throw new Error(`Password ***** is invalid`, { cause: 'PASSWORD_INVALID' });
|
|
502
|
-
}
|
|
503
|
-
const password = plainPassword || crypto.randomUUID().split('-').pop();
|
|
504
|
-
const user = await Users.findOneAndUpdate(
|
|
505
|
-
{ _id: userId },
|
|
506
|
-
{
|
|
507
|
-
$set: {
|
|
508
|
-
initialPassword: false,
|
|
509
|
-
'services.password': await this.hashPassword(
|
|
510
|
-
password || crypto.randomUUID().split('-').pop(),
|
|
511
|
-
),
|
|
512
|
-
},
|
|
513
|
-
},
|
|
514
|
-
{ returnDocument: 'after' },
|
|
515
|
-
);
|
|
516
|
-
if (!user) return null;
|
|
517
|
-
await emit('USER_UPDATE_PASSWORD', {
|
|
518
|
-
user: removeConfidentialServiceHashes(user),
|
|
519
|
-
});
|
|
520
|
-
return user;
|
|
521
|
-
},
|
|
522
|
-
|
|
523
|
-
async resetPassword(token: string, newPassword: string) {
|
|
524
|
-
const resetToken = await this.findResetToken(token);
|
|
525
|
-
if (!resetToken) return null;
|
|
526
|
-
const updatedUser = await this.setPassword(resetToken.userId, newPassword);
|
|
527
|
-
if (updatedUser) {
|
|
528
|
-
// Now invalidate the reset token
|
|
529
|
-
await Users.updateOne(
|
|
530
|
-
{
|
|
531
|
-
_id: resetToken.userId,
|
|
532
|
-
},
|
|
533
|
-
{
|
|
534
|
-
$pull: {
|
|
535
|
-
'services.password.reset': {
|
|
536
|
-
token: resetToken.token,
|
|
537
|
-
},
|
|
538
|
-
},
|
|
539
|
-
},
|
|
540
|
-
);
|
|
541
|
-
await emit('USER_ACCOUNT_ACTION', {
|
|
542
|
-
action: UserAccountAction.PASSWORD_RESETTED,
|
|
543
|
-
userId: updatedUser._id,
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
await this.verifyEmail(updatedUser._id, resetToken.address);
|
|
547
|
-
}
|
|
548
|
-
return updatedUser;
|
|
549
|
-
},
|
|
550
|
-
|
|
551
|
-
updateAvatar: async (_id: string, fileId: string) => {
|
|
552
|
-
const userFilter = generateDbFilterById(_id);
|
|
553
|
-
const modifier = {
|
|
554
|
-
$set: {
|
|
555
|
-
avatarId: fileId,
|
|
556
|
-
updated: new Date(),
|
|
557
|
-
},
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
const user = await Users.findOneAndUpdate(userFilter, modifier, {
|
|
561
|
-
returnDocument: 'after',
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
if (!user) return null;
|
|
565
|
-
await emit('USER_UPDATE_AVATAR', {
|
|
566
|
-
user: removeConfidentialServiceHashes(user),
|
|
567
|
-
});
|
|
568
|
-
return user;
|
|
569
|
-
},
|
|
570
|
-
|
|
571
|
-
updateGuest: async (user: User, guest: boolean) => {
|
|
572
|
-
const updatedUser = await Users.findOneAndUpdate(
|
|
573
|
-
generateDbFilterById(user._id),
|
|
574
|
-
{
|
|
575
|
-
$set: { guest },
|
|
576
|
-
},
|
|
577
|
-
{ returnDocument: 'after' },
|
|
578
|
-
);
|
|
579
|
-
if (!updatedUser) return null;
|
|
580
|
-
await emit('USER_UPDATE_GUEST', {
|
|
581
|
-
user: removeConfidentialServiceHashes({
|
|
582
|
-
...updatedUser,
|
|
583
|
-
guest,
|
|
584
|
-
}),
|
|
585
|
-
});
|
|
586
|
-
return updatedUser;
|
|
587
|
-
},
|
|
588
|
-
|
|
589
|
-
updateHeartbeat: async (userId: string, lastLogin: UserLastLogin) => {
|
|
590
|
-
const user = await Users.findOneAndUpdate(
|
|
591
|
-
generateDbFilterById(userId),
|
|
592
|
-
{
|
|
593
|
-
$set: {
|
|
594
|
-
lastLogin: {
|
|
595
|
-
timestamp: new Date(),
|
|
596
|
-
...lastLogin,
|
|
597
|
-
},
|
|
598
|
-
},
|
|
599
|
-
},
|
|
600
|
-
{
|
|
601
|
-
returnDocument: 'after',
|
|
602
|
-
},
|
|
603
|
-
);
|
|
604
|
-
if (!user) return null;
|
|
605
|
-
await emit('USER_UPDATE_HEARTBEAT', {
|
|
606
|
-
user: removeConfidentialServiceHashes(user),
|
|
607
|
-
});
|
|
608
|
-
return user;
|
|
609
|
-
},
|
|
610
|
-
|
|
611
|
-
markDeleted: async (userId: string) => {
|
|
612
|
-
await db.collection('sessions').deleteMany({
|
|
613
|
-
session: insensitiveTrimmedRegexOperator(`"user":"${userId}"`),
|
|
614
|
-
});
|
|
615
|
-
const user = await Users.findOneAndUpdate(
|
|
616
|
-
{ _id: userId },
|
|
617
|
-
{
|
|
618
|
-
$set: {
|
|
619
|
-
username: `deleted-${Date.now()}`,
|
|
620
|
-
deleted: new Date(),
|
|
621
|
-
emails: [],
|
|
622
|
-
roles: [],
|
|
623
|
-
services: {},
|
|
624
|
-
pushSubscriptions: [],
|
|
625
|
-
initialPassword: false,
|
|
626
|
-
},
|
|
627
|
-
$unset: {
|
|
628
|
-
profile: 1,
|
|
629
|
-
lastBillingAddress: 1,
|
|
630
|
-
lastContact: 1,
|
|
631
|
-
lastLogin: 1,
|
|
632
|
-
avatarId: 1,
|
|
633
|
-
},
|
|
634
|
-
},
|
|
635
|
-
{ returnDocument: 'after' },
|
|
636
|
-
);
|
|
637
|
-
if (!user) return null;
|
|
638
|
-
|
|
639
|
-
await emit('USER_REMOVE', {
|
|
640
|
-
user,
|
|
641
|
-
});
|
|
642
|
-
return user;
|
|
643
|
-
},
|
|
644
|
-
|
|
645
|
-
deletePermanently: async ({ userId }: { userId: string }) => {
|
|
646
|
-
return Users.findOneAndDelete({ _id: userId });
|
|
647
|
-
},
|
|
648
|
-
|
|
649
|
-
updateProfile: async (userId: string, updatedData: { profile?: UserProfile; meta?: any }) => {
|
|
650
|
-
const userFilter = generateDbFilterById(userId);
|
|
651
|
-
const { meta, profile } = updatedData;
|
|
652
|
-
|
|
653
|
-
if (!meta && !profile) {
|
|
654
|
-
return Users.findOne(userFilter, {});
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const modifier = { $set: {} };
|
|
658
|
-
|
|
659
|
-
if (profile) {
|
|
660
|
-
modifier.$set = Object.keys(profile).reduce((acc, profileKey) => {
|
|
661
|
-
return {
|
|
662
|
-
...acc,
|
|
663
|
-
[`profile.${profileKey}`]: profile[profileKey],
|
|
664
|
-
};
|
|
665
|
-
}, {});
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
if (meta) {
|
|
669
|
-
// eslint-disable-next-line
|
|
670
|
-
// @ts-ignore
|
|
671
|
-
modifier.$set.meta = meta;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const user = await Users.findOneAndUpdate(userFilter, modifier, {
|
|
675
|
-
returnDocument: 'after',
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
if (!user) return null;
|
|
679
|
-
await emit('USER_UPDATE_PROFILE', {
|
|
680
|
-
user: removeConfidentialServiceHashes(user),
|
|
681
|
-
});
|
|
682
|
-
return user;
|
|
683
|
-
},
|
|
684
|
-
|
|
685
|
-
updateLastBillingAddress: async (_id: string, lastBillingAddress: Address) => {
|
|
686
|
-
const userFilter = generateDbFilterById(_id);
|
|
687
|
-
const user = await Users.findOne(userFilter, {});
|
|
688
|
-
if (!user) return null;
|
|
689
|
-
if (!lastBillingAddress) return user;
|
|
690
|
-
|
|
691
|
-
const modifier = {
|
|
692
|
-
$set: {
|
|
693
|
-
lastBillingAddress,
|
|
694
|
-
updated: new Date(),
|
|
695
|
-
},
|
|
696
|
-
};
|
|
697
|
-
const profile = user.profile || {};
|
|
698
|
-
const isGuest = !!user.guest;
|
|
699
|
-
|
|
700
|
-
if (!profile.displayName || isGuest) {
|
|
701
|
-
modifier.$set['profile.displayName'] = [
|
|
702
|
-
lastBillingAddress.firstName,
|
|
703
|
-
lastBillingAddress.lastName,
|
|
704
|
-
]
|
|
705
|
-
.filter(Boolean)
|
|
706
|
-
.join(' ');
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const updatedUser = await Users.findOneAndUpdate(generateDbFilterById(_id), modifier, {
|
|
710
|
-
returnDocument: 'after',
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
if (!updatedUser) return null;
|
|
714
|
-
await emit('USER_UPDATE_BILLING_ADDRESS', {
|
|
715
|
-
user: removeConfidentialServiceHashes(updatedUser),
|
|
716
|
-
});
|
|
717
|
-
return updatedUser;
|
|
718
|
-
},
|
|
719
|
-
|
|
720
|
-
updateLastContact: async (_id: string, lastContact: Contact) => {
|
|
721
|
-
const userFilter = generateDbFilterById(_id);
|
|
722
|
-
const user = await Users.findOne(userFilter, {});
|
|
723
|
-
if (!user) return null;
|
|
724
|
-
const profile = user.profile || {};
|
|
725
|
-
const isGuest = !!user.guest;
|
|
726
|
-
|
|
727
|
-
const modifier = {
|
|
728
|
-
$set: {
|
|
729
|
-
updated: new Date(),
|
|
730
|
-
lastContact,
|
|
731
|
-
},
|
|
732
|
-
};
|
|
733
|
-
|
|
734
|
-
if ((!profile.phoneMobile || isGuest) && lastContact.telNumber) {
|
|
735
|
-
// Backport the contact phone number to the user profile
|
|
736
|
-
modifier.$set['profile.phoneMobile'] = lastContact.telNumber;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
const updatedUser = await Users.findOneAndUpdate(userFilter, modifier, {
|
|
740
|
-
returnDocument: 'after',
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
if (!updatedUser) return null;
|
|
744
|
-
await emit('USER_UPDATE_LAST_CONTACT', {
|
|
745
|
-
user: removeConfidentialServiceHashes(user),
|
|
746
|
-
});
|
|
747
|
-
return updatedUser;
|
|
748
|
-
},
|
|
749
|
-
|
|
750
|
-
updateRoles: async (_id: string, roles: string[]) => {
|
|
751
|
-
const modifier = {
|
|
752
|
-
$set: {
|
|
753
|
-
updated: new Date(),
|
|
754
|
-
roles,
|
|
755
|
-
},
|
|
756
|
-
};
|
|
757
|
-
|
|
758
|
-
const user = await Users.findOneAndUpdate(generateDbFilterById(_id), modifier, {
|
|
759
|
-
returnDocument: 'after',
|
|
760
|
-
});
|
|
761
|
-
if (!user) return null;
|
|
762
|
-
await emit('USER_UPDATE_ROLE', {
|
|
763
|
-
user: removeConfidentialServiceHashes(user),
|
|
764
|
-
});
|
|
765
|
-
return user;
|
|
766
|
-
},
|
|
767
|
-
|
|
768
|
-
updateTags: async (_id: string, tags: string[]) => {
|
|
769
|
-
const user = await Users.findOneAndUpdate(
|
|
770
|
-
generateDbFilterById(_id),
|
|
771
|
-
{
|
|
772
|
-
$set: {
|
|
773
|
-
updated: new Date(),
|
|
774
|
-
tags,
|
|
775
|
-
},
|
|
776
|
-
},
|
|
777
|
-
{
|
|
778
|
-
returnDocument: 'after',
|
|
779
|
-
},
|
|
780
|
-
);
|
|
781
|
-
if (!user) return null;
|
|
782
|
-
await emit('USER_UPDATE_TAGS', {
|
|
783
|
-
user: removeConfidentialServiceHashes(user),
|
|
784
|
-
});
|
|
785
|
-
return user;
|
|
786
|
-
},
|
|
787
|
-
|
|
788
|
-
updateUser: async (
|
|
789
|
-
selector: mongodb.Filter<User>,
|
|
790
|
-
modifier: mongodb.UpdateFilter<User>,
|
|
791
|
-
updateOptions?: mongodb.FindOneAndUpdateOptions,
|
|
792
|
-
) => {
|
|
793
|
-
const user = await Users.findOneAndUpdate(selector, modifier, {
|
|
794
|
-
...updateOptions,
|
|
795
|
-
returnDocument: 'after',
|
|
796
|
-
});
|
|
797
|
-
if (!user) return null;
|
|
798
|
-
await emit('USER_UPDATE', {
|
|
799
|
-
user: removeConfidentialServiceHashes(user),
|
|
800
|
-
});
|
|
801
|
-
return user;
|
|
802
|
-
},
|
|
803
|
-
|
|
804
|
-
addPushSubscription: async (
|
|
805
|
-
userId: string,
|
|
806
|
-
subscription: any,
|
|
807
|
-
subscriptionOptions?: {
|
|
808
|
-
userAgent: string;
|
|
809
|
-
unsubscribeFromOtherUsers: boolean;
|
|
810
|
-
},
|
|
811
|
-
): Promise<void> => {
|
|
812
|
-
const updateResult = await Users.updateOne(
|
|
813
|
-
{ _id: userId, 'pushSubscriptions.keys.p256dh': { $ne: subscription?.keys?.p256dh } },
|
|
814
|
-
{
|
|
815
|
-
$push: {
|
|
816
|
-
pushSubscriptions: {
|
|
817
|
-
userAgent: subscriptionOptions?.userAgent,
|
|
818
|
-
...subscription,
|
|
819
|
-
},
|
|
820
|
-
},
|
|
821
|
-
},
|
|
822
|
-
{},
|
|
823
|
-
);
|
|
824
|
-
if (updateResult.modifiedCount === 1 && subscriptionOptions?.unsubscribeFromOtherUsers) {
|
|
825
|
-
await Users.updateMany(
|
|
826
|
-
{ _id: { $ne: userId }, 'pushSubscriptions.keys.p256dh': subscription?.keys?.p256dh },
|
|
827
|
-
{
|
|
828
|
-
$pull: {
|
|
829
|
-
pushSubscriptions: { 'keys.p256dh': subscription?.keys?.p256dh },
|
|
830
|
-
},
|
|
831
|
-
} as mongodb.UpdateFilter<User>,
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
},
|
|
835
|
-
|
|
836
|
-
removePushSubscription: async (userId: string, p256dh: string): Promise<void> => {
|
|
837
|
-
await Users.updateOne(
|
|
838
|
-
{ _id: userId },
|
|
839
|
-
{
|
|
840
|
-
$pull: {
|
|
841
|
-
pushSubscriptions: { 'keys.p256dh': p256dh },
|
|
842
|
-
},
|
|
843
|
-
} as mongodb.UpdateFilter<User>,
|
|
844
|
-
{},
|
|
845
|
-
);
|
|
846
|
-
},
|
|
847
|
-
existingTags: async (): Promise<string[]> => {
|
|
848
|
-
const tags = (await Users.distinct('tags', {
|
|
849
|
-
tags: { $exists: true },
|
|
850
|
-
deleted: { $exists: false },
|
|
851
|
-
})) as string[];
|
|
852
|
-
return tags.filter(Boolean).toSorted();
|
|
853
|
-
},
|
|
854
|
-
};
|
|
855
|
-
};
|
|
856
|
-
|
|
857
|
-
export type UsersModule = Awaited<ReturnType<typeof configureUsersModule>>;
|