Преглед изворни кода

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
Domagoj Kriskovic пре 2 година
родитељ
комит
af4b22fab5

+ 7 - 2
services/web/app/src/Features/Email/Bodies/NoCTAEmailBody.js

@@ -29,9 +29,14 @@ module.exports = _.template(`\
 											<%= paragraph %>
 										</p>
 									<% }) %>
-								</td>
+                  <% if (highlightedText) { %>
+                    <div style="text-align: center; color: #1B222C; font-size: 20px; margin: 16px 0; padding: 16px 8px; border-radius: 8px; background: #F4F5F6;">
+                      <b><%= highlightedText %></b>
+                    </div>
+                  <% } %>
+                </td>
 							</tr>
-						</tr>
+            </tr>
 					</table>
 				</th>
 			</tr>

+ 30 - 0
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)

+ 7 - 1
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)

+ 26 - 0
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