diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js b/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js index 861ba74496..55b50f515c 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsInviteController.js +++ b/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 ), diff --git a/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js b/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js index 8a1baa7a37..0629aa4284 100644 --- a/services/web/app/src/Features/Collaborators/CollaboratorsInviteHandler.js +++ b/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), diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessController.js b/services/web/app/src/Features/TokenAccess/TokenAccessController.js index 1cc5437d31..158aa20003 100644 --- a/services/web/app/src/Features/TokenAccess/TokenAccessController.js +++ b/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({ diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.js b/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.js index 50eaebd5ba..56d8722a98 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.js +++ b/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 () { diff --git a/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js b/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js index e5b0db2f39..de9c0bd96c 100644 --- a/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js +++ b/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 } ) })