@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.
@@ -0,0 +1,2 @@
1
+ import type { MigrationRepository } from '@unchainedshop/mongodb';
2
+ export default function normalizeProfilePhone(repository: MigrationRepository): void;
@@ -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
- if (profile) {
627
- modifier.$set = Object.keys(profile).reduce((acc, profileKey) => {
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}`]: 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) && lastContact.telNumber) {
694
- modifier.$set['profile.phoneMobile'] = lastContact.telNumber;
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
  }
@@ -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>;
@@ -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.2",
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": "^25.0.0",
62
- "typescript": "^5.8.3"
60
+ "@types/node": "^26.2.0",
61
+ "typescript": "^6.0.3"
63
62
  }
64
63
  }