@unchainedshop/core-users 4.0.0-rc.2 → 4.0.0-rc.21
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/configureUsersModule.d.ts +32 -31
- package/lib/module/configureUsersModule.d.ts.map +1 -1
- package/lib/module/configureUsersModule.js +81 -41
- 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 +8 -7
- package/lib/users-settings.d.ts.map +1 -1
- package/lib/users-settings.js +34 -20
- package/lib/users-settings.js.map +1 -1
- package/package.json +10 -10
- package/src/db/UsersCollection.ts +33 -29
- package/src/module/buildFindSelector.test.ts +5 -2
- package/src/module/configureUsersModule.ts +126 -85
- 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 +43 -30
- package/tsconfig.json +2 -1
|
@@ -27,7 +27,7 @@ export interface UserLastLogin {
|
|
|
27
27
|
export interface PushSubscriptionObject {
|
|
28
28
|
userAgent: string;
|
|
29
29
|
endpoint: string;
|
|
30
|
-
expirationTime
|
|
30
|
+
expirationTime: number;
|
|
31
31
|
keys: {
|
|
32
32
|
auth: string;
|
|
33
33
|
p256dh: string;
|
|
@@ -40,7 +40,7 @@ export interface Email {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export type User = {
|
|
43
|
-
_id
|
|
43
|
+
_id: string;
|
|
44
44
|
deleted?: Date;
|
|
45
45
|
avatarId?: string;
|
|
46
46
|
emails: Email[];
|
|
@@ -69,6 +69,37 @@ export type UserQuery = mongodb.Filter<User> & {
|
|
|
69
69
|
export const UsersCollection = async (db: mongodb.Db) => {
|
|
70
70
|
const Users = db.collection<User>('users');
|
|
71
71
|
|
|
72
|
+
if (!isDocumentDBCompatModeEnabled()) {
|
|
73
|
+
await buildDbIndexes<User>(Users, [
|
|
74
|
+
{
|
|
75
|
+
index: {
|
|
76
|
+
_id: 'text',
|
|
77
|
+
username: 'text',
|
|
78
|
+
'emails.address': 'text',
|
|
79
|
+
'profile.displayName': 'text',
|
|
80
|
+
'lastBillingAddress.firstName': 'text',
|
|
81
|
+
'lastBillingAddress.lastName': 'text',
|
|
82
|
+
'lastBillingAddress.company': 'text',
|
|
83
|
+
'lastBillingAddress.addressLine': 'text',
|
|
84
|
+
'lastBillingAddress.addressLine2': 'text',
|
|
85
|
+
} as any,
|
|
86
|
+
options: {
|
|
87
|
+
weights: {
|
|
88
|
+
_id: 9,
|
|
89
|
+
'emails.address': 7,
|
|
90
|
+
'profile.displayName': 5,
|
|
91
|
+
'lastBillingAddress.firstName': 3,
|
|
92
|
+
'lastBillingAddress.lastName': 3,
|
|
93
|
+
'lastBillingAddress.company': 1,
|
|
94
|
+
'lastBillingAddress.addressLine': 1,
|
|
95
|
+
'lastBillingAddress.addressLine2': 1,
|
|
96
|
+
},
|
|
97
|
+
name: 'user_fulltext_search',
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
|
|
72
103
|
await buildDbIndexes<User>(Users, [
|
|
73
104
|
{
|
|
74
105
|
index: {
|
|
@@ -137,33 +168,6 @@ export const UsersCollection = async (db: mongodb.Db) => {
|
|
|
137
168
|
sparse: true,
|
|
138
169
|
},
|
|
139
170
|
},
|
|
140
|
-
|
|
141
|
-
!isDocumentDBCompatModeEnabled() && {
|
|
142
|
-
index: {
|
|
143
|
-
_id: 'text',
|
|
144
|
-
username: 'text',
|
|
145
|
-
'emails.address': 'text',
|
|
146
|
-
'profile.displayName': 'text',
|
|
147
|
-
'lastBillingAddress.firstName': 'text',
|
|
148
|
-
'lastBillingAddress.lastName': 'text',
|
|
149
|
-
'lastBillingAddress.company': 'text',
|
|
150
|
-
'lastBillingAddress.addressLine': 'text',
|
|
151
|
-
'lastBillingAddress.addressLine2': 'text',
|
|
152
|
-
} as any,
|
|
153
|
-
options: {
|
|
154
|
-
weights: {
|
|
155
|
-
_id: 9,
|
|
156
|
-
'emails.address': 7,
|
|
157
|
-
'profile.displayName': 5,
|
|
158
|
-
'lastBillingAddress.firstName': 3,
|
|
159
|
-
'lastBillingAddress.lastName': 3,
|
|
160
|
-
'lastBillingAddress.company': 1,
|
|
161
|
-
'lastBillingAddress.addressLine': 1,
|
|
162
|
-
'lastBillingAddress.addressLine2': 1,
|
|
163
|
-
},
|
|
164
|
-
name: 'user_fulltext_search',
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
171
|
]);
|
|
168
172
|
|
|
169
173
|
return Users;
|
|
@@ -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,
|
|
@@ -20,7 +20,12 @@ import {
|
|
|
20
20
|
} from '../db/UsersCollection.js';
|
|
21
21
|
import { emit, registerEvents } from '@unchainedshop/events';
|
|
22
22
|
import { systemLocale, SortDirection, SortOption, sha256 } from '@unchainedshop/utils';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
UserAccountAction,
|
|
25
|
+
UserRegistrationData,
|
|
26
|
+
userSettings,
|
|
27
|
+
UserSettingsOptions,
|
|
28
|
+
} from '../users-settings.js';
|
|
24
29
|
import { configureUsersWebAuthnModule } from './configureUsersWebAuthnModule.js';
|
|
25
30
|
import * as pbkdf2 from './pbkdf2.js';
|
|
26
31
|
|
|
@@ -42,7 +47,7 @@ const USER_EVENTS = [
|
|
|
42
47
|
'USER_REMOVE',
|
|
43
48
|
];
|
|
44
49
|
export const removeConfidentialServiceHashes = (rawUser: User): User => {
|
|
45
|
-
const user = rawUser;
|
|
50
|
+
const user = { ...rawUser };
|
|
46
51
|
delete user?.services;
|
|
47
52
|
return user;
|
|
48
53
|
};
|
|
@@ -53,14 +58,18 @@ export const buildFindSelector = ({
|
|
|
53
58
|
queryString,
|
|
54
59
|
emailVerified,
|
|
55
60
|
lastLogin,
|
|
61
|
+
tags,
|
|
56
62
|
...rest
|
|
57
63
|
}: UserQuery) => {
|
|
58
64
|
const selector: mongodb.Filter<User> = { ...rest };
|
|
59
|
-
if (!includeDeleted) selector.deleted = null;
|
|
60
|
-
if (!includeGuests) selector.guest = { $
|
|
65
|
+
if (!includeDeleted) selector.deleted = null as any;
|
|
66
|
+
if (!includeGuests) selector.guest = { $ne: true };
|
|
61
67
|
if (emailVerified === true) {
|
|
62
68
|
selector['emails.verified'] = true;
|
|
63
69
|
}
|
|
70
|
+
if (Array.isArray(tags) && tags?.length) {
|
|
71
|
+
selector.tags = { $in: tags };
|
|
72
|
+
}
|
|
64
73
|
if (emailVerified === false) {
|
|
65
74
|
// We need to use $ne here else we'd also find users with many emails where one is
|
|
66
75
|
// unverified
|
|
@@ -82,12 +91,13 @@ export const buildFindSelector = ({
|
|
|
82
91
|
return selector;
|
|
83
92
|
};
|
|
84
93
|
|
|
85
|
-
export const configureUsersModule = async (
|
|
94
|
+
export const configureUsersModule = async (moduleInput: ModuleInput<UserSettingsOptions>) => {
|
|
95
|
+
const { db, options } = moduleInput;
|
|
86
96
|
userSettings.configureSettings(options || {}, db);
|
|
87
97
|
registerEvents(USER_EVENTS);
|
|
88
98
|
const Users = await UsersCollection(db);
|
|
89
99
|
|
|
90
|
-
const webAuthn = await configureUsersWebAuthnModule(
|
|
100
|
+
const webAuthn = await configureUsersWebAuthnModule(moduleInput);
|
|
91
101
|
|
|
92
102
|
return {
|
|
93
103
|
// Queries
|
|
@@ -97,17 +107,17 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
97
107
|
return userCount;
|
|
98
108
|
},
|
|
99
109
|
|
|
100
|
-
async findUserById(userId: string)
|
|
110
|
+
async findUserById(userId: string) {
|
|
101
111
|
if (!userId) return null;
|
|
102
112
|
return Users.findOne(generateDbFilterById(userId), {});
|
|
103
113
|
},
|
|
104
114
|
|
|
105
|
-
async findUserByUsername(username: string)
|
|
115
|
+
async findUserByUsername(username: string) {
|
|
106
116
|
if (!username) return null;
|
|
107
117
|
return Users.findOne({ username: insensitiveTrimmedRegexOperator(username) }, {});
|
|
108
118
|
},
|
|
109
119
|
|
|
110
|
-
async findUserByEmail(email: string)
|
|
120
|
+
async findUserByEmail(email: string) {
|
|
111
121
|
if (!email) return null;
|
|
112
122
|
return Users.findOne({ 'emails.address': insensitiveTrimmedRegexOperator(email) }, {});
|
|
113
123
|
},
|
|
@@ -116,7 +126,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
116
126
|
userId: string;
|
|
117
127
|
address: string;
|
|
118
128
|
when: Date;
|
|
119
|
-
}> {
|
|
129
|
+
} | null> {
|
|
120
130
|
if (!plainToken) return null;
|
|
121
131
|
const token = await sha256(plainToken);
|
|
122
132
|
const user = await Users.findOne(
|
|
@@ -124,7 +134,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
124
134
|
'services.email.verificationTokens': {
|
|
125
135
|
$elemMatch: {
|
|
126
136
|
token,
|
|
127
|
-
when: { $gt:
|
|
137
|
+
when: { $gt: userSettings.earliestValidTokenDate(UserAccountAction.VERIFY_EMAIL) },
|
|
128
138
|
},
|
|
129
139
|
},
|
|
130
140
|
},
|
|
@@ -158,7 +168,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
158
168
|
|
|
159
169
|
if (updated.modifiedCount > 0) {
|
|
160
170
|
await emit('USER_ACCOUNT_ACTION', {
|
|
161
|
-
action:
|
|
171
|
+
action: UserAccountAction.EMAIL_VERIFIED,
|
|
162
172
|
address,
|
|
163
173
|
userId,
|
|
164
174
|
});
|
|
@@ -169,14 +179,14 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
169
179
|
userId: string;
|
|
170
180
|
address: string;
|
|
171
181
|
when: Date;
|
|
172
|
-
}> {
|
|
182
|
+
} | null> {
|
|
173
183
|
const token = await sha256(plainToken);
|
|
174
184
|
const user = await Users.findOne(
|
|
175
185
|
{
|
|
176
186
|
'services.password.reset': {
|
|
177
187
|
$elemMatch: {
|
|
178
188
|
token,
|
|
179
|
-
when: { $gt:
|
|
189
|
+
when: { $gt: userSettings.earliestValidTokenDate(UserAccountAction.RESET_PASSWORD) },
|
|
180
190
|
},
|
|
181
191
|
},
|
|
182
192
|
},
|
|
@@ -190,7 +200,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
190
200
|
};
|
|
191
201
|
},
|
|
192
202
|
|
|
193
|
-
async findUserByToken(plainToken: string)
|
|
203
|
+
async findUserByToken(plainToken: string) {
|
|
194
204
|
const token = await sha256(plainToken);
|
|
195
205
|
|
|
196
206
|
if (token) {
|
|
@@ -202,10 +212,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
202
212
|
return null;
|
|
203
213
|
},
|
|
204
214
|
|
|
205
|
-
async findUser(
|
|
206
|
-
query: UserQuery & { sort?: SortOption[] },
|
|
207
|
-
findOptions?: mongodb.FindOptions,
|
|
208
|
-
): Promise<User> {
|
|
215
|
+
async findUser(query: UserQuery & { sort?: SortOption[] }, findOptions?: mongodb.FindOptions) {
|
|
209
216
|
const selector = buildFindSelector(query);
|
|
210
217
|
return Users.findOne(selector, findOptions);
|
|
211
218
|
},
|
|
@@ -239,7 +246,10 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
239
246
|
},
|
|
240
247
|
|
|
241
248
|
async userExists({ userId }: { userId: string }): Promise<boolean> {
|
|
242
|
-
const userCount = await Users.countDocuments(
|
|
249
|
+
const userCount = await Users.countDocuments(
|
|
250
|
+
{ _id: userId, deleted: { $exists: false } },
|
|
251
|
+
{ limit: 1 },
|
|
252
|
+
);
|
|
243
253
|
return userCount === 1;
|
|
244
254
|
},
|
|
245
255
|
|
|
@@ -250,7 +260,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
250
260
|
)?.[0];
|
|
251
261
|
},
|
|
252
262
|
|
|
253
|
-
userLocale(user: User): Intl.Locale {
|
|
263
|
+
userLocale(user: User | null): Intl.Locale {
|
|
254
264
|
if (!user?.lastLogin?.locale) return systemLocale;
|
|
255
265
|
try {
|
|
256
266
|
return new Intl.Locale(user.lastLogin.locale);
|
|
@@ -339,7 +349,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
339
349
|
/* */
|
|
340
350
|
}
|
|
341
351
|
|
|
342
|
-
const user = await Users.findOne({ _id: userId }, {});
|
|
352
|
+
const user = (await Users.findOne({ _id: userId }, {})) as User;
|
|
343
353
|
await emit('USER_CREATE', {
|
|
344
354
|
user: removeConfidentialServiceHashes(user),
|
|
345
355
|
});
|
|
@@ -416,7 +426,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
416
426
|
);
|
|
417
427
|
|
|
418
428
|
await emit('USER_ACCOUNT_ACTION', {
|
|
419
|
-
action: isEnrollment ?
|
|
429
|
+
action: isEnrollment ? UserAccountAction.ENROLL_ACCOUNT : UserAccountAction.RESET_PASSWORD,
|
|
420
430
|
userId,
|
|
421
431
|
...resetToken,
|
|
422
432
|
token: plainToken,
|
|
@@ -441,46 +451,53 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
441
451
|
);
|
|
442
452
|
|
|
443
453
|
await emit('USER_ACCOUNT_ACTION', {
|
|
444
|
-
action:
|
|
454
|
+
action: UserAccountAction.VERIFY_EMAIL,
|
|
445
455
|
userId,
|
|
446
456
|
...verificationToken,
|
|
447
457
|
token: plainToken,
|
|
448
458
|
});
|
|
449
459
|
},
|
|
450
460
|
|
|
451
|
-
addRoles: async (userId: string, roles: string[])
|
|
461
|
+
addRoles: async (userId: string, roles: string[]) => {
|
|
452
462
|
const selector = generateDbFilterById(userId);
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
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;
|
|
456
472
|
|
|
457
|
-
const user = await Users.findOne(selector, {});
|
|
458
473
|
await emit('USER_ADD_ROLES', {
|
|
459
474
|
user: removeConfidentialServiceHashes(user),
|
|
460
475
|
});
|
|
461
476
|
|
|
462
|
-
return
|
|
477
|
+
return user;
|
|
463
478
|
},
|
|
464
479
|
|
|
465
480
|
async setUsername(userId: string, username: string) {
|
|
466
481
|
if (!(await userSettings.validateUsername(username))) {
|
|
467
482
|
throw new Error(`Username ${username} is invalid`, { cause: 'USERNAME_INVALID' });
|
|
468
483
|
}
|
|
469
|
-
await Users.
|
|
484
|
+
const user = await Users.findOneAndUpdate(
|
|
470
485
|
{ _id: userId },
|
|
471
486
|
{
|
|
472
487
|
$set: {
|
|
473
488
|
username: username.trim(),
|
|
474
489
|
},
|
|
475
490
|
},
|
|
491
|
+
{ returnDocument: 'after' },
|
|
476
492
|
);
|
|
477
|
-
|
|
493
|
+
if (!user) return null;
|
|
478
494
|
await emit('USER_UPDATE_USERNAME', {
|
|
479
495
|
user: removeConfidentialServiceHashes(user),
|
|
480
496
|
});
|
|
497
|
+
return user;
|
|
481
498
|
},
|
|
482
499
|
|
|
483
|
-
async setPassword(userId: string, plainPassword: string)
|
|
500
|
+
async setPassword(userId: string, plainPassword: string) {
|
|
484
501
|
if (!(await userSettings.validatePassword(plainPassword))) {
|
|
485
502
|
throw new Error(`Password ***** is invalid`, { cause: 'PASSWORD_INVALID' });
|
|
486
503
|
}
|
|
@@ -497,13 +514,14 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
497
514
|
},
|
|
498
515
|
{ returnDocument: 'after' },
|
|
499
516
|
);
|
|
517
|
+
if (!user) return null;
|
|
500
518
|
await emit('USER_UPDATE_PASSWORD', {
|
|
501
519
|
user: removeConfidentialServiceHashes(user),
|
|
502
520
|
});
|
|
503
521
|
return user;
|
|
504
522
|
},
|
|
505
523
|
|
|
506
|
-
async resetPassword(token: string, newPassword: string)
|
|
524
|
+
async resetPassword(token: string, newPassword: string) {
|
|
507
525
|
const resetToken = await this.findResetToken(token);
|
|
508
526
|
if (!resetToken) return null;
|
|
509
527
|
const updatedUser = await this.setPassword(resetToken.userId, newPassword);
|
|
@@ -522,7 +540,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
522
540
|
},
|
|
523
541
|
);
|
|
524
542
|
await emit('USER_ACCOUNT_ACTION', {
|
|
525
|
-
action:
|
|
543
|
+
action: UserAccountAction.PASSWORD_RESETTED,
|
|
526
544
|
userId: updatedUser._id,
|
|
527
545
|
});
|
|
528
546
|
|
|
@@ -531,7 +549,7 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
531
549
|
return updatedUser;
|
|
532
550
|
},
|
|
533
551
|
|
|
534
|
-
updateAvatar: async (_id: string, fileId: string)
|
|
552
|
+
updateAvatar: async (_id: string, fileId: string) => {
|
|
535
553
|
const userFilter = generateDbFilterById(_id);
|
|
536
554
|
const modifier = {
|
|
537
555
|
$set: {
|
|
@@ -544,44 +562,54 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
544
562
|
returnDocument: 'after',
|
|
545
563
|
});
|
|
546
564
|
|
|
565
|
+
if (!user) return null;
|
|
547
566
|
await emit('USER_UPDATE_AVATAR', {
|
|
548
567
|
user: removeConfidentialServiceHashes(user),
|
|
549
568
|
});
|
|
550
569
|
return user;
|
|
551
570
|
},
|
|
552
571
|
|
|
553
|
-
updateGuest: async (user: User, guest: boolean)
|
|
554
|
-
const
|
|
555
|
-
|
|
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;
|
|
556
581
|
await emit('USER_UPDATE_GUEST', {
|
|
557
582
|
user: removeConfidentialServiceHashes({
|
|
558
|
-
...
|
|
583
|
+
...updatedUser,
|
|
559
584
|
guest,
|
|
560
585
|
}),
|
|
561
586
|
});
|
|
587
|
+
return updatedUser;
|
|
562
588
|
},
|
|
563
589
|
|
|
564
|
-
updateHeartbeat: async (userId: string, lastLogin: UserLastLogin)
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
+
},
|
|
570
599
|
},
|
|
571
600
|
},
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
601
|
+
{
|
|
602
|
+
returnDocument: 'after',
|
|
603
|
+
},
|
|
604
|
+
);
|
|
605
|
+
if (!user) return null;
|
|
578
606
|
await emit('USER_UPDATE_HEARTBEAT', {
|
|
579
607
|
user: removeConfidentialServiceHashes(user),
|
|
580
608
|
});
|
|
581
609
|
return user;
|
|
582
610
|
},
|
|
583
611
|
|
|
584
|
-
markDeleted: async (userId: string)
|
|
612
|
+
markDeleted: async (userId: string) => {
|
|
585
613
|
await db.collection('sessions').deleteMany({
|
|
586
614
|
session: insensitiveTrimmedRegexOperator(`"user":"${userId}"`),
|
|
587
615
|
});
|
|
@@ -593,18 +621,21 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
593
621
|
deleted: new Date(),
|
|
594
622
|
emails: [],
|
|
595
623
|
roles: [],
|
|
596
|
-
profile: null,
|
|
597
|
-
lastBillingAddress: null,
|
|
598
624
|
services: {},
|
|
599
625
|
pushSubscriptions: [],
|
|
600
|
-
avatarId: null,
|
|
601
626
|
initialPassword: false,
|
|
602
|
-
|
|
603
|
-
|
|
627
|
+
},
|
|
628
|
+
$unset: {
|
|
629
|
+
profile: 1,
|
|
630
|
+
lastBillingAddress: 1,
|
|
631
|
+
lastContact: 1,
|
|
632
|
+
lastLogin: 1,
|
|
633
|
+
avatarId: 1,
|
|
604
634
|
},
|
|
605
635
|
},
|
|
606
636
|
{ returnDocument: 'after' },
|
|
607
637
|
);
|
|
638
|
+
if (!user) return null;
|
|
608
639
|
|
|
609
640
|
await emit('USER_REMOVE', {
|
|
610
641
|
user,
|
|
@@ -612,14 +643,11 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
612
643
|
return user;
|
|
613
644
|
},
|
|
614
645
|
|
|
615
|
-
deletePermanently: async ({ userId }: { userId: string })
|
|
646
|
+
deletePermanently: async ({ userId }: { userId: string }) => {
|
|
616
647
|
return Users.findOneAndDelete({ _id: userId });
|
|
617
648
|
},
|
|
618
649
|
|
|
619
|
-
updateProfile: async (
|
|
620
|
-
userId: string,
|
|
621
|
-
updatedData: { profile?: UserProfile; meta?: any },
|
|
622
|
-
): Promise<User> => {
|
|
650
|
+
updateProfile: async (userId: string, updatedData: { profile?: UserProfile; meta?: any }) => {
|
|
623
651
|
const userFilter = generateDbFilterById(userId);
|
|
624
652
|
const { meta, profile } = updatedData;
|
|
625
653
|
|
|
@@ -648,16 +676,17 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
648
676
|
returnDocument: 'after',
|
|
649
677
|
});
|
|
650
678
|
|
|
679
|
+
if (!user) return null;
|
|
651
680
|
await emit('USER_UPDATE_PROFILE', {
|
|
652
681
|
user: removeConfidentialServiceHashes(user),
|
|
653
682
|
});
|
|
654
683
|
return user;
|
|
655
684
|
},
|
|
656
685
|
|
|
657
|
-
updateLastBillingAddress: async (_id: string, lastBillingAddress: Address)
|
|
686
|
+
updateLastBillingAddress: async (_id: string, lastBillingAddress: Address) => {
|
|
658
687
|
const userFilter = generateDbFilterById(_id);
|
|
659
688
|
const user = await Users.findOne(userFilter, {});
|
|
660
|
-
|
|
689
|
+
if (!user) return null;
|
|
661
690
|
if (!lastBillingAddress) return user;
|
|
662
691
|
|
|
663
692
|
const modifier = {
|
|
@@ -682,18 +711,17 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
682
711
|
returnDocument: 'after',
|
|
683
712
|
});
|
|
684
713
|
|
|
685
|
-
if (updatedUser)
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
}
|
|
690
|
-
|
|
714
|
+
if (!updatedUser) return null;
|
|
715
|
+
await emit('USER_UPDATE_BILLING_ADDRESS', {
|
|
716
|
+
user: removeConfidentialServiceHashes(updatedUser),
|
|
717
|
+
});
|
|
691
718
|
return updatedUser;
|
|
692
719
|
},
|
|
693
720
|
|
|
694
|
-
updateLastContact: async (_id: string, lastContact: Contact)
|
|
721
|
+
updateLastContact: async (_id: string, lastContact: Contact) => {
|
|
695
722
|
const userFilter = generateDbFilterById(_id);
|
|
696
723
|
const user = await Users.findOne(userFilter, {});
|
|
724
|
+
if (!user) return null;
|
|
697
725
|
const profile = user.profile || {};
|
|
698
726
|
const isGuest = !!user.guest;
|
|
699
727
|
|
|
@@ -713,13 +741,14 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
713
741
|
returnDocument: 'after',
|
|
714
742
|
});
|
|
715
743
|
|
|
744
|
+
if (!updatedUser) return null;
|
|
716
745
|
await emit('USER_UPDATE_LAST_CONTACT', {
|
|
717
746
|
user: removeConfidentialServiceHashes(user),
|
|
718
747
|
});
|
|
719
748
|
return updatedUser;
|
|
720
749
|
},
|
|
721
750
|
|
|
722
|
-
updateRoles: async (_id: string, roles: string[])
|
|
751
|
+
updateRoles: async (_id: string, roles: string[]) => {
|
|
723
752
|
const modifier = {
|
|
724
753
|
$set: {
|
|
725
754
|
updated: new Date(),
|
|
@@ -730,23 +759,27 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
730
759
|
const user = await Users.findOneAndUpdate(generateDbFilterById(_id), modifier, {
|
|
731
760
|
returnDocument: 'after',
|
|
732
761
|
});
|
|
733
|
-
|
|
762
|
+
if (!user) return null;
|
|
734
763
|
await emit('USER_UPDATE_ROLE', {
|
|
735
764
|
user: removeConfidentialServiceHashes(user),
|
|
736
765
|
});
|
|
737
766
|
return user;
|
|
738
767
|
},
|
|
739
768
|
|
|
740
|
-
updateTags: async (_id: string, tags: string[])
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
+
},
|
|
745
777
|
},
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
778
|
+
{
|
|
779
|
+
returnDocument: 'after',
|
|
780
|
+
},
|
|
781
|
+
);
|
|
782
|
+
if (!user) return null;
|
|
750
783
|
await emit('USER_UPDATE_TAGS', {
|
|
751
784
|
user: removeConfidentialServiceHashes(user),
|
|
752
785
|
});
|
|
@@ -757,11 +790,12 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
757
790
|
selector: mongodb.Filter<User>,
|
|
758
791
|
modifier: mongodb.UpdateFilter<User>,
|
|
759
792
|
updateOptions?: mongodb.FindOneAndUpdateOptions,
|
|
760
|
-
)
|
|
793
|
+
) => {
|
|
761
794
|
const user = await Users.findOneAndUpdate(selector, modifier, {
|
|
762
795
|
...updateOptions,
|
|
763
796
|
returnDocument: 'after',
|
|
764
797
|
});
|
|
798
|
+
if (!user) return null;
|
|
765
799
|
await emit('USER_UPDATE', {
|
|
766
800
|
user: removeConfidentialServiceHashes(user),
|
|
767
801
|
});
|
|
@@ -811,6 +845,13 @@ export const configureUsersModule = async ({ db, options }: ModuleInput<UserSett
|
|
|
811
845
|
{},
|
|
812
846
|
);
|
|
813
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
|
+
},
|
|
814
855
|
};
|
|
815
856
|
};
|
|
816
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');
|