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