@unchainedshop/core-users 4.0.0-rc.8 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/db/UsersCollection.d.ts +2 -2
- package/lib/db/UsersCollection.d.ts.map +1 -1
- package/lib/db/UsersCollection.js +30 -26
- package/lib/db/UsersCollection.js.map +1 -1
- package/lib/module/addMigrations.d.ts +3 -0
- package/lib/module/addMigrations.d.ts.map +1 -0
- package/lib/module/addMigrations.js +57 -0
- package/lib/module/addMigrations.js.map +1 -0
- package/lib/module/configureUsersModule.d.ts +32 -31
- package/lib/module/configureUsersModule.d.ts.map +1 -1
- package/lib/module/configureUsersModule.js +74 -34
- package/lib/module/configureUsersModule.js.map +1 -1
- package/lib/module/configureUsersWebAuthnModule.d.ts +5 -5
- package/lib/module/configureUsersWebAuthnModule.d.ts.map +1 -1
- package/lib/module/configureUsersWebAuthnModule.js +9 -3
- package/lib/module/configureUsersWebAuthnModule.js.map +1 -1
- package/lib/module/pbkdf2.d.ts.map +1 -1
- package/lib/module/pbkdf2.js +9 -2
- package/lib/module/pbkdf2.js.map +1 -1
- package/lib/services/getUserCountryService.d.ts +3 -0
- package/lib/services/getUserCountryService.d.ts.map +1 -0
- package/lib/services/getUserCountryService.js +7 -0
- package/lib/services/getUserCountryService.js.map +1 -0
- package/lib/services/getUserLanguageService.d.ts +3 -0
- package/lib/services/getUserLanguageService.d.ts.map +1 -0
- package/lib/services/getUserLanguageService.js +7 -0
- package/lib/services/getUserLanguageService.js.map +1 -0
- package/lib/services/updateUserAvatarAfterUploadService.d.ts +3 -0
- package/lib/services/updateUserAvatarAfterUploadService.d.ts.map +1 -0
- package/lib/services/updateUserAvatarAfterUploadService.js +24 -0
- package/lib/services/updateUserAvatarAfterUploadService.js.map +1 -0
- package/lib/services/userServices.d.ts +3 -0
- package/lib/services/userServices.d.ts.map +1 -0
- package/lib/services/userServices.js +9 -0
- package/lib/services/userServices.js.map +1 -0
- package/lib/users-settings.d.ts +1 -14
- package/lib/users-settings.d.ts.map +1 -1
- package/lib/users-settings.js +24 -24
- package/lib/users-settings.js.map +1 -1
- package/package.json +8 -8
- package/src/db/UsersCollection.ts +33 -29
- package/src/module/buildFindSelector.test.ts +5 -2
- package/src/module/configureUsersModule.ts +114 -78
- package/src/module/configureUsersWebAuthnModule.ts +8 -8
- package/src/module/pbkdf2.ts +9 -2
- package/src/module/removeConfidentialServiceHashes.test.ts +1 -2
- package/src/users-settings.ts +31 -45
- package/tsconfig.json +2 -1
|
@@ -4,7 +4,10 @@ import { buildFindSelector } from './configureUsersModule.js';
|
|
|
4
4
|
|
|
5
5
|
describe('buildFindSelector', () => {
|
|
6
6
|
it('Return the correct filter when no parameter is passed', () => {
|
|
7
|
-
assert.deepStrictEqual(buildFindSelector({}), {
|
|
7
|
+
assert.deepStrictEqual(buildFindSelector({}), {
|
|
8
|
+
deleted: null,
|
|
9
|
+
guest: { $ne: true },
|
|
10
|
+
});
|
|
8
11
|
});
|
|
9
12
|
it('Return the correct filter when no parameter is passed queryString and includeGuest: true', () => {
|
|
10
13
|
assert.deepStrictEqual(buildFindSelector({ queryString: 'Hello world', includeGuests: true }), {
|
|
@@ -23,7 +26,7 @@ describe('buildFindSelector', () => {
|
|
|
23
26
|
{
|
|
24
27
|
'profile.displayName': 'mikael',
|
|
25
28
|
deleted: null,
|
|
26
|
-
guest: { $
|
|
29
|
+
guest: { $ne: true },
|
|
27
30
|
$text: { $search: 'Hello world' },
|
|
28
31
|
},
|
|
29
32
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as bcrypt from '
|
|
1
|
+
import * as bcrypt from 'bcryptjs';
|
|
2
2
|
import {
|
|
3
3
|
ModuleInput,
|
|
4
4
|
Address,
|
|
@@ -47,7 +47,7 @@ const USER_EVENTS = [
|
|
|
47
47
|
'USER_REMOVE',
|
|
48
48
|
];
|
|
49
49
|
export const removeConfidentialServiceHashes = (rawUser: User): User => {
|
|
50
|
-
const user = rawUser;
|
|
50
|
+
const user = { ...rawUser };
|
|
51
51
|
delete user?.services;
|
|
52
52
|
return user;
|
|
53
53
|
};
|
|
@@ -58,14 +58,18 @@ export const buildFindSelector = ({
|
|
|
58
58
|
queryString,
|
|
59
59
|
emailVerified,
|
|
60
60
|
lastLogin,
|
|
61
|
+
tags,
|
|
61
62
|
...rest
|
|
62
63
|
}: UserQuery) => {
|
|
63
64
|
const selector: mongodb.Filter<User> = { ...rest };
|
|
64
|
-
if (!includeDeleted) selector.deleted = null;
|
|
65
|
-
if (!includeGuests) selector.guest = { $
|
|
65
|
+
if (!includeDeleted) selector.deleted = null as any;
|
|
66
|
+
if (!includeGuests) selector.guest = { $ne: true };
|
|
66
67
|
if (emailVerified === true) {
|
|
67
68
|
selector['emails.verified'] = true;
|
|
68
69
|
}
|
|
70
|
+
if (Array.isArray(tags) && tags?.length) {
|
|
71
|
+
selector.tags = { $in: tags };
|
|
72
|
+
}
|
|
69
73
|
if (emailVerified === false) {
|
|
70
74
|
// We need to use $ne here else we'd also find users with many emails where one is
|
|
71
75
|
// unverified
|
|
@@ -87,12 +91,13 @@ export const buildFindSelector = ({
|
|
|
87
91
|
return selector;
|
|
88
92
|
};
|
|
89
93
|
|
|
90
|
-
export const configureUsersModule = async (
|
|
94
|
+
export const configureUsersModule = async (moduleInput: ModuleInput<UserSettingsOptions>) => {
|
|
95
|
+
const { db, options } = moduleInput;
|
|
91
96
|
userSettings.configureSettings(options || {}, db);
|
|
92
97
|
registerEvents(USER_EVENTS);
|
|
93
98
|
const Users = await UsersCollection(db);
|
|
94
99
|
|
|
95
|
-
const webAuthn = await configureUsersWebAuthnModule(
|
|
100
|
+
const webAuthn = await configureUsersWebAuthnModule(moduleInput);
|
|
96
101
|
|
|
97
102
|
return {
|
|
98
103
|
// Queries
|
|
@@ -102,17 +107,17 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
102
107
|
return userCount;
|
|
103
108
|
},
|
|
104
109
|
|
|
105
|
-
async findUserById(userId: string)
|
|
110
|
+
async findUserById(userId: string) {
|
|
106
111
|
if (!userId) return null;
|
|
107
112
|
return Users.findOne(generateDbFilterById(userId), {});
|
|
108
113
|
},
|
|
109
114
|
|
|
110
|
-
async findUserByUsername(username: string)
|
|
115
|
+
async findUserByUsername(username: string) {
|
|
111
116
|
if (!username) return null;
|
|
112
117
|
return Users.findOne({ username: insensitiveTrimmedRegexOperator(username) }, {});
|
|
113
118
|
},
|
|
114
119
|
|
|
115
|
-
async findUserByEmail(email: string)
|
|
120
|
+
async findUserByEmail(email: string) {
|
|
116
121
|
if (!email) return null;
|
|
117
122
|
return Users.findOne({ 'emails.address': insensitiveTrimmedRegexOperator(email) }, {});
|
|
118
123
|
},
|
|
@@ -121,7 +126,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
121
126
|
userId: string;
|
|
122
127
|
address: string;
|
|
123
128
|
when: Date;
|
|
124
|
-
}> {
|
|
129
|
+
} | null> {
|
|
125
130
|
if (!plainToken) return null;
|
|
126
131
|
const token = await sha256(plainToken);
|
|
127
132
|
const user = await Users.findOne(
|
|
@@ -174,7 +179,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
174
179
|
userId: string;
|
|
175
180
|
address: string;
|
|
176
181
|
when: Date;
|
|
177
|
-
}> {
|
|
182
|
+
} | null> {
|
|
178
183
|
const token = await sha256(plainToken);
|
|
179
184
|
const user = await Users.findOne(
|
|
180
185
|
{
|
|
@@ -195,7 +200,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
195
200
|
};
|
|
196
201
|
},
|
|
197
202
|
|
|
198
|
-
async findUserByToken(plainToken: string)
|
|
203
|
+
async findUserByToken(plainToken: string) {
|
|
199
204
|
const token = await sha256(plainToken);
|
|
200
205
|
|
|
201
206
|
if (token) {
|
|
@@ -207,10 +212,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
207
212
|
return null;
|
|
208
213
|
},
|
|
209
214
|
|
|
210
|
-
async findUser(
|
|
211
|
-
query: UserQuery & { sort?: SortOption[] },
|
|
212
|
-
findOptions?: mongodb.FindOptions,
|
|
213
|
-
): Promise<User> {
|
|
215
|
+
async findUser(query: UserQuery & { sort?: SortOption[] }, findOptions?: mongodb.FindOptions) {
|
|
214
216
|
const selector = buildFindSelector(query);
|
|
215
217
|
return Users.findOne(selector, findOptions);
|
|
216
218
|
},
|
|
@@ -244,7 +246,10 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
244
246
|
},
|
|
245
247
|
|
|
246
248
|
async userExists({ userId }: { userId: string }): Promise<boolean> {
|
|
247
|
-
const userCount = await Users.countDocuments(
|
|
249
|
+
const userCount = await Users.countDocuments(
|
|
250
|
+
{ _id: userId, deleted: { $exists: false } },
|
|
251
|
+
{ limit: 1 },
|
|
252
|
+
);
|
|
248
253
|
return userCount === 1;
|
|
249
254
|
},
|
|
250
255
|
|
|
@@ -255,7 +260,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
255
260
|
)?.[0];
|
|
256
261
|
},
|
|
257
262
|
|
|
258
|
-
userLocale(user: User): Intl.Locale {
|
|
263
|
+
userLocale(user: User | null): Intl.Locale {
|
|
259
264
|
if (!user?.lastLogin?.locale) return systemLocale;
|
|
260
265
|
try {
|
|
261
266
|
return new Intl.Locale(user.lastLogin.locale);
|
|
@@ -344,7 +349,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
344
349
|
/* */
|
|
345
350
|
}
|
|
346
351
|
|
|
347
|
-
const user = await Users.findOne({ _id: userId }, {});
|
|
352
|
+
const user = (await Users.findOne({ _id: userId }, {})) as User;
|
|
348
353
|
await emit('USER_CREATE', {
|
|
349
354
|
user: removeConfidentialServiceHashes(user),
|
|
350
355
|
});
|
|
@@ -453,39 +458,46 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
453
458
|
});
|
|
454
459
|
},
|
|
455
460
|
|
|
456
|
-
addRoles: async (userId: string, roles: string[])
|
|
461
|
+
addRoles: async (userId: string, roles: string[]) => {
|
|
457
462
|
const selector = generateDbFilterById(userId);
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
463
|
+
const user = await Users.findOneAndUpdate(
|
|
464
|
+
selector,
|
|
465
|
+
{
|
|
466
|
+
$addToSet: { roles: { $each: roles } },
|
|
467
|
+
},
|
|
468
|
+
{ returnDocument: 'after' },
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
if (!user) return null;
|
|
461
472
|
|
|
462
|
-
const user = await Users.findOne(selector, {});
|
|
463
473
|
await emit('USER_ADD_ROLES', {
|
|
464
474
|
user: removeConfidentialServiceHashes(user),
|
|
465
475
|
});
|
|
466
476
|
|
|
467
|
-
return
|
|
477
|
+
return user;
|
|
468
478
|
},
|
|
469
479
|
|
|
470
480
|
async setUsername(userId: string, username: string) {
|
|
471
481
|
if (!(await userSettings.validateUsername(username))) {
|
|
472
482
|
throw new Error(`Username ${username} is invalid`, { cause: 'USERNAME_INVALID' });
|
|
473
483
|
}
|
|
474
|
-
await Users.
|
|
484
|
+
const user = await Users.findOneAndUpdate(
|
|
475
485
|
{ _id: userId },
|
|
476
486
|
{
|
|
477
487
|
$set: {
|
|
478
488
|
username: username.trim(),
|
|
479
489
|
},
|
|
480
490
|
},
|
|
491
|
+
{ returnDocument: 'after' },
|
|
481
492
|
);
|
|
482
|
-
|
|
493
|
+
if (!user) return null;
|
|
483
494
|
await emit('USER_UPDATE_USERNAME', {
|
|
484
495
|
user: removeConfidentialServiceHashes(user),
|
|
485
496
|
});
|
|
497
|
+
return user;
|
|
486
498
|
},
|
|
487
499
|
|
|
488
|
-
async setPassword(userId: string, plainPassword: string)
|
|
500
|
+
async setPassword(userId: string, plainPassword: string) {
|
|
489
501
|
if (!(await userSettings.validatePassword(plainPassword))) {
|
|
490
502
|
throw new Error(`Password ***** is invalid`, { cause: 'PASSWORD_INVALID' });
|
|
491
503
|
}
|
|
@@ -502,13 +514,14 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
502
514
|
},
|
|
503
515
|
{ returnDocument: 'after' },
|
|
504
516
|
);
|
|
517
|
+
if (!user) return null;
|
|
505
518
|
await emit('USER_UPDATE_PASSWORD', {
|
|
506
519
|
user: removeConfidentialServiceHashes(user),
|
|
507
520
|
});
|
|
508
521
|
return user;
|
|
509
522
|
},
|
|
510
523
|
|
|
511
|
-
async resetPassword(token: string, newPassword: string)
|
|
524
|
+
async resetPassword(token: string, newPassword: string) {
|
|
512
525
|
const resetToken = await this.findResetToken(token);
|
|
513
526
|
if (!resetToken) return null;
|
|
514
527
|
const updatedUser = await this.setPassword(resetToken.userId, newPassword);
|
|
@@ -536,7 +549,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
536
549
|
return updatedUser;
|
|
537
550
|
},
|
|
538
551
|
|
|
539
|
-
updateAvatar: async (_id: string, fileId: string)
|
|
552
|
+
updateAvatar: async (_id: string, fileId: string) => {
|
|
540
553
|
const userFilter = generateDbFilterById(_id);
|
|
541
554
|
const modifier = {
|
|
542
555
|
$set: {
|
|
@@ -549,44 +562,54 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
549
562
|
returnDocument: 'after',
|
|
550
563
|
});
|
|
551
564
|
|
|
565
|
+
if (!user) return null;
|
|
552
566
|
await emit('USER_UPDATE_AVATAR', {
|
|
553
567
|
user: removeConfidentialServiceHashes(user),
|
|
554
568
|
});
|
|
555
569
|
return user;
|
|
556
570
|
},
|
|
557
571
|
|
|
558
|
-
updateGuest: async (user: User, guest: boolean)
|
|
559
|
-
const
|
|
560
|
-
|
|
572
|
+
updateGuest: async (user: User, guest: boolean) => {
|
|
573
|
+
const updatedUser = await Users.findOneAndUpdate(
|
|
574
|
+
generateDbFilterById(user._id),
|
|
575
|
+
{
|
|
576
|
+
$set: { guest },
|
|
577
|
+
},
|
|
578
|
+
{ returnDocument: 'after' },
|
|
579
|
+
);
|
|
580
|
+
if (!updatedUser) return null;
|
|
561
581
|
await emit('USER_UPDATE_GUEST', {
|
|
562
582
|
user: removeConfidentialServiceHashes({
|
|
563
|
-
...
|
|
583
|
+
...updatedUser,
|
|
564
584
|
guest,
|
|
565
585
|
}),
|
|
566
586
|
});
|
|
587
|
+
return updatedUser;
|
|
567
588
|
},
|
|
568
589
|
|
|
569
|
-
updateHeartbeat: async (userId: string, lastLogin: UserLastLogin)
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
590
|
+
updateHeartbeat: async (userId: string, lastLogin: UserLastLogin) => {
|
|
591
|
+
const user = await Users.findOneAndUpdate(
|
|
592
|
+
generateDbFilterById(userId),
|
|
593
|
+
{
|
|
594
|
+
$set: {
|
|
595
|
+
lastLogin: {
|
|
596
|
+
timestamp: new Date(),
|
|
597
|
+
...lastLogin,
|
|
598
|
+
},
|
|
575
599
|
},
|
|
576
600
|
},
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
601
|
+
{
|
|
602
|
+
returnDocument: 'after',
|
|
603
|
+
},
|
|
604
|
+
);
|
|
605
|
+
if (!user) return null;
|
|
583
606
|
await emit('USER_UPDATE_HEARTBEAT', {
|
|
584
607
|
user: removeConfidentialServiceHashes(user),
|
|
585
608
|
});
|
|
586
609
|
return user;
|
|
587
610
|
},
|
|
588
611
|
|
|
589
|
-
markDeleted: async (userId: string)
|
|
612
|
+
markDeleted: async (userId: string) => {
|
|
590
613
|
await db.collection('sessions').deleteMany({
|
|
591
614
|
session: insensitiveTrimmedRegexOperator(`"user":"${userId}"`),
|
|
592
615
|
});
|
|
@@ -598,18 +621,21 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
598
621
|
deleted: new Date(),
|
|
599
622
|
emails: [],
|
|
600
623
|
roles: [],
|
|
601
|
-
profile: null,
|
|
602
|
-
lastBillingAddress: null,
|
|
603
624
|
services: {},
|
|
604
625
|
pushSubscriptions: [],
|
|
605
|
-
avatarId: null,
|
|
606
626
|
initialPassword: false,
|
|
607
|
-
|
|
608
|
-
|
|
627
|
+
},
|
|
628
|
+
$unset: {
|
|
629
|
+
profile: 1,
|
|
630
|
+
lastBillingAddress: 1,
|
|
631
|
+
lastContact: 1,
|
|
632
|
+
lastLogin: 1,
|
|
633
|
+
avatarId: 1,
|
|
609
634
|
},
|
|
610
635
|
},
|
|
611
636
|
{ returnDocument: 'after' },
|
|
612
637
|
);
|
|
638
|
+
if (!user) return null;
|
|
613
639
|
|
|
614
640
|
await emit('USER_REMOVE', {
|
|
615
641
|
user,
|
|
@@ -617,14 +643,11 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
617
643
|
return user;
|
|
618
644
|
},
|
|
619
645
|
|
|
620
|
-
deletePermanently: async ({ userId }: { userId: string })
|
|
646
|
+
deletePermanently: async ({ userId }: { userId: string }) => {
|
|
621
647
|
return Users.findOneAndDelete({ _id: userId });
|
|
622
648
|
},
|
|
623
649
|
|
|
624
|
-
updateProfile: async (
|
|
625
|
-
userId: string,
|
|
626
|
-
updatedData: { profile?: UserProfile; meta?: any },
|
|
627
|
-
): Promise<User> => {
|
|
650
|
+
updateProfile: async (userId: string, updatedData: { profile?: UserProfile; meta?: any }) => {
|
|
628
651
|
const userFilter = generateDbFilterById(userId);
|
|
629
652
|
const { meta, profile } = updatedData;
|
|
630
653
|
|
|
@@ -653,16 +676,17 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
653
676
|
returnDocument: 'after',
|
|
654
677
|
});
|
|
655
678
|
|
|
679
|
+
if (!user) return null;
|
|
656
680
|
await emit('USER_UPDATE_PROFILE', {
|
|
657
681
|
user: removeConfidentialServiceHashes(user),
|
|
658
682
|
});
|
|
659
683
|
return user;
|
|
660
684
|
},
|
|
661
685
|
|
|
662
|
-
updateLastBillingAddress: async (_id: string, lastBillingAddress: Address)
|
|
686
|
+
updateLastBillingAddress: async (_id: string, lastBillingAddress: Address) => {
|
|
663
687
|
const userFilter = generateDbFilterById(_id);
|
|
664
688
|
const user = await Users.findOne(userFilter, {});
|
|
665
|
-
|
|
689
|
+
if (!user) return null;
|
|
666
690
|
if (!lastBillingAddress) return user;
|
|
667
691
|
|
|
668
692
|
const modifier = {
|
|
@@ -687,18 +711,17 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
687
711
|
returnDocument: 'after',
|
|
688
712
|
});
|
|
689
713
|
|
|
690
|
-
if (updatedUser)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
695
|
-
|
|
714
|
+
if (!updatedUser) return null;
|
|
715
|
+
await emit('USER_UPDATE_BILLING_ADDRESS', {
|
|
716
|
+
user: removeConfidentialServiceHashes(updatedUser),
|
|
717
|
+
});
|
|
696
718
|
return updatedUser;
|
|
697
719
|
},
|
|
698
720
|
|
|
699
|
-
updateLastContact: async (_id: string, lastContact: Contact)
|
|
721
|
+
updateLastContact: async (_id: string, lastContact: Contact) => {
|
|
700
722
|
const userFilter = generateDbFilterById(_id);
|
|
701
723
|
const user = await Users.findOne(userFilter, {});
|
|
724
|
+
if (!user) return null;
|
|
702
725
|
const profile = user.profile || {};
|
|
703
726
|
const isGuest = !!user.guest;
|
|
704
727
|
|
|
@@ -718,13 +741,14 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
718
741
|
returnDocument: 'after',
|
|
719
742
|
});
|
|
720
743
|
|
|
744
|
+
if (!updatedUser) return null;
|
|
721
745
|
await emit('USER_UPDATE_LAST_CONTACT', {
|
|
722
746
|
user: removeConfidentialServiceHashes(user),
|
|
723
747
|
});
|
|
724
748
|
return updatedUser;
|
|
725
749
|
},
|
|
726
750
|
|
|
727
|
-
updateRoles: async (_id: string, roles: string[])
|
|
751
|
+
updateRoles: async (_id: string, roles: string[]) => {
|
|
728
752
|
const modifier = {
|
|
729
753
|
$set: {
|
|
730
754
|
updated: new Date(),
|
|
@@ -735,23 +759,27 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
735
759
|
const user = await Users.findOneAndUpdate(generateDbFilterById(_id), modifier, {
|
|
736
760
|
returnDocument: 'after',
|
|
737
761
|
});
|
|
738
|
-
|
|
762
|
+
if (!user) return null;
|
|
739
763
|
await emit('USER_UPDATE_ROLE', {
|
|
740
764
|
user: removeConfidentialServiceHashes(user),
|
|
741
765
|
});
|
|
742
766
|
return user;
|
|
743
767
|
},
|
|
744
768
|
|
|
745
|
-
updateTags: async (_id: string, tags: string[])
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
769
|
+
updateTags: async (_id: string, tags: string[]) => {
|
|
770
|
+
const user = await Users.findOneAndUpdate(
|
|
771
|
+
generateDbFilterById(_id),
|
|
772
|
+
{
|
|
773
|
+
$set: {
|
|
774
|
+
updated: new Date(),
|
|
775
|
+
tags,
|
|
776
|
+
},
|
|
750
777
|
},
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
778
|
+
{
|
|
779
|
+
returnDocument: 'after',
|
|
780
|
+
},
|
|
781
|
+
);
|
|
782
|
+
if (!user) return null;
|
|
755
783
|
await emit('USER_UPDATE_TAGS', {
|
|
756
784
|
user: removeConfidentialServiceHashes(user),
|
|
757
785
|
});
|
|
@@ -762,11 +790,12 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
762
790
|
selector: mongodb.Filter<User>,
|
|
763
791
|
modifier: mongodb.UpdateFilter<User>,
|
|
764
792
|
updateOptions?: mongodb.FindOneAndUpdateOptions,
|
|
765
|
-
)
|
|
793
|
+
) => {
|
|
766
794
|
const user = await Users.findOneAndUpdate(selector, modifier, {
|
|
767
795
|
...updateOptions,
|
|
768
796
|
returnDocument: 'after',
|
|
769
797
|
});
|
|
798
|
+
if (!user) return null;
|
|
770
799
|
await emit('USER_UPDATE', {
|
|
771
800
|
user: removeConfidentialServiceHashes(user),
|
|
772
801
|
});
|
|
@@ -816,6 +845,13 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
816
845
|
{},
|
|
817
846
|
);
|
|
818
847
|
},
|
|
848
|
+
existingTags: async (): Promise<string[]> => {
|
|
849
|
+
const tags = (await Users.distinct('tags', {
|
|
850
|
+
tags: { $exists: true },
|
|
851
|
+
deleted: { $exists: false },
|
|
852
|
+
})) as string[];
|
|
853
|
+
return tags.filter(Boolean).toSorted();
|
|
854
|
+
},
|
|
819
855
|
};
|
|
820
856
|
};
|
|
821
857
|
|
|
@@ -6,7 +6,7 @@ import type { PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOpti
|
|
|
6
6
|
|
|
7
7
|
const logger = createLogger('unchained:core-users');
|
|
8
8
|
|
|
9
|
-
const { ROOT_URL, EMAIL_WEBSITE_NAME } = process.env;
|
|
9
|
+
const { ROOT_URL = 'http://localhost:4010', EMAIL_WEBSITE_NAME = 'Unchained' } = process.env;
|
|
10
10
|
|
|
11
11
|
type SerializedOptions<T> = Omit<T, 'challenge' | 'requestId'> & {
|
|
12
12
|
challenge: string;
|
|
@@ -86,7 +86,8 @@ export const configureUsersWebAuthnModule = async ({ db }: ModuleInput<any>) =>
|
|
|
86
86
|
if (!f2l) return null;
|
|
87
87
|
|
|
88
88
|
const registrationOptions = await f2l.attestationOptions(extensionOptions);
|
|
89
|
-
|
|
89
|
+
// Convert challenge to base64 without using Buffer
|
|
90
|
+
const challenge = btoa(String.fromCharCode(...new Uint8Array(registrationOptions.challenge)));
|
|
90
91
|
const { insertedId } = await WebAuthnCredentialsCreationRequests.insertOne({
|
|
91
92
|
_id: new Date().getTime(),
|
|
92
93
|
challenge,
|
|
@@ -102,15 +103,12 @@ export const configureUsersWebAuthnModule = async ({ db }: ModuleInput<any>) =>
|
|
|
102
103
|
} as SerializedOptions<PublicKeyCredentialCreationOptions>;
|
|
103
104
|
},
|
|
104
105
|
|
|
105
|
-
createCredentialRequestOptions: async (
|
|
106
|
-
origin: string,
|
|
107
|
-
username?: string,
|
|
108
|
-
extensionOptions?: any,
|
|
109
|
-
) => {
|
|
106
|
+
createCredentialRequestOptions: async (origin: string, username: string, extensionOptions?: any) => {
|
|
110
107
|
if (!f2l) return null;
|
|
111
108
|
|
|
112
109
|
const loginOptions = await f2l.assertionOptions(extensionOptions);
|
|
113
|
-
|
|
110
|
+
// Convert challenge to base64 without using Buffer
|
|
111
|
+
const challenge = btoa(String.fromCharCode(...new Uint8Array(loginOptions.challenge)));
|
|
114
112
|
const { insertedId } = await WebAuthnCredentialsCreationRequests.insertOne({
|
|
115
113
|
_id: new Date().getTime(),
|
|
116
114
|
challenge,
|
|
@@ -135,6 +133,7 @@ export const configureUsersWebAuthnModule = async ({ db }: ModuleInput<any>) =>
|
|
|
135
133
|
},
|
|
136
134
|
{ sort: { _id: -1 } },
|
|
137
135
|
);
|
|
136
|
+
if (!request) return null;
|
|
138
137
|
|
|
139
138
|
const attestationExpectations = {
|
|
140
139
|
challenge: request.challenge,
|
|
@@ -184,6 +183,7 @@ export const configureUsersWebAuthnModule = async ({ db }: ModuleInput<any>) =>
|
|
|
184
183
|
},
|
|
185
184
|
{ sort: { _id: -1 } },
|
|
186
185
|
);
|
|
186
|
+
if (!request) return null;
|
|
187
187
|
|
|
188
188
|
const id = Buffer.from(credentials.id, 'base64');
|
|
189
189
|
const authenticatorData = Buffer.from(credentials.response.authenticatorData, 'base64');
|
package/src/module/pbkdf2.ts
CHANGED
|
@@ -5,7 +5,11 @@ const PBKDF2_SALT_LENGTH = 16; // Bytes
|
|
|
5
5
|
|
|
6
6
|
export function generateSalt(saltLength = PBKDF2_SALT_LENGTH) {
|
|
7
7
|
const array = new Uint8Array(saltLength);
|
|
8
|
-
|
|
8
|
+
crypto.getRandomValues(array);
|
|
9
|
+
// Convert to hex string without using Buffer
|
|
10
|
+
return Array.from(array)
|
|
11
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
12
|
+
.join('');
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
export async function getDerivedKey(
|
|
@@ -30,7 +34,10 @@ export async function getDerivedKey(
|
|
|
30
34
|
importedKey,
|
|
31
35
|
keyLength,
|
|
32
36
|
);
|
|
33
|
-
|
|
37
|
+
// Convert ArrayBuffer to hex string without using Buffer
|
|
38
|
+
return Array.from(new Uint8Array(bits))
|
|
39
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
40
|
+
.join('');
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export async function compare(password: string, hash: string, salt: string) {
|
|
@@ -7,7 +7,6 @@ import { User } from '../db/UsersCollection.js';
|
|
|
7
7
|
describe('removeConfidentialServiceHashes', () => {
|
|
8
8
|
it('Should remove sensitive user credentials ', () => {
|
|
9
9
|
assert.notStrictEqual(user.services, undefined);
|
|
10
|
-
removeConfidentialServiceHashes(user as unknown as User);
|
|
11
|
-
assert.strictEqual(user.services, undefined);
|
|
10
|
+
assert.strictEqual(removeConfidentialServiceHashes(user as unknown as User)?.services, undefined);
|
|
12
11
|
});
|
|
13
12
|
});
|
package/src/users-settings.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { insensitiveTrimmedRegexOperator, mongodb } from '@unchainedshop/mongodb';
|
|
2
2
|
import { User } from './db/UsersCollection.js';
|
|
3
|
-
|
|
4
3
|
export interface UserRegistrationData extends Partial<User> {
|
|
5
4
|
email?: string;
|
|
6
5
|
password: string | null;
|
|
@@ -14,22 +13,6 @@ export enum UserAccountAction {
|
|
|
14
13
|
PASSWORD_RESETTED = 'password-resetted',
|
|
15
14
|
EMAIL_VERIFIED = 'email-verified',
|
|
16
15
|
}
|
|
17
|
-
export interface UserSettingsOptions {
|
|
18
|
-
mergeUserCartsOnLogin?: boolean;
|
|
19
|
-
autoMessagingAfterUserCreation?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
* Function to calculate the earliest valid token date. Defaults to 1h if not set.
|
|
22
|
-
* @param type The type of user account action.
|
|
23
|
-
* @returns A date. All tokens created after that date are considered valid.
|
|
24
|
-
*/
|
|
25
|
-
earliestValidTokenDate?: (
|
|
26
|
-
type: UserAccountAction.VERIFY_EMAIL | UserAccountAction.RESET_PASSWORD,
|
|
27
|
-
) => Date;
|
|
28
|
-
validateEmail?: (email: string) => Promise<boolean>;
|
|
29
|
-
validateUsername?: (username: string) => Promise<boolean>;
|
|
30
|
-
validateNewUser?: (user: UserRegistrationData) => Promise<UserRegistrationData>;
|
|
31
|
-
validatePassword?: (password: string) => Promise<boolean>;
|
|
32
|
-
}
|
|
33
16
|
export interface UserSettings {
|
|
34
17
|
mergeUserCartsOnLogin: boolean;
|
|
35
18
|
autoMessagingAfterUserCreation: boolean;
|
|
@@ -43,14 +26,37 @@ export interface UserSettings {
|
|
|
43
26
|
configureSettings: (options: UserSettingsOptions, db: mongodb.Db) => void;
|
|
44
27
|
}
|
|
45
28
|
|
|
29
|
+
export type UserSettingsOptions = Omit<Partial<UserSettings>, 'configureSettings'>;
|
|
30
|
+
|
|
31
|
+
const defaultAutoMessagingAfterUserCreation = true;
|
|
32
|
+
const defaultMergeUserCartsOnLogin = true;
|
|
33
|
+
|
|
34
|
+
const defaultEarliestValidTokenDate = () => {
|
|
35
|
+
// 1 hour ago
|
|
36
|
+
return new Date(new Date().getTime() - 1000 * 60 * 60);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const defaultValidateNewUser = async (user: UserRegistrationData) => {
|
|
40
|
+
return {
|
|
41
|
+
...user,
|
|
42
|
+
username: user.username?.trim().toLowerCase(),
|
|
43
|
+
email: user.email?.trim().toLowerCase(),
|
|
44
|
+
password: user.password ?? null,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const defaultValidatePassword = async (password: string) => {
|
|
49
|
+
return password?.length >= 8;
|
|
50
|
+
};
|
|
51
|
+
|
|
46
52
|
export const userSettings: UserSettings = {
|
|
47
|
-
autoMessagingAfterUserCreation:
|
|
48
|
-
mergeUserCartsOnLogin:
|
|
49
|
-
earliestValidTokenDate:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
validatePassword:
|
|
53
|
+
autoMessagingAfterUserCreation: defaultAutoMessagingAfterUserCreation,
|
|
54
|
+
mergeUserCartsOnLogin: defaultMergeUserCartsOnLogin,
|
|
55
|
+
earliestValidTokenDate: defaultEarliestValidTokenDate,
|
|
56
|
+
validateNewUser: defaultValidateNewUser,
|
|
57
|
+
validateEmail: () => Promise.resolve(true),
|
|
58
|
+
validateUsername: () => Promise.resolve(true),
|
|
59
|
+
validatePassword: () => Promise.resolve(true),
|
|
54
60
|
|
|
55
61
|
configureSettings: (
|
|
56
62
|
{
|
|
@@ -61,17 +67,9 @@ export const userSettings: UserSettings = {
|
|
|
61
67
|
validateUsername,
|
|
62
68
|
validateNewUser,
|
|
63
69
|
validatePassword,
|
|
64
|
-
}
|
|
70
|
+
},
|
|
65
71
|
db: mongodb.Db,
|
|
66
72
|
) => {
|
|
67
|
-
const defaultAutoMessagingAfterUserCreation = true;
|
|
68
|
-
const defaultMergeUserCartsOnLogin = true;
|
|
69
|
-
|
|
70
|
-
const defaultEarliestValidTokenDate = () => {
|
|
71
|
-
// 1 hour ago
|
|
72
|
-
return new Date(new Date().getTime() - 1000 * 60 * 60);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
73
|
const defaultValidateEmail = async (rawEmail: string) => {
|
|
76
74
|
if (!rawEmail?.includes?.('@')) return false;
|
|
77
75
|
const emailAlreadyExists = await db
|
|
@@ -88,18 +86,6 @@ export const userSettings: UserSettings = {
|
|
|
88
86
|
if (usernameAlreadyExists) return false;
|
|
89
87
|
return true;
|
|
90
88
|
};
|
|
91
|
-
const defaultValidateNewUser = async (user: UserRegistrationData) => {
|
|
92
|
-
return {
|
|
93
|
-
...user,
|
|
94
|
-
username: user.username?.trim().toLowerCase(),
|
|
95
|
-
email: user.email?.trim().toLowerCase(),
|
|
96
|
-
password: user.password ?? null,
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const defaultValidatePassword = async (password: string) => {
|
|
101
|
-
return password?.length >= 8;
|
|
102
|
-
};
|
|
103
89
|
|
|
104
90
|
userSettings.mergeUserCartsOnLogin = mergeUserCartsOnLogin ?? defaultMergeUserCartsOnLogin;
|
|
105
91
|
userSettings.autoMessagingAfterUserCreation =
|