@unchainedshop/core-users 3.0.0-alpha6 → 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 +106 -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 +153 -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
|
|
@@ -359,6 +320,7 @@ export const configureUsersModule = async ({
|
|
|
359
320
|
_id: normalizedUserData._id || generateDbObjectId(),
|
|
360
321
|
roles: roles || [],
|
|
361
322
|
initialPassword: Boolean(initialPassword),
|
|
323
|
+
services,
|
|
362
324
|
emails: email ? [{ address: email, verified: false }] : [],
|
|
363
325
|
created: new Date(),
|
|
364
326
|
};
|
|
@@ -398,7 +360,9 @@ export const configureUsersModule = async ({
|
|
|
398
360
|
return userId;
|
|
399
361
|
},
|
|
400
362
|
|
|
401
|
-
async hashPassword(password) {
|
|
363
|
+
async hashPassword(password: string): Promise<{
|
|
364
|
+
pbkdf2: string;
|
|
365
|
+
}> {
|
|
402
366
|
const salt = pbkdf2.generateSalt();
|
|
403
367
|
const hashedPassword = await pbkdf2.getDerivedKey(salt, password);
|
|
404
368
|
return { pbkdf2: `${salt}:${hashedPassword}` };
|
|
@@ -409,7 +373,7 @@ export const configureUsersModule = async ({
|
|
|
409
373
|
plainPassword: string,
|
|
410
374
|
): Promise<boolean> {
|
|
411
375
|
if (bcryptHash) {
|
|
412
|
-
const password = await sha256
|
|
376
|
+
const password = await sha256(plainPassword);
|
|
413
377
|
return bcrypt.compare(password, bcryptHash);
|
|
414
378
|
}
|
|
415
379
|
if (pbkdf2SaltAndHash) {
|
|
@@ -450,9 +414,9 @@ export const configureUsersModule = async ({
|
|
|
450
414
|
async sendResetPasswordEmail(userId: string, email: string, isEnrollment?: boolean): Promise<void> {
|
|
451
415
|
const plainToken = crypto.randomUUID();
|
|
452
416
|
const resetToken = {
|
|
453
|
-
token: await sha256
|
|
417
|
+
token: await sha256(plainToken),
|
|
454
418
|
address: email,
|
|
455
|
-
when: new Date()
|
|
419
|
+
when: new Date(),
|
|
456
420
|
};
|
|
457
421
|
|
|
458
422
|
await Users.updateOne(
|
|
@@ -475,9 +439,9 @@ export const configureUsersModule = async ({
|
|
|
475
439
|
async sendVerificationEmail(userId: string, email: string): Promise<void> {
|
|
476
440
|
const plainToken = crypto.randomUUID();
|
|
477
441
|
const verificationToken = {
|
|
478
|
-
token: await sha256
|
|
442
|
+
token: await sha256(plainToken),
|
|
479
443
|
address: email,
|
|
480
|
-
when: new Date()
|
|
444
|
+
when: new Date(),
|
|
481
445
|
};
|
|
482
446
|
|
|
483
447
|
await Users.updateOne(
|
|
@@ -529,12 +493,12 @@ export const configureUsersModule = async ({
|
|
|
529
493
|
});
|
|
530
494
|
},
|
|
531
495
|
|
|
532
|
-
async setPassword(userId: string, plainPassword: string) {
|
|
496
|
+
async setPassword(userId: string, plainPassword: string): Promise<User> {
|
|
533
497
|
if (!(await userSettings.validatePassword(plainPassword))) {
|
|
534
498
|
throw new Error(`Password ***** is invalid`, { cause: 'PASSWORD_INVALID' });
|
|
535
499
|
}
|
|
536
500
|
const password = plainPassword || crypto.randomUUID().split('-').pop();
|
|
537
|
-
await Users.
|
|
501
|
+
const user = await Users.findOneAndUpdate(
|
|
538
502
|
{ _id: userId },
|
|
539
503
|
{
|
|
540
504
|
$set: {
|
|
@@ -544,11 +508,28 @@ export const configureUsersModule = async ({
|
|
|
544
508
|
),
|
|
545
509
|
},
|
|
546
510
|
},
|
|
511
|
+
{ returnDocument: 'after' },
|
|
547
512
|
);
|
|
548
|
-
const user = await Users.findOne({ _id: userId }, {});
|
|
549
513
|
await emit('USER_UPDATE_PASSWORD', {
|
|
550
514
|
user: removeConfidentialServiceHashes(user),
|
|
551
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;
|
|
552
533
|
},
|
|
553
534
|
|
|
554
535
|
updateAvatar: async (_id: string, fileId: string): Promise<User> => {
|
|
@@ -601,42 +582,38 @@ export const configureUsersModule = async ({
|
|
|
601
582
|
return user;
|
|
602
583
|
},
|
|
603
584
|
|
|
604
|
-
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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,
|
|
619
602
|
},
|
|
620
|
-
];
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
const obfuscatedUsername = existingUser.username ? `${existingUser.username}-${uuid}` : null;
|
|
624
|
-
|
|
625
|
-
Users.updateOne(userFilter, {
|
|
626
|
-
$set: {
|
|
627
|
-
emails: obfuscatedEmails,
|
|
628
|
-
username: obfuscatedUsername,
|
|
629
|
-
services: {},
|
|
630
603
|
},
|
|
631
|
-
|
|
604
|
+
{ returnDocument: 'after' },
|
|
605
|
+
);
|
|
632
606
|
|
|
633
|
-
const user = await Users.findOneAndDelete(userFilter);
|
|
634
607
|
await emit('USER_REMOVE', {
|
|
635
|
-
user
|
|
608
|
+
user,
|
|
636
609
|
});
|
|
637
610
|
return user;
|
|
638
611
|
},
|
|
639
612
|
|
|
613
|
+
deletePermanently: async ({ userId }: { userId: string }): Promise<User> => {
|
|
614
|
+
return Users.findOneAndDelete({ _id: userId });
|
|
615
|
+
},
|
|
616
|
+
|
|
640
617
|
updateProfile: async (
|
|
641
618
|
userId: string,
|
|
642
619
|
updatedData: { profile?: UserProfile; meta?: any },
|
|
@@ -679,6 +656,8 @@ export const configureUsersModule = async ({
|
|
|
679
656
|
const userFilter = generateDbFilterById(_id);
|
|
680
657
|
const user = await Users.findOne(userFilter, {});
|
|
681
658
|
|
|
659
|
+
if (!lastBillingAddress) return user;
|
|
660
|
+
|
|
682
661
|
const modifier = {
|
|
683
662
|
$set: {
|
|
684
663
|
lastBillingAddress,
|
|
@@ -701,9 +680,12 @@ export const configureUsersModule = async ({
|
|
|
701
680
|
returnDocument: 'after',
|
|
702
681
|
});
|
|
703
682
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
683
|
+
if (updatedUser) {
|
|
684
|
+
await emit('USER_UPDATE_BILLING_ADDRESS', {
|
|
685
|
+
user: removeConfidentialServiceHashes(updatedUser),
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
707
689
|
return updatedUser;
|
|
708
690
|
},
|
|
709
691
|
|
|
@@ -829,3 +811,5 @@ export const configureUsersModule = async ({
|
|
|
829
811
|
},
|
|
830
812
|
};
|
|
831
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 ', () => {
|