Просмотр исходного кода

Merge pull request #13367 from overleaf/ab-group-invite-halfway-out-of-login

[web] Move the group invite page half-way outside the login wall

GitOrigin-RevId: 8d846df6e248a08433ab2ca991644c78cf9ff330
Davinder Singh 3 лет назад
Родитель
Сommit
4991f9cdc7

+ 0 - 1
services/web/app/src/Features/Subscription/SubscriptionRouter.js

@@ -64,7 +64,6 @@ module.exports = {
     // Team invites
     // Team invites
     webRouter.get(
     webRouter.get(
       '/subscription/invites/:token/',
       '/subscription/invites/:token/',
-      AuthenticationController.requireLogin(),
       TeamInvitesController.viewInvite
       TeamInvitesController.viewInvite
     )
     )
     webRouter.put(
     webRouter.put(

+ 107 - 100
services/web/app/src/Features/Subscription/TeamInvitesController.js

@@ -4,122 +4,129 @@ const SessionManager = require('../Authentication/SessionManager')
 const SubscriptionLocator = require('./SubscriptionLocator')
 const SubscriptionLocator = require('./SubscriptionLocator')
 const ErrorController = require('../Errors/ErrorController')
 const ErrorController = require('../Errors/ErrorController')
 const EmailHelper = require('../Helpers/EmailHelper')
 const EmailHelper = require('../Helpers/EmailHelper')
+const UserGetter = require('../User/UserGetter')
+const { expressify } = require('../../util/promises')
 
 
-module.exports = {
-  createInvite(req, res, next) {
-    const teamManagerId = SessionManager.getLoggedInUserId(req.session)
-    const subscription = req.entity
-    const email = EmailHelper.parseEmail(req.body.email)
-    if (!email) {
-      return res.status(422).json({
-        error: {
-          code: 'invalid_email',
-          message: req.i18n.translate('invalid_email'),
-        },
-      })
-    }
+function createInvite(req, res, next) {
+  const teamManagerId = SessionManager.getLoggedInUserId(req.session)
+  const subscription = req.entity
+  const email = EmailHelper.parseEmail(req.body.email)
+  if (!email) {
+    return res.status(422).json({
+      error: {
+        code: 'invalid_email',
+        message: req.i18n.translate('invalid_email'),
+      },
+    })
+  }
 
 
-    TeamInvitesHandler.createInvite(
-      teamManagerId,
-      subscription,
-      email,
-      function (err, inviteUserData) {
-        if (err) {
-          if (err.alreadyInTeam) {
-            return res.status(400).json({
-              error: {
-                code: 'user_already_added',
-                message: req.i18n.translate('user_already_added'),
-              },
-            })
-          }
-          if (err.limitReached) {
-            return res.status(400).json({
-              error: {
-                code: 'group_full',
-                message: req.i18n.translate('group_full'),
-              },
-            })
-          }
-          return next(err)
+  TeamInvitesHandler.createInvite(
+    teamManagerId,
+    subscription,
+    email,
+    function (err, inviteUserData) {
+      if (err) {
+        if (err.alreadyInTeam) {
+          return res.status(400).json({
+            error: {
+              code: 'user_already_added',
+              message: req.i18n.translate('user_already_added'),
+            },
+          })
         }
         }
-        res.json({ user: inviteUserData })
+        if (err.limitReached) {
+          return res.status(400).json({
+            error: {
+              code: 'group_full',
+              message: req.i18n.translate('group_full'),
+            },
+          })
+        }
+        return next(err)
       }
       }
-    )
-  },
+      res.json({ user: inviteUserData })
+    }
+  )
+}
 
 
-  viewInvite(req, res, next) {
-    const { token } = req.params
-    const userId = SessionManager.getLoggedInUserId(req.session)
+async function viewInvite(req, res, next) {
+  const { token } = req.params
+  const userId = SessionManager.getLoggedInUserId(req.session)
 
 
-    TeamInvitesHandler.getInvite(
-      token,
-      function (err, invite, teamSubscription) {
-        if (err) {
-          return next(err)
-        }
+  const { invite } = await TeamInvitesHandler.promises.getInvite(token)
+  if (!invite) {
+    return ErrorController.notFound(req, res)
+  }
 
 
-        if (!invite) {
-          return ErrorController.notFound(req, res, next)
-        }
+  if (userId) {
+    const personalSubscription =
+      await SubscriptionLocator.promises.getUsersSubscription(userId)
 
 
-        SubscriptionLocator.getUsersSubscription(
-          userId,
-          function (err, personalSubscription) {
-            if (err) {
-              return next(err)
-            }
+    const hasIndividualRecurlySubscription =
+      personalSubscription &&
+      personalSubscription.groupPlan === false &&
+      personalSubscription.recurlyStatus?.state !== 'canceled' &&
+      personalSubscription.recurlySubscription_id &&
+      personalSubscription.recurlySubscription_id !== ''
 
 
-            const hasIndividualRecurlySubscription =
-              personalSubscription &&
-              personalSubscription.groupPlan === false &&
-              personalSubscription.recurlyStatus?.state !== 'canceled' &&
-              personalSubscription.recurlySubscription_id &&
-              personalSubscription.recurlySubscription_id !== ''
-
-            res.render('subscriptions/team/invite', {
-              inviterName: invite.inviterName,
-              inviteToken: invite.token,
-              hasIndividualRecurlySubscription,
-              appName: settings.appName,
-              expired: req.query.expired,
-            })
-          }
-        )
-      }
+    res.render('subscriptions/team/invite', {
+      inviterName: invite.inviterName,
+      inviteToken: invite.token,
+      hasIndividualRecurlySubscription,
+      appName: settings.appName,
+      expired: req.query.expired,
+    })
+  } else {
+    const userByEmail = await UserGetter.promises.getUserByMainEmail(
+      invite.email
     )
     )
-  },
 
 
-  acceptInvite(req, res, next) {
-    const { token } = req.params
-    const userId = SessionManager.getLoggedInUserId(req.session)
+    res.render('subscriptions/team/invite_logged_out', {
+      inviterName: invite.inviterName,
+      inviteToken: invite.token,
+      appName: settings.appName,
+      accountExists: userByEmail != null,
+      emailAddress: invite.email,
+    })
+  }
+}
+
+function acceptInvite(req, res, next) {
+  const { token } = req.params
+  const userId = SessionManager.getLoggedInUserId(req.session)
+
+  TeamInvitesHandler.acceptInvite(token, userId, function (err, results) {
+    if (err) {
+      return next(err)
+    }
+    res.sendStatus(204)
+  })
+}
+
+function revokeInvite(req, res, next) {
+  const subscription = req.entity
+  const email = EmailHelper.parseEmail(req.params.email)
+  const teamManagerId = SessionManager.getLoggedInUserId(req.session)
+  if (!email) {
+    return res.sendStatus(400)
+  }
 
 
-    TeamInvitesHandler.acceptInvite(token, userId, function (err, results) {
+  TeamInvitesHandler.revokeInvite(
+    teamManagerId,
+    subscription,
+    email,
+    function (err, results) {
       if (err) {
       if (err) {
         return next(err)
         return next(err)
       }
       }
       res.sendStatus(204)
       res.sendStatus(204)
-    })
-  },
-
-  revokeInvite(req, res, next) {
-    const subscription = req.entity
-    const email = EmailHelper.parseEmail(req.params.email)
-    const teamManagerId = SessionManager.getLoggedInUserId(req.session)
-    if (!email) {
-      return res.sendStatus(400)
     }
     }
+  )
+}
 
 
-    TeamInvitesHandler.revokeInvite(
-      teamManagerId,
-      subscription,
-      email,
-      function (err, results) {
-        if (err) {
-          return next(err)
-        }
-        res.sendStatus(204)
-      }
-    )
-  },
+module.exports = {
+  createInvite,
+  viewInvite: expressify(viewInvite),
+  acceptInvite,
+  revokeInvite,
 }
 }

+ 125 - 102
services/web/app/src/Features/Subscription/TeamInvitesHandler.js

@@ -1,4 +1,3 @@
-let TeamInvitesHandler
 const logger = require('@overleaf/logger')
 const logger = require('@overleaf/logger')
 const crypto = require('crypto')
 const crypto = require('crypto')
 const async = require('async')
 const async = require('async')
@@ -17,120 +16,125 @@ const EmailHandler = require('../Email/EmailHandler')
 const EmailHelper = require('../Helpers/EmailHelper')
 const EmailHelper = require('../Helpers/EmailHelper')
 
 
 const Errors = require('../Errors/Errors')
 const Errors = require('../Errors/Errors')
+const { promisifyMultiResult, promisify } = require('../../util/promises')
 
 
-module.exports = TeamInvitesHandler = {
-  getInvite(token, callback) {
-    Subscription.findOne(
-      { 'teamInvites.token': token },
-      function (err, subscription) {
-        if (err) {
-          return callback(err)
-        }
-        if (!subscription) {
-          return callback(new Errors.NotFoundError('team not found'))
-        }
-
-        const invite = subscription.teamInvites.find(i => i.token === token)
-        callback(null, invite, subscription)
+function getInvite(token, callback) {
+  Subscription.findOne(
+    { 'teamInvites.token': token },
+    function (err, subscription) {
+      if (err) {
+        return callback(err)
+      }
+      if (!subscription) {
+        return callback(new Errors.NotFoundError('team not found'))
       }
       }
-    )
-  },
 
 
-  createInvite(teamManagerId, subscription, email, callback) {
-    email = EmailHelper.parseEmail(email)
-    if (!email) {
-      return callback(new Error('invalid email'))
+      const invite = subscription.teamInvites.find(i => i.token === token)
+      callback(null, invite, subscription)
     }
     }
-    UserGetter.getUser(teamManagerId, function (error, teamManager) {
+  )
+}
+
+function createInvite(teamManagerId, subscription, email, callback) {
+  email = EmailHelper.parseEmail(email)
+  if (!email) {
+    return callback(new Error('invalid email'))
+  }
+  UserGetter.getUser(teamManagerId, function (error, teamManager) {
+    if (error) {
+      return callback(error)
+    }
+
+    _removeLegacyInvite(subscription.id, email, function (error) {
       if (error) {
       if (error) {
         return callback(error)
         return callback(error)
       }
       }
-
-      removeLegacyInvite(subscription.id, email, function (error) {
-        if (error) {
-          return callback(error)
-        }
-        createInvite(subscription, email, teamManager, callback)
-      })
+      _createInvite(subscription, email, teamManager, callback)
     })
     })
-  },
-
-  importInvite(subscription, inviterName, email, token, sentAt, callback) {
-    checkIfInviteIsPossible(
-      subscription,
-      email,
-      function (error, possible, reason) {
-        if (error) {
-          return callback(error)
-        }
-        if (!possible) {
-          return callback(reason)
-        }
-
-        subscription.teamInvites.push({
-          email,
-          inviterName,
-          token,
-          sentAt,
-        })
-
-        subscription.save(callback)
-      }
-    )
-  },
+  })
+}
 
 
-  acceptInvite(token, userId, callback) {
-    TeamInvitesHandler.getInvite(token, function (err, invite, subscription) {
-      if (err) {
-        return callback(err)
+function importInvite(
+  subscription,
+  inviterName,
+  email,
+  token,
+  sentAt,
+  callback
+) {
+  _checkIfInviteIsPossible(
+    subscription,
+    email,
+    function (error, possible, reason) {
+      if (error) {
+        return callback(error)
       }
       }
-      if (!invite) {
-        return callback(new Errors.NotFoundError('invite not found'))
+      if (!possible) {
+        return callback(reason)
       }
       }
 
 
-      SubscriptionUpdater.addUserToGroup(
-        subscription._id,
-        userId,
-        function (err) {
-          if (err) {
-            return callback(err)
-          }
+      subscription.teamInvites.push({
+        email,
+        inviterName,
+        token,
+        sentAt,
+      })
 
 
-          removeInviteFromTeam(subscription.id, invite.email, callback)
-        }
-      )
-    })
-  },
+      subscription.save(callback)
+    }
+  )
+}
 
 
-  revokeInvite(teamManagerId, subscription, email, callback) {
-    email = EmailHelper.parseEmail(email)
-    if (!email) {
-      return callback(new Error('invalid email'))
+function acceptInvite(token, userId, callback) {
+  getInvite(token, function (err, invite, subscription) {
+    if (err) {
+      return callback(err)
+    }
+    if (!invite) {
+      return callback(new Errors.NotFoundError('invite not found'))
     }
     }
-    removeInviteFromTeam(subscription.id, email, callback)
-  },
 
 
-  // Legacy method to allow a user to receive a confirmation email if their
-  // email is in Subscription.invited_emails when they join. We'll remove this
-  // after a short while.
-  createTeamInvitesForLegacyInvitedEmail(email, callback) {
-    SubscriptionLocator.getGroupsWithEmailInvite(email, function (err, teams) {
-      if (err) {
-        return callback(err)
+    SubscriptionUpdater.addUserToGroup(
+      subscription._id,
+      userId,
+      function (err) {
+        if (err) {
+          return callback(err)
+        }
+
+        _removeInviteFromTeam(subscription.id, invite.email, callback)
       }
       }
+    )
+  })
+}
 
 
-      async.map(
-        teams,
-        (team, cb) =>
-          TeamInvitesHandler.createInvite(team.admin_id, team, email, cb),
-        callback
-      )
-    })
-  },
+function revokeInvite(teamManagerId, subscription, email, callback) {
+  email = EmailHelper.parseEmail(email)
+  if (!email) {
+    return callback(new Error('invalid email'))
+  }
+  _removeInviteFromTeam(subscription.id, email, callback)
+}
+
+// Legacy method to allow a user to receive a confirmation email if their
+// email is in Subscription.invited_emails when they join. We'll remove this
+// after a short while.
+function createTeamInvitesForLegacyInvitedEmail(email, callback) {
+  SubscriptionLocator.getGroupsWithEmailInvite(email, function (err, teams) {
+    if (err) {
+      return callback(err)
+    }
+
+    async.map(
+      teams,
+      (team, cb) => createInvite(team.admin_id, team, email, cb),
+      callback
+    )
+  })
 }
 }
 
 
-function createInvite(subscription, email, inviter, callback) {
-  checkIfInviteIsPossible(
+function _createInvite(subscription, email, inviter, callback) {
+  _checkIfInviteIsPossible(
     subscription,
     subscription,
     email,
     email,
     function (error, possible, reason) {
     function (error, possible, reason) {
@@ -155,7 +159,7 @@ function createInvite(subscription, email, inviter, callback) {
             }
             }
 
 
             // legacy: remove any invite that might have been created in the past
             // legacy: remove any invite that might have been created in the past
-            removeInviteFromTeam(subscription._id, email, error => {
+            _removeInviteFromTeam(subscription._id, email, error => {
               const inviteUserData = {
               const inviteUserData = {
                 email: inviter.email,
                 email: inviter.email,
                 first_name: inviter.first_name,
                 first_name: inviter.first_name,
@@ -168,7 +172,7 @@ function createInvite(subscription, email, inviter, callback) {
         )
         )
       }
       }
 
 
-      const inviterName = getInviterName(inviter)
+      const inviterName = _getInviterName(inviter)
       let invite = subscription.teamInvites.find(
       let invite = subscription.teamInvites.find(
         invite => invite.email === email
         invite => invite.email === email
       )
       )
@@ -206,20 +210,20 @@ function createInvite(subscription, email, inviter, callback) {
   )
   )
 }
 }
 
 
-function removeInviteFromTeam(subscriptionId, email, callback) {
+function _removeInviteFromTeam(subscriptionId, email, callback) {
   const searchConditions = { _id: new ObjectId(subscriptionId.toString()) }
   const searchConditions = { _id: new ObjectId(subscriptionId.toString()) }
   const removeInvite = { $pull: { teamInvites: { email } } }
   const removeInvite = { $pull: { teamInvites: { email } } }
 
 
   async.series(
   async.series(
     [
     [
       cb => Subscription.updateOne(searchConditions, removeInvite, cb),
       cb => Subscription.updateOne(searchConditions, removeInvite, cb),
-      cb => removeLegacyInvite(subscriptionId, email, cb),
+      cb => _removeLegacyInvite(subscriptionId, email, cb),
     ],
     ],
     callback
     callback
   )
   )
 }
 }
 
 
-const removeLegacyInvite = (subscriptionId, email, callback) =>
+const _removeLegacyInvite = (subscriptionId, email, callback) =>
   Subscription.updateOne(
   Subscription.updateOne(
     {
     {
       _id: new ObjectId(subscriptionId.toString()),
       _id: new ObjectId(subscriptionId.toString()),
@@ -232,7 +236,7 @@ const removeLegacyInvite = (subscriptionId, email, callback) =>
     callback
     callback
   )
   )
 
 
-function checkIfInviteIsPossible(subscription, email, callback) {
+function _checkIfInviteIsPossible(subscription, email, callback) {
   if (!subscription.groupPlan) {
   if (!subscription.groupPlan) {
     logger.debug(
     logger.debug(
       { subscriptionId: subscription.id },
       { subscriptionId: subscription.id },
@@ -273,7 +277,7 @@ function checkIfInviteIsPossible(subscription, email, callback) {
   })
   })
 }
 }
 
 
-function getInviterName(inviter) {
+function _getInviterName(inviter) {
   let inviterName
   let inviterName
   if (inviter.first_name && inviter.last_name) {
   if (inviter.first_name && inviter.last_name) {
     inviterName = `${inviter.first_name} ${inviter.last_name} (${inviter.email})`
     inviterName = `${inviter.first_name} ${inviter.last_name} (${inviter.email})`
@@ -283,3 +287,22 @@ function getInviterName(inviter) {
 
 
   return inviterName
   return inviterName
 }
 }
+
+module.exports = {
+  getInvite,
+  createInvite,
+  importInvite,
+  acceptInvite,
+  revokeInvite,
+  createTeamInvitesForLegacyInvitedEmail,
+  promises: {
+    getInvite: promisifyMultiResult(getInvite, ['invite', 'subscription']),
+    createInvite: promisify(createInvite),
+    importInvite: promisify(importInvite),
+    acceptInvite: promisify(acceptInvite),
+    revokeInvite: promisify(revokeInvite),
+    createTeamInvitesForLegacyInvitedEmail: promisify(
+      createTeamInvitesForLegacyInvitedEmail
+    ),
+  },
+}

+ 24 - 0
services/web/app/views/subscriptions/team/invite_logged_out.pug

@@ -0,0 +1,24 @@
+extends ../../layout
+
+block content
+	main.content.content-alt.team-invite#main-content
+		.container
+			.row
+				.col-md-8.col-md-offset-2.text-center
+					.card
+						.page-header
+							h1.text-centered #{translate("invited_to_group", {inviterName: inviterName, appName: appName})}
+
+						if (accountExists)
+							div
+								p #{translate("invited_to_group_login_benefits", {appName: appName})}
+								p #{translate("invited_to_group_login", {emailAddress: emailAddress})}
+								p
+									a.btn.btn.btn-primary(href=`/login?redir=/subscription/invites/${inviteToken}`) #{translate("login_to_accept_invitation")}
+						else
+							div
+								p #{translate("invited_to_group_register_benefits", {appName: appName})}
+								p #{translate("invited_to_group_register", {inviterName: inviterName})}
+								p
+									a.btn.btn.btn-primary(href=`/register?redir=/subscription/invites/${inviteToken}`) #{translate("register_to_accept_invitation")}
+

+ 6 - 0
services/web/locales/en.json

@@ -798,6 +798,10 @@
   "invite_not_valid": "This is not a valid project invite",
   "invite_not_valid": "This is not a valid project invite",
   "invite_not_valid_description": "The invite may have expired. Please contact the project owner",
   "invite_not_valid_description": "The invite may have expired. Please contact the project owner",
   "invited_to_group": "__inviterName__ has invited you to join a group subscription on __appName__",
   "invited_to_group": "__inviterName__ has invited you to join a group subscription on __appName__",
+  "invited_to_group_login": "To accept this invitation you need to log in as __emailAddress__.",
+  "invited_to_group_login_benefits": "As part of this group, you’ll have access to __appName__ premium features such as additional collaborators, greater maximum compile time, and real-time track changes.",
+  "invited_to_group_register": "To accept __inviterName__’s invitation you’ll need to create an account.",
+  "invited_to_group_register_benefits": "__appName__ is a collaborative online LaTeX editor, with thousands of ready-to-use templates and an array of LaTeX learning resources to help you get started.",
   "invited_to_join": "You have been invited to join",
   "invited_to_join": "You have been invited to join",
   "ip_address": "IP Address",
   "ip_address": "IP Address",
   "is_email_affiliated": "Is your email affiliated with an institution? ",
   "is_email_affiliated": "Is your email affiliated with an institution? ",
@@ -911,6 +915,7 @@
   "login_here": "Login here",
   "login_here": "Login here",
   "login_or_password_wrong_try_again": "Your login or password is incorrect. Please try again",
   "login_or_password_wrong_try_again": "Your login or password is incorrect. Please try again",
   "login_register_or": "or",
   "login_register_or": "or",
+  "login_to_accept_invitation": "Login to accept invitation",
   "login_to_overleaf": "Log in to Overleaf",
   "login_to_overleaf": "Log in to Overleaf",
   "login_with_email": "Log in with your email",
   "login_with_email": "Log in with your email",
   "login_with_service": "Log in with __service__",
   "login_with_service": "Log in with __service__",
@@ -1268,6 +1273,7 @@
   "register": "Register",
   "register": "Register",
   "register_error": "Registration error",
   "register_error": "Registration error",
   "register_intercept_sso": "You can link your __authProviderName__ account from the Account Settings page after logging in.",
   "register_intercept_sso": "You can link your __authProviderName__ account from the Account Settings page after logging in.",
+  "register_to_accept_invitation": "Register to accept invitation",
   "register_to_edit_template": "Please register to edit the __templateName__ template",
   "register_to_edit_template": "Please register to edit the __templateName__ template",
   "register_using_email": "Register using your email",
   "register_using_email": "Register using your email",
   "register_using_service": "Register using __service__",
   "register_using_service": "Register using __service__",

+ 10 - 10
services/web/test/unit/src/Subscription/TeamInvitesHandlerTests.js

@@ -340,18 +340,18 @@ describe('TeamInvitesHandler', function () {
     it('sends an invitation email to addresses in the legacy invited_emails field', function (done) {
     it('sends an invitation email to addresses in the legacy invited_emails field', function (done) {
       this.TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail(
       this.TeamInvitesHandler.createTeamInvitesForLegacyInvitedEmail(
         'eddard@example.com',
         'eddard@example.com',
-        (err, invite) => {
+        (err, invites) => {
           expect(err).not.to.exist
           expect(err).not.to.exist
+          expect(invites.length).to.eq(1)
 
 
-          this.TeamInvitesHandler.createInvite
-            .calledWith(
-              this.subscription.admin_id,
-              this.subscription,
-              'eddard@example.com'
-            )
-            .should.eq(true)
-
-          this.TeamInvitesHandler.createInvite.callCount.should.eq(1)
+          const [invite] = invites
+          expect(invite.token).to.eq(this.newToken)
+          expect(invite.email).to.eq('eddard@example.com')
+          expect(invite.inviterName).to.eq(
+            'Daenerys Targaryen (daenerys@example.com)'
+          )
+          expect(invite.invite).to.be.true
+          expect(this.subscription.teamInvites).to.deep.include(invite)
 
 
           done()
           done()
         }
         }