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

[web] Revoke invite after joining project via link sharing

GitOrigin-RevId: 5071c9fbb226e5eef8c3e5c82991da73529d7396
This commit is contained in:
roo hutton 2024-10-10 10:14:59 +01:00 committed by Copybot
parent d6de6da781
commit 7d53b3e4a8
5 changed files with 121 additions and 6 deletions

View file

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

View file

@ -3,6 +3,7 @@ const { ProjectInvite } = require('../../models/ProjectInvite')
const logger = require('@overleaf/logger') const logger = require('@overleaf/logger')
const CollaboratorsEmailHandler = require('./CollaboratorsEmailHandler') const CollaboratorsEmailHandler = require('./CollaboratorsEmailHandler')
const CollaboratorsHandler = require('./CollaboratorsHandler') const CollaboratorsHandler = require('./CollaboratorsHandler')
const CollaboratorsInviteGetter = require('./CollaboratorsInviteGetter')
const CollaboratorsInviteHelper = require('./CollaboratorsInviteHelper') const CollaboratorsInviteHelper = require('./CollaboratorsInviteHelper')
const UserGetter = require('../User/UserGetter') const UserGetter = require('../User/UserGetter')
const ProjectGetter = require('../Project/ProjectGetter') const ProjectGetter = require('../Project/ProjectGetter')
@ -91,6 +92,21 @@ const CollaboratorsInviteHandler = {
return _.pick(invite, ['_id', 'email', 'privileges']) 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) { async revokeInvite(projectId, inviteId) {
logger.debug({ projectId, inviteId }, 'removing invite') logger.debug({ projectId, inviteId }, 'removing invite')
const invite = await ProjectInvite.findOneAndDelete({ const invite = await ProjectInvite.findOneAndDelete({
@ -185,6 +201,9 @@ const CollaboratorsInviteHandler = {
module.exports = { module.exports = {
promises: CollaboratorsInviteHandler, promises: CollaboratorsInviteHandler,
inviteToProject: callbackify(CollaboratorsInviteHandler.inviteToProject), inviteToProject: callbackify(CollaboratorsInviteHandler.inviteToProject),
revokeInviteForUser: callbackify(
CollaboratorsInviteHandler.revokeInviteForUser
),
revokeInvite: callbackify(CollaboratorsInviteHandler.revokeInvite), revokeInvite: callbackify(CollaboratorsInviteHandler.revokeInvite),
generateNewInvite: callbackify(CollaboratorsInviteHandler.generateNewInvite), generateNewInvite: callbackify(CollaboratorsInviteHandler.generateNewInvite),
acceptInvite: callbackify(CollaboratorsInviteHandler.acceptInvite), acceptInvite: callbackify(CollaboratorsInviteHandler.acceptInvite),

View file

@ -10,6 +10,7 @@ const AuthorizationManager = require('../Authorization/AuthorizationManager')
const PrivilegeLevels = require('../Authorization/PrivilegeLevels') const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler') const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
const SplitTestHandler = require('../SplitTests/SplitTestHandler') const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const CollaboratorsInviteHandler = require('../Collaborators/CollaboratorsInviteHandler')
const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler') const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
const EditorRealTimeController = require('../Editor/EditorRealTimeController') const EditorRealTimeController = require('../Editor/EditorRealTimeController')
const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter') const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
@ -372,13 +373,20 @@ async function grantTokenAccessReadAndWrite(req, res, next) {
: PrivilegeLevels.READ_AND_WRITE, : PrivilegeLevels.READ_AND_WRITE,
{ pendingEditor } { 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, // Should be a noop if the user is already a member,
// and would redirect transparently into the project. // and would redirect transparently into the project.
EditorRealTimeController.emitToRoom( EditorRealTimeController.emitToRoom(
project._id, project._id,
'project:membership:changed', 'project:membership:changed',
{ members: true } { members: true, invites: true }
) )
return res.json({ return res.json({

View file

@ -47,6 +47,12 @@ describe('CollaboratorsInviteHandler', function () {
hashInviteToken: sinon.stub().returns(this.tokenHmac), hashInviteToken: sinon.stub().returns(this.tokenHmac),
} }
this.CollaboratorsInviteGetter = {
promises: {
getAllInvites: sinon.stub(),
},
}
this.SplitTestHandler = { this.SplitTestHandler = {
promises: { promises: {
getAssignmentForUser: sinon.stub().resolves(), getAssignmentForUser: sinon.stub().resolves(),
@ -76,6 +82,7 @@ describe('CollaboratorsInviteHandler', function () {
'../Project/ProjectGetter': this.ProjectGetter, '../Project/ProjectGetter': this.ProjectGetter,
'../Notifications/NotificationsBuilder': this.NotificationsBuilder, '../Notifications/NotificationsBuilder': this.NotificationsBuilder,
'./CollaboratorsInviteHelper': this.CollaboratorsInviteHelper, './CollaboratorsInviteHelper': this.CollaboratorsInviteHelper,
'./CollaboratorsInviteGetter': this.CollaboratorsInviteGetter,
'../SplitTests/SplitTestHandler': this.SplitTestHandler, '../SplitTests/SplitTestHandler': this.SplitTestHandler,
'../Subscription/LimitationsManager': this.LimitationsManager, '../Subscription/LimitationsManager': this.LimitationsManager,
'../Project/ProjectAuditLogHandler': this.ProjectAuditLogHandler, '../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 () { describe('revokeInvite', function () {
beforeEach(function () { beforeEach(function () {

View file

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