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

Merge pull request #20561 from overleaf/rh-duplicate-invite

[web] Revoke invite after joining project via link sharing

GitOrigin-RevId: 5071c9fbb226e5eef8c3e5c82991da73529d7396
roo hutton 2 лет назад
Родитель
Сommit
7d53b3e4a8

+ 3 - 1
services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js

@@ -187,7 +187,6 @@ const CollaboratorsInviteController = {
     )
     res.json({ invite })
   },
-
   async revokeInvite(req, res) {
     const projectId = req.params.Project_id
     const inviteId = req.params.invite_id
@@ -404,6 +403,9 @@ module.exports = {
   getAllInvites: expressify(CollaboratorsInviteController.getAllInvites),
   inviteToProject: expressify(CollaboratorsInviteController.inviteToProject),
   revokeInvite: expressify(CollaboratorsInviteController.revokeInvite),
+  revokeInviteForUser: expressify(
+    CollaboratorsInviteController.revokeInviteForUser
+  ),
   generateNewInvite: expressify(
     CollaboratorsInviteController.generateNewInvite
   ),

+ 19 - 0
services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js

@@ -3,6 +3,7 @@ const { ProjectInvite } = require('../../models/ProjectInvite')
 const logger = require('@overleaf/logger')
 const CollaboratorsEmailHandler = require('./CollaboratorsEmailHandler')
 const CollaboratorsHandler = require('./CollaboratorsHandler')
+const CollaboratorsInviteGetter = require('./CollaboratorsInviteGetter')
 const CollaboratorsInviteHelper = require('./CollaboratorsInviteHelper')
 const UserGetter = require('../User/UserGetter')
 const ProjectGetter = require('../Project/ProjectGetter')
@@ -91,6 +92,21 @@ const CollaboratorsInviteHandler = {
     return _.pick(invite, ['_id', 'email', 'privileges'])
   },
 
+  async revokeInviteForUser(projectId, targetEmails) {
+    logger.debug({ projectId }, 'getting all active invites for project')
+    const invites =
+      await CollaboratorsInviteGetter.promises.getAllInvites(projectId)
+    const matchingInvite = invites.find(invite =>
+      targetEmails.some(emailData => emailData.email === invite.email)
+    )
+    if (matchingInvite) {
+      await CollaboratorsInviteHandler.revokeInvite(
+        projectId,
+        matchingInvite._id
+      )
+    }
+  },
+
   async revokeInvite(projectId, inviteId) {
     logger.debug({ projectId, inviteId }, 'removing invite')
     const invite = await ProjectInvite.findOneAndDelete({
@@ -185,6 +201,9 @@ const CollaboratorsInviteHandler = {
 module.exports = {
   promises: CollaboratorsInviteHandler,
   inviteToProject: callbackify(CollaboratorsInviteHandler.inviteToProject),
+  revokeInviteForUser: callbackify(
+    CollaboratorsInviteHandler.revokeInviteForUser
+  ),
   revokeInvite: callbackify(CollaboratorsInviteHandler.revokeInvite),
   generateNewInvite: callbackify(CollaboratorsInviteHandler.generateNewInvite),
   acceptInvite: callbackify(CollaboratorsInviteHandler.acceptInvite),

+ 10 - 2
services/web/app/src/Features/TokenAccess/TokenAccessController.js

@@ -10,6 +10,7 @@ const AuthorizationManager = require('../Authorization/AuthorizationManager')
 const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
 const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
 const SplitTestHandler = require('../SplitTests/SplitTestHandler')
+const CollaboratorsInviteHandler = require('../Collaborators/CollaboratorsInviteHandler')
 const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
 const EditorRealTimeController = require('../Editor/EditorRealTimeController')
 const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
@@ -372,13 +373,20 @@ async function grantTokenAccessReadAndWrite(req, res, next) {
           : PrivilegeLevels.READ_AND_WRITE,
         { pendingEditor }
       )
-      // Does not remove any pending invite or the invite notification
+
+      // remove pending invite and notification
+      const userEmails =
+        await UserGetter.promises.getUserConfirmedEmails(userId)
+      await CollaboratorsInviteHandler.promises.revokeInviteForUser(
+        project._id,
+        userEmails
+      )
       // Should be a noop if the user is already a member,
       // and would redirect transparently into the project.
       EditorRealTimeController.emitToRoom(
         project._id,
         'project:membership:changed',
-        { members: true }
+        { members: true, invites: true }
       )
 
       return res.json({

+ 76 - 0
services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.js

@@ -47,6 +47,12 @@ describe('CollaboratorsInviteHandler', function () {
       hashInviteToken: sinon.stub().returns(this.tokenHmac),
     }
 
+    this.CollaboratorsInviteGetter = {
+      promises: {
+        getAllInvites: sinon.stub(),
+      },
+    }
+
     this.SplitTestHandler = {
       promises: {
         getAssignmentForUser: sinon.stub().resolves(),
@@ -76,6 +82,7 @@ describe('CollaboratorsInviteHandler', function () {
         '../Project/ProjectGetter': this.ProjectGetter,
         '../Notifications/NotificationsBuilder': this.NotificationsBuilder,
         './CollaboratorsInviteHelper': this.CollaboratorsInviteHelper,
+        './CollaboratorsInviteGetter': this.CollaboratorsInviteGetter,
         '../SplitTests/SplitTestHandler': this.SplitTestHandler,
         '../Subscription/LimitationsManager': this.LimitationsManager,
         '../Project/ProjectAuditLogHandler': this.ProjectAuditLogHandler,
@@ -249,6 +256,75 @@ describe('CollaboratorsInviteHandler', function () {
       })
     })
   })
+  describe('revokeInviteForUser', function () {
+    beforeEach(function () {
+      this.targetInvite = {
+        _id: new ObjectId(),
+        email: 'fake2@example.org',
+        two: 2,
+      }
+      this.fakeInvites = [
+        { _id: new ObjectId(), email: 'fake1@example.org', one: 1 },
+        this.targetInvite,
+      ]
+      this.fakeInvitesWithoutUser = [
+        { _id: new ObjectId(), email: 'fake1@example.org', one: 1 },
+        { _id: new ObjectId(), email: 'fake3@example.org', two: 2 },
+      ]
+      this.targetEmail = [{ email: 'fake2@example.org' }]
+
+      this.CollaboratorsInviteGetter.promises.getAllInvites.resolves(
+        this.fakeInvites
+      )
+      this.CollaboratorsInviteHandler.promises.revokeInvite = sinon
+        .stub()
+        .resolves(this.targetInvite)
+
+      this.call = async () => {
+        return await this.CollaboratorsInviteHandler.promises.revokeInviteForUser(
+          this.projectId,
+          this.targetEmail
+        )
+      }
+    })
+
+    describe('for a valid user', function () {
+      it('should have called CollaboratorsInviteGetter.getAllInvites', async function () {
+        await this.call()
+        this.CollaboratorsInviteGetter.promises.getAllInvites.callCount.should.equal(
+          1
+        )
+        this.CollaboratorsInviteGetter.promises.getAllInvites
+          .calledWith(this.projectId)
+          .should.equal(true)
+      })
+
+      it('should have called revokeInvite', async function () {
+        await this.call()
+        this.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal(
+          1
+        )
+
+        this.CollaboratorsInviteHandler.promises.revokeInvite
+          .calledWith(this.projectId, this.targetInvite._id)
+          .should.equal(true)
+      })
+    })
+
+    describe('for a user without an invite in the project', function () {
+      beforeEach(function () {
+        this.CollaboratorsInviteGetter.promises.getAllInvites.resolves(
+          this.fakeInvitesWithoutUser
+        )
+      })
+      it('should not have called CollaboratorsInviteHandler.revokeInvite', async function () {
+        await this.call()
+        this.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal(
+          0
+        )
+      })
+    })
+  })
 
   describe('revokeInvite', function () {
     beforeEach(function () {

+ 13 - 3
services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js

@@ -83,6 +83,12 @@ describe('TokenAccessController', function () {
       },
     }
 
+    this.CollaboratorsInviteHandler = {
+      promises: {
+        revokeInviteForUser: sinon.stub().resolves(),
+      },
+    }
+
     this.CollaboratorsHandler = {
       promises: {
         addUserIdToProject: sinon.stub().resolves(),
@@ -120,6 +126,8 @@ describe('TokenAccessController', function () {
             return null
           }
         }),
+        getUserEmail: sinon.stub().resolves(),
+        getUserConfirmedEmails: sinon.stub().resolves(),
       },
     }
 
@@ -143,6 +151,8 @@ describe('TokenAccessController', function () {
         '../SplitTests/SplitTestHandler': this.SplitTestHandler,
         '../Errors/Errors': (this.Errors = { NotFoundError: sinon.stub() }),
         '../Collaborators/CollaboratorsHandler': this.CollaboratorsHandler,
+        '../Collaborators/CollaboratorsInviteHandler':
+          this.CollaboratorsInviteHandler,
         '../Collaborators/CollaboratorsGetter': this.CollaboratorsGetter,
         '../Editor/EditorRealTimeController': this.EditorRealTimeController,
         '../Project/ProjectGetter': this.ProjectGetter,
@@ -279,7 +289,7 @@ describe('TokenAccessController', function () {
           ).to.have.been.calledWith(
             this.project._id,
             'project:membership:changed',
-            { members: true }
+            { members: true, invites: true }
           )
         })
 
@@ -365,7 +375,7 @@ describe('TokenAccessController', function () {
             ).to.have.been.calledWith(
               this.project._id,
               'project:membership:changed',
-              { members: true }
+              { members: true, invites: true }
             )
           })
 
@@ -440,7 +450,7 @@ describe('TokenAccessController', function () {
             ).to.have.been.calledWith(
               this.project._id,
               'project:membership:changed',
-              { members: true }
+              { members: true, invites: true }
             )
           })