From e294d52f8faa11a9cf30665ae02e8aa84827ee68 Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 28 Mar 2025 19:26:39 +0200 Subject: [PATCH 1/4] Add replace email utility --- docs/_config.yml | 1 + docs/generators/changelog/versions/3.0.0.md | 1 + docs/source/api/passwords.md | 2 + packages/accounts-base/accounts-base.d.ts | 2 + packages/accounts-password/password_server.js | 46 +++++++++++++++++++ packages/accounts-password/password_tests.js | 26 +++++++++++ v3-docs/docs/api/accounts.md | 2 + .../generators/changelog/versions/3.0.0.md | 1 + 8 files changed, 81 insertions(+) diff --git a/docs/_config.yml b/docs/_config.yml index 6834fe8ced..395aaca44c 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -213,6 +213,7 @@ redirects: /#/full/accounts-setusername: 'api/passwords.html#accounts-setusername' /#/full/accounts-addemail: 'api/passwords.html#accounts-addemail' /#/full/accounts-removeemail: 'api/passwords.html#accounts-removeemail' + /#/full/accounts_replaceemail: 'api/passwords.html#Accounts-replaceEmail' /#/full/accounts_verifyemail: 'api/passwords.html#Accounts-verifyEmail' /#/full/accounts-finduserbyusername: 'api/passwords.html#accounts-finduserbyusername' /#/full/accounts-finduserbyemail: 'api/passwords.html#accounts-finduserbyemail' diff --git a/docs/generators/changelog/versions/3.0.0.md b/docs/generators/changelog/versions/3.0.0.md index c5fa1f1a1e..4c863db6ad 100644 --- a/docs/generators/changelog/versions/3.0.0.md +++ b/docs/generators/changelog/versions/3.0.0.md @@ -55,6 +55,7 @@ - `Accounts.sendVerificationEmail` - `Accounts.addEmail` - `Accounts.removeEmail` + - `Accounts.replaceEmailAsync` - `Accounts.verifyEmail` - `Accounts.createUserVerifyingEmail` - `Accounts.createUser` diff --git a/docs/source/api/passwords.md b/docs/source/api/passwords.md index bb0ddf6b17..908617aa3e 100644 --- a/docs/source/api/passwords.md +++ b/docs/source/api/passwords.md @@ -59,6 +59,8 @@ By default, an email address is added with `{ verified: false }`. Use [`Accounts.sendVerificationEmail`](#Accounts-sendVerificationEmail) to send an email with a link the user can use to verify their email address. +{% apibox "Accounts.replaceEmailAsync" %} + {% apibox "Accounts.removeEmail" %} {% apibox "Accounts.verifyEmail" %} diff --git a/packages/accounts-base/accounts-base.d.ts b/packages/accounts-base/accounts-base.d.ts index 7fbbfde0a6..b11344ce08 100644 --- a/packages/accounts-base/accounts-base.d.ts +++ b/packages/accounts-base/accounts-base.d.ts @@ -188,6 +188,8 @@ export namespace Accounts { function removeEmail(userId: string, email: string): Promise; + function replaceEmailAsync(userId: string, oldEmail: string, newEmail: string, verified?: boolean): Promise; + function onCreateUser( func: (options: { profile?: {} | undefined }, user: Meteor.User) => void ): void; diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 455c071cc3..6477bdcc3d 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -1022,6 +1022,52 @@ Meteor.methods( } }); + +/** + * @summary Asynchronously replace an email address for a user. Use this instead of directly + * updating the database. The operation will fail if there is a different user + * with an email only differing in case. If the specified user has an existing + * email only differing in case however, we replace it. + * @locus Server + * @param {String} userId The ID of the user to update. + * @param {String} oldEmail The email address to replace. + * @param {String} newEmail The new email address to use. + * @param {Boolean} [verified] Optional - whether the new email address should + * be marked as verified. Defaults to false. + * @importFromPackage accounts-base + */ +Accounts.replaceEmailAsync = async (userId, oldEmail, newEmail, verified) => { + check(userId, NonEmptyString); + check(oldEmail, NonEmptyString); + check(newEmail, NonEmptyString); + check(verified, Match.Optional(Boolean)); + + if (verified === void 0) { + verified = false; + } + + const user = await getUserById(userId, { fields: { _id: 1 } }); + if (!user) + throw new Meteor.Error(403, "User not found"); + + // Ensure no user already has this new email + await Accounts._checkForCaseInsensitiveDuplicates( + "emails.address", + "Email", + newEmail, + user._id + ); + + const result = await Meteor.users.updateAsync( + { _id: user._id, 'emails.address': oldEmail }, + { $set: { 'emails.$.address': newEmail, 'emails.$.verified': verified } } + ); + + if (result.modifiedCount === 0) { + throw new Meteor.Error(404, "No user could be found with old email"); + } +}; + /** * @summary Asynchronously add an email address for a user. Use this instead of directly * updating the database. The operation will fail if there is a different user diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index e488c9a89e..08c9b0c4ca 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1789,6 +1789,32 @@ if (Meteor.isServer) (() => { ]); }); + Tinytest.addAsync("passwords, replace email", async test => { + const origEmail = `${ Random.id() }@turing.com`; + const userId = await Accounts.createUser({ + email: origEmail + }); + + const newEmail = `${ Random.id() }@turing.com`; + await Accounts.addEmailAsync(userId, newEmail); + + const thirdEmail = `${ Random.id() }@turing.com`; + await Accounts.addEmailAsync(userId, thirdEmail, true); + const u1 = await Accounts._findUserByQuery({ id: userId }) + test.equal(u1.emails, [ + { address: origEmail, verified: false }, + { address: newEmail, verified: false }, + { address: thirdEmail, verified: true } + ]); + + await Accounts.replaceEmailAsync(userId, newEmail, thirdEmail); + const u2 = await Accounts._findUserByQuery({ id: userId }) + test.equal(u2.emails, [ + { address: origEmail, verified: false }, + { address: thirdEmail, verified: true } + ]); + }) + Tinytest.addAsync("passwords - remove email", async test => { const origEmail = `${ Random.id() }@turing.com`; diff --git a/v3-docs/docs/api/accounts.md b/v3-docs/docs/api/accounts.md index 2ac9317275..ce5b8699f2 100644 --- a/v3-docs/docs/api/accounts.md +++ b/v3-docs/docs/api/accounts.md @@ -888,6 +888,8 @@ email with a link the user can use to verify their email address. + + If the user trying to verify the email has 2FA enabled, this error will be thrown: diff --git a/v3-docs/docs/generators/changelog/versions/3.0.0.md b/v3-docs/docs/generators/changelog/versions/3.0.0.md index 15008c98fe..66fcf78b5b 100644 --- a/v3-docs/docs/generators/changelog/versions/3.0.0.md +++ b/v3-docs/docs/generators/changelog/versions/3.0.0.md @@ -58,6 +58,7 @@ - `Accounts.sendVerificationEmail` - `Accounts.addEmail` - `Accounts.removeEmail` + - `Accounts.replaceEmail` - `Accounts.verifyEmail` - `Accounts.createUserVerifyingEmail` - `Accounts.createUser` From d5e479291eaa5bfe2274a0f362db217ace415256 Mon Sep 17 00:00:00 2001 From: harryadel Date: Fri, 28 Mar 2025 23:03:24 +0200 Subject: [PATCH 2/4] Fix test --- packages/accounts-password/password_tests.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 08c9b0c4ca..8b14a84b9f 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1790,28 +1790,22 @@ if (Meteor.isServer) (() => { }); Tinytest.addAsync("passwords, replace email", async test => { - const origEmail = `${ Random.id() }@turing.com`; + const origEmail = `originalemail@test.com`; const userId = await Accounts.createUser({ email: origEmail }); - const newEmail = `${ Random.id() }@turing.com`; - await Accounts.addEmailAsync(userId, newEmail); + const newEmail = `newemail@test.com`; - const thirdEmail = `${ Random.id() }@turing.com`; - await Accounts.addEmailAsync(userId, thirdEmail, true); const u1 = await Accounts._findUserByQuery({ id: userId }) test.equal(u1.emails, [ - { address: origEmail, verified: false }, - { address: newEmail, verified: false }, - { address: thirdEmail, verified: true } + { address: origEmail, verified: false } ]); - await Accounts.replaceEmailAsync(userId, newEmail, thirdEmail); + await Accounts.replaceEmailAsync(userId, origEmail, newEmail); const u2 = await Accounts._findUserByQuery({ id: userId }) test.equal(u2.emails, [ - { address: origEmail, verified: false }, - { address: thirdEmail, verified: true } + { address: newEmail, verified: false } ]); }) From b620ff222d8f0d8d54f24a0d68ec37a8c9b65b78 Mon Sep 17 00:00:00 2001 From: harryadel Date: Wed, 2 Apr 2025 19:00:00 +0200 Subject: [PATCH 3/4] Apply Nacho fixes --- packages/accounts-password/password_tests.js | 37 +++++++++++--------- v3-docs/docs/api/accounts.md | 2 +- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 8b14a84b9f..f34e172dc7 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1789,27 +1789,30 @@ if (Meteor.isServer) (() => { ]); }); - Tinytest.addAsync("passwords, replace email", async test => { - const origEmail = `originalemail@test.com`; - const userId = await Accounts.createUser({ - email: origEmail - }); + - const newEmail = `newemail@test.com`; +Tinytest.addAsync("accounts emails - replace email", async test => { + const origEmail = `originalemail@test.com`; + const userId = await Accounts.createUserAsync({ + email: origEmail, + password: 'password' + }); - const u1 = await Accounts._findUserByQuery({ id: userId }) - test.equal(u1.emails, [ - { address: origEmail, verified: false } - ]); + const newEmail = `newemail@test.com`; - await Accounts.replaceEmailAsync(userId, origEmail, newEmail); - const u2 = await Accounts._findUserByQuery({ id: userId }) - test.equal(u2.emails, [ - { address: newEmail, verified: false } - ]); - }) + const u1 = await Accounts._findUserByQuery({ id: userId }) + test.equal(u1.emails, [ + { address: origEmail, verified: false } + ]); - Tinytest.addAsync("passwords - remove email", + await Accounts.replaceEmailAsync(userId, origEmail, newEmail); + const u2 = await Accounts._findUserByQuery({ id: userId }) + test.equal(u2.emails, [ + { address: newEmail, verified: false } + ]); +}) + + Tinytest.addAsync("passwords - remove email", async test => { const origEmail = `${ Random.id() }@turing.com`; const userId = await Accounts.createUser({ diff --git a/v3-docs/docs/api/accounts.md b/v3-docs/docs/api/accounts.md index ce5b8699f2..a40c6ef496 100644 --- a/v3-docs/docs/api/accounts.md +++ b/v3-docs/docs/api/accounts.md @@ -888,7 +888,7 @@ email with a link the user can use to verify their email address. - + From cc360a3e362d6f6499614dbbdb36cfcd6037eb81 Mon Sep 17 00:00:00 2001 From: harryadel Date: Wed, 2 Apr 2025 19:01:22 +0200 Subject: [PATCH 4/4] Fix doc --- v3-docs/docs/generators/changelog/versions/3.0.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v3-docs/docs/generators/changelog/versions/3.0.0.md b/v3-docs/docs/generators/changelog/versions/3.0.0.md index 66fcf78b5b..202729af5d 100644 --- a/v3-docs/docs/generators/changelog/versions/3.0.0.md +++ b/v3-docs/docs/generators/changelog/versions/3.0.0.md @@ -58,7 +58,7 @@ - `Accounts.sendVerificationEmail` - `Accounts.addEmail` - `Accounts.removeEmail` - - `Accounts.replaceEmail` + - `Accounts.replaceEmailAsync` - `Accounts.verifyEmail` - `Accounts.createUserVerifyingEmail` - `Accounts.createUser`