From bf7a18db8b24d7ce2ca68aabe7c0d4bd636e4ee7 Mon Sep 17 00:00:00 2001 From: roo hutton Date: Tue, 9 Apr 2024 13:15:46 +0100 Subject: [PATCH] Merge pull request #17730 from overleaf/rh-acct-delete-email [web] Send email notification on account deletion GitOrigin-RevId: 03c0effba0ee3b829f5b4fe377fe67d05776ba3f --- .../app/src/Features/Email/EmailBuilder.js | 29 +++++++++++++++++-- .../web/app/src/Features/User/UserDeleter.js | 9 ++++++ .../test/unit/src/User/UserDeleterTests.js | 17 +++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/services/web/app/src/Features/Email/EmailBuilder.js b/services/web/app/src/Features/Email/EmailBuilder.js index cdaf502ec8..a24ddfeff5 100644 --- a/services/web/app/src/Features/Email/EmailBuilder.js +++ b/services/web/app/src/Features/Email/EmailBuilder.js @@ -527,8 +527,8 @@ templates.groupSSOReauthenticate = ctaTemplate({ return [ `Hi,
- Single sign-on for your Overleaf group has been updated. - This means you need to reauthenticate your Overleaf account with your group’s SSO provider. + Single sign-on for your Overleaf group has been updated. + This means you need to reauthenticate your Overleaf account with your group’s SSO provider.
`, ] @@ -786,6 +786,31 @@ templates.userOnboardingEmail = NoCTAEmailTemplate({ }, }) +templates.deletedAccount = NoCTAEmailTemplate({ + subject() { + return 'Overleaf security note: account deletion confirmation' + }, + title() { + return 'Account deleted' + }, + message(opts, isPlainText) { + const dateFormatted = moment().format('dddd D MMMM YYYY') + const timeFormatted = moment().format('HH:mm') + const helpLink = EmailMessageHelper.displayLink( + 'quick guide', + `${settings.siteUrl}/learn/how-to/Keeping_your_account_secure`, + isPlainText + ) + + return [ + `We are writing to let you know that your ${settings.appName} account was deleted on ${dateFormatted} at ${timeFormatted} GMT.`, + `If this was you, you're all set and can ignore this email.`, + `If you did not take this action, please get in touch with our support team at ${settings.adminEmail} to report this as potentially suspicious activity on your account.`, + `For tips on keeping your ${settings.appName} account secure, read our ${helpLink}.`, + ] + }, +}) + templates.securityAlert = NoCTAEmailTemplate({ subject(opts) { return `Overleaf security note: ${opts.action}` diff --git a/services/web/app/src/Features/User/UserDeleter.js b/services/web/app/src/Features/User/UserDeleter.js index 49d0f0b264..71fcc345c0 100644 --- a/services/web/app/src/Features/User/UserDeleter.js +++ b/services/web/app/src/Features/User/UserDeleter.js @@ -16,6 +16,7 @@ const InstitutionsAPI = require('../Institutions/InstitutionsAPI') const Modules = require('../../infrastructure/Modules') const Errors = require('../Errors/Errors') const OnboardingDataCollectionManager = require('../OnboardingDataCollection/OnboardingDataCollectionManager') +const EmailHandler = require('../Email/EmailHandler') module.exports = { deleteUser: callbackify(deleteUser), @@ -48,6 +49,7 @@ async function deleteUser(userId, options) { await Modules.promises.hooks.fire('deleteUser', userId) await _createDeletedUser(user, options) await ProjectDeleter.promises.deleteUsersProjects(user._id) + await _sendDeleteEmail(user) await deleteMongoUser(user._id) } catch (error) { logger.warn({ error, userId }, 'something went wrong deleting the user') @@ -110,6 +112,13 @@ async function ensureCanDeleteUser(user) { } } +async function _sendDeleteEmail(user) { + const emailOptions = { + to: user.email, + } + await EmailHandler.promises.sendEmail('deletedAccount', emailOptions) +} + async function _createDeletedUser(user, options) { await DeletedUser.updateOne( { 'deleterData.deletedUserId': user._id }, diff --git a/services/web/test/unit/src/User/UserDeleterTests.js b/services/web/test/unit/src/User/UserDeleterTests.js index 2550ec0f8c..bf837fa655 100644 --- a/services/web/test/unit/src/User/UserDeleterTests.js +++ b/services/web/test/unit/src/User/UserDeleterTests.js @@ -100,6 +100,12 @@ describe('UserDeleter', function () { deleteOnboardingDataCollection: sinon.stub().resolves(), } + this.EmailHandler = { + promises: { + sendEmail: sinon.stub().resolves(), + }, + } + this.UserDeleter = SandboxedModule.require(modulePath, { requires: { '../../models/User': { User }, @@ -119,6 +125,7 @@ describe('UserDeleter', function () { '../../infrastructure/Modules': this.Modules, '../OnboardingDataCollection/OnboardingDataCollectionManager': this.OnboardingDataCollectionManager, + '../Email/EmailHandler': this.EmailHandler, }, }) }) @@ -251,6 +258,16 @@ describe('UserDeleter', function () { await this.UserDeleter.promises.deleteUser(this.userId, {}) this.DeletedUserMock.verify() }) + + it('should email the user', async function () { + await this.UserDeleter.promises.deleteUser(this.userId, {}) + const emailOptions = { + to: 'bob@bob.com', + } + expect( + this.EmailHandler.promises.sendEmail + ).to.have.been.calledWith('deletedAccount', emailOptions) + }) }) describe('when unsubscribing from mailchimp fails', function () {