@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.
- package/lib/db/UsersCollection.d.ts +1 -1
- 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/addMigrations.d.ts +1 -1
- package/lib/module/addMigrations.d.ts.map +1 -1
- package/lib/module/configureUsersModule.d.ts +128 -3
- package/lib/module/configureUsersModule.d.ts.map +1 -1
- package/lib/module/configureUsersModule.js +279 -51
- package/lib/module/configureUsersModule.js.map +1 -1
- package/lib/module/configureUsersWebAuthnModule.d.ts +12 -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 +4 -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/updateUserAvatarAfterUploadService.d.ts +5 -1
- package/lib/services/updateUserAvatarAfterUploadService.d.ts.map +1 -1
- package/lib/services/updateUserAvatarAfterUploadService.js.map +1 -1
- package/lib/services/userServices.d.ts +6 -1
- package/lib/services/userServices.d.ts.map +1 -1
- package/lib/services/userServices.js +3 -5
- package/lib/services/userServices.js.map +1 -1
- package/lib/types.d.ts +88 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/users-index.d.ts +4 -2
- package/lib/users-index.d.ts.map +1 -1
- package/lib/users-index.js +4 -2
- package/lib/users-index.js.map +1 -1
- package/lib/users-settings.d.ts +21 -0
- package/lib/users-settings.d.ts.map +1 -0
- package/lib/users-settings.js +48 -0
- package/lib/users-settings.js.map +1 -0
- package/package.json +10 -11
- package/src/db/UsersCollection.ts +1 -10
- package/src/db/WebAuthnCredentialsCreationRequestsCollection.ts +20 -0
- package/src/module/addMigrations.ts +1 -1
- package/src/module/configureUsersModule.ts +480 -68
- package/src/module/configureUsersWebAuthnModule.ts +221 -0
- package/src/module/pbkdf2.ts +39 -0
- package/src/module/removeConfidentialServiceHashes.test.ts +1 -1
- package/src/module/sha256.ts +5 -0
- package/src/services/migrateUserDataService.ts +37 -0
- package/src/services/updateUserAvatarAfterUploadService.ts +7 -1
- package/src/services/userServices.ts +11 -6
- package/src/types.ts +101 -0
- package/src/users-index.ts +4 -3
- package/src/users-settings.ts +78 -0
- package/tests/mock/user-mock.ts +0 -8
- package/lib/services/getUserCountryService.d.ts +0 -3
- package/lib/services/getUserCountryService.d.ts.map +0 -1
- package/lib/services/getUserCountryService.js +0 -7
- package/lib/services/getUserCountryService.js.map +0 -1
- package/lib/services/getUserLanguageService.d.ts +0 -3
- package/lib/services/getUserLanguageService.d.ts.map +0 -1
- package/lib/services/getUserLanguageService.js +0 -7
- package/lib/services/getUserLanguageService.js.map +0 -1
- package/src/services/getUserCountryService.ts +0 -8
- package/src/services/getUserLanguageService.ts +0 -8
|
@@ -1,25 +1,110 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { ModuleInput, UnchainedCore } from '@unchainedshop/
|
|
3
|
-
import { User, UserQuery,
|
|
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 {
|
|
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/
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
75
|
-
}
|
|
162
|
+
async findUserByUsername(username: string): Promise<User> {
|
|
163
|
+
if (!username) return null;
|
|
164
|
+
return Users.findOne({ username }, {});
|
|
165
|
+
},
|
|
76
166
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
291
|
+
return userCount === 1;
|
|
127
292
|
},
|
|
128
293
|
|
|
129
294
|
// Transformations
|
|
130
|
-
primaryEmail:
|
|
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:
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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}@${
|
|
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}-${
|
|
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
|
-
|
|
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 (
|
|
363
|
-
|
|
364
|
-
|
|
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 (
|
|
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
|
-
|
|
808
|
+
|
|
809
|
+
removePushSubscription: async (userId: string, p256dh: string): Promise<void> => {
|
|
398
810
|
await Users.updateOne(
|
|
399
811
|
{ _id: userId },
|
|
400
812
|
{
|