@unchainedshop/core-users 3.0.0-alpha7 → 3.0.0-rc.11
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 +54 -2
- package/lib/db/UsersCollection.d.ts.map +1 -1
- package/lib/db/UsersCollection.js.map +1 -1
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts +6 -1
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts.map +1 -1
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js.map +1 -1
- package/lib/module/addMigrations.d.ts.map +1 -1
- package/lib/module/addMigrations.js +32 -41
- package/lib/module/addMigrations.js.map +1 -1
- package/lib/module/configureUsersModule.d.ts +49 -70
- package/lib/module/configureUsersModule.d.ts.map +1 -1
- package/lib/module/configureUsersModule.js +105 -71
- package/lib/module/configureUsersModule.js.map +1 -1
- package/lib/module/configureUsersWebAuthnModule.d.ts +21 -8
- package/lib/module/configureUsersWebAuthnModule.d.ts.map +1 -1
- package/lib/module/configureUsersWebAuthnModule.js +32 -11
- package/lib/module/configureUsersWebAuthnModule.js.map +1 -1
- package/lib/module/pbkdf2.js +1 -1
- package/lib/module/pbkdf2.js.map +1 -1
- package/lib/users-index.d.ts +3 -2
- package/lib/users-index.d.ts.map +1 -1
- package/lib/users-index.js +3 -2
- package/lib/users-index.js.map +1 -1
- package/lib/users-settings.d.ts +1 -1
- package/lib/users-settings.d.ts.map +1 -1
- package/lib/users-settings.js.map +1 -1
- package/package.json +26 -18
- package/src/db/UsersCollection.ts +59 -2
- package/src/db/WebAuthnCredentialsCreationRequestsCollection.ts +7 -1
- package/src/module/addMigrations.ts +40 -53
- package/src/module/configureUsersModule.ts +152 -169
- package/src/module/configureUsersWebAuthnModule.ts +47 -44
- package/src/module/pbkdf2.ts +1 -1
- package/src/module/removeConfidentialServiceHashes.test.ts +1 -1
- package/src/users-index.ts +3 -2
- package/src/users-settings.ts +1 -2
- package/tests/mock/user-mock.ts +2 -3
- package/tsconfig.json +5 -7
- package/lib/module/sha256.d.ts +0 -2
- package/lib/module/sha256.d.ts.map +0 -1
- package/lib/module/sha256.js +0 -6
- package/lib/module/sha256.js.map +0 -1
- package/lib/services/migrateUserDataService.d.ts +0 -4
- package/lib/services/migrateUserDataService.d.ts.map +0 -1
- package/lib/services/migrateUserDataService.js +0 -18
- package/lib/services/migrateUserDataService.js.map +0 -1
- package/lib/services/updateUserAvatarAfterUploadService.d.ts +0 -15
- package/lib/services/updateUserAvatarAfterUploadService.d.ts.map +0 -1
- package/lib/services/updateUserAvatarAfterUploadService.js +0 -24
- package/lib/services/updateUserAvatarAfterUploadService.js.map +0 -1
- package/lib/services/userServices.d.ts +0 -8
- package/lib/services/userServices.d.ts.map +0 -1
- package/lib/services/userServices.js +0 -7
- package/lib/services/userServices.js.map +0 -1
- package/lib/types.d.ts +0 -88
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
- package/src/module/sha256.ts +0 -5
- package/src/services/migrateUserDataService.ts +0 -37
- package/src/services/updateUserAvatarAfterUploadService.ts +0 -42
- package/src/services/userServices.ts +0 -15
- package/src/types.ts +0 -101
|
@@ -1,99 +1,39 @@
|
|
|
1
|
-
import bcrypt from '
|
|
2
|
-
import { ModuleInput } from '@unchainedshop/mongodb';
|
|
3
|
-
import { User, UserQuery, Email, UserLastLogin, UserProfile, UserData } from '../types.js';
|
|
4
|
-
import { emit, registerEvents } from '@unchainedshop/events';
|
|
1
|
+
import * as bcrypt from '@node-rs/bcrypt';
|
|
5
2
|
import {
|
|
3
|
+
ModuleInput,
|
|
4
|
+
Address,
|
|
5
|
+
Contact,
|
|
6
6
|
generateDbFilterById,
|
|
7
7
|
buildSortOptions,
|
|
8
8
|
mongodb,
|
|
9
9
|
generateDbObjectId,
|
|
10
10
|
} from '@unchainedshop/mongodb';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
import {
|
|
12
|
+
User,
|
|
13
|
+
UserQuery,
|
|
14
|
+
Email,
|
|
15
|
+
UserLastLogin,
|
|
16
|
+
UserProfile,
|
|
17
|
+
UsersCollection,
|
|
18
|
+
} from '../db/UsersCollection.js';
|
|
19
|
+
import { emit, registerEvents } from '@unchainedshop/events';
|
|
20
|
+
import { systemLocale, SortDirection, SortOption, sha256 } from '@unchainedshop/utils';
|
|
15
21
|
import addMigrations from './addMigrations.js';
|
|
16
22
|
import { userSettings, UserSettingsOptions } from '../users-settings.js';
|
|
17
|
-
import { configureUsersWebAuthnModule
|
|
23
|
+
import { configureUsersWebAuthnModule } from './configureUsersWebAuthnModule.js';
|
|
18
24
|
import * as pbkdf2 from './pbkdf2.js';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
findUserByToken: (hashedToken?: string) => Promise<User>;
|
|
32
|
-
findUserByResetToken: (token: string) => Promise<User>;
|
|
33
|
-
findUnverifiedEmailToken: (token: string) => Promise<{
|
|
34
|
-
userId: string;
|
|
35
|
-
address: string;
|
|
36
|
-
when: Date;
|
|
37
|
-
}>;
|
|
38
|
-
findUserByEmail(email: string): Promise<User>;
|
|
39
|
-
findUserByUsername(username: string): Promise<User>;
|
|
40
|
-
findUser: (
|
|
41
|
-
selector: UserQuery & { sort?: Array<SortOption> },
|
|
42
|
-
options?: mongodb.FindOptions,
|
|
43
|
-
) => Promise<User>;
|
|
44
|
-
findUsers: (
|
|
45
|
-
query: UserQuery & {
|
|
46
|
-
sort?: Array<SortOption>;
|
|
47
|
-
limit?: number;
|
|
48
|
-
offset?: number;
|
|
49
|
-
},
|
|
50
|
-
) => Promise<Array<User>>;
|
|
51
|
-
userExists: (query: { userId: string }) => Promise<boolean>;
|
|
52
|
-
// Transformations
|
|
53
|
-
primaryEmail: (user: User) => Email;
|
|
54
|
-
userLocale: (user: User) => Intl.Locale;
|
|
55
|
-
|
|
56
|
-
// Mutations
|
|
57
|
-
createUser: (
|
|
58
|
-
userData: UserData,
|
|
59
|
-
options: { skipMessaging?: boolean; skipPasswordEnrollment?: boolean },
|
|
60
|
-
) => Promise<string>;
|
|
61
|
-
addEmail: (userId: string, email: string) => Promise<void>;
|
|
62
|
-
removeEmail: (userId: string, email: string) => Promise<void>;
|
|
63
|
-
sendVerificationEmail: (userId: string, email: string) => Promise<void>;
|
|
64
|
-
sendResetPasswordEmail: (userId: string, email: string, isEnrollment?: boolean) => Promise<void>;
|
|
65
|
-
verifyEmail: (userId: string, email: string) => Promise<void>;
|
|
66
|
-
setUsername: (userId: string, newUsername: string) => Promise<void>;
|
|
67
|
-
setPassword: (userId: string, newPassword?: string) => Promise<void>;
|
|
68
|
-
verifyPassword: (hashObject: Record<string, string>, password: string) => Promise<boolean>;
|
|
69
|
-
addRoles: (userId: string, roles: Array<string>) => Promise<number>;
|
|
70
|
-
updateAvatar: (_id: string, fileId: string) => Promise<User>;
|
|
71
|
-
updateGuest: (user: User, guest: boolean) => Promise<void>;
|
|
72
|
-
updateHeartbeat: (userId: string, doc: UserLastLogin) => Promise<User>;
|
|
73
|
-
updateLastBillingAddress: (_id: string, lastAddress: Address) => Promise<User>;
|
|
74
|
-
updateLastContact: (_id: string, lastContact: Contact) => Promise<User>;
|
|
75
|
-
updateProfile: (
|
|
76
|
-
_id: string,
|
|
77
|
-
{ profile, meta }: { profile?: UserProfile; meta?: any },
|
|
78
|
-
) => Promise<User>;
|
|
79
|
-
delete: (userId: string) => Promise<User>;
|
|
80
|
-
updateRoles: (_id: string, roles: Array<string>) => Promise<User>;
|
|
81
|
-
updateTags: (_id: string, tags: Array<string>) => Promise<User>;
|
|
82
|
-
updateUser: (
|
|
83
|
-
selector: mongodb.Filter<User>,
|
|
84
|
-
modifier: mongodb.UpdateFilter<User>,
|
|
85
|
-
options: mongodb.FindOneAndUpdateOptions,
|
|
86
|
-
) => Promise<User>;
|
|
87
|
-
addPushSubscription: (
|
|
88
|
-
userId: string,
|
|
89
|
-
subscription: any,
|
|
90
|
-
options?: {
|
|
91
|
-
userAgent: string;
|
|
92
|
-
unsubscribeFromOtherUsers: boolean;
|
|
93
|
-
},
|
|
94
|
-
) => Promise<void>;
|
|
95
|
-
removePushSubscription: (userId: string, p256dh: string) => Promise<void>;
|
|
96
|
-
};
|
|
25
|
+
|
|
26
|
+
export interface UserData {
|
|
27
|
+
email?: string;
|
|
28
|
+
guest?: boolean;
|
|
29
|
+
initialPassword?: boolean;
|
|
30
|
+
lastBillingAddress?: User['lastBillingAddress'];
|
|
31
|
+
password: string | null;
|
|
32
|
+
webAuthnPublicKeyCredentials?: any;
|
|
33
|
+
profile?: UserProfile;
|
|
34
|
+
roles?: Array<string>;
|
|
35
|
+
username?: string;
|
|
36
|
+
}
|
|
97
37
|
|
|
98
38
|
const USER_EVENTS = [
|
|
99
39
|
'USER_ACCOUNT_ACTION',
|
|
@@ -118,36 +58,44 @@ export const removeConfidentialServiceHashes = (rawUser: User): User => {
|
|
|
118
58
|
return user;
|
|
119
59
|
};
|
|
120
60
|
|
|
121
|
-
export const buildFindSelector = ({
|
|
61
|
+
export const buildFindSelector = ({
|
|
62
|
+
includeGuests,
|
|
63
|
+
queryString,
|
|
64
|
+
emailVerified,
|
|
65
|
+
lastLogin,
|
|
66
|
+
...rest
|
|
67
|
+
}: UserQuery) => {
|
|
122
68
|
const selector: mongodb.Filter<User> = { ...rest, deleted: null };
|
|
123
69
|
if (!includeGuests) selector.guest = { $in: [false, null] };
|
|
70
|
+
if (emailVerified === true) {
|
|
71
|
+
selector['emails.verified'] = true;
|
|
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
|
+
}
|
|
124
87
|
if (queryString) {
|
|
125
88
|
(selector as any).$text = { $search: queryString };
|
|
126
89
|
}
|
|
127
90
|
return selector;
|
|
128
91
|
};
|
|
129
92
|
|
|
130
|
-
FileDirector.registerFileUploadCallback<{
|
|
131
|
-
services: {
|
|
132
|
-
users: UserServices;
|
|
133
|
-
files: FileServices;
|
|
134
|
-
};
|
|
135
|
-
modules: {
|
|
136
|
-
users: UsersModule;
|
|
137
|
-
files: FilesModule;
|
|
138
|
-
};
|
|
139
|
-
}>('user-avatars', async (file, context) => {
|
|
140
|
-
const { services } = context;
|
|
141
|
-
return services.users.updateUserAvatarAfterUpload({ file }, context);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
93
|
export const configureUsersModule = async ({
|
|
145
94
|
db,
|
|
146
95
|
options,
|
|
147
96
|
migrationRepository,
|
|
148
97
|
}: ModuleInput<UserSettingsOptions>) => {
|
|
149
98
|
userSettings.configureSettings(options || {}, db);
|
|
150
|
-
|
|
151
99
|
registerEvents(USER_EVENTS);
|
|
152
100
|
const Users = await UsersCollection(db);
|
|
153
101
|
|
|
@@ -185,13 +133,13 @@ export const configureUsersModule = async ({
|
|
|
185
133
|
when: Date;
|
|
186
134
|
}> {
|
|
187
135
|
if (!plainToken) return null;
|
|
188
|
-
const token = await sha256
|
|
136
|
+
const token = await sha256(plainToken);
|
|
189
137
|
const user = await Users.findOne(
|
|
190
138
|
{
|
|
191
139
|
'services.email.verificationTokens': {
|
|
192
140
|
$elemMatch: {
|
|
193
141
|
token,
|
|
194
|
-
when: { $gt: new Date().getTime() },
|
|
142
|
+
when: { $gt: new Date(new Date().getTime() - 1000 * 60 * 60) },
|
|
195
143
|
},
|
|
196
144
|
},
|
|
197
145
|
},
|
|
@@ -206,48 +154,59 @@ export const configureUsersModule = async ({
|
|
|
206
154
|
},
|
|
207
155
|
|
|
208
156
|
async verifyEmail(userId: string, address: string): Promise<void> {
|
|
209
|
-
await Users.updateOne(
|
|
210
|
-
{
|
|
157
|
+
const updated = await Users.updateOne(
|
|
158
|
+
{
|
|
159
|
+
_id: userId,
|
|
160
|
+
emails: { $elemMatch: { address: { $regex: address, $options: 'i' }, verified: false } },
|
|
161
|
+
},
|
|
211
162
|
{
|
|
212
163
|
$set: {
|
|
213
|
-
'emails
|
|
164
|
+
'emails.$.verified': true,
|
|
214
165
|
},
|
|
215
166
|
$pull: {
|
|
216
167
|
'services.email.verificationTokens': {
|
|
217
|
-
address,
|
|
168
|
+
address: { $regex: address, $options: 'i' },
|
|
218
169
|
},
|
|
219
170
|
},
|
|
220
171
|
},
|
|
221
|
-
{
|
|
222
|
-
arrayFilters: [{ 'email.address': address }],
|
|
223
|
-
},
|
|
224
172
|
);
|
|
225
173
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
174
|
+
if (updated.modifiedCount > 0) {
|
|
175
|
+
await emit('USER_ACCOUNT_ACTION', {
|
|
176
|
+
action: 'email-verified',
|
|
177
|
+
address,
|
|
178
|
+
userId,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
231
181
|
},
|
|
232
182
|
|
|
233
|
-
async
|
|
234
|
-
|
|
183
|
+
async findResetToken(plainToken: string): Promise<{
|
|
184
|
+
userId: string;
|
|
185
|
+
address: string;
|
|
186
|
+
when: Date;
|
|
187
|
+
}> {
|
|
188
|
+
const token = await sha256(plainToken);
|
|
235
189
|
const user = await Users.findOne(
|
|
236
190
|
{
|
|
237
191
|
'services.password.reset': {
|
|
238
192
|
$elemMatch: {
|
|
239
193
|
token,
|
|
240
|
-
when: { $gt: new Date().getTime() },
|
|
194
|
+
when: { $gt: new Date(new Date().getTime() - 1000 * 60 * 60) },
|
|
241
195
|
},
|
|
242
196
|
},
|
|
243
197
|
},
|
|
244
198
|
{},
|
|
245
199
|
);
|
|
246
|
-
return
|
|
200
|
+
if (!user) return null;
|
|
201
|
+
const resetToken = user.services.password.reset.find((v) => v.token === token);
|
|
202
|
+
return {
|
|
203
|
+
userId: user._id,
|
|
204
|
+
...resetToken,
|
|
205
|
+
};
|
|
247
206
|
},
|
|
248
207
|
|
|
249
|
-
async findUserByToken(plainToken
|
|
250
|
-
const token = await sha256
|
|
208
|
+
async findUserByToken(plainToken: string): Promise<User> {
|
|
209
|
+
const token = await sha256(plainToken);
|
|
251
210
|
|
|
252
211
|
if (token) {
|
|
253
212
|
return Users.findOne({
|
|
@@ -276,7 +235,7 @@ export const configureUsersModule = async ({
|
|
|
276
235
|
offset?: number;
|
|
277
236
|
}): Promise<Array<User>> {
|
|
278
237
|
const defaultSort = [{ key: 'created', value: SortDirection.ASC }] as SortOption[];
|
|
279
|
-
const selector = buildFindSelector(query);
|
|
238
|
+
const selector = buildFindSelector({ ...query });
|
|
280
239
|
|
|
281
240
|
if (query.queryString) {
|
|
282
241
|
return Users.find(selector, {
|
|
@@ -295,22 +254,24 @@ export const configureUsersModule = async ({
|
|
|
295
254
|
},
|
|
296
255
|
|
|
297
256
|
async userExists({ userId }: { userId: string }): Promise<boolean> {
|
|
298
|
-
const
|
|
299
|
-
selector.deleted = null; // skip deleted users when checked for existance!
|
|
300
|
-
const userCount = await Users.countDocuments(selector, { limit: 1 });
|
|
257
|
+
const userCount = await Users.countDocuments({ _id: userId, deleted: null }, { limit: 1 });
|
|
301
258
|
return userCount === 1;
|
|
302
259
|
},
|
|
303
260
|
|
|
304
261
|
// Transformations
|
|
305
262
|
primaryEmail(user: User): Email {
|
|
306
|
-
return (user.emails || []).
|
|
263
|
+
return (user.emails || []).toSorted(
|
|
307
264
|
(left, right) => Number(right.verified) - Number(left.verified),
|
|
308
265
|
)?.[0];
|
|
309
266
|
},
|
|
310
267
|
|
|
311
268
|
userLocale(user: User): Intl.Locale {
|
|
312
269
|
if (!user?.lastLogin?.locale) return systemLocale;
|
|
313
|
-
|
|
270
|
+
try {
|
|
271
|
+
return new Intl.Locale(user.lastLogin.locale);
|
|
272
|
+
} catch {
|
|
273
|
+
return systemLocale;
|
|
274
|
+
}
|
|
314
275
|
},
|
|
315
276
|
|
|
316
277
|
// Mutations
|
|
@@ -399,7 +360,9 @@ export const configureUsersModule = async ({
|
|
|
399
360
|
return userId;
|
|
400
361
|
},
|
|
401
362
|
|
|
402
|
-
async hashPassword(password) {
|
|
363
|
+
async hashPassword(password: string): Promise<{
|
|
364
|
+
pbkdf2: string;
|
|
365
|
+
}> {
|
|
403
366
|
const salt = pbkdf2.generateSalt();
|
|
404
367
|
const hashedPassword = await pbkdf2.getDerivedKey(salt, password);
|
|
405
368
|
return { pbkdf2: `${salt}:${hashedPassword}` };
|
|
@@ -410,7 +373,7 @@ export const configureUsersModule = async ({
|
|
|
410
373
|
plainPassword: string,
|
|
411
374
|
): Promise<boolean> {
|
|
412
375
|
if (bcryptHash) {
|
|
413
|
-
const password = await sha256
|
|
376
|
+
const password = await sha256(plainPassword);
|
|
414
377
|
return bcrypt.compare(password, bcryptHash);
|
|
415
378
|
}
|
|
416
379
|
if (pbkdf2SaltAndHash) {
|
|
@@ -451,9 +414,9 @@ export const configureUsersModule = async ({
|
|
|
451
414
|
async sendResetPasswordEmail(userId: string, email: string, isEnrollment?: boolean): Promise<void> {
|
|
452
415
|
const plainToken = crypto.randomUUID();
|
|
453
416
|
const resetToken = {
|
|
454
|
-
token: await sha256
|
|
417
|
+
token: await sha256(plainToken),
|
|
455
418
|
address: email,
|
|
456
|
-
when: new Date()
|
|
419
|
+
when: new Date(),
|
|
457
420
|
};
|
|
458
421
|
|
|
459
422
|
await Users.updateOne(
|
|
@@ -476,9 +439,9 @@ export const configureUsersModule = async ({
|
|
|
476
439
|
async sendVerificationEmail(userId: string, email: string): Promise<void> {
|
|
477
440
|
const plainToken = crypto.randomUUID();
|
|
478
441
|
const verificationToken = {
|
|
479
|
-
token: await sha256
|
|
442
|
+
token: await sha256(plainToken),
|
|
480
443
|
address: email,
|
|
481
|
-
when: new Date()
|
|
444
|
+
when: new Date(),
|
|
482
445
|
};
|
|
483
446
|
|
|
484
447
|
await Users.updateOne(
|
|
@@ -530,12 +493,12 @@ export const configureUsersModule = async ({
|
|
|
530
493
|
});
|
|
531
494
|
},
|
|
532
495
|
|
|
533
|
-
async setPassword(userId: string, plainPassword: string) {
|
|
496
|
+
async setPassword(userId: string, plainPassword: string): Promise<User> {
|
|
534
497
|
if (!(await userSettings.validatePassword(plainPassword))) {
|
|
535
498
|
throw new Error(`Password ***** is invalid`, { cause: 'PASSWORD_INVALID' });
|
|
536
499
|
}
|
|
537
500
|
const password = plainPassword || crypto.randomUUID().split('-').pop();
|
|
538
|
-
await Users.
|
|
501
|
+
const user = await Users.findOneAndUpdate(
|
|
539
502
|
{ _id: userId },
|
|
540
503
|
{
|
|
541
504
|
$set: {
|
|
@@ -545,11 +508,28 @@ export const configureUsersModule = async ({
|
|
|
545
508
|
),
|
|
546
509
|
},
|
|
547
510
|
},
|
|
511
|
+
{ returnDocument: 'after' },
|
|
548
512
|
);
|
|
549
|
-
const user = await Users.findOne({ _id: userId }, {});
|
|
550
513
|
await emit('USER_UPDATE_PASSWORD', {
|
|
551
514
|
user: removeConfidentialServiceHashes(user),
|
|
552
515
|
});
|
|
516
|
+
return user;
|
|
517
|
+
},
|
|
518
|
+
|
|
519
|
+
async resetPassword(token: string, newPassword: string): Promise<User> {
|
|
520
|
+
const resetToken = await this.findResetToken(token);
|
|
521
|
+
if (!resetToken) return null;
|
|
522
|
+
const updatedUser = await this.setPassword(resetToken.userId, newPassword);
|
|
523
|
+
if (updatedUser) {
|
|
524
|
+
// Now invalidate the reset token
|
|
525
|
+
await emit('USER_ACCOUNT_ACTION', {
|
|
526
|
+
action: 'password-resetted',
|
|
527
|
+
userId: updatedUser._id,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
await this.verifyEmail(updatedUser._id, resetToken.address);
|
|
531
|
+
}
|
|
532
|
+
return updatedUser;
|
|
553
533
|
},
|
|
554
534
|
|
|
555
535
|
updateAvatar: async (_id: string, fileId: string): Promise<User> => {
|
|
@@ -602,42 +582,38 @@ export const configureUsersModule = async ({
|
|
|
602
582
|
return user;
|
|
603
583
|
},
|
|
604
584
|
|
|
605
|
-
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
585
|
+
markDeleted: async (userId: string): Promise<User> => {
|
|
586
|
+
const user = await Users.findOneAndUpdate(
|
|
587
|
+
{ _id: userId },
|
|
588
|
+
{
|
|
589
|
+
$set: {
|
|
590
|
+
username: `deleted-${Date.now()}`,
|
|
591
|
+
deleted: new Date(),
|
|
592
|
+
emails: [],
|
|
593
|
+
roles: [],
|
|
594
|
+
profile: null,
|
|
595
|
+
lastBillingAddress: null,
|
|
596
|
+
services: {},
|
|
597
|
+
pushSubscriptions: [],
|
|
598
|
+
avatarId: null,
|
|
599
|
+
initialPassword: false,
|
|
600
|
+
lastContact: null,
|
|
601
|
+
lastLogin: null,
|
|
620
602
|
},
|
|
621
|
-
];
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
const obfuscatedUsername = existingUser.username ? `${existingUser.username}-${uuid}` : null;
|
|
625
|
-
|
|
626
|
-
Users.updateOne(userFilter, {
|
|
627
|
-
$set: {
|
|
628
|
-
emails: obfuscatedEmails,
|
|
629
|
-
username: obfuscatedUsername,
|
|
630
|
-
services: {},
|
|
631
603
|
},
|
|
632
|
-
|
|
604
|
+
{ returnDocument: 'after' },
|
|
605
|
+
);
|
|
633
606
|
|
|
634
|
-
const user = await Users.findOneAndDelete(userFilter);
|
|
635
607
|
await emit('USER_REMOVE', {
|
|
636
|
-
user
|
|
608
|
+
user,
|
|
637
609
|
});
|
|
638
610
|
return user;
|
|
639
611
|
},
|
|
640
612
|
|
|
613
|
+
deletePermanently: async ({ userId }: { userId: string }): Promise<User> => {
|
|
614
|
+
return Users.findOneAndDelete({ _id: userId });
|
|
615
|
+
},
|
|
616
|
+
|
|
641
617
|
updateProfile: async (
|
|
642
618
|
userId: string,
|
|
643
619
|
updatedData: { profile?: UserProfile; meta?: any },
|
|
@@ -680,6 +656,8 @@ export const configureUsersModule = async ({
|
|
|
680
656
|
const userFilter = generateDbFilterById(_id);
|
|
681
657
|
const user = await Users.findOne(userFilter, {});
|
|
682
658
|
|
|
659
|
+
if (!lastBillingAddress) return user;
|
|
660
|
+
|
|
683
661
|
const modifier = {
|
|
684
662
|
$set: {
|
|
685
663
|
lastBillingAddress,
|
|
@@ -702,9 +680,12 @@ export const configureUsersModule = async ({
|
|
|
702
680
|
returnDocument: 'after',
|
|
703
681
|
});
|
|
704
682
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
683
|
+
if (updatedUser) {
|
|
684
|
+
await emit('USER_UPDATE_BILLING_ADDRESS', {
|
|
685
|
+
user: removeConfidentialServiceHashes(updatedUser),
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
708
689
|
return updatedUser;
|
|
709
690
|
},
|
|
710
691
|
|
|
@@ -830,3 +811,5 @@ export const configureUsersModule = async ({
|
|
|
830
811
|
},
|
|
831
812
|
};
|
|
832
813
|
};
|
|
814
|
+
|
|
815
|
+
export type UsersModule = Awaited<ReturnType<typeof configureUsersModule>>;
|
|
@@ -1,30 +1,8 @@
|
|
|
1
|
-
/// <reference lib="dom" />
|
|
2
1
|
import { ModuleInput } from '@unchainedshop/mongodb';
|
|
3
|
-
import {
|
|
4
|
-
Fido2Lib,
|
|
5
|
-
PublicKeyCredentialCreationOptions,
|
|
6
|
-
PublicKeyCredentialRequestOptions,
|
|
7
|
-
} from 'fido2-lib';
|
|
8
2
|
import { createLogger } from '@unchainedshop/logger';
|
|
9
3
|
import { WebAuthnCredentialsCreationRequestsCollection } from '../db/WebAuthnCredentialsCreationRequestsCollection.js';
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
findMDSMetadataForAAGUID: (aaguid: string) => Promise<any>;
|
|
13
|
-
|
|
14
|
-
createCredentialCreationOptions: (
|
|
15
|
-
origin: string,
|
|
16
|
-
username: string,
|
|
17
|
-
extensionOptions?: any,
|
|
18
|
-
) => Promise<any>;
|
|
19
|
-
verifyCredentialCreation: (username: string, credentials: any) => Promise<any>;
|
|
20
|
-
|
|
21
|
-
createCredentialRequestOptions: (
|
|
22
|
-
origin: string,
|
|
23
|
-
username?: string,
|
|
24
|
-
extensionOptions?: any,
|
|
25
|
-
) => Promise<any>;
|
|
26
|
-
verifyCredentialRequest: (userPublicKeys: any[], username: string, credentials: any) => Promise<any>;
|
|
27
|
-
}
|
|
5
|
+
import type { PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions } from 'fido2-lib';
|
|
28
6
|
|
|
29
7
|
const logger = createLogger('unchained:core-users');
|
|
30
8
|
|
|
@@ -40,6 +18,9 @@ const setupMDSCollection = async () => {
|
|
|
40
18
|
try {
|
|
41
19
|
const tocResult = await fetch('https://mds.fidoalliance.org');
|
|
42
20
|
const tocBase64 = await tocResult.text();
|
|
21
|
+
// eslint-disable-next-line
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
const { Fido2Lib } = await import('fido2-lib');
|
|
43
24
|
const mc = (Fido2Lib as any).createMdsCollection('FIDO MDS v3'); // createMdsCollection exists but not typed in official package!
|
|
44
25
|
const tocObj = await mc.addToc(tocBase64);
|
|
45
26
|
return tocObj.entries;
|
|
@@ -66,24 +47,30 @@ export function buf2hex(buffer) {
|
|
|
66
47
|
.join('');
|
|
67
48
|
}
|
|
68
49
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
50
|
+
const initF2L = async () => {
|
|
51
|
+
try {
|
|
52
|
+
const thisDomain = new URL(ROOT_URL).hostname;
|
|
53
|
+
const { Fido2Lib } = await import('fido2-lib');
|
|
54
|
+
const f2l = new Fido2Lib({
|
|
55
|
+
rpId: thisDomain,
|
|
56
|
+
rpName: EMAIL_WEBSITE_NAME,
|
|
57
|
+
rpIcon:
|
|
58
|
+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARCSURBVHgB7Ve9UxNbFL8fm7yA4ktegHnlMk98dMbOMnR2D6pnGTqtCH+BWFoBpZXQaaelFbGzEztHHF26DKizzijDx+Zef+fu3vXuJgsxztjonQnZOZw993d+5zOM/epHslFO1a/KS/UbuvTnETsKQ/YDh3+XNi72yuVlzVgbL1YhCaOTk2ssDAI24hHDKsrpfxZkufwCj6t0OUAEBMkrlR6wHzjDMVCd872yemeete7gczeKoh2wQTICsyi0DrTWVc55eHqwu8OGPEMCMNSby5hS89H7Nx0Se9NXVvF1J69O7MDwJsKzgfCcmSP9SQhvxcV6S5cnummCHYVH4kJ9DE9NfHx1+HGLxKo0sSOkvIXHSnyz7mjOySufdLmUN3Vp4slZiSoyXk7NrhHVQui1fGzhzTq+QsZ5U07NtowQ3oH2FavDhXjS2389w7W+lrDgI2+2yfaZAEpT/y5LopjztnEkf1F6GXsc38TvWKO9g91NfO3EBGgjpxzooTosCISvXQjAm7zc1Fyvm7IChRHQM+uVcxGBAbstI84bVcqyUE3lAMyVWkrky0UsuGUYRqeni4zQk1cAk15ELxMYy07OqElKqo5ieVWWymsDAbhKGa9Qatag8P5omcQiUEot9nmb1c9cFkm5FJPJWqWp2UYegKkCMf7XHjRaeGwgs+9T1iPTA8ibkM/h5ev4XwVJthEd7D7kF+ozANRI9B9RlpO+lUO/IcZqz0jGPr8PUUFU7k185mwFZUIwBAuGUqWUob8nxCqLQ0H6qbeOnNmQGftOBVHO9QEw+ozZRKLE8y0wxDxFLISIE6n7KsDfjUS8kBoludZbCYBvl8XNaCMPLAOASsde5vYAlFPbeqWV+q/Pq5xR6HRS6zkWzPzIlXdmGKWXFaM/V06zwAGQaVppSJ3yzk7DAqpcb7UQD/q8cuRoxb75dtlxm5Zb3mzALEj7O7I/zWRUBRurHcO7G9Sw5MXJT+rLh+dmRlSqe6iOm1aOC/5nBELr2yTDOw3YO4Z+x3icq7iB01BMXm4j4dbIO+rtVi6nr7xLBg0tIjN20mGGbBPdVs+8J07mvcjzkbnbGf14tNNeYcb4wJVMH358jpo2zSf1lgCM1V4m6CsFXiUG9Iref9txe4kQXgVT9jqXejNZaEKAvFu4D5ikStBjus3bJcPxdjALNE8OduetHep+yIsXGQcZf9wTxyusGwSFS2mCvmaMcn6Lj9d9OV77BGN7iOuCYQFeqcMPT43+peln2IraFH/EdsvuALDTTTthvE0tIRnvsc/x/8/diIq2HnsSFoKEhXUAWM6zYA5VwoDtaLiV7O85XypFy+hVnbRlhCXoo9zZHWkpGWY3/L613D25PZFzeVVxvZqs6ywSYKZ7/ro+2g8TOu6eSBXAGfWIio2z2n871GY8OgN06BcSfitQuZr1CxuQ3Zh/6qkk0/P3GeV8BeBCYUHc/WHIAAAAAElFTkSuQmCC',
|
|
59
|
+
challengeSize: 128,
|
|
60
|
+
attestation: 'none',
|
|
61
|
+
});
|
|
62
|
+
return f2l;
|
|
63
|
+
} catch {
|
|
64
|
+
logger.warn(`optional peer npm package 'fido2-lib' not installed, WebAuthn will not work`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
84
68
|
|
|
69
|
+
export const configureUsersWebAuthnModule = async ({ db }: ModuleInput<any>) => {
|
|
70
|
+
const WebAuthnCredentialsCreationRequests = await WebAuthnCredentialsCreationRequestsCollection(db);
|
|
71
|
+
const f2l = await initF2L();
|
|
85
72
|
return {
|
|
86
|
-
findMDSMetadataForAAGUID: async (aaguid) => {
|
|
73
|
+
findMDSMetadataForAAGUID: async (aaguid: string) => {
|
|
87
74
|
const mdsCollection = await fetchMDS();
|
|
88
75
|
const foundEntry = mdsCollection.find((entry) => {
|
|
89
76
|
return entry.aaguid === aaguid;
|
|
@@ -91,7 +78,13 @@ export const configureUsersWebAuthnModule = async ({
|
|
|
91
78
|
return foundEntry?.metadataStatement;
|
|
92
79
|
},
|
|
93
80
|
|
|
94
|
-
createCredentialCreationOptions: async (
|
|
81
|
+
createCredentialCreationOptions: async (
|
|
82
|
+
origin: string,
|
|
83
|
+
username: string,
|
|
84
|
+
extensionOptions?: any,
|
|
85
|
+
) => {
|
|
86
|
+
if (!f2l) return null;
|
|
87
|
+
|
|
95
88
|
const registrationOptions = await f2l.attestationOptions(extensionOptions);
|
|
96
89
|
const challenge = Buffer.from(registrationOptions.challenge).toString('base64');
|
|
97
90
|
const { insertedId } = await WebAuthnCredentialsCreationRequests.insertOne({
|
|
@@ -109,7 +102,13 @@ export const configureUsersWebAuthnModule = async ({
|
|
|
109
102
|
} as SerializedOptions<PublicKeyCredentialCreationOptions>;
|
|
110
103
|
},
|
|
111
104
|
|
|
112
|
-
createCredentialRequestOptions: async (
|
|
105
|
+
createCredentialRequestOptions: async (
|
|
106
|
+
origin: string,
|
|
107
|
+
username?: string,
|
|
108
|
+
extensionOptions?: any,
|
|
109
|
+
) => {
|
|
110
|
+
if (!f2l) return null;
|
|
111
|
+
|
|
113
112
|
const loginOptions = await f2l.assertionOptions(extensionOptions);
|
|
114
113
|
const challenge = Buffer.from(loginOptions.challenge).toString('base64');
|
|
115
114
|
const { insertedId } = await WebAuthnCredentialsCreationRequests.insertOne({
|
|
@@ -127,7 +126,9 @@ export const configureUsersWebAuthnModule = async ({
|
|
|
127
126
|
} as SerializedOptions<PublicKeyCredentialRequestOptions>;
|
|
128
127
|
},
|
|
129
128
|
|
|
130
|
-
verifyCredentialCreation: async (username, credentials) => {
|
|
129
|
+
verifyCredentialCreation: async (username: string, credentials: any) => {
|
|
130
|
+
if (!f2l) return null;
|
|
131
|
+
|
|
131
132
|
const request = await WebAuthnCredentialsCreationRequests.findOne(
|
|
132
133
|
{
|
|
133
134
|
username,
|
|
@@ -174,7 +175,9 @@ export const configureUsersWebAuthnModule = async ({
|
|
|
174
175
|
return { publicKey, counter, id: credentials.id, aaguid, created: new Date() };
|
|
175
176
|
},
|
|
176
177
|
|
|
177
|
-
verifyCredentialRequest: async (userPublicKeys, username, credentials) => {
|
|
178
|
+
verifyCredentialRequest: async (userPublicKeys: any[], username: string, credentials: any) => {
|
|
179
|
+
if (!f2l) return null;
|
|
180
|
+
|
|
178
181
|
const request = await WebAuthnCredentialsCreationRequests.findOne(
|
|
179
182
|
{
|
|
180
183
|
_id: credentials.requestId,
|
|
@@ -193,7 +196,7 @@ export const configureUsersWebAuthnModule = async ({
|
|
|
193
196
|
return credentials.id === publicCredentials.id;
|
|
194
197
|
}) || {};
|
|
195
198
|
|
|
196
|
-
if (!publicKey)
|
|
199
|
+
if (!publicKey) return null;
|
|
197
200
|
|
|
198
201
|
const assertionExpectations = {
|
|
199
202
|
challenge: request.challenge,
|
package/src/module/pbkdf2.ts
CHANGED
|
@@ -4,7 +4,7 @@ const PBKDF2_KEY_LENGTH = 256; // Bytes
|
|
|
4
4
|
const PBKDF2_SALT_LENGTH = 16; // Bytes
|
|
5
5
|
|
|
6
6
|
export function generateSalt(saltLength = PBKDF2_SALT_LENGTH) {
|
|
7
|
-
const array = new
|
|
7
|
+
const array = new Uint8Array(saltLength);
|
|
8
8
|
return Buffer.from(crypto.getRandomValues(array)).toString('hex');
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { User } from '../types.js';
|
|
2
1
|
import { removeConfidentialServiceHashes } from './configureUsersModule.js';
|
|
3
2
|
import user from '../../tests/mock/user-mock.js';
|
|
3
|
+
import { User } from '../db/UsersCollection.js';
|
|
4
4
|
|
|
5
5
|
describe('removeConfidentialServiceHashes', () => {
|
|
6
6
|
it('Should remove sensitive user credentials ', () => {
|
package/src/users-index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './db/UsersCollection.js';
|
|
2
|
+
export * from './db/WebAuthnCredentialsCreationRequestsCollection.js';
|
|
2
3
|
export * from './module/configureUsersModule.js';
|
|
3
|
-
export * from './
|
|
4
|
+
export * from './module/configureUsersWebAuthnModule.js';
|
|
4
5
|
export * from './users-settings.js';
|