From 29ca9b4ca347fdbae00c4080267cdad293b4000a Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Wed, 1 Nov 2023 12:46:35 +0100 Subject: [PATCH] Confirm email new routes (#15377) * confirm email routes * Style the email confirmation template (#15196) * error handling * prettier * error message * rename variables * message codes change * v1 redirect * fix assigning to session * rename rate limitter * rate limitter per email * add try/catch * added stub * prettier * confirm email acceptance test * confirm when created * tests * added rate limit tests * new email text * subscribe to newsletter * beforeEach/afterEach test both variants * move tests to OverleafAuthenticationTests * Revert "move tests to OverleafAuthenticationTests" This reverts commit 3c745382815da1594044a811882ba3daa24a7a3a. * cacheflow reset after each * remove test archive request * use crypto for random code * rate limit in userEmailsConfirmationHandler * ratelimiter per type * req.session.pendingUserRegistration * spy in before/after each * without deleteMany * delete staffUser in afterEach * stub response, format * rate limiter outside userEmailConfirmationHandler * mock ratelimitter * fix subscribe promise * add email to logger * logger calls * using tsscmp * fix lint * resendConfirmationCode rate limiter in router * remove redirect --------- Co-authored-by: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com> GitOrigin-RevId: 786c477966cf2c5f6e28417fe486146ee5c10884 --- .../Features/Email/Bodies/NoCTAEmailBody.js | 9 ++++-- .../app/src/Features/Email/EmailBuilder.js | 30 +++++++++++++++++++ .../web/app/src/Features/User/UserCreator.js | 8 ++++- .../User/UserEmailsConfirmationHandler.js | 26 ++++++++++++++++ 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/services/web/app/src/Features/Email/Bodies/NoCTAEmailBody.js b/services/web/app/src/Features/Email/Bodies/NoCTAEmailBody.js index eb5b59d215..fb68ecc7f9 100644 --- a/services/web/app/src/Features/Email/Bodies/NoCTAEmailBody.js +++ b/services/web/app/src/Features/Email/Bodies/NoCTAEmailBody.js @@ -29,9 +29,14 @@ module.exports = _.template(`\ <%= paragraph %>

<% }) %> - + <% if (highlightedText) { %> +
+ <%= highlightedText %> +
+ <% } %> + - + diff --git a/services/web/app/src/Features/Email/EmailBuilder.js b/services/web/app/src/Features/Email/EmailBuilder.js index 7c28d9eb0d..6070f03cfa 100644 --- a/services/web/app/src/Features/Email/EmailBuilder.js +++ b/services/web/app/src/Features/Email/EmailBuilder.js @@ -112,6 +112,10 @@ The ${settings.appName} Team - ${settings.siteUrl}\ title: typeof content.title === 'function' ? content.title(opts) : undefined, greeting: content.greeting(opts), + highlightedText: + typeof content.highlightedText === 'function' + ? content.highlightedText(opts) + : undefined, message: content.message(opts), StringHelper, }) @@ -242,6 +246,32 @@ templates.confirmEmail = ctaTemplate({ }, }) +templates.confirmCode = NoCTAEmailTemplate({ + greeting(opts) { + return '' + }, + subject(opts) { + return `Confirm your email address on Overleaf (${opts.confirmCode})` + }, + title(opts) { + return 'Confirm your email address' + }, + message(opts, isPlainText) { + const msg = [ + `Welcome to Overleaf! We're so glad you joined us.`, + 'Use this 6-digit confirmation code to finish your setup.', + ] + + if (isPlainText && opts.confirmCode) { + msg.push(opts.confirmCode) + } + return msg + }, + highlightedText(opts) { + return opts.confirmCode + }, +}) + templates.projectInvite = ctaTemplate({ subject(opts) { const safeName = SpamSafe.isSafeProjectName(opts.project.name) diff --git a/services/web/app/src/Features/User/UserCreator.js b/services/web/app/src/Features/User/UserCreator.js index 73e891e86f..34a3ea7cf3 100644 --- a/services/web/app/src/Features/User/UserCreator.js +++ b/services/web/app/src/Features/User/UserCreator.js @@ -94,13 +94,19 @@ async function createNewUser(attributes, options = {}) { emailData.samlProviderId = attributes.samlIdentifiers[0].providerId } + const affiliationOptions = options.affiliationOptions || {} + + if (options.confirmedAt) { + emailData.confirmedAt = options.confirmedAt + affiliationOptions.confirmedAt = options.confirmedAt + } user.emails = [emailData] user = await user.save() if (Features.hasFeature('affiliations')) { try { - user = await _addAffiliation(user, options.affiliationOptions || {}) + user = await _addAffiliation(user, affiliationOptions) } catch (error) { if (options.requireAffiliation) { await UserDeleter.promises.deleteMongoUser(user._id) diff --git a/services/web/app/src/Features/User/UserEmailsConfirmationHandler.js b/services/web/app/src/Features/User/UserEmailsConfirmationHandler.js index b73344c76b..e541042b99 100644 --- a/services/web/app/src/Features/User/UserEmailsConfirmationHandler.js +++ b/services/web/app/src/Features/User/UserEmailsConfirmationHandler.js @@ -6,10 +6,12 @@ const Errors = require('../Errors/Errors') const UserUpdater = require('./UserUpdater') const UserGetter = require('./UserGetter') const { callbackify, promisify } = require('util') +const crypto = require('crypto') // Reject email confirmation tokens after 90 days const TOKEN_EXPIRY_IN_S = 90 * 24 * 60 * 60 const TOKEN_USE = 'email_confirmation' +const CONFIRMATION_CODE_EXPIRY_IN_S = 10 * 60 function sendConfirmationEmail(userId, email, emailTemplate, callback) { if (arguments.length === 3) { @@ -46,6 +48,26 @@ function sendConfirmationEmail(userId, email, emailTemplate, callback) { ) } +async function sendConfirmationCode(email) { + if (!EmailHelper.parseEmail(email)) { + throw new Error('invalid email') + } + + const confirmCode = crypto.randomInt(0, 1_000_000).toString().padStart(6, '0') + const confirmCodeExpiresTimestamp = + Date.now() + CONFIRMATION_CODE_EXPIRY_IN_S * 1000 + + await EmailHandler.promises.sendEmail('confirmCode', { + to: email, + confirmCode, + }) + + return { + confirmCode, + confirmCodeExpiresTimestamp, + } +} + async function sendReconfirmationEmail(userId, email) { email = EmailHelper.parseEmail(email) if (!email) { @@ -112,6 +134,10 @@ const UserEmailsConfirmationHandler = { UserEmailsConfirmationHandler.promises = { sendConfirmationEmail: promisify(sendConfirmationEmail), + confirmEmailFromToken: promisify( + UserEmailsConfirmationHandler.confirmEmailFromToken + ), + sendConfirmationCode, } module.exports = UserEmailsConfirmationHandler