@unchainedshop/core-users 5.0.0-alpha.1 → 5.0.0-alpha.3
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.js +51 -29
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts +1 -0
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js +2 -5
- package/lib/migrations/20260625120200-normalize-profile-phone.d.ts +2 -0
- package/lib/migrations/20260625120200-normalize-profile-phone.js +52 -0
- package/lib/module/configureUsersModule.d.ts +6 -0
- package/lib/module/configureUsersModule.js +53 -9
- package/lib/module/configureUsersWebAuthnModule.js +3 -1
- package/lib/users-settings.d.ts +1 -0
- package/lib/users-settings.js +4 -1
- package/package.json +2 -3
|
@@ -1,36 +1,34 @@
|
|
|
1
|
-
import { buildDbIndexes,
|
|
1
|
+
import { buildDbIndexes, } from '@unchainedshop/mongodb';
|
|
2
2
|
export const UsersCollection = async (db) => {
|
|
3
3
|
const Users = db.collection('users');
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
'lastBillingAddress.addressLine2': 1,
|
|
28
|
-
},
|
|
29
|
-
name: 'user_fulltext_search',
|
|
4
|
+
await buildDbIndexes(Users, [
|
|
5
|
+
{
|
|
6
|
+
index: {
|
|
7
|
+
_id: 'text',
|
|
8
|
+
username: 'text',
|
|
9
|
+
'emails.address': 'text',
|
|
10
|
+
'profile.displayName': 'text',
|
|
11
|
+
'lastBillingAddress.firstName': 'text',
|
|
12
|
+
'lastBillingAddress.lastName': 'text',
|
|
13
|
+
'lastBillingAddress.company': 'text',
|
|
14
|
+
'lastBillingAddress.addressLine': 'text',
|
|
15
|
+
'lastBillingAddress.addressLine2': 'text',
|
|
16
|
+
},
|
|
17
|
+
options: {
|
|
18
|
+
weights: {
|
|
19
|
+
_id: 9,
|
|
20
|
+
'emails.address': 7,
|
|
21
|
+
'profile.displayName': 5,
|
|
22
|
+
'lastBillingAddress.firstName': 3,
|
|
23
|
+
'lastBillingAddress.lastName': 3,
|
|
24
|
+
'lastBillingAddress.company': 1,
|
|
25
|
+
'lastBillingAddress.addressLine': 1,
|
|
26
|
+
'lastBillingAddress.addressLine2': 1,
|
|
30
27
|
},
|
|
28
|
+
name: 'user_fulltext_search',
|
|
31
29
|
},
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
34
32
|
await buildDbIndexes(Users, [
|
|
35
33
|
{
|
|
36
34
|
index: {
|
|
@@ -96,6 +94,30 @@ export const UsersCollection = async (db) => {
|
|
|
96
94
|
sparse: true,
|
|
97
95
|
},
|
|
98
96
|
},
|
|
97
|
+
{
|
|
98
|
+
index: {
|
|
99
|
+
'services.web3.verified': 1,
|
|
100
|
+
},
|
|
101
|
+
options: {
|
|
102
|
+
sparse: true,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
index: {
|
|
107
|
+
tags: 1,
|
|
108
|
+
},
|
|
109
|
+
options: {
|
|
110
|
+
sparse: true,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
index: {
|
|
115
|
+
'lastLogin.timestamp': 1,
|
|
116
|
+
},
|
|
117
|
+
options: {
|
|
118
|
+
sparse: true,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
99
121
|
]);
|
|
100
122
|
return Users;
|
|
101
123
|
};
|
|
@@ -2,11 +2,8 @@ import { buildDbIndexes } from '@unchainedshop/mongodb';
|
|
|
2
2
|
export const WebAuthnCredentialsCreationRequestsCollection = async (db) => {
|
|
3
3
|
const WebAuthnCredentialsCreationRequests = db.collection('accounts_webauthn_credentials_creation_requests');
|
|
4
4
|
await buildDbIndexes(WebAuthnCredentialsCreationRequests, [
|
|
5
|
-
{
|
|
6
|
-
|
|
7
|
-
username: 1,
|
|
8
|
-
},
|
|
9
|
-
},
|
|
5
|
+
{ index: { username: 1 } },
|
|
6
|
+
{ index: { created: 1 }, options: { expireAfterSeconds: 15 * 60 } },
|
|
10
7
|
]);
|
|
11
8
|
return WebAuthnCredentialsCreationRequests;
|
|
12
9
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { normalizePhoneNumber } from '@unchainedshop/utils';
|
|
2
|
+
import { UsersCollection } from "../db/UsersCollection.js";
|
|
3
|
+
export default function normalizeProfilePhone(repository) {
|
|
4
|
+
repository?.register({
|
|
5
|
+
id: 20260625120200,
|
|
6
|
+
name: 'Normalize user.profile.phoneMobile and user.lastContact.telNumber to E.164 format',
|
|
7
|
+
up: async ({ logger }) => {
|
|
8
|
+
const Users = await UsersCollection(repository.db);
|
|
9
|
+
const users = await Users.find({
|
|
10
|
+
$or: [
|
|
11
|
+
{ 'profile.phoneMobile': { $exists: true, $nin: [null, ''] } },
|
|
12
|
+
{ 'lastContact.telNumber': { $exists: true, $nin: [null, ''] } },
|
|
13
|
+
],
|
|
14
|
+
}, {
|
|
15
|
+
projection: {
|
|
16
|
+
_id: true,
|
|
17
|
+
profile: true,
|
|
18
|
+
lastContact: true,
|
|
19
|
+
lastBillingAddress: true,
|
|
20
|
+
lastLogin: true,
|
|
21
|
+
},
|
|
22
|
+
}).toArray();
|
|
23
|
+
let changed = 0;
|
|
24
|
+
let skipped = 0;
|
|
25
|
+
for (const user of users) {
|
|
26
|
+
const defaultCountry = user.lastBillingAddress?.countryCode ||
|
|
27
|
+
user.profile?.address?.countryCode ||
|
|
28
|
+
user.lastLogin?.countryCode;
|
|
29
|
+
const $set = {};
|
|
30
|
+
const phoneMobile = user.profile?.phoneMobile;
|
|
31
|
+
const normalizedPhoneMobile = normalizePhoneNumber(phoneMobile, defaultCountry);
|
|
32
|
+
if (normalizedPhoneMobile && normalizedPhoneMobile !== phoneMobile) {
|
|
33
|
+
$set['profile.phoneMobile'] = normalizedPhoneMobile;
|
|
34
|
+
}
|
|
35
|
+
const telNumber = user.lastContact?.telNumber;
|
|
36
|
+
const normalizedTelNumber = normalizePhoneNumber(telNumber, defaultCountry);
|
|
37
|
+
if (normalizedTelNumber && normalizedTelNumber !== telNumber) {
|
|
38
|
+
$set['lastContact.telNumber'] = normalizedTelNumber;
|
|
39
|
+
}
|
|
40
|
+
if (Object.keys($set).length === 0) {
|
|
41
|
+
if ((phoneMobile && !normalizedPhoneMobile) || (telNumber && !normalizedTelNumber)) {
|
|
42
|
+
skipped += 1;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
await Users.updateOne({ _id: user._id }, { $set });
|
|
47
|
+
changed += 1;
|
|
48
|
+
}
|
|
49
|
+
logger?.info(`Normalize user phone numbers: ${changed} updated, ${skipped} left unchanged (unparseable)`);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -64,6 +64,12 @@ export declare const configureUsersModule: (moduleInput: ModuleInput<UserSetting
|
|
|
64
64
|
limit?: number;
|
|
65
65
|
offset?: number;
|
|
66
66
|
}): Promise<User[]>;
|
|
67
|
+
findGuestUserIds({ before }: {
|
|
68
|
+
before: Date;
|
|
69
|
+
}): Promise<string[]>;
|
|
70
|
+
findExistingUserIds({ userIds }: {
|
|
71
|
+
userIds: string[];
|
|
72
|
+
}): Promise<string[]>;
|
|
67
73
|
userExists({ userId }: {
|
|
68
74
|
userId: string;
|
|
69
75
|
}): Promise<boolean>;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as bcrypt from 'bcryptjs';
|
|
2
|
-
import { generateDbFilterById, buildSortOptions, generateDbObjectId, insensitiveTrimmedRegexOperator,
|
|
2
|
+
import { generateDbFilterById, buildSortOptions, generateDbObjectId, insensitiveTrimmedRegexOperator, } from '@unchainedshop/mongodb';
|
|
3
3
|
import { UsersCollection, } from "../db/UsersCollection.js";
|
|
4
4
|
import { emit, registerEvents } from '@unchainedshop/events';
|
|
5
|
-
import { systemLocale, SortDirection, sha256 } from '@unchainedshop/utils';
|
|
5
|
+
import { systemLocale, SortDirection, sha256, normalizePhoneNumber, } from '@unchainedshop/utils';
|
|
6
6
|
import { UserAccountAction, userSettings, } from "../users-settings.js";
|
|
7
7
|
import { configureUsersWebAuthnModule } from "./configureUsersWebAuthnModule.js";
|
|
8
8
|
import * as pbkdf2 from "./pbkdf2.js";
|
|
9
9
|
import { verifyWeb3Signature } from "../utils/web3-verification.js";
|
|
10
10
|
import convertUserLocale from "../migrations/20241218092300-convert-locale.js";
|
|
11
|
+
import normalizeProfilePhone from "../migrations/20260625120200-normalize-profile-phone.js";
|
|
11
12
|
const USER_EVENTS = [
|
|
12
13
|
'USER_ACCOUNT_ACTION',
|
|
13
14
|
'USER_CREATE',
|
|
@@ -75,7 +76,6 @@ export const buildFindSelector = ({ includeGuests, includeDeleted, queryString,
|
|
|
75
76
|
selector['lastLogin.timestamp'].$gte = new Date(lastLogin.start);
|
|
76
77
|
}
|
|
77
78
|
if (queryString) {
|
|
78
|
-
assertDocumentDBCompatMode();
|
|
79
79
|
selector.$text = { $search: queryString };
|
|
80
80
|
}
|
|
81
81
|
return selector;
|
|
@@ -83,6 +83,7 @@ export const buildFindSelector = ({ includeGuests, includeDeleted, queryString,
|
|
|
83
83
|
export const configureUsersModule = async (moduleInput) => {
|
|
84
84
|
const { db, options, migrationRepository } = moduleInput;
|
|
85
85
|
convertUserLocale(migrationRepository);
|
|
86
|
+
normalizeProfilePhone(migrationRepository);
|
|
86
87
|
userSettings.configureSettings(options || {}, db);
|
|
87
88
|
registerEvents(USER_EVENTS);
|
|
88
89
|
const Users = await UsersCollection(db);
|
|
@@ -198,6 +199,23 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
198
199
|
sort: buildSortOptions(query.sort || defaultSort),
|
|
199
200
|
}).toArray();
|
|
200
201
|
},
|
|
202
|
+
async findGuestUserIds({ before }) {
|
|
203
|
+
const users = await Users.find({
|
|
204
|
+
guest: true,
|
|
205
|
+
deleted: null,
|
|
206
|
+
created: { $lte: before },
|
|
207
|
+
$or: [
|
|
208
|
+
{ 'lastLogin.timestamp': { $exists: false } },
|
|
209
|
+
{ 'lastLogin.timestamp': { $lte: before } },
|
|
210
|
+
],
|
|
211
|
+
}, { projection: { _id: 1 } }).toArray();
|
|
212
|
+
return users.map((u) => u._id);
|
|
213
|
+
},
|
|
214
|
+
async findExistingUserIds({ userIds }) {
|
|
215
|
+
if (!userIds.length)
|
|
216
|
+
return [];
|
|
217
|
+
return Users.distinct('_id', { _id: { $in: userIds } });
|
|
218
|
+
},
|
|
201
219
|
async userExists({ userId }) {
|
|
202
220
|
const userCount = await Users.countDocuments({ _id: userId, deleted: null }, { limit: 1 });
|
|
203
221
|
return userCount === 1;
|
|
@@ -624,11 +642,28 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
624
642
|
return Users.findOne(userFilter, {});
|
|
625
643
|
}
|
|
626
644
|
const modifier = { $set: {} };
|
|
627
|
-
|
|
628
|
-
|
|
645
|
+
let normalizedProfile = profile;
|
|
646
|
+
if (profile?.phoneMobile) {
|
|
647
|
+
let defaultCountry = profile.address?.countryCode;
|
|
648
|
+
if (!defaultCountry) {
|
|
649
|
+
const existing = await Users.findOne(userFilter, {
|
|
650
|
+
projection: { lastBillingAddress: true, profile: true, lastLogin: true },
|
|
651
|
+
});
|
|
652
|
+
defaultCountry =
|
|
653
|
+
existing?.lastBillingAddress?.countryCode ||
|
|
654
|
+
existing?.profile?.address?.countryCode ||
|
|
655
|
+
existing?.lastLogin?.countryCode;
|
|
656
|
+
}
|
|
657
|
+
normalizedProfile = {
|
|
658
|
+
...profile,
|
|
659
|
+
phoneMobile: normalizePhoneNumber(profile.phoneMobile, defaultCountry) || profile.phoneMobile,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
if (normalizedProfile) {
|
|
663
|
+
modifier.$set = Object.keys(normalizedProfile).reduce((acc, profileKey) => {
|
|
629
664
|
return {
|
|
630
665
|
...acc,
|
|
631
|
-
[`profile.${profileKey}`]:
|
|
666
|
+
[`profile.${profileKey}`]: normalizedProfile[profileKey],
|
|
632
667
|
};
|
|
633
668
|
}, {});
|
|
634
669
|
}
|
|
@@ -685,14 +720,23 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
685
720
|
return null;
|
|
686
721
|
const profile = user.profile || {};
|
|
687
722
|
const isGuest = !!user.guest;
|
|
723
|
+
const defaultCountry = user.lastBillingAddress?.countryCode ||
|
|
724
|
+
profile.address?.countryCode ||
|
|
725
|
+
user.lastLogin?.countryCode;
|
|
726
|
+
const normalizedContact = lastContact?.telNumber
|
|
727
|
+
? {
|
|
728
|
+
...lastContact,
|
|
729
|
+
telNumber: normalizePhoneNumber(lastContact.telNumber, defaultCountry) || lastContact.telNumber,
|
|
730
|
+
}
|
|
731
|
+
: lastContact;
|
|
688
732
|
const modifier = {
|
|
689
733
|
$set: {
|
|
690
734
|
updated: new Date(),
|
|
691
|
-
lastContact,
|
|
735
|
+
lastContact: normalizedContact,
|
|
692
736
|
},
|
|
693
737
|
};
|
|
694
|
-
if ((!profile.phoneMobile || isGuest) &&
|
|
695
|
-
modifier.$set['profile.phoneMobile'] =
|
|
738
|
+
if ((!profile.phoneMobile || isGuest) && normalizedContact.telNumber) {
|
|
739
|
+
modifier.$set['profile.phoneMobile'] = normalizedContact.telNumber;
|
|
696
740
|
}
|
|
697
741
|
const updatedUser = await Users.findOneAndUpdate(userFilter, modifier, {
|
|
698
742
|
returnDocument: 'after',
|
|
@@ -36,7 +36,7 @@ async function fetchMDSEntriesImpl() {
|
|
|
36
36
|
}
|
|
37
37
|
return cache;
|
|
38
38
|
}
|
|
39
|
-
const fetchMDSEntries = memoizeWithTTL(fetchMDSEntriesImpl, ONE_DAY_MS);
|
|
39
|
+
const fetchMDSEntries = memoizeWithTTL(fetchMDSEntriesImpl, { ttl: ONE_DAY_MS });
|
|
40
40
|
export function toArrayBuffer(buffer) {
|
|
41
41
|
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
42
42
|
}
|
|
@@ -64,6 +64,7 @@ export const configureUsersWebAuthnModule = async ({ db }) => {
|
|
|
64
64
|
origin,
|
|
65
65
|
factor: 'either',
|
|
66
66
|
username,
|
|
67
|
+
created: new Date(),
|
|
67
68
|
});
|
|
68
69
|
return {
|
|
69
70
|
challenge,
|
|
@@ -96,6 +97,7 @@ export const configureUsersWebAuthnModule = async ({ db }) => {
|
|
|
96
97
|
origin,
|
|
97
98
|
factor: 'either',
|
|
98
99
|
username,
|
|
100
|
+
created: new Date(),
|
|
99
101
|
});
|
|
100
102
|
return {
|
|
101
103
|
challenge,
|
package/lib/users-settings.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type UserAccountAction = (typeof UserAccountAction)[keyof typeof UserAcco
|
|
|
16
16
|
export interface UserSettings {
|
|
17
17
|
mergeUserCartsOnLogin: boolean;
|
|
18
18
|
autoMessagingAfterUserCreation: boolean;
|
|
19
|
+
guestUserMaxAgeInDays: number;
|
|
19
20
|
earliestValidTokenDate: (type: typeof UserAccountAction.VERIFY_EMAIL | typeof UserAccountAction.RESET_PASSWORD) => Date;
|
|
20
21
|
validateEmail: (email: string) => Promise<boolean>;
|
|
21
22
|
validateUsername: (username: string) => Promise<boolean>;
|
package/lib/users-settings.js
CHANGED
|
@@ -8,6 +8,7 @@ export const UserAccountAction = {
|
|
|
8
8
|
};
|
|
9
9
|
const defaultAutoMessagingAfterUserCreation = true;
|
|
10
10
|
const defaultMergeUserCartsOnLogin = true;
|
|
11
|
+
const defaultGuestUserMaxAgeInDays = Number(process.env.UNCHAINED_GUEST_USER_EXPIRY_DAYS) || 30;
|
|
11
12
|
const defaultEarliestValidTokenDate = () => {
|
|
12
13
|
return new Date(new Date().getTime() - 1000 * 60 * 60);
|
|
13
14
|
};
|
|
@@ -25,12 +26,13 @@ const defaultValidatePassword = async (password) => {
|
|
|
25
26
|
export const userSettings = {
|
|
26
27
|
autoMessagingAfterUserCreation: defaultAutoMessagingAfterUserCreation,
|
|
27
28
|
mergeUserCartsOnLogin: defaultMergeUserCartsOnLogin,
|
|
29
|
+
guestUserMaxAgeInDays: defaultGuestUserMaxAgeInDays,
|
|
28
30
|
earliestValidTokenDate: defaultEarliestValidTokenDate,
|
|
29
31
|
validateNewUser: defaultValidateNewUser,
|
|
30
32
|
validateEmail: () => Promise.resolve(true),
|
|
31
33
|
validateUsername: () => Promise.resolve(true),
|
|
32
34
|
validatePassword: () => Promise.resolve(true),
|
|
33
|
-
configureSettings: ({ mergeUserCartsOnLogin, autoMessagingAfterUserCreation, earliestValidTokenDate, validateEmail, validateUsername, validateNewUser, validatePassword, }, db) => {
|
|
35
|
+
configureSettings: ({ mergeUserCartsOnLogin, autoMessagingAfterUserCreation, guestUserMaxAgeInDays, earliestValidTokenDate, validateEmail, validateUsername, validateNewUser, validatePassword, }, db) => {
|
|
34
36
|
const defaultValidateEmail = async (rawEmail) => {
|
|
35
37
|
if (!rawEmail?.includes?.('@'))
|
|
36
38
|
return false;
|
|
@@ -54,6 +56,7 @@ export const userSettings = {
|
|
|
54
56
|
userSettings.mergeUserCartsOnLogin = mergeUserCartsOnLogin ?? defaultMergeUserCartsOnLogin;
|
|
55
57
|
userSettings.autoMessagingAfterUserCreation =
|
|
56
58
|
autoMessagingAfterUserCreation ?? defaultAutoMessagingAfterUserCreation;
|
|
59
|
+
userSettings.guestUserMaxAgeInDays = guestUserMaxAgeInDays ?? defaultGuestUserMaxAgeInDays;
|
|
57
60
|
userSettings.earliestValidTokenDate = earliestValidTokenDate || defaultEarliestValidTokenDate;
|
|
58
61
|
userSettings.validateEmail = validateEmail || defaultValidateEmail;
|
|
59
62
|
userSettings.validateUsername = validateUsername || defaultValidateUsername;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unchainedshop/core-users",
|
|
3
3
|
"description": "User management module for the Unchained Engine with authentication support",
|
|
4
|
-
"version": "5.0.0-alpha.
|
|
4
|
+
"version": "5.0.0-alpha.3",
|
|
5
5
|
"main": "lib/users-index.js",
|
|
6
6
|
"types": "lib/users-index.d.ts",
|
|
7
7
|
"type": "module",
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
"@unchainedshop/events": "^5.0.0-alpha.1",
|
|
40
40
|
"@unchainedshop/logger": "^5.0.0-alpha.1",
|
|
41
41
|
"@unchainedshop/mongodb": "^5.0.0-alpha.1",
|
|
42
|
-
"@unchainedshop/roles": "^5.0.0-alpha.1",
|
|
43
42
|
"@unchainedshop/utils": "^5.0.0-alpha.1",
|
|
44
43
|
"bcryptjs": "^3.0.2"
|
|
45
44
|
},
|
|
@@ -58,7 +57,7 @@
|
|
|
58
57
|
"devDependencies": {
|
|
59
58
|
"@noble/curves": "^2.0.0",
|
|
60
59
|
"@noble/hashes": "^2.0.0",
|
|
61
|
-
"@types/node": "^
|
|
60
|
+
"@types/node": "^26.2.0",
|
|
62
61
|
"typescript": "^5.8.3"
|
|
63
62
|
}
|
|
64
63
|
}
|