@unchainedshop/core-users 5.0.0-alpha.2 → 5.0.0-alpha.4
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/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 +54 -9
- package/lib/module/configureUsersWebAuthnModule.js +1 -1
- package/lib/users-settings.d.ts +1 -0
- package/lib/users-settings.js +4 -1
- package/package.json +3 -4
|
@@ -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>;
|
|
@@ -2,12 +2,13 @@ import * as bcrypt from 'bcryptjs';
|
|
|
2
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',
|
|
@@ -45,12 +46,12 @@ export const buildFindSelector = ({ includeGuests, includeDeleted, queryString,
|
|
|
45
46
|
}
|
|
46
47
|
if (usernames?.length) {
|
|
47
48
|
selector.username = {
|
|
48
|
-
$in: usernames.map((u) => insensitiveTrimmedRegexOperator(u)),
|
|
49
|
+
$in: usernames.map((u) => insensitiveTrimmedRegexOperator(u).$regex),
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
if (emails?.length) {
|
|
52
53
|
selector['emails.address'] = {
|
|
53
|
-
$in: emails.map((e) => insensitiveTrimmedRegexOperator(e)),
|
|
54
|
+
$in: emails.map((e) => insensitiveTrimmedRegexOperator(e).$regex),
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
if (emailVerified === true) {
|
|
@@ -82,6 +83,7 @@ export const buildFindSelector = ({ includeGuests, includeDeleted, queryString,
|
|
|
82
83
|
export const configureUsersModule = async (moduleInput) => {
|
|
83
84
|
const { db, options, migrationRepository } = moduleInput;
|
|
84
85
|
convertUserLocale(migrationRepository);
|
|
86
|
+
normalizeProfilePhone(migrationRepository);
|
|
85
87
|
userSettings.configureSettings(options || {}, db);
|
|
86
88
|
registerEvents(USER_EVENTS);
|
|
87
89
|
const Users = await UsersCollection(db);
|
|
@@ -197,6 +199,23 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
197
199
|
sort: buildSortOptions(query.sort || defaultSort),
|
|
198
200
|
}).toArray();
|
|
199
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
|
+
},
|
|
200
219
|
async userExists({ userId }) {
|
|
201
220
|
const userCount = await Users.countDocuments({ _id: userId, deleted: null }, { limit: 1 });
|
|
202
221
|
return userCount === 1;
|
|
@@ -623,11 +642,28 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
623
642
|
return Users.findOne(userFilter, {});
|
|
624
643
|
}
|
|
625
644
|
const modifier = { $set: {} };
|
|
626
|
-
|
|
627
|
-
|
|
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) => {
|
|
628
664
|
return {
|
|
629
665
|
...acc,
|
|
630
|
-
[`profile.${profileKey}`]:
|
|
666
|
+
[`profile.${profileKey}`]: normalizedProfile[profileKey],
|
|
631
667
|
};
|
|
632
668
|
}, {});
|
|
633
669
|
}
|
|
@@ -684,14 +720,23 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
684
720
|
return null;
|
|
685
721
|
const profile = user.profile || {};
|
|
686
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;
|
|
687
732
|
const modifier = {
|
|
688
733
|
$set: {
|
|
689
734
|
updated: new Date(),
|
|
690
|
-
lastContact,
|
|
735
|
+
lastContact: normalizedContact,
|
|
691
736
|
},
|
|
692
737
|
};
|
|
693
|
-
if ((!profile.phoneMobile || isGuest) &&
|
|
694
|
-
modifier.$set['profile.phoneMobile'] =
|
|
738
|
+
if ((!profile.phoneMobile || isGuest) && normalizedContact.telNumber) {
|
|
739
|
+
modifier.$set['profile.phoneMobile'] = normalizedContact.telNumber;
|
|
695
740
|
}
|
|
696
741
|
const updatedUser = await Users.findOneAndUpdate(userFilter, modifier, {
|
|
697
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
|
}
|
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.4",
|
|
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": "^
|
|
62
|
-
"typescript": "^
|
|
60
|
+
"@types/node": "^26.2.0",
|
|
61
|
+
"typescript": "^6.0.3"
|
|
63
62
|
}
|
|
64
63
|
}
|